registerContentObserver回調兩次,ContentObserver回調兩次

背景

項目中用到了ContentProvider存取數據,同時想動態監聽數據變化。使用getContentResolver().registerContentObserver監聽。java

存數據代碼

Settings.Secure.putInt(getContentResolver(), SECURE_KEY_VOLUME_UP, 1);
getContentResolver().notifyChange(Settings.Secure.getUriFor(SECURE_KEY_VOLUME_UP), null);

代碼很簡單,Settings.Secure其實就是封裝的ContentProvider用法,put值,同時notify告知全部監聽者。node

監聽代碼

context.getContentResolver().registerContentObserver(uri, false, volumeUpObserver);
private ContentObserver volumeUpObserver = new ContentObserver(null) {
        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            int value = Settings.Secure.getInt(context.getContentResolver(), SECURE_KEY_VOLUME_UP, -1);
            Log.d(GabbroAssistantOemService.TAG, SECURE_KEY_VOLUME_UP + " : " + value);
        }
};

結果

打印日誌發現會有兩條onChange改變的日誌。web

解決此問題

1、首先用法是沒有問題的,很標準。因而網上搜有沒有同類問題。發現了一會相似的問題:
https://blog.csdn.net/serapme/article/details/7404233api

2、因而好奇爲啥會回調兩次,初步懷疑是getContentResolver內部未知緣由存了兩個observer。
一、先看下ContentResolver註冊代碼ide

public final void registerContentObserver(@NonNull Uri uri, boolean notifyForDescendants,
            @NonNull ContentObserver observer) {
        Preconditions.checkNotNull(uri, "uri");
        Preconditions.checkNotNull(observer, "observer");
        registerContentObserver(
                ContentProvider.getUriWithoutUserId(uri),
                notifyForDescendants,
                observer,
                ContentProvider.getUserIdFromUri(uri, mContext.getUserId()));
    }

    /** @hide - designated user version */
    @UnsupportedAppUsage
    public final void registerContentObserver(Uri uri, boolean notifyForDescendents,
            ContentObserver observer, @UserIdInt int userHandle) {
        try {
            getContentService().registerContentObserver(uri, notifyForDescendents,
                    observer.getContentObserver(), userHandle, mTargetSdkVersion);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

二、最終是調用了ContentService.registerContentObserver,代碼在ContentService.java中svg

@Override
    public void registerContentObserver(Uri uri, boolean notifyForDescendants,
            IContentObserver observer, int userHandle, int targetSdkVersion) {
        if (observer == null || uri == null) {
            throw new IllegalArgumentException("You must pass a valid uri and observer");
        }
		......
        synchronized (mRootNode) {
            mRootNode.addObserverLocked(uri, observer, notifyForDescendants, mRootNode,
                    uid, pid, userHandle);
            if (false) Log.v(TAG, "Registered observer " + observer + " at " + uri +
                    " with notifyForDescendants " + notifyForDescendants);
        }
    }
	
	public static final class ObserverNode {
	    private ArrayList<ObserverEntry> mObservers = new ArrayList<ObserverEntry>();
		private void addObserverLocked(Uri uri, int index, IContentObserver observer,
                                       boolean notifyForDescendants, Object observersLock,
                                       int uid, int pid, int userHandle) {
            // If this is the leaf node add the observer
            if (index == countUriSegments(uri)) {
                mObservers.add(new ObserverEntry(observer, notifyForDescendants, observersLock,
                        uid, pid, userHandle, uri));
                return;
            }

            // Look to see if the proper child already exists
            String segment = getUriSegment(uri, index);
            if (segment == null) {
                throw new IllegalArgumentException("Invalid Uri (" + uri + ") used for observer");
            }
            int N = mChildren.size();
            for (int i = 0; i < N; i++) {
                ObserverNode node = mChildren.get(i);
                if (node.mName.equals(segment)) {
                    node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,
                            observersLock, uid, pid, userHandle);
                    return;
                }
            }

            // No child found, create one
            ObserverNode node = new ObserverNode(segment);
            mChildren.add(node);
            node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,
                    observersLock, uid, pid, userHandle);
        }
   }

代碼很明顯,存到了mObservers,是個list,中間並無什麼邏輯添加兩次。
三、register沒問題,那會不會是notify有問題,因而看了notify代碼,代碼在ContentResolver.javaoop

