Android ContentProvider支持跨進程數(shù)據(jù)共享與"互斥有滑、同步"雜談

在開發(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抽象接口.jpg

了解了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ù)共享與"互斥妇斤、同步"

僅供參考摇锋,歡迎指正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市站超,隨后出現(xiàn)的幾起案子荸恕,更是在濱河造成了極大的恐慌,老刑警劉巖死相,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件融求,死亡現(xiàn)場離奇詭異,居然都是意外死亡算撮,警方通過查閱死者的電腦和手機生宛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門施掏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人茅糜,你說我怎么就攤上這事∷赝欤” “怎么了蔑赘?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長预明。 經(jīng)常有香客問我缩赛,道長,這世上最難降的妖魔是什么撰糠? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任酥馍,我火速辦了婚禮,結(jié)果婚禮上阅酪,老公的妹妹穿的比我還像新娘旨袒。我一直安慰自己,他們只是感情好术辐,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布砚尽。 她就那樣靜靜地躺著,像睡著了一般辉词。 火紅的嫁衣襯著肌膚如雪必孤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天瑞躺,我揣著相機與錄音敷搪,去河邊找鬼。 笑死幢哨,一個胖子當(dāng)著我的面吹牛赡勘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嘱么,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼狮含,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了曼振?” 一聲冷哼從身側(cè)響起几迄,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎冰评,沒想到半個月后映胁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡甲雅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年解孙,在試婚紗的時候發(fā)現(xiàn)自己被綠了坑填。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡弛姜,死狀恐怖脐瑰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情廷臼,我是刑警寧澤苍在,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站荠商,受9級特大地震影響寂恬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜莱没,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一初肉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧饰躲,春花似錦牙咏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至焦蘑,卻和暖如春盯拱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背例嘱。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工狡逢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拼卵。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓奢浑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親腋腮。 傳聞我的和親對象是個殘疾皇子雀彼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容