Android內容服務ContentService原理淺析

看書的小蝸牛發表於2019-03-03

ContentService可以看做Android中一個系統級別的訊息中心,可以說搭建了一個系統級的觀察者模型,APP可以向訊息中心註冊觀察者,選擇訂閱自己關心的訊息,也可以通過訊息中心傳送資訊,通知其他程式,簡單模型如下:

“ContentService簡單框架”.png

ContentService服務伴隨系統啟動,本身是一個Binder系統服務,執行在SystemServer程式。作為系統服務,最好能保持高效執行,因此ContentService通知APP都是非同步的,也就是oneway的,僅僅插入目標程式(執行緒)的Queue佇列,不必等待執行。下面簡單分析一下整體的架構,主要從一下幾個方面瞭解下執行流程:

  • ContentService啟動跟實質
  • 註冊觀察者
  • 管理觀察者
  • 訊息分發

ContentService啟動跟實質

ContentService服務伴隨系統啟動,更準確的說是伴隨SystemServer程式啟動,其入口函式如下:

public static ContentService main(Context context, boolean factoryTest) {
	 <!--新建Binder服務實體-->
    ContentService service = new ContentService(context, factoryTest);
    <!--新增到ServiceManager中-->
    ServiceManager.addService(ContentResolver.CONTENT_SERVICE_NAME, service);
    return service;
}
複製程式碼

