【鸿蒙应用开发】预加载优化冷启动性能
本文针对HarmonyNext平台应用冷启动性能问题,提出so并行加载优化方案。通过分析发现主线程串行加载so耗时严重,创新性地在AbilityStage.onCreate阶段启动子线程预加载so。方案通过新增napi模块实现四类so的分类加载(主线程so/预验证so/应用so/系统so),并采用napi_load_module方式确保稳定性。优化中需注意dlopen大锁竞争问题,通过控制子线程加
目录
摘要
某应用在HarmonyNext平台冷启动性能较其他平台差距较大,网上有较多舆情反馈启动缓慢。本文介绍了我们分析该应用冷启动trace发现so加载时间过长后,如何通过预加载/并行加载so的手段优化冷启动中的so加载时间,最终优化效果明显,最大可把所有so加载流程并行化。
背景分析
我们在分析应用冷启动trace时发现so加载既零碎耗时又多,且均在主线程串行执行,十分影响冷启动时长。

HandleLaunchAbility阶段结束后,主线程仍存在较多dlopen so的耗时
onForeground回调阶段结束后,主线程仍存在较多dlopen so的耗时
从上图可以看出,由于动态加载模块较多,HandleLaunchAbility或onForeground结束后,主线程仍存在较多dlopen so的耗时。所以我们提出可以把主线程的加载so动作提前,在应用刚启动时起子线程并行加载。并行加载有明显好处:
(1)避免加载启动过程不需要的冗余so
(2)充分利用Evaluate阶段前CPU能力
(3)子线程加载减少主线程负担
然后再分析可以被并行加载的so应该有如下几个特征:
1、应用侧在使用so中某个函数前,经过dlopen打开so。
2、so在dlopen之后的linking是进程内共享的,子线程dlopen与主线程基本上拥有相同效果,如果在主线程dlopen之前子线程已经dlopen过,主线程仅执行module init就可以了,并行加载主要省去的就是这段dlopen so的耗时。
3、因dlopen内存在一把大锁,尽可能不要同时在两个线程同时dlopen。
结合以上,可以在主线程开始dlopen so之前拉起一个子线程执行dlopen,或控制子线程执行时序,通过trace图找一个主线程不会dlopen的时间段进行dlopen操作。

