Android ContentProvider支持跨進程數據共享與"互斥、同步"雜談

在開發中,假如,A、B進程有部分信息需要同步,這個時候怎么處理呢?設想這么一個場景,有個業務復雜的Activity非常占用內存,并引發OOM,所以,想要把這個Activity放到單獨進程,以保證OOM時主進程不崩潰。但是,兩個整個APP有些信息需要保持同步,比如登陸信息等,無論哪個進程登陸或者修改了相應信息,都要同步到另一個進程中去,這個時候怎么做呢?

  • 第一種:一個進程里面的時候,經常采用SharePreference來做,但是SharePreference不支持多進程,它基于單個文件的,默認是沒有考慮同步互斥,而且,APP對SP對象做了緩存,不好互斥同步,雖然可以通過FileLock來實現互斥,但同步仍然是一個問題。
  • 第二種:基于Binder通信實現Service完成跨進程數據的共享,能夠保證單進程訪問數據,不會有互斥問題,可是同步的事情仍然需要開發者手動處理。
  • 第三種:基于Android提供的ContentProvider來實現,ContentProvider同樣基于Binder,不存在進程間互斥問題,對于同步,也做了很好的封裝,不需要開發者額外實現。

因此,在Android開發中,如果需要多進程同步互斥,ContentProvider是一個很好的選擇,本文就來看看,它的這個技術究竟是怎么實現的。

概述

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數據的存儲和獲取抽象了統一的接口,并支持在不同的應用程序之間共享數據,Android內置的許多數據都是使用ContentProvider形式供開發者調用的 (如視頻,音頻,圖片,通訊錄等),它采用索引表格的形式來組織數據,無論數據來源是什么,ContentProvider都會認為是一種表,這一點從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都需要自己實現增、刪、改、查的功能,因此,可以將ContentProvider看做Android提供一個抽象接口層,用于訪問表格類的存儲媒介,表格只是一個抽象,至于底層存儲媒介到底如何組織,完全看用戶實現,也就是說ContentProvider自身是沒有數據更新及操作能力,它只是將這種操作進行了統一抽象。

ContentProvider抽象接口.jpg

了解了ContentProvider的概念及作用后,下面就從用法來看看ContentProvider是如何支持多進程同步通信的。

ContentProvider代理的同步獲取

