在開發(fā)中晋南,假如头镊,A蚣驼、B進程有部分信息需要同步,這個時候怎么處理呢相艇?設(shè)想這么一個場景颖杏,有個業(yè)務(wù)復(fù)雜的Activity非常占用內(nèi)存,并引發(fā)OOM坛芽,所以留储,想要把這個Activity放到單獨進程,以保證OOM時主進程不崩潰咙轩。但是获讳,兩個整個APP有些信息需要保持同步,比如登陸信息等活喊,無論哪個進程登陸或者修改了相應(yīng)信息丐膝,都要同步到另一個進程中去,這個時候怎么做呢钾菊?
- 第一種:一個進程里面的時候帅矗,經(jīng)常采用SharePreference來做,但是SharePreference不支持多進程煞烫,它基于單個文件的浑此,默認(rèn)是沒有考慮同步互斥,而且滞详,APP對SP對象做了緩存凛俱,不好互斥同步,雖然可以通過FileLock來實現(xiàn)互斥料饥,但同步仍然是一個問題最冰。
- 第二種:基于Binder通信實現(xiàn)Service完成跨進程數(shù)據(jù)的共享,能夠保證單進程訪問數(shù)據(jù)稀火,不會有互斥問題,可是同步的事情仍然需要開發(fā)者手動處理赌朋。
- 第三種:基于Android提供的ContentProvider來實現(xiàn)凰狞,ContentProvider同樣基于Binder,不存在進程間互斥問題沛慢,對于同步赡若,也做了很好的封裝,不需要開發(fā)者額外實現(xiàn)团甲。
因此逾冬,在Android開發(fā)中,如果需要多進程同步互斥,ContentProvider是一個很好的選擇身腻,本文就來看看产还,它的這個技術(shù)究竟是怎么實現(xiàn)的。
概述
Content providers are one of the primary building blocks of Android applications, providing content to applications. They encapsulate data and provide it to applications through the single ContentResolver interface. A content provider is only required if you need to share data between multiple applications. For example, the contacts data is used by multiple applications and must be stored in a content provider. If you don't need to share data amongst multiple applications you can use a database directly via SQLiteDatabase.
ContentProvider為Android數(shù)據(jù)的存儲和獲取抽象了統(tǒng)一的接口嘀趟,并支持在不同的應(yīng)用程序之間共享數(shù)據(jù)脐区,Android內(nèi)置的許多數(shù)據(jù)都是使用ContentProvider形式供開發(fā)者調(diào)用的 (如視頻,音頻她按,圖片牛隅,通訊錄等),它采用索引表格的形式來組織數(shù)據(jù)酌泰,無論數(shù)據(jù)來源是什么媒佣,ContentProvider都會認(rèn)為是一種表,這一點從ContentProvider提供的抽象接口就能看出陵刹。
class XXX ContentProvider extends ContentProvider{
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
可以看到每個ContentProvider都需要自己實現(xiàn)增默伍、刪、改授霸、查的功能巡验,因此,可以將ContentProvider看做Android提供一個抽象接口層碘耳,用于訪問表格類的存儲媒介显设,表格只是一個抽象,至于底層存儲媒介到底如何組織辛辨,完全看用戶實現(xiàn)捕捂,也就是說ContentProvider自身是沒有數(shù)據(jù)更新及操作能力,它只是將這種操作進行了統(tǒng)一抽象斗搞。
了解了ContentProvider的概念及作用后指攒,下面就從用法來看看ContentProvider是如何支持多進程同步通信的。
ContentProvider代理的同步獲取
多進程對于ContentProvider的訪問請求最終都會進入ContentProvider進程僻焚,而在單進程中允悦,ContentProvider對于數(shù)據(jù)的訪問很容易做到多線程互斥,一個Sycronized關(guān)鍵字就能搞定虑啤,看一下基本用法:
ContentResolver contentResolver = AppProfile.getAppContext().getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(key, value);
contentResolver.insert(FileContentProvider.CONTENT_URI, contentValues);
contentResolver.notifyChange(FileContentProvider.CONTENT_URI, null);
getContentResolver 其實獲取的是一個ApplicationContentResolver實例隙弛,定義在ContextImpl中,只有在真正操作數(shù)據(jù)的時候才會去獲取Provider狞山, 詳細看一下插入操作:
public final @Nullable Uri insert(@NonNull Uri url, @Nullable ContentValues values) {
<!--首先獲取Provider代理-->
IContentProvider provider = acquireProvider(url);
try {
<!--利用IContentProvider代理插入數(shù)據(jù)-->
Uri createdRow = provider.insert(mPackageName, url, values);
return createdRow;
}
}
@Override
protected IContentProvider acquireUnstableProvider(Context c, String auth) {
return mMainThread.acquireProvider(c,
ContentProvider.getAuthorityWithoutUserId(auth),
resolveUserIdFromAuthority(auth), false);
}
這里是一個典型的基于Binder通信的AIDL實現(xiàn)全闷,IContentProvider的Proxy與Stub分別是ContentProviderProxy與ContentProvider的內(nèi)部類
abstract public class ContentProviderNative extends Binder implements IContentProvider
class Transport extends ContentProviderNative,
首先看一下ActivityThread的acquireProvider萍启,對于當(dāng)前進程而言acquireProvider是一個同步的過程总珠,如果ContentProvider所處的進程已經(jīng)啟動屏鳍,那么acquireProvider可以直接獲取服務(wù)代理,如果未啟動局服,則等待ContentProvider進程啟動钓瞭,再獲取代理。
public final IContentProvider acquireProvider(
Context c, String auth, int userId, boolean stable) {
final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
if (provider != null) {
return provider;
}
IActivityManager.ContentProviderHolder holder = null;
try {
<!--關(guān)鍵點1 獲取Provider腌逢,如果沒有安裝降淮,則等待安裝完畢-->
holder = ActivityManagerNative.getDefault().getContentProvider(
getApplicationThread(), auth, userId, stable);
} catch (RemoteException ex) {
}
if (holder == null) {
return null;
}
<!--關(guān)鍵點2 這里僅僅是增加計數(shù) ,Provider到這里其實已經(jīng)安裝完畢-->
// Install provider will increment the reference count for us, and break
// any ties in the race.
holder = installProvider(c, holder, holder.info,
true /*noisy*/, holder.noReleaseNeeded, stable);
return holder.provider;
}
首先看一下關(guān)鍵點1搏讶,這里阻塞等待直到獲取Provider代理佳鳖,如果Provider未啟動,則先啟動媒惕,直接看一下ActivityManagerService(其實Android四大組件都歸他管理)系吩,簡單看一下獲取流程(只描述個大概):
private final ContentProviderHolder getContentProviderImpl(IApplicationThread caller,
String name, IBinder token, boolean stable, int userId) {
ContentProviderRecord cpr;
ContentProviderConnection conn = null;
ProviderInfo cpi = null;
synchronized(this) {
...<!--關(guān)鍵點1 查看是否已有記錄-->
// First check if this content provider has been published...
cpr = mProviderMap.getProviderByName(name, userId);
...
boolean providerRunning = cpr != null;
<!--如果有-->
if (providerRunning) {
cpi = cpr.info;
String msg;
<!--關(guān)鍵點2 是否允許調(diào)用進程自己實現(xiàn)ContentProvider-->
if (r != null && cpr.canRunHere(r)) {
// This provider has been published or is in the process
// of being published... but it is also allowed to run
// in the caller's process, so don't make a connection
// and just let the caller instantiate its own instance.
ContentProviderHolder holder = cpr.newHolder(null);
// don't give caller the provider object, it needs
// to make its own.
holder.provider = null;
return holder;
}
final long origId = Binder.clearCallingIdentity();
<!--關(guān)鍵點3 使用ContentProvider進程中的ContentProvider,僅僅增加引用計數(shù)--> // In this case the provider instance already exists, so we can
// return it right away.
conn = incProviderCountLocked(r, cpr, token, stable);
...
}
boolean singleton;
<!--如果provider未啟動-->
if (!providerRunning) {
try {
checkTime(startTime, "getContentProviderImpl: before resolveContentProvider");
cpi = AppGlobals.getPackageManager().
resolveContentProvider(name,
STOCK_PM_FLAGS | PackageManager.GET_URI_PERMISSION_PATTERNS, userId);
} catch (RemoteException ex) {}
...
ComponentName comp = new ComponentName(cpi.packageName, cpi.name);
cpr = mProviderMap.getProviderByClass(comp, userId);
...
<!--查看目標(biāo)進程是否啟動-->
ProcessRecord proc = getProcessRecordLocked(
cpi.processName, cpr.appInfo.uid, false);
if (proc != null && proc.thread != null) {
if (!proc.pubProviders.containsKey(cpi.name)) {
proc.pubProviders.put(cpi.name, cpr);
try {
proc.thread.scheduleInstallProvider(cpi);
} catch (RemoteException e) {
}
}
} else {
<!--如果未啟動妒蔚,啟動進程穿挨,并安裝-->
proc = startProcessLocked(cpi.processName,
cpr.appInfo, false, 0, "content provider",
new ComponentName(cpi.applicationInfo.packageName,
cpi.name), false, false, false);
checkTime(startTime, "getContentProviderImpl: after start process");
if (proc == null) {
return null;
}
}
cpr.launchingApp = proc;
mLaunchingProviders.add(cpr);
} finally {
...
// 線程阻塞等待,直到provider啟動 published肴盏,Wait for the provider to be published...
synchronized (cpr) {
while (cpr.provider == null) {
try {
if (conn != null) {
conn.waiting = true;
}
cpr.wait();
} catch (InterruptedException ex) {
} finally {
if (conn != null) {
conn.waiting = false;
}
}
}
}
return cpr != null ? cpr.newHolder(conn) : null;
}
ContentProvider的啟動同Activity或者Service都是比較類似的科盛,如果進程未啟動,就去啟動進程菜皂,在創(chuàng)建進程之后贞绵,調(diào)用ActivityThread的attach方法,通知AMS新的進程創(chuàng)建完畢,并初始化ProcessRecord恍飘,隨后榨崩,查詢所有和本進程相關(guān)的ContentProvider信息,并調(diào)用bindApplication方法章母,通知新進程安裝并啟動這些ContentProvider母蛛。ContentProvider有些不一樣的就是: ContentProvider調(diào)用端會一直阻塞,直到ContentProvider published才會繼續(xù)執(zhí)行乳怎,這一點從下面可以看出:
synchronized (cpr) {
while (cpr.provider == null) {
其次彩郊,這里有個疑惑的地方,ContentProvider一般都是隨著進程啟動的蚪缀,不過為什么會存在進程啟動秫逝,但是ContentProvider未published的問題呢?不太理解椿胯,難道是中間可能存在什么同步問題嗎?下面這部分代碼完全看不出為什么存在:
if (proc != null && proc.thread != null) {
<!--如果進程啟動剃根,發(fā)消息安裝Providers-->
if (!proc.pubProviders.containsKey(cpi.name)) {
proc.pubProviders.put(cpi.name, cpr);
try {
proc.thread.scheduleInstallProvider(cpi);
} catch (RemoteException e) {
}
}
}
這里猜測是不是有多個Client請求的過程哩盲,可能中間有個間隙,進程已經(jīng)啟動,但是Provider還未安裝完成廉油,只完成了一部分惠险。
ContentProvider數(shù)據(jù)的更新
通過ContentProvider對于數(shù)據(jù)的操作都是同步的,不過contentResolver.notifyChange通知是異步的
contentResolver.insert(FileContentProvider.CONTENT_URI, contentValues);
contentResolver.notifyChange(FileContentProvider.CONTENT_URI, null);
ContentProviderProxy會發(fā)消息給服務(wù)端抒线,而服務(wù)端這里直接調(diào)用抽象的insert函數(shù)班巩,如果需要insert操作是同步的,那么再實現(xiàn)ContentProvider的時候嘶炭,就可以直接向數(shù)據(jù)庫寫數(shù)據(jù)抱慌,當(dāng)然也可以實現(xiàn)Handler,自己做異步處理眨猎。
abstract public class ContentProviderNative extends Binder implements IContentProvider {
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
...
case INSERT_TRANSACTION:
{
data.enforceInterface(IContentProvider.descriptor);
String callingPkg = data.readString();
Uri url = Uri.CREATOR.createFromParcel(data);
ContentValues values = ContentValues.CREATOR.createFromParcel(data);
Uri out = insert(callingPkg, url, values);
reply.writeNoException();
Uri.writeToParcel(reply, out);
return true;
}
這里有一點要注意抑进,Binder框架默認(rèn)是不支持Stub端同步的,也就是說睡陪,即時基于ContentProvider寺渗,如果需要對一個文件進行完全互斥訪問,在單個進程內(nèi)同樣需要處理互斥操作兰迫,不過單進程互斥好處理信殊,Sycronized關(guān)鍵字就可以了。
ContentProvider數(shù)據(jù)變更通知
ContentProvider支持多進程訪問汁果,當(dāng)一個進程操作ContentProvider變更數(shù)據(jù)之后涡拘,可能希望其他進程能收到通知,比如進程A往數(shù)據(jù)庫插入了一條聊天信息须鼎,希望在進程B的UI中展現(xiàn)出來鲸伴,這個時候就需要一個通知機制,Android也是提供了支持晋控,不過它是一個通用的數(shù)據(jù)變更同步通知:基于ContentService服務(wù):
<!--1 注冊-->
public static void registerObserver(ContentObserver contentObserver) {
ContentResolver contentResolver = AppProfile.getAppContext().getContentResolver();
contentResolver.registerContentObserver(FileContentProvider.CONTENT_URI, true, contentObserver);
}
<!--2 通知-->
contentResolver.notifyChange(FileContentProvider.CONTENT_URI, null);
上面的兩個可能在統(tǒng)一進程汞窗,也可能在不同進程,
public final void registerContentObserver(Uri uri, boolean notifyForDescendents,
ContentObserver observer, int userHandle) {
try {
getContentService().registerContentObserver(uri, notifyForDescendents,
observer.getContentObserver(), userHandle);
} catch (RemoteException e) {
}
}
其實這里跟ContentProvider的關(guān)系已經(jīng)不是很大赡译,這里牽扯到另一個服務(wù):ContentService仲吏,它是Android平臺中數(shù)據(jù)更新通知的執(zhí)行者,由SystemServer進程啟動蝌焚,所有APP都能調(diào)用它發(fā)送數(shù)據(jù)變動通知裹唆,其實就是一個觀察者模式,牽扯到另一個服務(wù)只洒,不過多講解许帐。
android:multiprocess在ContentProvider中的作用
默認(rèn)情況下是不指定android:process跟multiprocess的,它們的值默認(rèn)為false毕谴,會隨著應(yīng)用啟動的時候加載成畦,如果對provider指定android:process和android:multiprocess距芬,表現(xiàn)就會不一了,如果設(shè)置android:process循帐,那ContentProvider就不會隨著 應(yīng)用 啟動框仔,如果設(shè)置了android:multiprocess,則可能存在多個ContentProvider實例拄养。
If the app runs in multiple processes, this attribute determines whether multiple instances of the content provder are created. If true, each of the app's processes has its own content provider object. If false, the app's processes share only one content provider object. The default value is false.
Setting this flag to true may improve performance by reducing the overhead of interprocess communication, but it also increases the memory footprint of each process.
android:multiprocess的作用是:是否允許在調(diào)用者的進程里實例化provider离斩,如果android:multiprocess=false,則系統(tǒng)中只會存在一個provider實例瘪匿,否則跛梗,可以存在多個,多個的話柿顶,可能會提高性能茄袖,因為它避免了跨進程通信,畢竟嘁锯,對象就在自己的進程空間宪祥,可以直接訪問,但是家乘,這會增加系統(tǒng)負(fù)擔(dān)蝗羊,另外,對于單進程能夠保證的互斥問題仁锯,也會無效耀找,如果APP需要數(shù)據(jù)更新,還是保持不開啟的好业崖。
總結(jié)
- ContentProvider只是Android為了跨進程共享數(shù)據(jù)提供的一種機制野芒,
- 本身基于Binder實現(xiàn),
- 在操作數(shù)據(jù)上只是一種抽象双炕,具體要自己實現(xiàn)
- ContentProvider只能保證進程間的互斥狞悲,無法保證進程內(nèi),需要自己實現(xiàn)
作者:看書的小蝸牛
Android ContentProvider支持跨進程數(shù)據(jù)共享與"互斥妇斤、同步"
僅供參考摇锋,歡迎指正