同AMS、WMS等系統服務類似,ContentService也是一個Binder服務實體,而且受ServiceManager管理,需要註冊ServiceManager中,方便APP將來獲取該服務的代理。ContentService是一個Binder服務實體,具體實現如下:

    <!--關鍵點1-->
 public final class ContentService extends IContentService.Stub {
    private static final String TAG = "ContentService";
    private Context mContext;
    private boolean mFactoryTest;
    private final ObserverNode mRootNode = new ObserverNode("");
    private SyncManager mSyncManager = null;
    private final Object mSyncManagerLock = new Object();
	 。。。
複製程式碼

IContentService.Stub由IContentService.aidl檔案生成,IContentService.aidl檔案中定義了ContentService能提供的基本服務,比如註冊/登出觀察者、通知觀察者等,如下:

interface IContentService {
	<!--登出一個觀察者-->
	 void unregisterContentObserver(IContentObserver observer);
	 <!--註冊一個觀察者-->
    void registerContentObserver(in Uri uri, boolean notifyForDescendants,
            IContentObserver observer, int userHandle);
    <!--通知觀察者-->
    void notifyChange(in Uri uri, IContentObserver observer,
            boolean observerWantsSelfNotifications, boolean syncToNetwork,
            int userHandle);
    ...
}
複製程式碼

雖然從使用上來說,ContentService跟ContentProvider關係緊密,但是理論上講,這是完全獨立的兩套東西,ContentService是一個獨立的訊息分發模型,可以完全獨立於ContentProvider使用(總覺的這種設計是不是有些問題),看一下基本用法:

1、註冊一個觀察者:

public static void registerObserver(Context context,ContentObserver contentObserver) {
    ContentResolver contentResolver = context.getContentResolver();
    contentResolver.registerContentObserver(Uri.parse("content://"+"test"), true, contentObserver);
}
複製程式碼

2、通知觀察者

 public static void notity(Context context) {
    ContentResolver contentResolver = context.getContentResolver();
    contentResolver.notifyChange(Uri.parse("content://"+"test"),null);
}
複製程式碼

可以看到,期間只是借用了ContentResolver,但是並沒有牽扯到任何ContentProvider,也就是說,ContentService其實主要是為了提供了一個系統級的訊息中心,下面簡單看一下注冊跟通知流程

註冊觀察者流程

App一般都是藉助ContentResolver來註冊Content觀察者,ContextResoler其實是Context的一個成員變數,本身是一個ApplicationContentResolver物件,它是ContentResolver的子類,

    private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
			 ...
   			 mContentResolver = new ApplicationContentResolver(this, mainThread, user);
   			 ...
複製程式碼

通過ContentResolver註冊ContentObserver程式碼如下:

    public final void registerContentObserver(Uri uri, boolean notifyForDescendents,
            ContentObserver observer, int userHandle) {
        try {

	<!--獲取ContentService,並註冊-->
            getContentService().registerContentObserver(uri, notifyForDescendents,
                    observer.getContentObserver(), userHandle);
        } catch (RemoteException e) {
        }
    }
複製程式碼

可以看到,註冊的過程首先是獲取ContentService服務代理,然後通過這個代理像ContentService註冊觀察者,典型的Binder服務通訊模型,獲取服務的實現如下,

/** @hide */
public static final String CONTENT_SERVICE_NAME = "content";
/** @hide */
public static IContentService getContentService() {
    if (sContentService != null) {
        return sContentService;
    }
    IBinder b = ServiceManager.getService(CONTENT_SERVICE_NAME);
    sContentService = IContentService.Stub.asInterface(b);
    return sContentService;
}
複製程式碼

其實就是通過系統服務的名稱,向ServiceManager查詢並獲取服務代理,請求成功後,便可以通過代理髮送請求,這裡請求的任務是註冊,這裡有一點要注意,那就是在註冊的時候,要同時打通ContentService向APP傳送訊息的鏈路,這個鏈路其實就是另一個Binder通訊路線,具體做法就是將ContentObserver封裝成一個Binder服務實體註冊到ContentService中,註冊成功後,ContentService就會握有ContentObserver的代理,將來需要通知APP端的時候,就可以通過該代理髮送通知,雙C/S模型在Android框架中非常常見。具體程式碼是,通過ContentObserver獲取一個IContentObserver物件,APP端將該物件通過binder傳遞到ContentService服務,如此ContentService便能通過Binder向APP端傳送通知

 public IContentObserver getContentObserver() {
    synchronized (mLock) {
        if (mTransport == null) {
            mTransport = new Transport(this);
        }
        return mTransport;
    }
}
複製程式碼

mTransport本質是一個Binder服務實體,同時握有ContentObserver的強引用,將來通知到達的時候,便能通過ContentObserver分發通知

 private static final class Transport extends IContentObserver.Stub {
    private ContentObserver mContentObserver;

    public Transport(ContentObserver contentObserver) {
        mContentObserver = contentObserver;
    }

    @Override
    public void onChange(boolean selfChange, Uri uri, int userId) {
        ContentObserver contentObserver = mContentObserver;
        if (contentObserver != null) {

		<!--通過 contentObserver傳送回撥通知-->
            contentObserver.dispatchChange(selfChange, uri, userId);
        }
    }

    public void releaseContentObserver() {
        mContentObserver = null;
    }
}
複製程式碼

Transport本身是一個Binder實體物件,被註冊到ContentService中,ContentService會維護一個Transport代理的集合,通過代理,可以通知不同的程式,繼續看register流程,registerContentObserver通過binder通訊最終會呼叫都ContentService的registerContentObserver函式:

@Override
public void registerContentObserver(Uri uri, boolean notifyForDescendants,
        IContentObserver observer, int userHandle) {
    <!--許可權檢查-->
    if (callingUserHandle != userHandle &&
            mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    != PackageManager.PERMISSION_GRANTED) {
        enforceCrossUserPermission(userHandle,
                "no permission to observe other users` provider view");
    }
    ...
    <!--2 新增到監聽佇列-->
    synchronized (mRootNode) {
        mRootNode.addObserverLocked(uri, observer, notifyForDescendants, mRootNode,
                uid, pid, userHandle);
    }
}
複製程式碼

這裡主要看下點2:監聽物件的新增,ContentService物件內部維護了一個樹,用於管理監聽物件,主要是根據Uri的路徑進行分組,既方便管理,同時又提高查詢及插入效率,每個Uri路徑物件對應一個節點,也就是一個ObserverNode物件,每個節點中維護一個監聽List,而ContentService持有RootNode根物件,

 private final ObserverNode mRootNode = new ObserverNode("");
複製程式碼
Content樹.png

每個ObserverNode維護了一個ObserverEntry佇列,ObserverEntry與ContentObserver一一對應,一個Uri對應一個ObserverNode,一個ObserverNode下可以有多個ContentObserver,也就是會多個ObserverEntry,每個ObserverEntry還有一些其他輔助資訊,比如要跟Uri形成鍵值對,ObserverEntry還將自己設定成了Binder訃告的接受者,一旦APP端程式結束,可以通過Binder訃告機制讓ContentService端收到通知,並做一些清理工作,具體實現如下:

 public static final class ObserverNode {
    private class ObserverEntry implements IBinder.DeathRecipient {
        public final IContentObserver observer;
        public final int uid;
        public final int pid;
        public final boolean notifyForDescendants;
        private final int userHandle;
        private final Object observersLock;

        public ObserverEntry(IContentObserver o, boolean n, Object observersLock,
                int _uid, int _pid, int _userHandle) {
            this.observersLock = observersLock;
            observer = o;
            uid = _uid;
            pid = _pid;
            userHandle = _userHandle;
            notifyForDescendants = n;
            try {
                observer.asBinder().linkToDeath(this, 0);
            } catch (RemoteException e) {
                binderDied();
            }
        }
      <!--做一些清理工作,刪除observer-->
        public void binderDied() {
            synchronized (observersLock) {
                removeObserverLocked(observer);
            }
        }
		 。。。
    }

    public static final int INSERT_TYPE = 0;
    public static final int UPDATE_TYPE = 1;
    public static final int DELETE_TYPE = 2;

    private String mName;
    private ArrayList<ObserverNode> mChildren = new ArrayList<ObserverNode>();
    <!--維護自己node的回撥佇列-->
    private ArrayList<ObserverEntry> mObservers = new ArrayList<ObserverEntry>();	 	. ..
複製程式碼

繼續看看下Observer的add流程,ObserverNode 的addObserverLocked函式被外部呼叫(被rootnode)的時候,一般傳遞的index是0,自己遞迴呼叫的時候,才不是0,其實新增Observer的過程是一個遞迴的過程,首先通過Uri路徑,遞迴找到對應的ObserverNode,然後像ObserverNode的監聽佇列中新增Observer

    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
        <!--已經找到葉子節點,那麼可以直接在node中插入ObserverEntry->
        if (index == countUriSegments(uri)) {
            mObservers.add(new ObserverEntry(observer, notifyForDescendants, observersLock,
                    uid, pid, userHandle));
            return;
        }

        // Look to see if the proper child already exists
        <!--一層層往下剝離-->
        String segment = getUriSegment(uri, index);
		  ...
        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);
    }
