Android使用ContentProvider初始化SDK库方案总结
做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库的优缺点
优点:
- 不需要使用SDK库的开发者调用初始化库的流程,降低了接入成本
- 代码侵入更低,使得SDK库的代码隔离性做的更好,而且方便升级和维护。
缺点:
- 不一定适用SDK库的使用场景,因为在 ContentProvider 的 onCreate() 执行在 Application 的 onCreate() 方法之前,倘若你的库需要有其它业务的依赖,那么就不适合这种方式了。
- 需要注意应用安全漏洞问题,避免组件暴露,需要在声明provider的时候,配置exported为false。
- 必须注意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 用于生产环境中。
目前的推荐方案还是之前我们都使用过的:同步+异步初始化,并通过有向无环图拓扑排序的方式来保证内部依赖组件的初始化顺序。