Android ContentProvider支援跨程式資料共享與"互斥、同步" 雜談

看書的小蝸牛發表於2017-11-15

在開發中,假如,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抽象介面.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) {
                                    }
                                }
                            } 複製程式碼

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支援跨程式資料共享與"互斥、同步"

僅供參考,歡迎指正

相關文章