public void notifyChange(@NonNull Uri uri, ContentObserver observer, boolean syncToNetwork,
            @UserIdInt int userHandle) {
        try {
            getContentService().notifyChange(
                    uri, observer == null ? null : observer.getContentObserver(),
                    observer != null && observer.deliverSelfNotifications(),
                    syncToNetwork ? NOTIFY_SYNC_TO_NETWORK : 0,
                    userHandle, mTargetSdkVersion, mContext.getPackageName());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

四、再看下ContentService.notifyChange,代碼在ContentService.javaui

@Override
    public void notifyChange(Uri uri, IContentObserver observer,
            boolean observerWantsSelfNotifications, int flags, int userHandle,
            int targetSdkVersion, String callingPackage) {
		......
        // This makes it so that future permission checks will be in the context of this
        // process rather than the caller's process. We will restore this before returning.
        long identityToken = clearCallingIdentity();
        try {
            ArrayList<ObserverCall> calls = new ArrayList<ObserverCall>();
            synchronized (mRootNode) {
                mRootNode.collectObserversLocked(uri, 0, observer, observerWantsSelfNotifications,
                        flags, userHandle, calls);
            }
            final int numCalls = calls.size();
            for (int i=0; i<numCalls; i++) {
                ObserverCall oc = calls.get(i);
                try {
                    oc.mObserver.onChange(oc.mSelfChange, uri, userHandle);
                    if (DEBUG) Slog.d(TAG, "Notified " + oc.mObserver + " of " + "update at "
                            + uri);
                } catch (RemoteException ex) {
                    synchronized (mRootNode) {
                        Log.w(TAG, "Found dead observer, removing");
                        IBinder binder = oc.mObserver.asBinder();
                        final ArrayList<ObserverNode.ObserverEntry> list
                                = oc.mNode.mObservers;
                        int numList = list.size();
                        for (int j=0; j<numList; j++) {
                            ObserverNode.ObserverEntry oe = list.get(j);
                            if (oe.observer.asBinder() == binder) {
                                list.remove(j);
                                j--;
                                numList--;
                            }
                        }
                    }
                }
            }
            ......
        } finally {
            restoreCallingIdentity(identityToken);
        }
    }

代碼也很明顯,就是遍歷mObserver.onChange了。同時代碼裏注意下mObserver.asBinder(),跨進程傳遞了observer,這裏asBinder後的對象是客戶端的IBinder,能夠用來判斷是否同一個對象。this

3、研究了一通,發現彷佛沒什麼問題,可是不管怎麼搞就是onchange回調了兩次。接口api中也沒有明確指明這種漏洞。不過,忽然想到,會不會Settings.Secure.putInt自己就作了notify!!!!
一、先看下Settings.puInt接口spa

/** * Convenience function for updating a single settings value as an * integer. This will either create a new entry in the table if the * given name does not exist, or modify the value of the existing row * with that name. Note that internally setting values are always * stored as strings, so this function converts the given value to a * string before storing it. * * @param cr The ContentResolver to access. * @param name The name of the setting to modify. * @param value The new value for the setting. * @return true if the value was set, false on database errors */
        public static boolean putInt(ContentResolver cr, String name, int value) {
            return putIntForUser(cr, name, value, cr.getUserId());
        }

接口描述中沒提到notify的事情
二、繼續往代碼深處看,仍是在Settings.java類中

private static class NameValueCache {
        ......
        public boolean putStringForUser(ContentResolver cr, String name, String value,
                String tag, boolean makeDefault, final int userHandle) {
            try {
                Bundle arg = new Bundle();
                arg.putString(Settings.NameValueTable.VALUE, value);
                arg.putInt(CALL_METHOD_USER_KEY, userHandle);
                if (tag != null) {
                    arg.putString(CALL_METHOD_TAG_KEY, tag);
                }
                if (makeDefault) {
                    arg.putBoolean(CALL_METHOD_MAKE_DEFAULT_KEY, true);
                }
                IContentProvider cp = mProviderHolder.getProvider(cr);
                cp.call(cr.getPackageName(), mProviderHolder.mUri.getAuthority(),
                        mCallSetCommand, name, arg);
            } catch (RemoteException e) {
                Log.w(TAG, "Can't set key " + name + " in " + mUri, e);
                return false;
            }
            return true;
        }
   }

三、調用了IContentProvider.call(),代碼在SettingsProvider.java

private static int getRequestingUserId(Bundle args) {
        final int callingUserId = UserHandle.getCallingUserId();
        return (args != null) ? args.getInt(Settings.CALL_METHOD_USER_KEY, callingUserId)
                : callingUserId;
    }
    
    @Override
    public Bundle call(String method, String name, Bundle args) {
        final int requestingUserId = getRequestingUserId(args);
        switch (method) {
            case Settings.CALL_METHOD_PUT_SECURE: {
                String value = getSettingValue(args);
                String tag = getSettingTag(args);
                final boolean makeDefault = getSettingMakeDefault(args);
                insertSecureSetting(name, value, tag, makeDefault, requestingUserId, false);
                break;
            }
       }
  	}
	private boolean insertSecureSetting(String name, String value, String tag,
            boolean makeDefault, int requestingUserId, boolean forceNotify) {
        	......
            if (forceNotify || success) {
                notifyForSettingsChange(key, name);
            }
            return success;
    }
    
    private void notifyForSettingsChange(int key, String name) {
            ......
            // Always notify that our data changed
            mHandler.obtainMessage(MyHandler.MSG_NOTIFY_DATA_CHANGED).sendToTarget();
   }
   
   private final class MyHandler extends Handler {
            private static final int MSG_NOTIFY_URI_CHANGED = 1;
            private static final int MSG_NOTIFY_DATA_CHANGED = 2;

            public MyHandler(Looper looper) {
                super(looper);
            }

            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case MSG_NOTIFY_URI_CHANGED: {
                        final int userId = msg.arg1;
                        Uri uri = (Uri) msg.obj;
                        try {
                            getContext().getContentResolver().notifyChange(uri, null, true, userId);
                        } catch (SecurityException e) {
                            Slog.w(LOG_TAG, "Failed to notify for " + userId + ": " + uri, e);
                        }
                        if (DEBUG || true) {
                            Slog.v(LOG_TAG, "Notifying for " + userId + ": " + uri);
                        }
                    } break;

                    case MSG_NOTIFY_DATA_CHANGED: {
                        mBackupManager.dataChanged();
                    } break;
                }
            }
        }

handler內部作了getContext().getContentResolver().notifyChange

結案

永遠不要懷疑谷歌的源碼有bug(除非你是專家),很明顯,最終的結論是多此一舉了。Settings.Secure.putInt後並不須要本身作getContentResolver().notifyChange。因此若是你碰到onchange回調兩次的問題,能夠追查下put的代碼,看是否已經作了notify