子线程拉起时机
trace中 H:SourceTextModule::Instantiate是模块化解析,解析出ArkTS Module的依赖。H:SourceTextModule::Evaluate是so加载+ArkTS对象创建。AbilityStage是应用最早执行的代码,在其生命周期内启动子线程可以给出最多时间用于加载so。同时由于大锁的存在,加载时机应该在“应用进程可以创建子线程”到“主线程开始加载so”之间。综合一下,可以把加载so的动作放在应用一开始的AbilityStage的onCreate回调阶段,实现并行加载。
详细方案
应用阶段的并行加载——如上文提到的,在应用加载so之前创建子线程提前开始并行加载指定的应用so。为此我们新增了一个napi模块,通过修改配置让进程在进程刚启动时把这个模块拉起,在模块加载入口会启动子线程执行多个预加载任务,下面是此方案的详细应用适配。
1.添加新napi模块
- entry/src/main/cpp/types/libportal/Index.d.ts (napi模板,没有实际用途)
export const add: (a: number, b: number) => number;
- entry/src/main/cpp/types/libportal/oh-package.json5
{ "name": "libportal.so", "types": "./Index.d.ts", "version": "", "description": "" } - entry/src/main/cpp/CMakeList.txt
cmake_minimum_required(VERSION 3.4.1) project(portal) add_library(portal SHARED napi_init.cpp) target_link_libraries(portal PUBLIC libace_napi.z.so libhilog_ndk.z.so libqos.so libhitrace_ndk.z.so)
2. 添加并行加载so内容
并行加载so可以分四类:
(1)主线程so,必须在主线程加载的应用so,需要被最先加载
(2)预验证so,这部分应用so不能提前加载,但是可以提前鉴权
(3)预加载应用so,可以提前加载的应用so(需要把自身及依赖的so的nm_flags改成100,代表其为三方so,以支持在子线程中加载)
(4)预加载系统so,系统so有两种加载方式,
第一种是通过so名字进行加载,此方案需要注意,如果系统so的地址随版本变化,预加载也会在某个版本后失效,且当前部分系统so暂不支持在子线程中dlopen所以不建议用此方式。后续系统so的预加载方案在新系统版本应该会有其他优化方案,简化预加载流程。
void preload() {
pthread_setname_np(pthread_self(), "preload_test");
void *handle = dlopen("/system/lib64/module/libaccessibility_napi.z.so", RTLD_LAZY);
}
另一种是通过模块名方式加载,这种方法需要在子线程中初始化虚拟机环境,且napi_load_module会调用so的module init,虽然有冗余操作,但因为比较稳定,我们选用此方案。
void preload() {
pthread_setname_np(pthread_self(), "preload_test");
napi_env env;
napi_status ret = napi_create_ark_runtime(&env);
napi_value result;
napi_load_module(env, "@ohos.accessibility", &result);
}
entry/src/main/cpp/napi_init.cpp
#include "napi/native_api.h"
#include <thread>
#include <dlfcn.h>
#include "qos/qos.h"
const char *VALIDATING_SO_NAMES[] = {
"libxxx.so",
};
const char *SO_NAMES[] = {
"libyyy.so",
};
const char *SYS_MODULES[] = {
"@ohos.bluetooth.access",
"@ohos.arkui.observer",
"@ohos.hilog",
};
const char *MAIN_SO_NAMES[] = {
"libopenssl.so",
};
void preValidateAppSo() {
pthread_setname_np(pthread_self(), "prevalidate_app_so");
OH_QoS_SetThreadQoS(QoS_Level::QOS_USER_INTERACTIVE); // 任务优先级调最高
const size_t size = sizeof(VALIDATING_SO_NAMES) / sizeof(VALIDATING_SO_NAMES[0]);
for (size_t i = 0; i < size; ++i) {
dlopen(VALIDATING_SO_NAMES[i], RTLD_NLOAD); // 只提前鉴权
}
}
void preloadSysMod() {
pthread_setname_np(pthread_self(), "preload_sys_mod");
OH_QoS_SetThreadQoS(QoS_Level::QOS_USER_INTERACTIVE);
napi_env env;
napi_status ret = napi_create_ark_runtime(&env);
if (ret != napi_ok) {
return;
}
const size_t size = sizeof(SYS_MODULES) / sizeof(SYS_MODULES[0]);
for (size_t i = 0; i < size; ++i) {
napi_value result;
napi_load_module(env, SYS_MODULES[i], &result);
}
}
void preloadAppSo() {
pthread_setname_np(pthread_self(), "preload_app_so");
OH_QoS_SetThreadQoS(QoS_Level::QOS_USER_INTERACTIVE);
const size_t size = sizeof(SO_NAMES) / sizeof(SO_NAMES[0]);
for (size_t i = 0; i < size; ++i) {
dlopen(SO_NAMES[i], RTLD_NOW);
}
preloadSysMod(); // 预加载系统模块
}
static napi_value Add(napi_env env, napi_callback_info info) {
size_t requireArgc = 2;
size_t argc = 2;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
napi_valuetype valuetype0;
napi_typeof(env, args[0], &valuetype0);
napi_valuetype valuetype1;
napi_typeof(env, args[1], &valuetype1);
double value0;
napi_get_value_double(env, args[0], &value0);
double value1;
napi_get_value_double(env, args[1], &value1);
napi_value sum;
napi_create_double(env, value0 + value1, &sum);
return sum;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
const size_t size = sizeof(MAIN_SO_NAMES) / sizeof(MAIN_SO_NAMES[0]);
for (size_t i = 0; i < size; ++i) {
dlopen(MAIN_SO_NAMES[i], RTLD_NOW); // 只能主线程加载的so
}
std::thread t1(preValidateAppSo); // 只需要鉴权的so
t1.detach();
std::thread t2(preloadAppSo); // 预加载应用so
t2.detach();
napi_property_descriptor desc[] = {
{"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr},
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "portal",
.nm_priv = ((void *)0),
.reserved = {0}
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
napi_module_register(&demoModule);
}
3. 在entry中添加新模块
- entry/oh-package.json5
{ "license": "", "devDependencies": { "@ohos/hypium": "1.0.11" }, "author": "", "name": "startup", "description": "Application launch example.", "main": "", "version": "1.0.0", "dependencies": { "libportal.so": "file./src/main/cpp/types/libportal", // 其他应用模块 } } - entry/build-profile.json5
{ "apiType": "stageMode", "buildOption": { "arkOptions": { "runtimeOnly": { "sources": [], "packages": [ "libportal.so", // 其他应用so ] } }, "externalNativeOptions": { "path": "./src/main/cpp/CMakeLists.txt", "arguments": "", "cppFlags": "" } } }
在AbilityStage.ets中添加导入,注意这个文件需要是module.json5中定义的srcEntry,且必须是继承Ability生命周期的AbilityStage.ets或EntryAbility.ets
import libportal from 'libportal.so'
export class A extends AbilityStage {
onCreate() {
libportal.add(1, 2)
}
}
特别注意
- 由于加载so会需要调用到musl库的dlopen,而dlopen中有把大锁,当主线程和子线程同时dlopen的时候,会存在资源竞争,如果子线程先抢到锁,那么主线程就得等待子线程执行完才能继续往下执行。抢锁可能导致本来想通过子线程加载so来分担主线程的负载,结果适得其反,所以加载结束的时机还需要实际测试来确认。

抢锁导致等待
解决的思路有两种:
(1)优化大锁,可以对锁的粒度进行拆分,但需要系统侧修改,短期内不可行;
(2)寻找主线程没有加载so的空隙,穿插着执行子线程。短期内选择第二种解决方案。通过观察trace寻找主线程没有加载so的空隙,最终我们发现有一段170ms的空隙,尽量控制子线程so加载时间在170ms内。
- nm_flags:希望提前加载的so,必须把它和它依赖的所有so的nm_flags改为100,以支持子线程加载so。否则可能会导致so加载顺序混乱,可能导致问题。所以正确步骤应该先扫描so依赖关系,然后统一修改nm_flags,最后一起添加进预加载名单中。后续系统提供了替换接口napi_onLoad,在加载so时会自动查找符号napi_onLoad并自动调用,所以可以将nm_flags+constructor的注册方式替换为napi_onLoad。
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) { napi_module_register(&demoModule); } // 替换为 extern "C" void napi_onLoad() { napi_module_register(&demoModule); }
- napi_load_module,该接口是使用爬栈获取某些上下文信息的,某些子线程缺少上下文环境,所以在非主线程中可能会执行失败,请尽量使用napi_load_module_with_info接口替换,二者只是入参不同,使用没有差异。
最终结果
该性能优化措施理论上限是应用所有加载so的耗时,即使仅预加载无风险的系统so,收益也很可观,具体到应用A约为100ms;算上应用so优化时间差异更大,约200ms,下图是仅预加载系统so之后的trace结果。

预加载修改后trace图
更多推荐


所有评论(0)