複製程式碼

比如:要查詢content://A/B/C對應的ObserverNode,首先會找到Authority,找到A對應的ObserverNode,之後在A的children中查詢Path=B的Node,然後在B的Children中查詢Path=C的Node,找到該Node之後,往這個node的ObserverEntry列表中新增一個物件,到這裡就註冊就完成了。

通知流程

前文已經說過,ContentService可以看做是通知的中轉站,程式A想要通知其他註冊了某個Uri的程式,必須首先向ContentService分發中心傳送訊息,再由ContentService通知其他程式中的觀察者,簡化模型如下圖:

ContentService框架.png

簡單跟蹤下通知流程,入口函式如下

 public static void notity(Context context) {
    ContentResolver contentResolver = context.getContentResolver();
    contentResolver.notifyChange(Uri.parse("content://"+"test"),null);
}
複製程式碼

ContentResolver的notifyChange會進一步通過Binder,請求ContentService傳送通知,

public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork,
        int userHandle) {
    try {
        getContentService().notifyChange(
                uri, observer == null ? null : observer.getContentObserver(),
                observer != null && observer.deliverSelfNotifications(), syncToNetwork,
                userHandle);
    } catch (RemoteException e) {
    }
}
複製程式碼

ContentService收到請求進一步處理,無非就是搜尋之前的樹,找到對應的節點,將節點上註冊回撥List通知一遍,具體邏輯如下:

