郭霖说 Jetpack 新成员:App-Startup 一篇就懂
super.onCreate()LitePal.initialize(this)}...}
为什么 LitePal 要求先进行初始化呢?因为 Android 的数据库中有需要操作都是需要依赖于 Context 的,在初始化的时候传入一次 Context,LitePal 会在内部将其保存下来,这样所以有其他数据库接口就不需要再传入 Context 参数了,从而让 API 变得更加精简。
这确实是个不错的主意,但是并不是只有 LitePal 想到了这一点,许多库也提供了类似的初始化接口,因此如果你在项目当中引入了非常多的第三方库,那么 Application 中的代码就可能会变成这个样子:
class?MyApplication?:?Application()?{
override?fun?onCreate()?{super.onCreate()LitePal.initialize(this)AAA.initialize(this)BBB.initialize(this)CCC.initialize(this)DDD.initialize(this)EEE.initialize(this)}...}
这样的代码就会显得有些凌乱了对不对?随着你引用的第三方库越来越多,这种情况真的是有可能发生的。
于是,有些更加聪明的库设计者,他们想到了一种非常巧妙的办法来避免显示地调用初始化接口,而是可以自动调用初始化接口,这种办法就是借助 ContentProvider。
ContentProvider 我们都知道是 Android 四大组件之一,它的主要作用是跨应用程序共享数据。比如为什么我们可以读取到电话簿中的联系人、相册中的照片等数据,借助的都是 ContentProvider。
然而这些聪明的库设计者们并没有打算使用 ContentProvider 来跨应用程序共享数据,只是准备使用它进行初始化而已。我们来看如下代码:
class?MyProvider?:?ContentProvider()?{
override?fun?onCreate():?Boolean?{context?.let?{LitePal.initialize(it)}return?true}...}
这里我定义了一个 MyProvider,并让它继承自 Co
ntentProvider,然后我们在 onCreate()方法中调用了 LitePal 的初始化接口。注意在 ContentProvider 中也是可以获取到 Context 的。
当然,继承了 ContentProvider 之后,我们是要重写很多个方法的,只不过其他方法在我们这个场景下完全使用不到,所以你可以在那些方法中直接抛出一个异常,或者进行空实现都是可以的。
另外不要忘记,四大组件是需要在 AndroidManifest.xml 文件中进行注册才可以使用的,因此记得添加如下内容:
<application?...>
<providerandroid:name=".MyProvider"android:authorities="${applicationId}.myProvider"android:exported="false"?/>
</application>
authorities 在这里并没有固定的要求,填写什么值都是可以的,但必须保证这个值在整个手机上是唯一的,所以通常会使用 ${applicationId}作为前缀,以防止和其他应用程序冲突。
那么,自定义的这个 MyProvider 它会在什么时候执行呢?我们来看一下这张流程图:
可以看到,一个应用程序的执行顺序是这个样子的。首先调用 Application 的 attachBaseContext()方法,然后调用 ContentProvider 的 onCreate()方法,接下来调用 Application 的 onCreate()方法。
那么,假如 LitePal 在自己的库当中实现了上述的 MyProvider,会发生什么情况呢?
你会发现 LitePal.initialize()这个接口可以省略了,因为在 MyProvider 当中这个接口会被自动调用,这样在进入 Application 的 onCreate()方法时,LitePal 其实已经初始化过了。
有没有觉得这种设计方式很巧妙?它可以将库的用法进一步简化,不需要你主动去调用初始化接口,而是将这个工作在背后悄悄自动完成了。
那么有哪些库使用了这种设计方式呢?这个真的有很多了,比如说 Facebook 的库,Firebase 的库,还有我们所熟知的 WorkManager,Lifecycles 等等。这些库都没有提供一个像 LitePal 那样的初始化接口,其实就是使用了上述的技巧。
看上去如此巧妙的技术方案,那么它有没有什么缺点呢?
有,缺点就是,ContentProvider 会增加许多额外的耗时。
毕竟 ContentProvider 是 Android 四大组件之一,这个组件相对来说是比较重量级的。也就是说,本来我的初始化操作可能是一个非常轻量级的操作,依赖于 ContentProvider 之后就变成了一个重量级的操作了。
关于 ContentProvider 的耗时,Google 官方也有给出一个测试结果:
这是在一台搭载 Android 10 系统的 Pixel2 手机上测试的情况。可以看到,一个空的 ContentProvider 大约会占用 2ms 的耗时,随着 ContentProvider 的增加,耗时也会跟着一起增加。如果你的应用程序中使用了 50 个 ContentProvider,那么将会占用接近 20ms 的耗时。
注意这还只是空 ContentProvider 的耗时,并没有算上你在 ContentProvider 中执行逻辑的耗时。
这个测试结果告诉我们,虽然刚才所介绍的使用 ContentProvider 来进行初始化的设计方式很巧妙,但是如果每个第三方库都自己创建了一个 ContentProvider,那么最终我们 App 的启动速度就会受到比较大的影响。
有没有办法解决这个问题呢?
有,就是使用我们今天要介绍的主题:App Startup。
我上面花了很长的篇幅来介绍 App Startup 具体是用来解决什么问题的,因为这部分内容才是 App Startup 库的核心,只有了解了它是用来解决什么问题的,才能快速掌握它的用法。不然就会像刚开始说的那样,学着学着怎么学到 ContentProvider 上面去了,一头雾水。
那么 App Startup 是如何解决这个问题的呢?它可以将所有用于初始化的 ContentProvider 合并成一个,从而使 App 的启动速度变得更快。
具体来讲,App Startup 内部也创建了一个 ContentProvider,并提供了一套用于初始化的标准。然后对于其他第三方库来说,你们就不需要再自己创建 ContentProvider 了,都按我的这套标准进行实现就行了,我可以保证你们的库在 App 启动之前都成功进行初始化。
了解了 App Startup 具体是用来解决什么问题的,以及它的实现原理,接下来我们开始学习它的用法,这部分就非常简单了。
首先要使用 App Startup,我们要将这个库引入进来:
dependencies?{implementation?"androidx.startup:startup-runtime:1.0.0-alpha01"}
接下来我们要定义一个用于执行初始化的 Initializer,并实现 App Startup 库的 Initializer 接口,如下所示:
class?LitePalInitializer?:?Initializer<Unit>?{
override?fun?create(context:?Context)?{LitePal.initialize(context)}
override?fun?dependencies():?List<Class<out?Initializer<*>>>?{return?listOf(OtherInitializer::class.java)}
}
实现 Initializer 接口要求重现两个方法,在 create()方法中,我们去进行之前要进行的初始化操作就可以了,create()方法会把我们需要的 Context 参数传递进来。
dependencies()方法表示,当前的 LitePalInitializer 是否还依赖于其他的 Initializer,如果有的话,就在这里进行配置,App Startup 会保证先初始化依赖的 Initializer,然后才会初始化当前的 LitePalInitializer。
当然,绝大多数的情况下,我们的初始化操作都是不会依赖于其他 Initializer 的,所以通常直接返回一个 emptyList()就可以了,如下所示:
class?LitePalInitializer?:?Initializer<Unit>?{
override?fun?create(context:?Context)?{LitePal.initialize(context)}
override?fun?dependencies():?List<Class<out?Initializer<*>>>?{return?emptyList()}
}
定义好了 Initializer 之后,接下来还剩最后一步,将它配置到 AndroidManifest.xml 当中。但是注意,这里的配置是有比较严格的格式要求的,如下所示:
<application?...>
<providerandroid:name="androidx.startup.InitializationProvider"android:authorities="${applicationId}.androidx-startup"android:exported="false"tools:node="merge"><meta-dataandroid:name="com.example.LitePalInitializer"android:value="androidx.startup"?/></provider>
</application>
上述配置,我们能修改的地方并不多,只有 meta-data 中的 android:name 部分我们需要指定成我们自定义的 Initializer 的全路径类名,其他部分都是不能修改的,否则 App Startup 库可能会无法正常工作。
没错,App Startup 库的用法就是这么简单,基本我将它总结成了三步走的操作。
引入 App Startup 的库。
自定义一个用于初始化的 Initializer。
将自定义 Initializer 配置到 AndroidManifest.xml 当中。
这样,当 App 启动的时候会自动执行 App Startup 库中内置的 ContentProvider,并在它的 ContentProvider 中会搜寻所有注册的 Initializer,然后逐个调用它们的 create()方法来进行初始化操作。
只用一个 ContentProvider 就可以让所有库都正常初始化,Everyone is happy。
其实到这里为止,App Startup 库的知识就已经讲完了,最后再介绍一个不太常用的知识点吧:延迟初始化。
现在我们已经知道,所有的 Initializer 都会在 App 启动的时候自动执行初始化操作。但是如果我作为 LitePal 库的用户,就是不希望它在启动的时候自动初始化,而是想要在特定的时机手动初始化,这要怎么办呢?
首先,你得通过分析 LitePal 源码的方式,找到 LitePal 用于初始化的 Initializer 的全路径类名是什么,比如上述例子当中的 com.example.LitePalInitializer(注意这里我只是为了讲解这个知识点而举的例子,实际上 LitePal 还并没有接入 App Startup)。
评论