Android使用ContentProvider初始化SDK庫方案總結

灰色飄零發表於2021-04-25

做Android SDK開發的時候,一般我們會將初始化的方法封裝為,然後讓呼叫SDK的開發者在Application的onCreate方法中進行初始化。但是目前一些主流的SDK框架,並沒有提供相關的方法進行初始化,但是我們在使用的時候也能正常使用,通過挖掘其原始碼,可以看出來他們一般使用的ContentProvider來進行SDK的初始化的,目前使用ContentProvider的知名SDK有:ButterKnife、Leakcanary、BlockCanary...等等。

這裡補充一個概念,SDK初始化的本質是什麼?

SDK初始化的本質是將App的上下文(Context)注入到SDK中,使其能通過這個上下文訪問到App的資源與服務。也包括在初始化時呼叫SDK方法進行相關選項的自定義配置。

一、ContentProvider初始化SDK庫的實現

要實現在ContentProvider初始化SDK庫,首先要在庫中建立一個 ContentProvider,然後在 ContentProvider 的 onCreate() 方法中藉助 getContext() 返回的 Context 來完成你的庫初始化,當然,這個 Context 的實際型別就是應用的 Application。

下面是通過ContentProvider實現SDK庫初始化的示例程式碼:

class ToolContentProvider : ContentProvider() {

    override fun onCreate(): Boolean {
        Log.e(GlobalConfig.LOG_TAG, "ToolContentProvider onCreate")
        AppContextHelper.init(context!!.applicationContext)
        AppContextHelper.initRoomDB(context!!.applicationContext)
        return true
    }

    override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
        return null
    }

    override fun getType(uri: Uri): String? {
        return null
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        return null
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        return 0
    }

    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
        return 0
    }
}
<provider
      android:name=".ToolContentProvider"
      android:authorities="${applicationId}.library-tool"
      android:exported="false" />
class MaoApplication : Application() {

    private lateinit var currentActivityRef: WeakReference<Activity>;


    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        Log.e(GlobalConfig.LOG_TAG, "MaoApplication attachBaseContext")
    }

    override fun onCreate() {
        super.onCreate()
        Log.e(GlobalConfig.LOG_TAG, "MaoApplication onCreate")
        initMMKV()
        initCodeView()
	}

    /**
     * 初始化MMKV工具
     */
    private fun initMMKV() {
        Log.e(GlobalConfig.LOG_TAG, "init MMKV")
        MMKV.initialize(this);
    }

    private fun initCodeView() {
        CodeProcessor.init(this)
    }

}

通過ContentProvider實現SDK庫初始化的功能實現了,那麼 ContentProvider 的 onCreate() 方法是什麼時候被呼叫的呢?

下面是日誌輸出,來幫助助我們理解初始化時機:

com.renhui.maomaomedia E/MaoMaoMedia: MaoApplication attachBaseContext
com.renhui.maomaomedia E/MaoMaoMedia: ToolContentProvider onCreate
com.renhui.maomaomedia E/MaoMaoMedia: MaoApplication onCreate

可以看到,它是介於 Application 的 attachBaseContext(Context) 和 onCreate() 之間所呼叫的,Application 的 attachBaseContext(Context) 方法被呼叫這就意味著 Application 的 Context 被初始化了。這也再次說明我們確實可以通過ContentProvider來進行SDK庫的初始化,並且執行時間在Application的onCreate之前。

二、ContentProvider初始化SDK庫的優缺點

優點:

  1. 不需要使用SDK庫的開發者呼叫初始化庫的流程,降低了接入成本
  2. 程式碼侵入更低,使得SDK庫的程式碼隔離性做的更好,而且方便升級和維護。

缺點:

  1. 不一定適用SDK庫的使用場景,因為在 ContentProvider 的 onCreate() 執行在 Application 的 onCreate() 方法之前,倘若你的庫需要有其它業務的依賴,那麼就不適合這種方式了。
  2. 需要注意應用安全漏洞問題,避免元件暴露,需要在宣告provider的時候,配置exported為false。
  3. 必須注意Provider的authorities千萬別寫死,否則兩個引入同樣SDK的App就無法共存了

三、ContentProvider初始化SDK庫實現的原始碼分析