@Override
    public void notifyChange(Uri uri, IContentObserver observer,
            boolean observerWantsSelfNotifications, boolean syncToNetwork,
            int userHandle) {

        <!--許可權檢測-->
        // 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) {
            <!--1 從根節點開始查詢binder回撥代理-->
                mRootNode.collectObserversLocked(uri, 0, observer, observerWantsSelfNotifications,
                        userHandle, calls);
            }
            final int numCalls = calls.size();
            for (int i=0; i<numCalls; i++) {
                ObserverCall oc = calls.get(i);
                try {
                <!--2 通知-->
                    oc.mObserver.onChange(oc.mSelfChange, uri, userHandle);
                } 
               ...
複製程式碼

從上面程式碼可以看出,其實就是兩步,先蒐集所有的Binder回撥,之後通過回撥通知APP端,蒐集過程也是個遞迴的過程,也會存在父子粘連的一些回撥邏輯(子Uri是否有必要通知路徑中的父Uri回撥),理解很簡單,不再詳述。這步之後,訊息就通過Binder被傳送給App端,在APP端,Binder實體的onTransact被回撥,並處理相應的事務:

 private static final class Transport extends IContentObserver.Stub {
    private ContentObserver mContentObserver;

    public Transport(ContentObserver contentObserver) {
        mContentObserver = contentObserver;
    }

    @Override
    public void onChange(boolean selfChange, Uri uri, int userId) {
        ContentObserver contentObserver = mContentObserver;
        if (contentObserver != null) {

		<!--通過 contentObserver傳送回撥通知-->
            contentObserver.dispatchChange(selfChange, uri, userId);
        }
    }

    public void releaseContentObserver() {
        mContentObserver = null;
    }
}
複製程式碼

這裡有一點需要注意,那就是IContentObserver中onChange是一個oneway請求,可以說,總是非同步的,ContentService將訊息塞入到APP端Binder執行緒的執行佇列後就返回,不會等待處理結果才返回。

interface IContentObserver
{
    /**
     * This method is called when an update occurs to the cursor that is being
     * observed. selfUpdate is true if the update was caused by a call to
     * commit on the cursor that is being observed.
     */
     contentService 用的是oneway
    oneway void onChange(boolean selfUpdate, in Uri uri, int userId);
}
複製程式碼

之後其實就是呼叫ContentObserver的dispatchChange,dispatchChange可能是在Binder執行緒中同步執行,也可能是傳送到一個與Handler繫結的執行緒中執行,如下,

private void dispatchChange(boolean selfChange, Uri uri, int userId) {
    if (mHandler == null) {
        onChange(selfChange, uri, userId);
    } else {
        mHandler.post(new NotificationRunnable(selfChange, uri, userId));
    }
}
複製程式碼

但是整體上來看,由於Binder oneway的存在,ContentService的通知是個非同步的過程。

一個奇葩問題的注意事項 Binder迴圈呼叫

假設有這樣一個場景:

  • A程式notify,
  • A程式再收到通知
  • A程式請求獲取ContentProvider的資料,並且ContentProvider位於A程式

這個時候,如果,採用的是同步,也就是ContentObserver沒有設定Handler,那就會遇到一個問題,系統會提示你沒有許可權訪問ContentProvider,

java.lang.SecurityException: Permission Denial: reading XXX uri content://MyContentProvider from pid=0, uid=1000 requires the provider be exported, or grantUriPermission()

為什麼,明明是當前App中宣告的ContentProvider,為什麼不能訪問,並且pid=0, uid=1000 是怎麼來的,其實這個時候是因為Binder機制中的一個小”BUG”,需要使用者自己避免,ContentProvider在使用的時候會校驗許可權,

 /** {@hide} */
protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)
        throws SecurityException {
    final Context context = getContext();
    // Binder.getCallingPid獲取的可能不是我們想要的程式PID
    final int pid = Binder.getCallingPid();
    final int uid = Binder.getCallingUid();
    String missingPerm = null;
    int strongestMode = MODE_ALLOWED;
    ...

    final String failReason = mExported
            ? " requires " + missingPerm + ", or grantUriPermission()"
            : " requires the provider be exported, or grantUriPermission()";
    throw new SecurityException("Permission Denial: reading "
            + ContentProvider.this.getClass().getName() + " uri " + uri + " from pid=" + pid
            + ", uid=" + uid + failReason);
}
複製程式碼

Binder.getCallingPid()獲取的可能並不是我們想要的程式PID,因為之前同步訪問的時候 Binder.getCallingPid()被賦值為系統程式PID,在同步訪問的時候,由於ContentProvider本身在A程式中,會直接呼叫ContentProvider的相應服務函式,但是Binder.getCallingPid()返回值並沒有被更新,因為這個時候訪問的時候不會走跨程式, Binder.getCallingPid()的返回值不會被 更新,也就是說 Binder.getCallingPid()獲取的程式是上一個notify時候的系統程式,那麼自然也就沒有許可權。如果將ContentProvider放到A程式之外的程式,就不會有問題,當然,Android提供瞭解決方案,那就是

<!--將Binder.getCallingPid()的值設定為當前程式-->

final long identity = Binder.clearCallingIdentity();
...
<!--恢復之前儲存的值-->
Binder.restoreCallingIdentity(identity);
複製程式碼

以上兩個函式配合使用,就可以避免之前的問題。這個問題Google不能從Binder上在底層解決嗎?總覺是Binder通訊的BUG。

總結

  • ContentService是一個系統級別的訊息中心,提供系統級別的觀察者模型
  • ContentService的通訊模型 其實是典型的Android 雙C/S模型
  • ContentService內部是通過樹+list的方式管理ContentObserver回撥
  • ContentService在分發訊息的時候,整體上是非同步的,在APP端可以在Binder執行緒中同步處理,也可以傳送到Handler繫結的執行緒中非同步處理,具體看APP端配置

作者:看書的小蝸牛
Android內容服務ContentService原理淺析

僅供參考,歡迎指正

相關文章