目录

摘要

背景分析

详细方案

特别注意

最终结果


摘要

某应用在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图

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