背景
作为一个 Android 应用开发人员, 对于应用启动时的任务启动问题, 多少年来一直有不少业界精英尝试推出自家的解决方案. 直到 Android Jetpack 问世, Jetpack 也尝试推出官方的应用启动解决方案, 它就是 Jetpack App Startup.
而随着美国对中国高科技企业的持续打压与制裁, 起始于 Android 开源 OS 的华为, 也不得不与 Android 分道扬镳, 开始大力发展 HarmonyOS.
而对于移动平台面临的共性问题, 无论 Android, iOS 还是 HarmonyOS, 每一端都是逃避不了的. 而应用启动时的任务加载问题, 便是这些共性问题之一.
今天这篇文章就是对该问题在 Android 和 HarmonyOS平台的不同解决方案之间的对比分析, 希望借此加深对不同 OS 的语言及其特性的了解,
对于不同背景的工程师, 希望也能够扩大自己的眼界, 扩展自己的能力, 于此于彼, 竟也无一坏处.
Android Jetpack Startup
框架简介
作为一名 Android 研发人员, 大家对于 Android Jetpack 都比较熟了, 这里再提一嘴.
按照官网正式的说法, Android Jetpack 是一个由多个库组成的套件, 可帮助开发者遵循最佳做法, 减少样板代码, 并编写可在各种 Android 版本和设备中一致运行的代码, 让开发者可将精力集中于真正重要的编码工作.
而 Android Jetpack Startup 就是 Android Jetpack 复杂繁多的库中其中的一个. 而 Android App Startup 库, 则可以在应用启动时对简单, 高效地初始化组件提供切实可用的帮助.
这样借助 App Startup 库, 无论是库开发者和应用开发者, 都可以使用 App Startup 来简化启动序列并明确设置初始化顺序.
而 App Startup 的原理则是, 通过定义一个共用的 content provider 组件来初始化程序, 而无需为需要初始化的每个组件定义单独的 content provider. 这可以显著缩短应用启动时间.
初始设置
Android App Startup 库的初始设置非常简单, 只需要添加如下依赖即可:
dependencies {
implementation("androidx.startup:startup-runtime:$LATEST_VERSION") // up to now, it's 1.1.1
}
启动时初始化组件
几乎每一个商业应用都会有依赖于许多三方库或者自定义的任务. 而这些库和任务在许多情况下需要依赖于在应用启动时立即初始化其对应的组件. 而这些库和任务通常可以通过使用 content provider 初始化每个依赖项来满足此需求, 但 content provider 的实例化成本高昂, 并且可能必不可少地会不必要地减慢启动序列的速度. 此外, Android 初始化 content provider 的顺序可能还会是不确定的. 而 App Startup 则提供了一种更高效的方式, 让你能够在应用启动时初始化组件并明确定义其依赖项及初始化的顺序.
而 App Startup 则是定义了一组 API 来支持定义所有依赖项及其执行顺序. 我们接下来继续了解这些 API.
定义并实现组件初始化器
Android App Startup 提供了 Initializer<T>
接口来定义每个组件初始化程序. 此接口定义了两个重要的方法:
create()
方法, 包含初始化组件并返回 T 实例所需的所有操作.dependencies()
方法, 用于返回初始化程序所依赖的其他Initializer<T>
对象的列表. 你可以使用此方法控制应用在启动时运行初始化程序的顺序.
例如, 假设应用依赖于 WorkManager 并且需要在启动时对其进行初始化. 定义一个实现 Initializer<WorkManager>
的 WorkManagerInitializer
类:
// Initializes WorkManager.
class WorkManagerInitializer : 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<*>>> {
// No dependencies on other libraries.
return emptyList()
}
}
dependencies()
方法会返回一个空列表, 因为 WorkManager
不依赖于任何其他库.
假设应用还依赖于一个名为 ExampleLogger
的库, 而该库又依赖于 WorkManager
.
此依赖项意味着需要确保应用启动首先初始化 WorkManager
. 定义一个实现 Initializer<ExampleLogger>
的 ExampleLoggerInitializer
类:
// Initializes ExampleLogger.
class ExampleLoggerInitializer : Initializer<ExampleLogger> {
override fun create(context: Context): ExampleLogger {
// WorkManager.getInstance() is non-null only after
// WorkManager is initialized.
return ExampleLogger(WorkManager.getInstance(context))
}
override fun dependencies(): List<Class<out Initializer<*>>> {
// Defines a dependency on WorkManagerInitializer so it can be
// initialized after WorkManager is initialized.
return listOf(WorkManagerInitializer::class.java)
}
}
由于你在 dependencies()
方法中添加了 WorkManagerInitializer
, 因此 App Startup 会在 ExampleLogger
之前初始化 WorkManager
.
设置 AndroidManifest
App Startup 包含一个名为 InitializationProvider
的特殊 content provider, 它可用于发现和调用组件初始化程序.
应用启动功能会首先检查 InitializationProvider
清单条目下是否有 <meta-data>
条目, 从而发现组件初始化程序. 然后,
应用启动会针对其已发现的所有初始化程序调用 dependencies()
方法.
这意味着, 为了让应用启动能够发现组件初始化程序, 必须满足以下条件之一:
- 组件初始化程序在
InitializationProvider
清单条目下具有相应的<meta-data>
条目. - 组件初始化程序列在已发现的初始化程序的
dependencies()
方法中. 我们再来看一下带有WorkManagerInitializer
和ExampleLoggerInitializer
的示例. 为了确保应用启动能够发现这些初始化程序, 以下内容也必须添加到AndroidManifest
中:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- This entry makes ExampleLoggerInitializer discoverable. -->
<meta-data android:name="com.example.ExampleLoggerInitializer"
android:value="androidx.startup" />
</provider>
与此同时, WorkManagerInitializer
就不需要添加 <meta-data>
条目了, 因为 WorkManagerInitializer
是 ExampleLoggerInitializer
的依赖项. 这意味着, 如果 ExampleLoggerInitializer
可被检测到,
那么 WorkManagerInitializer
也可以被检测到.
而 tools:node="merge"
属性可确保清单合并工具正确解析任何冲突的条目.
运行 lint 检查
因为有可能诸多依赖项是无需要显示声明在 AndroidManifest 文件中的, 这也意味着这些依赖项是无法通过肉眼确保全部会被检测到的,
那么是否有什么工具可以对这些依赖项任务的可达性进行测试, 分析, 保证呢?
你还真别说, App Startup 库还真提供了一组 lint 规则, 可用于检查是否已正确定义组件初始化程序.
你可以通过从命令行运行 ./gradlew :app:lintDebug
来执行这些 lint 检查.
手动初始化组件
通常, 在使用应用启动的时候, InitializationProvider
对象会使用名为 AppInitializer
的实体在应用启动时自动发现并运行组件初始化程序.
不过, 我们也可以直接使用 AppInitializer, 以便手动初始化应用在启动时不需要的组件.
这称为“延迟初始化”, 有助于最大限度地降低启动对应用性能的消耗.
我们必须先为要手动初始化的任何组件停用自动初始化功能.
为单个组件停用自动初始化
如需停用单个组件的自动初始化功能, 请从清单中移除该组件初始化程序的 <meta-data>
条目.
例如, 将以下代码添加到清单文件中, 可停用 ExampleLogger
的自动初始化功能:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.ExampleLoggerInitializer"
tools:node="remove" />
</provider>
我们需要在该条目中使用 tools:node="remove"
, 而不是简单地移除该条目, 以确保合并工具也会从所有其他合并后的清单文件中移除该条目.
为某个组件停用自动初始化后, 该组件的依赖项的自动初始化也会一并停用.
为所有组件停用自动初始化
如需停用所有自动初始化功能, 请从清单中移除 InitializationProvider
的整个条目:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
手动调用组件初始化程序
如果某个组件的自动初始化功能已停用, 您可以使用 AppInitializer
手动初始化该组件及其依赖项.
例如, 以下代码会调用 AppInitializer
并手动初始化 ExampleLogger
:
AppInitializer.getInstance(context).initializeComponent(ExampleLoggerInitializer::class.java)
因此, 应用启动还会初始化 WorkManager
, 因为 WorkManager
是 ExampleLogger
的依赖项.
以上即为 Android Jetpack Startup的核心内容, 我们能够从中看到该库设计的优雅之处和性能方面的可预测性的提升, 对于处于各个不同开发阶段的 Android 应用, 该库都是推荐接入的.
但不足之处可能就是, 在此库出现之前的漫长历史时期内, 许多三方库也可能使用了 content provider 来初始化任务, 对于此种情况, 可以需要提醒对应库的开发人员提供 App Startup 版本的适配.
下面来对比看一下 HarmonyOS 平台上应用启动框架.
HarmonyOS AppStartup
同样的, 作为 HarmonyOS 平台, 也面临着这样的挑战: 即应用启动时通常需要执行一系列初始化启动任务. 但好在 HarmonyOS 能够在该平台诞生之初便能够提供通过总结端侧 OS 面临的通用问题而及早地提供相应地标准的解决方案. 其实这也是所谓的** 后发优势**.
因此, 与 Android Jetpack Startup 类似地, HarmonyOS AppStartup 提供了一种简单高效的应用启动方式, 可以支持多任务的多线程启动, 从而达到加快应用启动速度的目的. AppStartup 通过在一个配置文件中统一设置多个启动任务的执行线程, 执行顺序以及依赖关系, 让执行启动任务的代码变得更加简洁清晰, 易于维护.
AppStartup 支持以自动模式或手动模式执行启动任务, 默认采用自动模式. 在AbilityStage组件容器完成创建后开始加载开发者配置的启动任务, 并执行自动模式的启动任务. 开发者也可以在UIAbility创建完后调用startupManager.run方法, 执行手动模式的启动任务.
启动框架执行时机
但是仍然有 2 点事项需要注意:
- 启动框架只支持在entry类型的Module中使用.
- 启动任务之间不允许存在循环依赖.
定义启动框架配置文件
-
在应用主模块(即entry类型的Module)的
resources/base/profile
路径下, 新建启动框架配置文件. 配置文件为 json 格式, 文件名可以自定义, 本文以startup_config.json
为例.-
在启动框架配置文件
startup_config.json
中, 依次添加各个启动任务的配置信息.假设当前应用启动框架共包含6个启动任务, 任务之间的依赖关系如下图所示. 为了便于并发执行启动任务, 单个启动任务文件包含的启动任务应尽量单一, 本例中每个启动任务对应一个启动任务文件.
启动任务依赖关系图
- 在
ets/startup
路径下, 依次创建6个启动任务文件, 以及一个公共的启动参数配置文件. 文件名称必须确保唯一性.- 创建启动任务文件. 本例中的6个文件名分别为
StartupTask_001~006.ets
. - 创建启动任务参数配置文件. 本例中的文件名为
StartupConfig.ets
.
- 创建启动任务文件. 本例中的6个文件名分别为
- 在启动框架配置文件
startup_config.json
中, 添加所有启动任务以及启动参数配置文件的相关信息.startup_config.json
文件示例如下:
- 在
-
{
"startupTasks": [
{
"name": "StartupTask_001",
"srcEntry": "./ets/startup/StartupTask_001.ets",
"dependencies": [
"StartupTask_002",
"StartupTask_003"
],
"runOnThread": "taskPool",
"waitOnMainThread": false
},
{
"name": "StartupTask_002",
"srcEntry": "./ets/startup/StartupTask_002.ets",
"dependencies": [
"StartupTask_004"
],
"runOnThread": "taskPool",
"waitOnMainThread": false
},
{
"name": "StartupTask_003",
"srcEntry": "./ets/startup/StartupTask_003.ets",
"dependencies": [
"StartupTask_004"
],
"runOnThread": "taskPool",
"waitOnMainThread": false
},
{
"name": "StartupTask_004",
"srcEntry": "./ets/startup/StartupTask_004.ets",
"runOnThread": "taskPool",
"waitOnMainThread": false
},
{
"name": "StartupTask_005",
"srcEntry": "./ets/startup/StartupTask_005.ets",
"dependencies": [
"StartupTask_006"
],
"runOnThread": "mainThread",
"waitOnMainThread": true,
"excludeFromAutoStart": true
},
{
"name": "StartupTask_006",
"srcEntry": "./ets/startup/StartupTask_006.ets",
"runOnThread": "mainThread",
"waitOnMainThread": false,
"excludeFromAutoStart": true
}
],
"configEntry": "./ets/startup/StartupConfig.ets"
}
startup_config.json
配置文件标签说明
属性名称 | 含义 | 数据类型 | 是否可缺省 |
---|---|---|---|
startupTasks | 启动任务配置信息, 标签说明详见下表. | 对象数组 | 该标签不可缺省. |
configEntry | 启动参数配置文件所在路径. | 字符串 | 该标签不可缺省. |
startupTasks
标签说明
属性名称 | 含义 | 数据类型 | 是否可缺省 |
---|---|---|---|
name | 启动任务对应的类名. | 字符串 | 该标签不可缺省. |
srcEntry | 启动任务对应的文件路径. | 字符串 | 该标签不可缺省. |
dependencies | 启动任务依赖的其他启动任务的类名数组. | 对象数组 | 该标签可缺省, 缺省值为空. |
excludeFromAutoStart | 是否排除自动模式, 详细介绍可以查看修改启动模式. - true: 手动模式. - false: 自动模式. | 布尔值 | 该标签可缺省, 缺省值为false. |
runOnThread | 执行初始化所在的线程. - mainThread: 在主线程中执行. - taskPool: 在异步线程中执行. | 字符串 | 该标签可缺省, 缺省值为mainThread. |
waitOnMainThread | 主线程是否需要等待启动框架执行. 当runOnThread取值为taskPool时, 该字段生效. - true: 主线程等待启动框架执行完之后, 才会加载应用首页. - false: 主线程不等待启动任务执行. | 布尔值 | 该标签可缺省, 缺省值为true. |
- 在module.json5配置文件的appStartup标签中, 添加启动框架配置文件的索引. module.json5示例代码如下.
{
"module": {
"name": "entry",
"type": "entry",
// ...
"appStartup": "$profile:startup_config", // 启动框架的配置文件
// ...
}
}
设置启动参数
在启动参数配置文件(本文为ets/startup/StartupConfig.ets
文件)中, 使用StartupConfigEntry
接口实现启动框架公共参数的配置,
包括超时时间和启动任务的监听器等参数, 其中需要用到如下接口:
StartupConfig
: 用于设置任务超时时间和启动框架的监听器.StartupListener
: 用于监听启动任务是否执行成功.
import { StartupConfig, StartupConfigEntry, StartupListener } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
export default class MyStartupConfigEntry extends StartupConfigEntry {
onConfig() {
hilog.info(0x0000, 'testTag', `onConfig`);
let onCompletedCallback = (error: BusinessError<void>) => {
hilog.info(0x0000, 'testTag', `onCompletedCallback`);
if (error) {
hilog.info(0x0000, 'testTag', 'onCompletedCallback: %{public}d, message: %{public}s', error.code, error.message);
} else {
hilog.info(0x0000, 'testTag', `onCompletedCallback: success.`);
}
};
let startupListener: StartupListener = {
'onCompleted': onCompletedCallback
};
let config: StartupConfig = {
'timeoutMs': 10000,
'startupListener': startupListener
};
return config;
}
}
为每个待初始化组件添加启动任务
上述操作中已完成启动框架配置文件, 启动参数的配置, 还需要在每个组件对应的启动任务文件中, 通过实现StartupTask
来添加启动任务.
其中, 需要用到下面的两个方法:
init
: 启动任务初始化. 当该任务依赖的启动任务全部执行完毕, 即onDependencyCompleted
完成调用后, 才会执行init
方法对该任务进行初始化.onDependencyCompleted
: 当前任务依赖的启动任务执行完成时, 调用该方法. 下面以startup_config.json
中的StartupTask_001.ets
文件为例, 示例代码如下. 开发者需要分别为每个待初始化组件添加启动任务.
由于StartupTask采用了
Sendable
协议, 在继承该接口时, 必须添加Sendable
注解.
import { StartupTask, common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
@Sendable
export default class StartupTask_001 extends StartupTask {
constructor() {
super();
}
async init(context: common.AbilityStageContext) {
hilog.info(0x0000, 'testTag', 'StartupTask_001 init.');
return 'StartupTask_001';
}
onDependencyCompleted(dependence: string, result: Object): void {
hilog.info(0x0000, 'testTag', 'StartupTask_001 onDependencyCompleted, dependence: %{public}s, result: %{public}s',
dependence, JSON.stringify(result));
}
}
修改启动模式
AppStartup分别提供了自动和手动两种方式来执行启动任务, 默认采用自动模式. 开发者可以根据需要修改为手动模式.
- 自动模式: 当
AbilityStage
组件容器完成创建后, 自动执行启动任务. - 手动模式: 在
UIAbility
完成创建后手动调用, 来执行启动任务. 对于某些使用频率不高的模块, 不需要应用最开始启动时就进行初始化. 开发者可以选择将该部分启动任务修改为手动模式, 在应用启动完成后调用startupManager.run
方法来执行启动任务. 下面以UIAbility#onCreate
生命周期中为例, 介绍如何采用手动模式来启动任务, 示例代码如下.
import { AbilityConstant, UIAbility, Want, startupManager } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
let startParams = ['StartupTask_005', 'StartupTask_006'];
try {
startupManager.run(startParams).then(() => {
console.log('StartupTest startupManager run then, startParams = ');
}).catch((error: BusinessError) => {
console.info("StartupTest promise catch error, error = " + JSON.stringify(error));
console.info("StartupTest promise catch error, startParams = "
+ JSON.stringify(startParams));
})
} catch (error) {
let errMsg = JSON.stringify(error);
let errCode: number = error.code;
console.log('Startup catch error , errCode= ' + errCode);
console.log('Startup catch error ,error= ' + errMsg);
}
}
// ...
}
开发者还可以在页面加载完成后, 在页面中调用启动框架手动模式, 示例代码如下.
import { startupManager } from '@kit.AbilityKit';
@Entry
@Component
struct Index {
@State message: string = '手动模式';
@State startParams: Array<string> = ['StartupTask_006'];
build() {
RelativeContainer() {
Button(this.message)
.id('AppStartup')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.onClick(() => {
if (!startupManager.isStartupTaskInitialized("StartupTask_006")) { // 判断是否已经完成初始化
startupManager.run(this.startParams)
}
})
.alignRules({
center: {anchor: '__container__', align: VerticalAlign.Center},
middle: {anchor: '__container__', align: HorizontalAlign.Center}
})
}
.height('100%')
.width('100%')
}
}
总结一下
面对工程界相似的问题, 即应用启动时多任务的执行的线程及优先级, Android 和 HarmonyOS 从各自平台出发, 基于不同的语言及特性, 提出了相似的解决方案.
相同之处很明显, 相异之处也很突出.
两者的不同之处可以归纳为:
- 编程语言不同. 这是最大的不同, Android 为 Java/Kotlin, HarmonyOS 为 ArkTS.
- 多任务处理的入口点不同. Android 是基于其本身的组件 content provider 启动的时机一定先于任务 Activity 的特性, 保证了任务界面展示之前将多任务按照依赖图谱依次执行完成. 而 HarmonyOS 则是依赖了自身提供的启动配置入口点(StartupConfig) 保证了多任务的执行.
- 任务执行的基本单位不同. Android App Startup 包提供了
Initializer<T>
接口定义了要处理的任务(onCreate(Context):T
). 而 HarmonyOS 则是提供了StartupTask
的概念, 提供了单个任务的执行(init(AbilityStageContext)
) 及回调(onDependencyCompleted(dependence: string, Object)
). - 多任务的依赖配置不同. Android App Startup 包提供了
Initializer<T>
接口定义了每个任务的依赖任务(dependencies(): List<Initializer<*>>
). 而 HarmonyOS 中任务的依赖的配置则是通过startup_config.json
文件来执行的. - 编程时的方便程序有差异. Android 应用采用 Java/Kotlin + Android Studio 进行开发, 语言层面的自动补全功能十分发达. 而 HarmonyOS 则是采用 JSON+ArkTS + DevEco-Studio 进行开发, IDE 本身及其提供的便利功能尚有欠缺些.
好吧, 今天的文章就到这里啦! 一家之言, 欢迎拍砖!
Happy Coding! Stay GOLDEN!