为什么你写的ContentObserver不能及时收到回调?

1,862 阅读4分钟

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流程

  1. 应用层删除数据
  2. 删除数据时需要调用ContentProvider自身的delete()方法
  3. 在执行delete过程中,会调用getContext().getContentResolver.notifyDelete()
  4. 调用notifyDelete时,会添加一个flag来表明此操作代表删除,并回调notifyChange()
  5. 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系统中遇到这种情况,使用的就是一个后台进程,但是又必须及时的回调,可以有两种解决方案

  1. 找到需要使用的位置,在对应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的。

  1. 直接还原此处修改,一力降十会
//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();