【HarmonyOS】HMRouter关键原理-动态import
动态加载(动态import)
使用场景
如果希望根据条件导入模块或者按需导入模块,可以使用动态导入代替静态导入
- 当静态导入的模块明显降低了代码的加载速度而且被使用的可能性较低
- 当静态导入的模块明显占用的大量内存资源且被使用的可能很低
- 当被导入的模块在加载时并不存在,需要异步获取
- 当被导入的模块说明符,需要动态构建时(静态导入只能使用静态说明符)
- 当被导入的模块有副作用,如模块中会直接运行的代码时
动态import介绍
- 当前所有import中使用的模块名是依赖方oh-package.json5的dependencies中的别名
- import一个模块名,实际上机会import该模块的入口文件,一般为Index.ets
常量动态import
//HAR模块的Index.ets
export {add,swap} from './src/main/ets/pages/page2'
//HAP模块
import('testhar/Index').then((ns: ESObject) => {console.log('[result:]' + ns.add(1,2))
})
变量动态import
概述
- 在方舟编译器中,模块间的依赖关系通过oh-package.json5中的dependencies字段来配置,这字段中的所有模块都会进行安装,但是默认不参与编译
- HAP编译时会以入口文件开始搜索依赖关系,搜索的到的模块会加入编译,import顺序为后序遍历(不同的模块之间的联系抽象为树结构)
- 在编译时,静态import和常量动态import可以被打包工具识别,并被加入依赖树种,参与编译,最终生成方舟字节码。
- 但是如果是变量动态import,在编译时无法确定变量的值,也就无法加入编译,而要加入编译,就要额外进行一些配置,用于指定动态import的变量实际的值
配置runtimeOnly
//在在HAP/HSP/HAR的build-profile.json5中
"buildOption": {"arkOptions": {"runtimeOnly": {"packages": [ "myhar" ], // 配置本模块变量动态import其他模块名,要求与dependencies中配置的名字一致。"sources": [ "./src/main/ets/utils/Calc.ets" ] // 配置本模块变量动态import自己的文件路径,路径相对于当前build-profile.json5文件。}}
}
使用实例
// 变量动态import其他模块myhar
let harName = 'myhar';
import(harName).then((obj: ESObject) => {console.info('DynamicImport I am a har');
})// 变量动态import本模块自己的单文件src/main/ets/index.ets
let filePath = './utils/Calc';
import(filePath).then((obj: ESObject) => {console.info('DynamicImport I am a file');
})
为什么要使用动态import
HAR包之间依赖解耦
- 如果使用静态import或者常量动态import,当应用中含有多个HAR包时,在配置HAR之间的依赖关系时,可能会形成HAR之间循环依赖。
- 而如果使用变量动态import,就可以在将HAR包之间的依赖转移到HAP中去配置,HAR包之间无需配置依赖关系,从而达到HAR包之间的依赖解耦
使用实例
首先构筑一个存在循环依赖的项目,其中每个Har包之间的依赖关系都在各自的oh-package.json5和build-profile.json5中单独配置
在该项目工程中,Har2和Har1出现了循环依赖,导致ohmp无法安装Har2包
解决问题就需要将HAR之间的dependencies和runtimeOnly字段的相关配置转移到HAP中,实现HAR包之间的依赖解耦
// HAP's oh-package.json5
"dependencies": {"har1": "file:../har1","har2": "file:../har2","har3": "file:../har3"
}
// HAP's build-profiles.json5
"buildOption": {"arkOptions": {"runtimeOnly": {"packages" : [ // 仅用于变量动态加载的场景,静态加载或常量动态加载无需配置。"har1","har2","har3"]}}
}
基于Node-API加载模块
Node-API支持开发者在C++侧加载工程内的模块及文件
napi_load_module_with_info
该接口可以在主线程或者子线程中进行模块的加载,模块加载成功之后可以使用napi_get_property获取模块导出的变量,使用napi_get_named_property获取导出的函数
函数说明
napi_status napi_load_module_with_info(napi_env env, const char* path, const char* module_info, napi_value* result);
- env:当前的虚拟机环境
- path:加载的文件路径或者模块名
- module_info:bundleName/moudleName的路径拼接
- result:加载的模块
由于该接口使用范围更广,不仅可以在主线程也可以在子线程中使用,HMRouter中使用的就是napi_load_module_with_info来加载模块
napi_load_module
该接口用于在主线程中进行模块的加载,加载成功之后,可以使用同样的函数获取发哦出的变量和函数
函数说明
napi_status napi_load_module(napi_env env, const char* path, napi_value* result);
- env:当前的虚拟机环境
- path:加载的文件
该接口虽然只能在主线程中使用,但是入参更少,使用更便捷
实践
- 使用Node-API 我们首先首先创建一个Native工程或者模块,在这里以创建一个新的Native工程为例,创建完成之后在main目录下,相较于一般的工程会多出一个cpp文件夹
- 实现模块的注册以及ArkTS接口与C++接口的绑定、设置打包参数等(这些在创建好工程后,在代码中已经初始完毕)
下面给出模块注册和接口绑定的部分代
码
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{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
//准备模块加载的相关信息,将上述Init函数与本模块的名称记录下来
static napi_module demoModule = {.nm_version = 1,.nm_flags = 0,.nm_filename = nullptr,.nm_register_func = Init,.nm_modname = "entry",.nm_priv = ((void*)0),.reserved = { 0 },
};
//加载该模块的.so时,该函数会被自动调用,将上述demoModule模块注册到系统中
extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{napi_module_register(&demoModule);
}
- 实现Native侧的接口,新建工程后,系统会自动生成一个名为add的接口,为了便于演示,在这里我们直接修改接口中的内容
static napi_value Add(napi_env env, napi_callback_info info)
{napi_value result;// 1. 使用napi_load_module_with_info加载Test文件中的模块napi_status status = napi_load_module_with_info(env, "entry/src/main/ets/pages/test1", "com.example.NativeTest/entry", &result);if (status != napi_ok) {return nullptr;}napi_value testFn;// 2. 使用napi_get_named_property获取test函数napi_get_named_property(env, result, "test", &testFn);// 3. 使用napi_call_function调用函数testnapi_call_function(env, result, testFn, 0, nullptr, nullptr);return result;}
注意在使用API时,若API的参数中有文件路径的选项,一般不用加上文件的后缀
- ArkTS端被动态import的模块
function test() {console.log("Hello HarmonyOS");
}
export {test};
这里的函数名需要与Native侧中使用napi_call_function接口中传入的函数名一致
- ArkTS端调用
import testNapi from 'libentry.so';
testNapi.add(0,0)
为了方便演示,我并没有修改参数列表,所以两个参数可以随便穿,实际并没有用到
- 因为使用的是动态import,还是需要在当前模块的build-profile.json5中进行配置runtimeOnly字段,因为这是加载模块内的文件,所以不用配置oh-package.json5
"buildOption": {"arkOptions": {"runtimeOnly": {"sources": ['./src/main/ets/pages/test1.ets']}}}
需要注意的是若使用的是模拟器,还需要修改build-profile.json5中的abiFilters字段,该字段是表示当前鸿蒙支持的ABI编译环境,包括:
- arm64-v8a
- x64_64
arm64主要是当前移动设备的主流框架,x86则是主要用于电脑端的模拟器,该字段若不设置默认为arm64-v8a
HMRouter的动态加载原理
HMRouter编译期
hmrouter-plugin插件会在编译期自动解析代码中配置的装饰器,并在rawfile中生成系统路由表,方便HMRouter初始化时读取路由表
其中部分内容如下
{"name": "__interceptor__homeCheckName","pageSourceFile": "src/main/ets/model/HomeLoginInterceptor.ets","buildFunction": "","customData": {"name": "EditCheckInterceptor","interceptorName": "homeCheckName"},"moduleNodeDir": "D:\\Harmony\\Project\\HMjd\\feature\\home","ohmurl": "@normalized:N&&&home/src/main/ets/model/HomeLoginInterceptor&1.0.0","bundleName": "com.example.hmjd.shop","moduleName": "entry","packageName": "home"
},
这是在开发态时定义的一个路由拦截器经HMRouter插件编译后生成的路由表内容
- ohmurl中含有该模块的文件路径
- bundleName为App的bundleName
- moduleName
@HMInterceptor({ interceptorName: CommonConstants.homeCheckName }) export class EditCheckInterceptor implements IHMInterceptor { handle(info: HMInterceptorInfo): HMInterceptorAction { if (AppStorage.get(CommonConstants.curToken) != '') { return HMInterceptorAction.DO_NEXT } else { //info.context.getPromptAction().showToast({message: '请登录'}); AppStorage.setOrCreate(CommonConstants.homeBindSheetState, true)return HMInterceptorAction.DO_REJECT}} }
HMRouter运行期
在运行期使用napi_load_module_with_info所需的参数,由编译而成的路由表JSON数据解析而成
- path参数通过ohmurl解析而成
- module_info由bundleName和moduleName拼接而成
解析部分的源码
public importComponent<T>(): T | undefined {let modulePath: string | undefined = ImportUtil.getModuleInfoFromOhmUrl(this.ohmurl);if (!modulePath) {HMLogger.error('[HMRouter Store] get module path error,HMComponent data is %s', JSON.stringify(this));return undefined;}let moduleInfo = this.bundleName + HMRouterConst.MODULE_INFO_SEPARATOR + this.moduleName;return ImportUtil.nativeImport<T>(modulePath, moduleInfo, this.className);}
userNormalizedOHMUrl
一个ets文件在编译之后会成为安装包的一部分,每个ets文件对应的字节码称为一个字节码段,OHMUrl是用来定位一个字节码段的标识
HMRouter使用动态加载,可以在运行态动态加载指定的模块。使用时必须开启useNormalizedOHMUrl,若不开启,则会导致动态加载失效
useNormalizedOHMUrl的开启在工程级的build-profile.json5中设置
- 该字段表示是否使用标准化的OHMUrl
- 使用集成态HSP和字节码HAR需要使用标准化的OHMUrl
- 构建HAR有debug、release、字节码三种模式,前两种主要用于调试,debug会包含部分源码,release会包含js中间码,字节码HAR包中包含的是编译后的abc字节码,当被其他模块依赖时,不需要再对依赖的HAR中的代码进行语法检查和编译,相较于前两种构建效率更高
参考
https://gitee.com/hadss/hmrouter/wikis/HMRouter%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D#1332-hmrouter%E8%BF%90%E8%A1%8C%E6%9C%9F