Android App Initializer 实现方案

397 阅读5分钟

1 背景

  • 想必大家都了解,我们在做 Android 开发的时候,我们都会在 Application 中的 attachBaseContext() 方法或者 onCreate() 方法中去进行初始化操作,并顺便获取到 Application 的上下文,这里的初始化包含我们项目本身的类初始化或第三方库初始化或第三方 SDK 初始化。
  • 但是上述这种常规操作,只能满足一些简单的业务需求,比较复杂的情况就不太优雅了,比如说组件化等场景。
  • 那么有没有一种在应用启动时能够更加简单、高效的方式来初始化组件,且能适配各种复杂场景呢?

2 ContentProvider 方案进行初始化

2.1 概念

  • 利用 ContentProvider 进行初始化,定义一个 ContentProvider,然后在 onCreate() 方法中拿到上下文,然后在此进行初始化操作。很多第三方库就用到了这种办法,例如:LeakCanary 2.4Picasso 2.7AutoSize 1.1.2 等等。
  • ContentProvider 初始化流程图: Android Startup 初始化流程图.jpg

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<out String>?, p2: String?, p3: Array<out String>?, 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<out String>?): Int {
    throw IllegalStateException("Not allowed.")
  }

  override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
    throw IllegalStateException("Not allowed.")
  }
}

// AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="xxx">
  
  <application
    ...>
    
    <provider
        android:name="xxx.TestInitializer"
        android:authorities="${applicationId}.TestInitializer"
        android:exported="false" />
    
  </application>
  
</manifest>

3 Initializer 方案进行初始化

3.1 概念

  • 定义 Initializer 接口,然后新增一个类实现该接口,并在 AndroidManifest.xml 文件内,添加其对应的 meta-data 标签信息,最终在 Application 中的 attachBaseContext() 方法或者 onCreate() 方法中进行 init 操作。
  • Initializer 初始化流程图: Initializer 初始化流程图.jpg

3.2 优缺点

  • 优点:可以简化启动序列并显式设置初始化依赖顺序,且简单、高效,比较符合国内合规隐私要求。
  • 缺点:还是需要依赖在壳 AppApplication 中进行 AppInitializer.initialize(this) 操作。

3.3 源代码

// Initializer.kt
/**
 * 定义一个接口,用于在应用程序启动期间初始化库
 */
interface Initializer {

  /**
   * 初始化操作
   */
  fun initialize(context: Context)

  /**
   * 依赖关系,返回值是一个依赖组件的 List 集合
   */
  fun dependencies(): List<Class<out Initializer>>

}

// AppInitializer.kt
/**
 * 用于初始化所有发现的组件化初始化器
 * <br/>
 * 发现机制是通过合并的 `AndroidManifest.xml` 中的 `<meta-data>` 标签条目
 */
object AppInitializer {

  /** 记录当前已经初始化操作后的 Set 集合 */
  private val mInitialized = mutableSetOf<Class<out Initializer>>()

  /** 记录当前已经发现的 Set 集合 */
  private val mDiscovered = mutableSetOf<Class<out Initializer>>()

  /**
   * 初始化
   * @param context 上下文
   */
  fun initialize(context: Context) {
    discoveryAndInitialize(context)
  }

  /**
   * 初始化
   * @param context 上下文
   * @param component 组件
   */
  fun initializeComponent(context: Context, component: Class<out Initializer>) {
    doInitialize(context, component, mutableSetOf())
  }

  /**
   * 发现并准备初始化操作
   * @param context 上下文
   */
  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<Class<*>>()
      // 遍历 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<out Initializer>
            mDiscovered.add(component)
            InitializerLogger.i("Discovered $key")
            doInitialize(context, component, initializing)
          }
        }
      }
    } catch (e: PackageManager.NameNotFoundException) {
      e.printStackTrace()
      InitializerLogger.e("NameNotFoundException ${e.localizedMessage}")
    }
  }

  /**
   * 执行初始化操作
   * @param context 上下文
   * @param component 组件
   * @param initializing 正在执行初始化操作的 Set 集合
   */
  @Synchronized
  private fun doInitialize(context: Context, component: Class<out Initializer>, initializing: MutableSet<Class<*>>) {
    // 鲁棒性判断
    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
/**
 * 初始化器的 Log 日志类
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
object InitializerLogger {

  /** 日志标签 */
  private const val TAG = "InitializerLogger"

  /**
   * To enable logging set this to true.
   */
  /** 要启用日志记录,请将其设置为 true */
  private const val DEBUG = true

  /**
   * Info 级别日志记录
   *
   * @param message 日志消息
   */
  fun i(message: String) {
    if (DEBUG) Log.i(TAG, message)
  }

  /**
   * Warning 级别日志记录
   *
   * @param message 日志消息
   */
  fun w(message: String) {
    if (DEBUG) Log.w(TAG, message)
  }

  /**
   * Error 级别日志记录
   *
   * @param message 日志消息
   */
  fun e(message: String) {
    if (DEBUG) Log.e(TAG, message)
  }

}

// values.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="open_initialize" translatable="false">com.open.initialize</string>
</resources>

3.4 使用步骤

// TestInitializer.kt
class TestInitializer: Initializer {

  override fun initialize(context: Context) {
    SamplesLogger.i("Initializer Test initialize: ")
  }

  override fun dependencies(): List<Class<out Initializer>> {
    SamplesLogger.i("Initializer Test dependencies: ")
    return emptyList()
  }

}

// AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="xxx">
  
  <application
    android:name=".MyApplication"
    ...>
    
    <meta-data
      android:name="xxx.TestInitializer"
      android:value="com.open.initialize" />
    
  </application>
  
</manifest>

// 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 初始化流程图: Jetpack App Startup 初始化流程图.jpg

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<Unit> {
  
  /**
   * 初始化操作,返回的初始化结果将被缓存,其中 context 参数是 Application
   */
  override fun create(context: Context) {
    SamplesLogger.i("Startup Test onCreate: ")
  }
  
  /**
   * 依赖关系,返回值是一个依赖组件的列表,如果不需要依赖于其它组件,返回一个空列表。
   * App Startup 在初始化当前组件时,会保证所依赖的组件已经完成初始化。
   */
  override fun dependencies(): MutableList<Class<out Initializer<*>>> {
    SamplesLogger.i("Startup Test dependencies: ")
    return mutableListOf()
  }

}

4.3.3 自动初始化

// AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="xxx">
  
  <application
    ...>
    
    <provider
      android:name="androidx.startup.InitializationProvider"
      android:authorities="${applicationId}.androidx-startup"
      android:exported="false"
      tools:node="merge">
      <meta-data
        android:name="xxx.TestInitializer"
        android:value="androidx.startup" />
    </provider>
    
  </application>
  
</manifest>
  • 注意点:
  • 1)组件名必须是 androidx.startup.InitializationProvider
  • 2)需要声明 android:exported="false",以限制其他应用访问此组件;
  • 3)要求 android:authorities 在整个手机唯一,通常使用 ${applicationId} 作为前缀;
  • 4)需要声明 tools:node="merge",确保 manifest merger tool 能够正确解析冲突的节点;
  • 5)meta-data name 为组件的 Initializer 实现类全限定名,valueandroidx.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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="xxx">
  
  <application
    ...>
    
    <provider
      android:name="androidx.startup.InitializationProvider"
      android:authorities="${applicationId}.androidx-startup"
      android:exported="false"
      tools:node="merge">
      <meta-data
        android:name="xxx.TestInitializer"
        tools:node="remove" />
    </provider>
    
  </application>
  
</manifest>

4.3.6 禁止所有的自动初始化

  • 假如需要禁止 App Startup 自动初始化,同样也需要利用 manifest merger tool 的合并规则:
// AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="xxx">
  
  <application
    ...>
    
    <provider
      android:name="androidx.startup.InitializationProvider"
      android:authorities="${applicationId}.androidx-startup"
      tools:node="remove" />
    
  </application>
  
</manifest>

5、Android Startup 方案进行初始化

5.1 概念

  • Android Startup 提供一种在应用启动时能够更加简单、高效的方式来初始化组件。开发人员可以使用 Android Startup 来简化启动序列,并显式地设置初始化顺序与组件之间的依赖关系。 与此同时 Android Startup 支持同步与异步等待,并通过有向无环图拓扑排序的方式来保证内部依赖组件的初始化顺序。
  • Android Startup 初始化流程图: Android Startup 初始化流程图.jpg

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<String>() {

  /**
   * 用来控制 create() 方法调时所在的线程,返回 true 代表在主线程执行
   */
  override fun callCreateOnMainThread(): Boolean = true

  /**
   * 用来控制当前初始化的组件是否需要在主线程进行等待其完成。如果返回 true,将在主线程等待,并且阻塞主线程
   */
  override fun waitOnMainThread(): Boolean = false

  /**
   * 组件初始化方法,执行需要处理的初始化逻辑,支持返回一个 T 类型的实例
   */
  override fun create(context: Context): String? {
    // todo something
    SamplesLogger.i("Android Startup Test onCreate: ")
    return this.javaClass.simpleName
  }

  /**
   * 返回 String 类型的 list 集合。用来表示当前组件在执行之前需要依赖的组件
   */
  override fun dependenciesByName(): List<String>? {
    SamplesLogger.i("Android Startup Test dependenciesByName: ")
    return super.dependenciesByName()
  }

}
  • 注意:dependenciesByName() 方法会被回调多次。

5.3.3 自动初始化

// AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="xxx">
  
  <application
    ...>
    
    <provider
      android:name="com.rousetime.android_startup.provider.StartupProvider"
      android:authorities="${applicationId}.android_startup"
      android:exported="false">
      <meta-data
        android:name="xxx.TestInitializer"
        android:value="android.startup" />
    </provider>
    
  </application>
  
</manifest>

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 方案进行初始化时间对照表

类型TotalTimeWaitTimeMethodTime
单次38638822
单次40540824
单次38839023
单次38839024
单次39740025
平均392.8395.223.6

6.2 Initializer 方案进行初始化时间对照表

类型TotalTimeWaitTimeMethodTime
单次44444625
单次45245425
单次45545726
单次43343525
单次42542823
平均441.844424.8

6.3 Jetpack App Startup 方案进行初始化时间对照表

类型TotalTimeWaitTimeMethodTime
单次39539722
单次38538723
单次38939121
单次39439623
单次37437720
平均387.4389.621.8

6.4 Andorid Startup 方案进行初始化时间对照表

类型TotalTimeWaitTimeMethodTime
单次44945127
单次43143327
单次43043128
单次43243427
单次43543727
平均435.4437.227.2

6.5 对比总结

  • 通过对比上述四种方案,其实都是有其存在的价值,具体取决于当前你的使用场景,然后根据场景选取不同的实现方案。

7 参考资料