Android 在ContentObserver是有延迟机制的。先来了解下ContentObserver developer.android.google.cn/reference/a…
对于Android 相关问题,我都是习惯先去看看官方文档,毕竟官方文档给出来的结论和demo才是最权威的,以官方文档为基础,再结合网上的其他开源资料。关于如何使用ContentObserver不在本篇讨论范围,我们只关注ContentObserver在framework中做了哪些事情。
首先ContentObserver的操作对象是Uri,在ContentObserver正常工作的情况下,为什么明明监听的Uri状态发生了变化,却没有第一时间收到回调呢?
首先我们的ContentObserver观察的MediaProvider数据发生变化,此时MediaProvider本身会将这个变化通知给ContengObserver,然后ContengObserver会回调自己的onChange来将这个变化dispatch下去。
Android R
实际debug过的一个场景,应用层注册了ContentObserver来观察相册,然后手动删除照片,当应用受到onChange回调时,就弹出一个Dialog。事实发现,无论在什么场景下,删除照片之后都要等待10秒才会弹出Dialog,我们回顾一下这其中的framework流程
- 应用层删除数据
- 删除数据时需要调用ContentProvider自身的delete()方法
- 在执行delete过程中,会调用getContext().getContentResolver.notifyDelete()
- 调用notifyDelete时,会添加一个flag来表明此操作代表删除,并回调notifyChange()
- notifyChange()最终回调到ContentService,通过for循环依次通知注册此ContentObserver的对象并回调那里的onChange()。
以上就是ContentObserver在使用过程中,从数据变化到应用层收到onChange回调的全部framework流程。
在Android R版本有上一笔patch对后台进程的ContentObserver主动延迟10ms,以此来优化系统整体性功能。BugID:140249142 感兴趣可以去看下这笔修改
这笔修改涉及ContentServer中的一个关键方法dispatch()
/frameworks/base/services/core/java/com/android/server/content/ContentService.java
556 public void dispatch() {
557 for (int i = 0; i < collected.size(); i++) {
558 final Key key = collected.keyAt(i);
559 final List<Uri> value = collected.valueAt(i);
560
561 final Runnable task = () -> {
562 try {
563 key.observer.onChangeEtc(key.selfChange,
564 value.toArray(new Uri[value.size()]), key.flags, key.userId);
565 } catch (RemoteException ignored) {
566 }
567 };
568
569 // Immediately dispatch notifications to foreground apps that
570 // are important to the user; all other background observers are
571 // delayed to avoid stampeding
572 final boolean noDelay = (key.flags & ContentResolver.NOTIFY_NO_DELAY) != 0;
573 final int procState = LocalServices.getService(ActivityManagerInternal.class)
574 .getUidProcessState(key.uid);
// 这里是收到延迟回调的关键
575 if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND || noDelay) {
576 task.run();
577 } else {
578 BackgroundThread.getHandler().postDelayed(task, BACKGROUND_OBSERVER_DELAY);
579 }
580 }
581 }
582 }
在这个if判断当中,先是通过getService来得到了当前的procState,然后判断线程优先级,如果当前线程的优先级,小于等于前台进程,那么就会直接开始run,也就不会延迟10秒再dispatch;或者是满足noDelay条件,也会直接run。
这里用小于等于是因为在系统内表示优先级,是越小代表优先级越高的,那么后台进程的优先级当然会比较大。再来关注下这里noDelay是怎么定义的
final boolean noDelay = (key.flags & ContentResolver.NOTIFY_NO_DELAY) != 0;
public static final int NOTIFY_NO_DELAY = 1 << 15;
可以看到这里的NOTIFY_NO_DELAY是个标志位,意思就是说,要去判断ContentObserver中传过来的notifyChange的flag是否包含了NOTIFY_NO_DELAY信息,如果包含了此信息,那么就会满足noDelay条件,同样是直接回调,不用等待延迟。所以如果是在Android R系统中遇到这种情况,使用的就是一个后台进程,但是又必须及时的回调,可以有两种解决方案
- 找到需要使用的位置,在对应Provider数据库的notify方法中,手动给flag位添加上NOTIFY_NO_CHANGE
就拿此问题举例,此时如果我们是通过ContentObserver观察MediaProvider的数据,在监听相册照片的删除,那么我们就直接在MediaProvider删除照片,notifyDelete到ContentService时,添加标志位。
/packages/providers/MediaProvider/src/com/android/providers/media/DatabaseHelper.java
600 public void notifyDelete(@NonNull Uri uri) {
601 notifyChange(uri, ContentResolver.NOTIFY_DELETE);
602 }
这个是原本的写法,可以看到在notifyDelete的时候,调用notifyChange时传进去的flag是ContentResolver.NOTIFY_DELETE,我们可以直接通过运算符,把标志位加上去
600 public void notifyDelete(@NonNull Uri uri) {
601 //notifyChange(uri, ContentResolver.NOTIFY_DELETE);
notifyChange(uri, ContentResolver.NOTIFY_DELETE | ContentResolver.NOTIFY_NO_DELAY);
602 }
这样修改的好处是点对点解决问题,不太会影响到全局的performance,只针对单个notify生效。比如MediaProvider、DownloadProvider、ContactsProvider等等,也可以只对其中的一种比如notifyDelete添加flag,但这样修改的风险在于,如果某处对notifyChange中的flga判断采用“ == ”,那么这样修改是有可能导致error或crash的。
- 直接还原此处修改,一力降十会
//final boolean noDelay = (key.flags & ContentResolver.NOTIFY_NO_DELAY) != 0;
final boolean noDelay = true;
这样修改相当于把Android R上的这笔改动还原,没有什么sanity风险,但是作用域是全局的,任何ContentObserver都不会delay,带来的一个明显问题就是performance可能会变差。
Android Q
在Android Q版本中,我们去看这个MeidiaProvider自己的notifyChange方法
/packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
573 /**
574 * Notify that the given {@link Uri} has changed. This enqueues the
575 * notification if currently inside a transaction, and they'll be
576 * clustered and sent when the transaction completes.
577 */
578 public void notifyChange(Uri uri) {
//这个log是有一定标志性,可以作为debug此类问题的关键节点
579 if (LOCAL_LOGV) Log.v(TAG, "Notifying " + uri);
580 final List<Uri> uris = mNotifyChanges.get();
581 if (uris != null) {
582 uris.add(uri);
583 } else {
//出现了,相比于之前版本,Android Q在这里多了一个postDelayed
584 BackgroundThread.getHandler().postDelayed(() -> {
585 notifyChangeInternal(uri);
586 }, sBackgroundDelay);
//也要注意这里写进去的sBackgroundDelay
587 }
588 }
这里是直接调用了bgThread来跑,传进去的delay时间就是sBackgroundDelay,检查一下你的log中,Uri开始变化和收到回调的时间差,是不是刚好和这里的delay时间一样。如果不一样的话,那说明系统当前可能有其他卡顿,就不是同一类问题了。
/frameworks/base/core/java/android/os/Handler.java
474 public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
475 return sendMessageDelayed(getPostMessage(r), delayMillis);
476 }
669 public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
670 if (delayMillis < 0) {
671 delayMillis = 0;
672 }
673 return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
674 }
这里直接发送了一个延迟消息,当这里消息发出之后,就会开始跑前面delay的runable方法,也就是notifyChangeInternal(uri),此处会调用到ContengResolver的notifyChange(),然后会调用到ContengService的notifyChange,并最终由ContentService分发notify给各注册的uri,调用他们的onChange回调,这就是整个ContentObserver的闭环。
/packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
590 private void notifyChangeInternal(Uri uri) {
591 Trace.traceBegin(TRACE_TAG_DATABASE, "notifyChange");
592 try {
//通知给ContentResolver
593 mContext.getContentResolver().notifyChange(uri, null);
594 } finally {
595 Trace.traceEnd(TRACE_TAG_DATABASE);
596 }
597 }
598 }
/frameworks/base/core/java/android/content/ContentResolver.java
2384 public void notifyChange(@NonNull Uri uri, ContentObserver observer, boolean syncToNetwork,
2385 @UserIdInt int userHandle) {
2386 try {
2387 getContentService().notifyChange(
2388 uri, observer == null ? null : observer.getContentObserver(),
2389 observer != null && observer.deliverSelfNotifications(),
2390 syncToNetwork ? NOTIFY_SYNC_TO_NETWORK : 0,
2391 userHandle, mTargetSdkVersion, mContext.getPackageName());
2392 } catch (RemoteException e) {
2393 throw e.rethrowFromSystemServer();
2394 }
2395 }
/frameworks/base/services/core/java/com/android/server/content/ContentService.java
这里dispatch的代码太长了,就不列举了。
389 @Override
390 public void notifyChange(Uri uri, IContentObserver observer,
391 boolean observerWantsSelfNotifications, int flags, int userHandle,
392 int targetSdkVersion, String callingPackage) {
393 if (DEBUG) Slog.d(TAG, "Notifying update of " + uri + " for user " + userHandle
394 + " from observer " + observer + ", flags " + Integer.toHexString(flags));
395
396 if (uri == null) {
397 throw new NullPointerException("Uri must not be null");
398 }
399
400 final int callingUid = Binder.getCallingUid();
401 final int callingPid = Binder.getCallingPid();
402 final int callingUserHandle = UserHandle.getCallingUserId();