Android 应用启动性能 | 延迟初始化

image

上一篇文章 中,我展示了 content provider (它出现在应用合并后的 manifest 文件) 是如何在应用启动的时候自动加载第三方库以及模块的。

在这篇文章中,我会介绍如何使用 AndroidX 的 应用启动 (App Startup) 库来进一步控制那些库该在何时以及以何种方式被加载。也许,我是说也许,我们也会顺便发现该如何缩短应用的启动时间。

使用应用启动库自动初始化

使用应用启动库 (App Startup) 最简单的方式是利用它的 content provider 在后台初始化其他库。您既可以指定应用启动库该如何初始化其他的库,也可以从合并后的 manifest 文件中移除其他库的 content provider。避免使用多个 content provider 执行启动任务,而是将资源用于加载应用启动库,然后再加载其他内容。

您可以通过如下三步实现上述操作,首先在您工程的 build.gradle 文件中添加应用启动库作为依赖,其次为每一个需要初始化的库创建一个 Initializer,最后在您工程的 Manifest.xml 文件中添加相关信息。

让我们再看一遍我在 第一篇文章 中使用的 WorkManager 示例。为了通过应用启动库加载 WorkManager,我先在应用的 build.gradle 文件中添加了应用启动库:

// 查看最新的版本号 https://developer.android.google.cn/jetpack/androidx/releases/startup
def startup_version = "1.0.0"
implementation “androidx.startup:startup-runtime:$startup_version”

然后,基于应用启动库提供的 Initializer 接口,我创建了一个 Initializer:

class MyWorkManagerInitializer : Initializer<WorkManager> {
    override fun create(context: Context): WorkManager {
        val configuration = Configuration.Builder().build()
        WorkManager.initialize(context, configuration)
        return WorkManager.getInstance(context)
    }

    override fun dependencies(): List<Class<out Initializer<*>>> {
        // 没有其他依赖库
        return emptyList()
    }
}

每一个 Initializer 有两个方法需要复写: create()dependencies()dependencies() 被用来指定多个依赖库的初始化顺序。在这个示例中我并不需要这个功能,因为我只需要处理 WorkManager。如果您需要在应用中使用多个库,请查看 应用启动使用手册 中关于使用 dependencies() 的详情。

对于 create() 方法,我模仿了 WorkManager’s content provider 中的实现。

顺便说一下,其实这个方法在使用应用启动库的时候很常用。一个库的 content provider 负责了其初始化的实现,所以您通常都可以参考那个类中的代码来手动实现它。有些库可能比较麻烦,因为它们使用了隐藏的或者内部的 API,但是好在 WorkManager 并不是,所以我可以这么做,希望该方法也适用于您的情况。

最后,我在 Manifest.xml 文件的 <application> 代码块中添加了两个 provider 的标签。第一个如下所示:

<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager-init"
    android:exported="false"
    tools:node="remove" />

WorkManagerInitializer 标签很重要,因为它表示需要 Android Studio 删除自动生成的 provider,而该 provider 是在 build.gradle 文件中添加 WorkManager 后生成的。如果没有这个特殊的标签,这个库仍然会在应用启动的时候自动初始化,继而在应用启动库尝试初始化它的时候报错,因为它已经被初始化了。

下面是我添加的第二个 provider 标签:

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data     android:name="com.example.startuplibtest.MyWorkManagerInitializer"
    android:value="androidx.startup" />
</provider>

InitializationProvider 标签和通过添加应用启动库到 build.gradle 文件中自动生成的标签基本相同 (您可以通过查看合并后的 manifest 文件来验证 -- 详情请查看 第一篇文章),但是它们有两个很重要的不同点:

tools:node="merge"

这个参数主要用于 Android Studio 所负责的 manifest 合并操作。它告诉工具在最终合并的 manifest 文件中合并这个标签的多个实例。在这个例子中,它会合并由库依赖自动生成的 <provider> 到这个版本的 provider,这样在最终合并的 manifest 文件中只会有这一个标签实例。

另一行包含了这个 meta-data:

<meta-data  android:name="com.example.startuplibtest.MyWorkManagerInitializer"
    android:value="androidx.startup" />

这个 provider 中的 metadata 标签告诉应用启动库如何找到您的 Initializer 代码,这些代码会在应用启动的时候执行来初始化这个库。请注意这导致的区别: 如果您没有使用应用启动库,就会自动执行相关初始化,因为 Android 会在那个库中创建并执行 content provider,之后会自动初始化这个库本身。但是通过应用启动库指定您的 Initializer,以及在合并 manifest 文件中去除 WorkManager 的 provider,相当于告诉 Android 转而使用应用启动库的 content provider 来加载 WorkManager 库。如果通过这个方式初始化多个库,您可以利用应用启动库的这个单独的 content provider 有效地管理这些请求,而不是导致每个库都创建自己的 content provider。

偷个懒...如果您想的话

当优化应用启动性能的时候,我们不能改变那些无法控制的代码实现。所以这里的思路并不是加速我们使用库的初始化,而是控制这些库什么时候以及如何被初始化。尤其是我们可以决定任一个库是否需要在应用启动的时候被初始化 (要么使用库的默认机制添加 content provider 到合并的 manifest 文件,或者也可以利用应用启动库的 content provider 来集中管理初始化请求),还是需要稍候再加载它们。

