文章目录
- 1、背景
- 2、ContentProvider 方案进行初始化
-
- 3、Initializer 方案进行初始化
- 3.1 概念
- 3.2 优缺点
- 3.3 源代码
- 3.4 使用步骤
- 4、Jetpack App Startup 方案进行初始化
- 4.1 概念
- 4.2 优缺点
- 4.3 使用步骤:
- 4.3.1 dependencies 配置
- 4.3.2 实现 Initializer 接口
- 4.3.3 自动初始化
- 4.3.4 手动初始化
- 4.3.5 取消自动化
- 4.3.6 禁止所有的自动初始化
- 5、Android Startup 方案进行初始化
- 5.1 概念
- 5.2 优缺点
- 5.3 使用步骤:
- 5.3.1 dependencies 配置
- 5.3.2 实现 AndroidStartup 接口
- 5.3.3 自动初始化
- 5.3.4 手动初始化
- 5.3.5 更多请参考
- 6、上述四种方案对比
- 6.1 ContentProvider 方案进行初始化时间对照表
- 6.2 Initializer 方案进行初始化时间对照表
- 6.3 Jetpack App Startup 方案进行初始化时间对照表
- 6.4 Andorid Startup 方案进行初始化时间对照表
- 6.5 对比总结
- 7、参考资料
1、背景
- 想必大家都了解,我们在做 Android 开发的时候,我们都会在 Application 中的 attachBaseContext() 方法或者 onCreate() 方法中去进行初始化操作,并顺便获取到 Application 的上下文,这里的初始化包含我们项目本身的类初始化或第三方库初始化或第三方 SDK 初始化。
- 但是上述这种常规操作,只能满足一些简单的业务需求,比较复杂的情况就不太优雅了,比如说组件化等场景。
- 那么有没有一种在应用启动时能够更加简单、高效的方式来初始化组件,且能适配各种复杂场景呢?
2、ContentProvider 方案进行初始化
2.1 概念
- 利用 ContentProvider 进行初始化,定义一个 ContentProvider,然后在 onCreate() 方法中拿到上下文,然后在此进行初始化操作。很多第三方库就用到了这种办法,例如:LeakCanary 2.4、Picasso 2.7、AutoSize 1.1.2 等等。
- ContentProvider 初始化流程图:
2.2 优缺点
- 优点:可以偷偷摸摸就进行初始化操作,而不是在 Application 中书写代码进行初始化操作。
- 缺点:如果 ContentProvider 过多,启动过多的 ContentProvider 会增加应用的启动时间。
2.3 使用步骤
// TestInitializer.kt
class TestInitializer: ContentProvider() {
override fun onCreate(): Boolean {
val application = context !!.applicationContext as Application
SamplesLogger.i("ContentProvider Test onCreate: ")
return true
}
override fun query(p0: Uri, p1: Array?, p2: String?, p3: Array?, p4: String?): Cursor? {
throw IllegalStateException("Not allowed.")
}
override fun getType(p0: Uri): String? {
throw IllegalStateException("Not allowed.")
}
override fun insert(p0: Uri, p1: ContentValues?): Uri? {
throw IllegalStateException("Not allowed.")
}
override fun delete(p0: Uri, p1: String?, p2: Array?): Int {
throw IllegalStateException("Not allowed.")
}
override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array?): Int {
throw IllegalStateException("Not allowed.")
}
}
// AndroidManifest.xml
3、Initializer 方案进行初始化
3.1 概念
- 定义 Initializer 接口,然后新增一个类实现该接口,并在 AndroidManifest.xml 文件内,添加其对应的 meta-data 标签信息,最终在 Application 中的 attachBaseContext() 方法或者 onCreate() 方法中进行 init 操作。
- Initializer 初始化流程图:
3.2 优缺点
- 优点:可以简化启动序列并显式设置初始化依赖顺序,且简单、高效,比较符合国内合规隐私要求。
- 缺点:还是需要依赖在壳 App 的 Application 中进行 AppInitializer.initialize(this) 操作。
3.3 源代码
// Initializer.kt
interface Initializer {
fun initialize(context: Context)
fun dependencies(): List>
}
// AppInitializer.kt
object AppInitializer {
private val mInitialized = mutableSetOf>()
private val mDiscovered = mutableSetOf>()
fun initialize(context: Context) {
discoveryAndInitialize(context)
}
fun initializeComponent(context: Context, component: Class) {
doInitialize(context, component, mutableSetOf())
}
private fun discoveryAndInitialize(context: Context) {
try {
// 获取当前应用的 ApplicationInfo 信息
val applicationInfo: ApplicationInfo = context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
// 获取当前应用的 metaData 信息
val metaData: Bundle = applicationInfo.metaData
// 获取 meta-data 标签 com.open.initialize
val initialize = context.getString(R.string.open_initialize)
// 存储正在执行初始化操作的 Set 集合
val initializing = mutableSetOf>()
// 遍历 meta-data 标签集合
metaData.keySet().forEach {key ->
val value = metaData.get(key)
if (initialize == value) {
// 通过反射获取类
val clazz = Class.forName(key)
if (Initializer::class.java.isAssignableFrom(clazz)) {
val component = clazz as Class
mDiscovered.add(component)
InitializerLogger.i("Discovered $key")
doInitialize(context, component, initializing)
}
}
}
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
InitializerLogger.e("NameNotFoundException ${e.localizedMessage}")
}
}
@Synchronized
private fun doInitialize(context: Context, component: Class, initializing: MutableSet>) {
// 鲁棒性判断
if (initializing.contains(component)) {
InitializerLogger.e("Cannot initialize ${component.name}")
return
}
if (! mInitialized.contains(component)) {
initializing.add(component)
try {
// 获取声明的构造函数
val instance = component.getDeclaredConstructor().newInstance()
val initializer = instance as Initializer
// 获取依赖关系的 List 集合
val dependencies = initializer.dependencies()
if (dependencies.isNotEmpty()) {
// 如果存在依赖关系,优先初始化依赖关系
dependencies.forEach {clazz ->
if (! mInitialized.contains(clazz)) {
doInitialize(context, clazz, initializing)
}
}
}
InitializerLogger.i("Initializing ${component.name}")
// 开始执行初始化操作
initializer.initialize(context)
InitializerLogger.i("Initialized ${component.name}")
// 初始化执行完成后,将其从正在进行初始化 Set 集合中移除
initializing.remove(component)
// 然后将其加入已经初始化完毕的 Set 集合中
mInitialized.add(component)
} catch (e: Throwable) {
InitializerLogger.i("Initialize error -> ${e.localizedMessage}")
}
}
}
}
// InitializerLogger.kt
@RestrictTo(RestrictTo.Scope.LIBRARY)
object InitializerLogger {
private const val TAG = "InitializerLogger"
private const val DEBUG = true
fun i(message: String) {
if (DEBUG) Log.i(TAG, message)
}
fun w(message: String) {
if (DEBUG) Log.w(TAG, message)
}
fun e(message: String) {
if (DEBUG) Log.e(TAG, message)
}
}
// values.xml
com.open.initialize
3.4 使用步骤
// TestInitializer.kt
class TestInitializer: Initializer {
override fun initialize(context: Context) {
SamplesLogger.i("Initializer Test initialize: ")
}
override fun dependencies(): List> {
SamplesLogger.i("Initializer Test dependencies: ")
return emptyList()
}
}
// AndroidManifest.xml
// MyApplication.kt
class MyApplication: Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
}
override fun onCreate() {
super.onCreate()
AppInitializer.initialize(this)
}
}
4、Jetpack App Startup 方案进行初始化
4.1 概念
- Android Jetpack 提供的 App Startup 方案有点像 ContentProvider 中初始化 和 Initializer 中初始化 方案的结合体,相对来说属于更优的实现(推荐)。
- Jetpack App Startup 初始化流程图:
4.2 优缺点
- 优点:使用 App StartUp 框架,可以简化启动序列并显式设置初始化依赖顺序,在简单、高效这方面,App Startup 基本满足需求。
- 缺点:App Startup 框架的不足也是因为它太简单了,提供的特性太过简单,往往并不能完美契合商业化需求。例如以下特性 App Startup 就无法满足:
- (1)缺乏异步等待:同步等待指的是在当前线程先初始化所依赖的组件,再初始化当前组件,App Startup 是支持的,但是异步等待就不支持了。举个例子,所依赖的组件需要执行一个耗时的异步任务才能完成初始化,那么 App Startup 就无法等待异步任务返回。
- (2)缺乏依赖回调:当前组件所依赖的组件初始化完成后,未发出回调。
4.3 使用步骤:
4.3.1 dependencies 配置
dependencies {
implementation "androidx.startup:startup-runtime:1.1.1"
}
4.3.2 实现 Initializer 接口
class TestInitializer: Initializer {
override fun create(context: Context) {
SamplesLogger.i("Startup Test onCreate: ")
}
override fun dependencies(): MutableList>> {
SamplesLogger.i("Startup Test dependencies: ")
return mutableListOf()
}
}
4.3.3 自动初始化
// AndroidManifest.xml
- 注意点:
- (1)组件名必须是 androidx.startup.InitializationProvider;
- (2)需要声明 android:exported="false",以限制其他应用访问此组件;
- (3)要求 android:authorities 在整个手机唯一,通常使用 ${applicationId} 作为前缀;
- (4)需要声明 tools:node="merge",确保 manifest merger tool 能够正确解析冲突的节点;
- (5)meta-data name 为组件的 Initializer 实现类全限定名,value 为 androidx.startup。
4.3.4 手动初始化
- 在组件需要进行懒加载时(耗时任务),可以进行手动初始化。需要手动初始化的 Initializer 不需要在 AndroidManifest 中进行声明,也不应该被其它组件依赖。调用以下方即可进行手动初始化:
AppInitializer.getInstance(context).initializeComponent(TestInitializer::class.java)
- 注意事项:App Startup 中会缓存初始化后的结果,重复调用 initializeComponent() 不会导致重复初始化。
4.3.5 取消自动化
- 假如有些库已经配置了自动初始化,而我们又希望进行懒加载时,就需要利用 manifest merger tool 的合并规则来移除这个库对应的 Initializer。具体如下:
// AndroidManifest.xml
4.3.6 禁止所有的自动初始化
- 假如需要禁止 App Startup 自动初始化,同样也需要利用 manifest merger tool 的合并规则:
// AndroidManifest.xml
5、Android Startup 方案进行初始化
5.1 概念
- Android Startup 提供一种在应用启动时能够更加简单、高效的方式来初始化组件。开发人员可以使用 Android Startup 来简化启动序列,并显式地设置初始化顺序与组件之间的依赖关系。 与此同时 Android Startup 支持同步与异步等待,并通过有向无环图拓扑排序的方式来保证内部依赖组件的初始化顺序。
- Android Startup 初始化流程图:
5.2 优缺点
- 优点:使用 Andorid StartUp 框架,可以简化启动序列并显式设置初始化依赖顺序,且支持同步与异步等待,并通过有向无环图拓扑排序的方式来保证内部依赖组件的初始化顺序。
- 缺点:稳定性怀疑,没有进行验证。
5.3 使用步骤:
5.3.1 dependencies 配置
repositories {
mavenCentral()
}
dependencies {
implementation 'io.github.idisfkj:android-startup:1.1.0'
}
5.3.2 实现 AndroidStartup 接口
class TestInitializer: AndroidStartup() {
override fun callCreateOnMainThread(): Boolean = true
override fun waitOnMainThread(): Boolean = false
override fun create(context: Context): String? {
// todo something
SamplesLogger.i("Android Startup Test onCreate: ")
return this.javaClass.simpleName
}
override fun dependenciesByName(): List? {
SamplesLogger.i("Android Startup Test dependenciesByName: ")
return super.dependenciesByName()
}
}
- 注意:dependenciesByName() 方法会被回调多次。
5.3.3 自动初始化
// AndroidManifest.xml
5.3.4 手动初始化
StartupManager.Builder()
.addStartup(TestInitializer())
.build(this)
.start()
.await()
5.3.5 更多请参考
6、上述四种方案对比
- 基于上述四种方案,简单写了一个 Demo 示例,以 10 为基数,然后分别冷启动 5 次,得出下述数据,仅供参考。
- 查看启动的时间,其对应的 adb 命令:
adb shell am start -W -n 包名/类名
6.1 ContentProvider 方案进行初始化时间对照表
| 类型 | TotalTime | WaitTime | MethodTime |
|---|
| 单次 | 386 | 388 | 22 |
| 单次 | 405 | 408 | 24 |
| 单次 | 388 | 390 | 23 |
| 单次 | 388 | 390 | 24 |
| 单次 | 397 | 400 | 25 |
| 平均 | 392.8 | 395.2 | 23.6 |
6.2 Initializer 方案进行初始化时间对照表
| 类型 | TotalTime | WaitTime | MethodTime |
|---|
| 单次 | 444 | 446 | 25 |
| 单次 | 452 | 454 | 25 |
| 单次 | 455 | 457 | 26 |
| 单次 | 433 | 435 | 25 |
| 单次 | 425 | 428 | 23 |
| 平均 | 441.8 | 444 | 24.8 |
6.3 Jetpack App Startup 方案进行初始化时间对照表
| 类型 | TotalTime | WaitTime | MethodTime |
|---|
| 单次 | 395 | 397 | 22 |
| 单次 | 385 | 387 | 23 |
| 单次 | 389 | 391 | 21 |
| 单次 | 394 | 396 | 23 |
| 单次 | 374 | 377 | 20 |
| 平均 | 387.4 | 389.6 | 21.8 |
6.4 Andorid Startup 方案进行初始化时间对照表
| 类型 | TotalTime | WaitTime | MethodTime |
|---|
| 单次 | 449 | 451 | 27 |
| 单次 | 431 | 433 | 27 |
| 单次 | 430 | 431 | 28 |
| 单次 | 432 | 434 | 27 |
| 单次 | 435 | 437 | 27 |
| 平均 | 435.4 | 437.2 | 27.2 |
6.5 对比总结
- 通过对比上述四种方案,其实都是有其存在的价值,具体取决于当前你的使用场景,然后根据场景选取不同的实现方案。
7、参考资料
- 《Android | 使用 ContentProvider 无侵入获取 Context》 – 彭旭锐 著
- 《组件化:代码隔离也难不倒组件的按序初始化》 —— leobert-lan 著
- 《合并多个清单文件》 —— Android Developers
- 《App Startup》 —— Android Developers
- 《探究 | App Startup真的能减少启动耗时吗? 》 – jimuzz 著
- 《Android | App Startup 可能比你想象中要简单》 – 彭旭锐 著
- 《从源码看 Jetpack(5)Startup 源码详解》 —— 叶志陈 著
- 《我为何弃用 Jetpack 的 App Startup?》 —— 午后一小憩 著
- 《Android Startup》 —— Android Startup GitHub
- 《Android Startup 中文文档》 – Android Startup GitHub
- 《更快!这才是我想要的 Android Startup 库!》 —— idisfkj 著