那麼為什麼在ContentProvider做初始化,能獲取到application context的呢?看一下下面幾段原始碼就能知道了。

 private void handleBindApplication(AppBindData data) {
     ....
     final InstrumentationInfo ii;
     ....
     if (ii != null) {
       //1.建立ContentImpl
       final ContextImpl instrContext = ContextImpl.createAppContext(this, pi);
            try {
                final ClassLoader cl = instrContext.getClassLoader();
                mInstrumentation = (Instrumentation)
                    cl.loadClass(data.instrumentationName.getClassName()).newInstance();
            } catch (Exception e) {
                throw new RuntimeException(
                    "Unable to instantiate instrumentation "
                    + data.instrumentationName + ": " + e.toString(), e);
            }

        //2.建立Instrumentation
      final ComponentName component = new ComponentName(ii.packageName, ii.name);
            mInstrumentation.init(this, instrContext, appContext, component,
                    data.instrumentationWatcher, data.instrumentationUiAutomationConnection);
        ....
        //3.建立Application物件
         Application app;
         app = data.info.makeApplication(data.restrictedBackupMode, null);

         // Propagate autofill compat state
            app.setAutofillCompatibilityEnabled(data.autofillCompatibilityEnabled);

            mInitialApplication = app;

        ...
        //4.啟動當前程式中的ContentProvider和呼叫其onCreate方法

        if (!data.restrictedBackupMode) {
                if (!ArrayUtils.isEmpty(data.providers)) {
                    installContentProviders(app, data.providers);
                    // For process that contains content providers, we want to
                    // ensure that the JIT is enabled "at some point".
                    mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
                }
            }

        //5.呼叫Application的onCreate方法
        try {
                mInstrumentation.callApplicationOnCreate(app);
            } catch (Exception e) {
                if (!mInstrumentation.onException(app, e)) {
                    throw new RuntimeException(
                      "Unable to create application " + app.getClass().getName()
                      + ": " + e.toString(), e);
                }
            }
    }
 }
private void attachInfo(Context context, ProviderInfo info, boolean testing) {
    mNoPerms = testing;

    /*
     * Only allow it to be set once, so after the content service gives
     * this to us clients can't change it.
     */
    if (mContext == null) {
        mContext = context;
        if (context != null) {
            mTransport.mAppOpsManager = (AppOpsManager) context.getSystemService (Context.APP_OPS_SERVICE);
        }
        mMyUid = Process.myUid();
        if (info != null) {
            setReadPermission(info.readPermission);
            setWritePermission(info.writePermission);
            setPathPermissions(info.pathPermissions);
            mExported = info.exported;
            mSingleUser = (info.flags & ProviderInfo.FLAG_SINGLE_USER) != 0;
            setAuthorities(info.authority);
        }
        ContentProvider.this.onCreate();
    }
}

可以看到App的啟動過程中載入了provider,並且傳了一個Application例項進去,最終在ContentProvider中呼叫了onCreate()方法。因此,在自定義的ContentProvider中,通過getContext()方法就可以獲取到Application的例項了。

其實從這段原始碼中,我們也可以看到,ContentProvider中的onCreate()方法是先於Application中的onCreate()方法執行的(注意:此時Application物件已經建立)。

四、谷歌的新元件 - App Startup

谷歌推出的App Startup提供了一種在應用程式啟動時高效、直接初始化元件的方法。SDK開發人員和APP開發人員都可以使用App Startup簡化啟動順序並顯式設定初始化順序。App Startup還允許通過定義共享的ContentProvider統一元件的初始化,大大縮短應用啟動時間。

如果專案中的初始化都是同步初始化的話,並且使用到了多個ContentProvider,App Startup 還是不錯的,畢竟統一到了一個ContentProvider中,同時支援了簡單的順序依賴。

但是如果在追求App效能與啟動速度的場景中,多個SDK同時利用各自定義的ContentProvider實現“自啟動”, 在各種有先後順序與依賴的SDK初始化下做優化,那麼 App Startup 就不是很好用了。也正式這個原因,目前不建議將 App Startup 用於生產環境中。

目前的推薦方案還是之前我們都使用過的:同步+非同步初始化,並通過有向無環圖拓撲排序的方式來保證內部依賴元件的初始化順序。

相關文章