举个例子,或许在您应用的一个特殊的流程中需要某一个包含 content provider 初始化的库,但是这个库并不需要在应用启动的时候立即被加载,又或者在某些情况下它根本不需要被加载。如果是这样的话,为什么要因为只在某个特殊代码路径中需要而在应用启动时花时间初始化一个很大的库呢?为什么不等到这个库真正被需要的时候再引入相关的初始化开销呢?

这正是应用启动库高明的地方,它能帮您从合并的 manifest 文件中和应用启动的过程中移除隐藏的 content provider,也能帮您延迟或者更有目的地加载这些库。

使用应用启动库实现延迟初始化

现在我们已经知道该如何使用应用启动库实现自动加载以及初始化库。接下来让我们更进一步地来看看,如果您不想在启动的时候初始化,该如何实现延迟初始化。

其实上面的代码已经很接近了,在 build.gradle 文件中您需要同样的启动依赖和其他您想使用的库,也还是需要特殊的 "移除" provider 标签来去除每个库自动生成的 content provider。我们只需要向 manifest 文件添加多一点信息来告诉它同样移除应用启动库的 provider。这样在应用启动的时候就不会有任何 content provider 初始化发生,而完全由您来决定什么时候应该触发相关初始化。

为了达到这个目的,我用下面的代码替换了前面使用的 InitializationProvider。上面所展示的代码告诉了系统该如何定位 content provider 中自动初始化您库的代码。因为稍后要手动触发初始化,这一次我要跳过那个部分,而只留下在应用启动的时候去除自动生成的 content provider 的部分。

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="remove" />

在我做了这个改动后,在合并的 manifest 文件中不再有任何 content provider 了,所以应用启动库和 WorkManager 都不会在应用启动的时候被自动初始化了。

为了手动初始化这些库,我在应用的其他地方添加了如下代码来实现它:

val initializer = AppInitializer.getInstance(context)
initializer.initializeComponent(MyWorkManagerInitializer::class.java)

AppInitializer 是应用启动库提供的连接所有这些部分的类。您需要使用一个 context 对象来创建 AppInitializer 对象,然后可以向其传递一个您为初始化各种不同库创建的 Initializer 引用。在这个示例中,我使用的是 MyWorkManagerInitializer,然后就搞定了。

时间就是一切

我做了几次测试 (使用的是我在 测试应用启动性能 文章中提到的计时方法) 来比较几种不同的启动应用和初始化库的方法。我统计了不带任何库、带 WorkManager (使用默认自动生成的 content provider)、在启动时使用应用启动库自动初始化 WorkManager 以及使用 AppInitializer 延迟初始化 WorkManager 和应用启动库。

需要注意的是,就像我们在 之前的文章 中讨论的,所有的这些时间计算都是基于锁定的 CPU 主频,所以这些时长都要比在没有锁定 CPU 主频的机器上大很多。它们只在相互之间比较的时候有意义,而并不能代表真实的情况。下面是我发现的:

  • 不带 WorkManager: 1244 ms
  • 带 WorkManager 并且通过 content provider 加载: 1311 ms
  • 带 WorkManager 并且通过 App Startup 加载: 1315 ms
  • 带 WorkManager (延迟加载): 1268 ms

最后,我统计了利用 AppInitalizer 手动初始化 WorkManager 的耗时:

  • 利用 AppInitializer 初始化 WorkManager: 51 ms

这个数据给我们带来一些启示。首先,在应用启动的时候加载 WorkManager 会给我的应用平均增加 67 毫秒 (1311–1244) 的启动时间。需要注意的是: 加载这个库的常规方式 (使用 content provider) 使用的时间和使用应用启动库的 (1315 – 1244 = 71 ms) 差不多。这是因为应用启动库在单个库的例子中并不会帮我们节省时间,我们只不过是转移逻辑到另一个代码路径中运行。如果使用应用启动库加载多个库,我们会得到相应的优化效果,但是针对这里的单个库的例子,使用这个方法不会有任何节省时间的优势。

同时延迟初始化 WorkManager 让我可以 "节省" 大约 51 毫秒的时间。

这个差别是否足够明显到您需要担心呢?答案永远是 "看情况而定"。

51 毫秒占了 1.3 秒总时长的不到 4%,而对于一个真实应用来说,通常都会比我这个简单的应用更复杂,这个耗时占总启动时间的百分比会更低。这种情况下这个时长可能不值得担心。但是有时候您可能发现有些库需要太长时间来初始化,更有可能的是,您可能使用了几个自带 content provider 的库,而它们每一个都会增加一点您应用的启动时间。如果您可以将上述大部分或者全部工作推迟到一个更为合适的时间点,并且从启动过程中剥离,或许您会发现应用的启动速度会有显著的提高。

像所有的性能优化项目,您可以做的最重要的事情是分析细节、测量以及决定:

  • 检查您项目合并后的 manifest 文件。您可以看到多少 <provider> 标签?
  • 您能否利用应用启动库从合并的 manifest 文件中移除一些甚至所有这些 content provider,并观察它如何影响启动时间?您能否在实现这个的同时不影响运行时行为呢?(值得注意的是: 您需要保证在应用开始依赖相关库的功能之前,确保初始化它们。)

最后,尽情享受性能测试和优化。我会继续找寻更多分析和优化应用的性能办法,如果发现什么有价值的东西我会发布相关的内容。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容