ContentService可以看做Android中一個系統級別的訊息中心,可以說搭建了一個系統級的觀察者模型,APP可以向訊息中心註冊觀察者,選擇訂閱自己關心的訊息,也可以通過訊息中心傳送資訊,通知其他程式,簡單模型如下:
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("");
複製程式碼
每個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通知其他程式中的觀察者,簡化模型如下圖:
簡單跟蹤下通知流程,入口函式如下
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原理淺析
僅供參考,歡迎指正