Android內容服務ContentService原理淺析

ContentService能夠看作Android中一個系統級別的消息中心,能夠說搭建了一個系統級的觀察者模型,APP能夠向消息中心註冊觀察者,選擇訂閱本身關心的消息,也能夠經過消息中心發送信息,通知其餘進程,簡單模型以下:java

「ContentService簡單框架」.png

ContentService服務伴隨系統啓動,自己是一個Binder系統服務,運行在SystemServer進程。做爲系統服務,最好能保持高效運行,所以ContentService通知APP都是異步的,也就是oneway的,僅僅插入目標進程(線程)的Queue隊列,沒必要等待執行。下面簡單分析一下總體的架構,主要從一下幾個方面瞭解下運行流程:node

  • 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使用(總覺的這種設計是否是有些問題),看一下基本用法:ide

一、註冊一個觀察者:函數

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

二、通知觀察者post

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

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

註冊觀察者流程

App通常都是藉助ContentResolver來註冊Content觀察者,ContextResoler實際上是Context的一個成員變量,自己是一個ApplicationContentResolver對象,它是ContentResolver的子類,this

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原理淺析

僅供參考,歡迎指正

相關文章
相關標籤/搜索