多進程對于ContentProvider的訪問請求最終都會進入ContentProvider進程,而在單進程中,ContentProvider對于數據的訪問很容易做到多線程互斥,一個Sycronized關鍵字就能搞定,看一下基本用法:

    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中,只有在真正操作數據的時候才會去獲取Provider, 詳細看一下插入操作:

    public final @Nullable Uri insert(@NonNull Uri url, @Nullable ContentValues values) {
    <!--首先獲取Provider代理-->
        IContentProvider provider = acquireProvider(url);
        try {
    <!--利用IContentProvider代理插入數據-->
            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實現,IContentProvider的Proxy與Stub分別是ContentProviderProxy與ContentProvider的內部類

abstract public class ContentProviderNative extends Binder implements IContentProvider 

class Transport extends ContentProviderNative,

首先看一下ActivityThread的acquireProvider,對于當前進程而言acquireProvider是一個同步的過程,如果ContentProvider所處的進程已經啟動,那么acquireProvider可以直接獲取服務代理,如果未啟動,則等待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 {
        <!--關鍵點1 獲取Provider,如果沒有安裝,則等待安裝完畢-->
            holder = ActivityManagerNative.getDefault().getContentProvider(
                    getApplicationThread(), auth, userId, stable);
        } catch (RemoteException ex) {
        }
        if (holder == null) {
            return null;
        }

        <!--關鍵點2 這里僅僅是增加計數 ,Provider到這里其實已經安裝完畢-->
        // 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;
    }

首先看一下關鍵點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) {
            ...<!--關鍵點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;
                  <!--關鍵點2 是否允許調用進程自己實現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();

               <!--關鍵點3 使用ContentProvider進程中的ContentProvider,僅僅增加引用計數-->                        // 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);
                ...
                <!--查看目標進程是否啟動-->
                        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都是比較類似的,如果進程未啟動,就去啟動進程,在創建進程之后,調用ActivityThread的attach方法,通知AMS新的進程創建完畢,并初始化ProcessRecord,隨后,查詢所有和本進程相關的ContentProvider信息,并調用bindApplication方法,通知新進程安裝并啟動這些ContentProvider。ContentProvider有些不一樣的就是: ContentProvider調用端會一直阻塞,直到ContentProvider published才會繼續執行,這一點從下面可以看出:

  synchronized (cpr) {
                while (cpr.provider == null) {      

其次,這里有個疑惑的地方,ContentProvider一般都是隨著進程啟動的,不過為什么會存在進程啟動,但是ContentProvider未published的問題呢?不太理解,難道是中間可能存在什么同步問題嗎?下面這部分代碼完全看不出為什么存在:

   if (proc != null && proc.thread != null) {
                             <!--如果進程啟動,發消息安裝Providers-->
                                if (!proc.pubProviders.containsKey(cpi.name)) {
                                    proc.pubProviders.put(cpi.name, cpr);
                                    try {
                                        proc.thread.scheduleInstallProvider(cpi);
                                    } catch (RemoteException e) {
                                    }
                                }
                            } 

這里猜測是不是有多個Client請求的過程,可能中間有個間隙,進程已經啟動,但是Provider還未安裝完成,只完成了一部分。

ContentProvider數據的更新

通過ContentProvider對于數據的操作都是同步的,不過contentResolver.notifyChange通知是異步的

 contentResolver.insert(FileContentProvider.CONTENT_URI, contentValues);
 contentResolver.notifyChange(FileContentProvider.CONTENT_URI, null);

ContentProviderProxy會發消息給服務端,而服務端這里直接調用抽象的insert函數,如果需要insert操作是同步的,那么再實現ContentProvider的時候,就可以直接向數據庫寫數據,當然也可以實現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框架默認是不支持Stub端同步的,也就是說,即時基于ContentProvider,如果需要對一個文件進行完全互斥訪問,在單個進程內同樣需要處理互斥操作,不過單進程互斥好處理,Sycronized關鍵字就可以了。

ContentProvider數據變更通知

ContentProvider支持多進程訪問,當一個進程操作ContentProvider變更數據之后,可能希望其他進程能收到通知,比如進程A往數據庫插入了一條聊天信息,希望在進程B的UI中展現出來,這個時候就需要一個通知機制,Android也是提供了支持,不過它是一個通用的數據變更同步通知:基于ContentService服務:

<!--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);

上面的兩個可能在統一進程,也可能在不同進程,

public final void registerContentObserver(Uri uri, boolean notifyForDescendents,
        ContentObserver observer, int userHandle) {
    try {
        getContentService().registerContentObserver(uri, notifyForDescendents,
                observer.getContentObserver(), userHandle);
    } catch (RemoteException e) {
    }
}

其實這里跟ContentProvider的關系已經不是很大,這里牽扯到另一個服務:ContentService,它是Android平臺中數據更新通知的執行者,由SystemServer進程啟動,所有APP都能調用它發送數據變動通知,其實就是一個觀察者模式,牽扯到另一個服務,不過多講解。

android:multiprocess在ContentProvider中的作用

默認情況下是不指定android:process跟multiprocess的,它們的值默認為false,會隨著應用啟動的時候加載,如果對provider指定android:process和android:multiprocess,表現就會不一了,如果設置android:process,那ContentProvider就不會隨著 應用 啟動,如果設置了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的作用是:是否允許在調用者的進程里實例化provider,如果android:multiprocess=false,則系統中只會存在一個provider實例,否則,可以存在多個,多個的話,可能會提高性能,因為它避免了跨進程通信,畢竟,對象就在自己的進程空間,可以直接訪問,但是,這會增加系統負擔,另外,對于單進程能夠保證的互斥問題,也會無效,如果APP需要數據更新,還是保持不開啟的好。

總結

  • ContentProvider只是Android為了跨進程共享數據提供的一種機制,
  • 本身基于Binder實現,
  • 在操作數據上只是一種抽象,具體要自己實現
  • ContentProvider只能保證進程間的互斥,無法保證進程內,需要自己實現

作者:看書的小蝸牛
Android ContentProvider支持跨進程數據共享與"互斥、同步"

僅供參考,歡迎指正

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,076評論 25 708
  • afinalAfinal是一個android的ioc,orm框架 https://github.com/yangf...
    passiontim閱讀 15,491評論 2 45
  • 偶然在頭條上看到一條關于電影《春夏秋冬又一春》的推薦,難得駐足,便找了電影來看。 電影是我喜歡的。深谷幽寺,青燈古...
    鬧吧閱讀 1,208評論 0 1
  • 今天早上,我很看見了一個人,我走過去了。 突然,我看見的是一個小女孩,她手里有一張照片。我小心翼翼的...
    愛心格格閱讀 323評論 1 1
  • 這是一個被背包吐槽的古怪外星人,抵御外星人侵襲的故事。這個故事的名字叫做《格洛肯勇士》(Glorkian Warr...
    假藥君閱讀 1,647評論 0 6