Arclin

Advocate Technology. Enjoy Technology.

0%

鸿蒙SDK开发探索

本文简述开发鸿蒙第三方 SDK 的选型以及注意事项

HAP

Harmony Ability Package

鸿蒙的 App 可以有多个”窗口“,比如微信作为一个窗口,微信小程序作为另一个窗口,不同的窗口之间有独立的 UIAbility,但是需要保证有一个 Entry
每个 HAP 虽然也可以创建多个 ability,但是官方并不推荐,因为即便是分了多个 ability, 也会加载所有在 HAP 内的资源,用多 HAP 的方式就可以按需加载。
比如视频中同一个应用同时打开了两个窗口,两个窗口各自有各自的生命周期

官方推荐多 HAP 应用以一个 entry,多 feature 为结构

流程图

创建

对应第一个 Empty Ability

创建新 HAP 后目录结构与 entry 目录结构基本一致

多 HAP 跳转

参考如下代码进行跳转
entry : 页面 1, 使用 want 创建目标位置,通过 startAbility 方法进行跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { common, Want } from '@kit.AbilityKit'

@Entry
@Component
struct Index {
context = getContext(this) as common.UIAbilityContext
build() {
Column({ space: 20 }) {
Blank()
Text("Ability")
.fontSize(30)
.fontWeight(FontWeight.Medium)
Button("跳转到另一个Ability")
.onClick(() => {
let want: Want = {
bundleName: "com.example.testsdk", // 在项目根目录/AppScope/app.json5可以得到bundleName
abilityName: "SecondEntryAbility" //在HAP目录/src/main/module.json5中可以得到module.abilities.0.name即为入口abilitiyname
}
this.context.startAbility(want).then(() => {
console.log("跳转成功")
}).catch((error: Error) => {
console.log(`跳转失败: ${error.message}`)
})
})
Blank()
}
.height('100%')
.width('100%')
}
}

feature: 页面 2, 添加一个返回按钮,点击销毁当前 Ability,系统会返回上个 HAP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { common } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'

@Entry
@Component
struct Index {
context = getContext(this) as common.UIAbilityContext
build() {
Column({ space: 20 }) {
Blank()
Text("Second Entry")
.fontSize(30)
.fontWeight(FontWeight.Medium)
Button("Back")
.onClick(() => {
this.context.terminateSelf((error: BusinessError) => {
console.log(`terminateSelf failed, code is ${error.code}, message is ${error.message}`)
return
})
})
Blank()
}
.height('100%')
.width('100%')
}
}

需要配置依赖才可以进行跳转

勾选两个 Module

获取 bundleName

限制

HAP 不支持导出接口和模块,所以只能作为页面/模块入口来使用

AppStorage 表现

AppStorage 是系统提供的应用全局数据共享的单例,使用@StroageLink 或者@StorageProp 可以实现多 Ability 同步,不同 Ability 绑定相同 Key,可以实现实时更新,@StroageLink 则可以实现双向同步

HAR

Harmony Archive

相当于静态库,可以导出接口供其他依赖方使用。按照官方说法,HAR 包会 copy 到各个依赖方,会导致包体有重复内容,内存管理也是独立的,依赖方各自管理各自的 HAR 的内存。

创建

对应 Static Library

提供接口

每个 HAR 根目录里面都有一个 index.ets, 可以通过类似如下语法导出接口

1
export { MainPage } from './src/main/ets/components/MainPage'

页面跳转

添加依赖

如果需要跳转到某个 HAR 的页面,那么需要当前组件添加对该 HAR 的依赖
比如被依赖组件名为 HARLibraryA
在 oh-package.json5 添加 dependencies

1
2
3
"dependencies": {
"@ohos/HARLibraryA": "file:../HARLibraryA"
}

HAP -> HAR

通过 NavigationStack
  1. 主页和被依赖的页面都需要添加 Navigation 包装一层
  2. 主页需要添加 navDestination 判断字符串确定要跳转的页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Component
export struct MainPage {
@Consume('pathStack') pathStack: NavPathStack

@State message: string = 'This is HAR Library';

/// 接收参数
aboutToAppear(): void {
const params = JSON.stringify(this.pathStack.getParamByName("HARLibraryA"))
console.log(para)
}

build() {
NavDestination() {
Column({space: 20}) {
Blank()
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button("Back")
.onClick(() => {
this.pathStack.pop()
})
Blank()
}
}
}
}
import { MainPage } from '@ohos/HARLibraryA'

@Component
struct Index {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack()

@Builder
pageMap(name: string) {
if (name == "HARLibraryA") {
MainPage()
}
}

build() {
Navigation(this.pathStack) {
...
Button("跳转到另一个HAR -- 1")
.onClick(() => {
let param = JSON.stringify({
"data": 'pass parameters'
})
let pathInfo = new NavPathInfo('HARLibraryA', param)
this.pathStack.pushPath(pathInfo, true)
})
...
}
.height('100%')
.width('100%')
}
.navDestination(this.pageMap)
}
}
通过 Router
  1. 被依赖页面添加@Entry({ routeName: 'xxx'})标记页面名字
  2. 主页直接使用 router.pushNamedRoute()跳转

@Entry 修饰的页面不能用 onPageShow()、onPageHide()监听组件生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { router } from '@kit.ArkUI';

@Entry({ routeName: 'HARPage1'})
@Component
export struct MainPage {

@State message: string = 'This is HAR Library';
/// 接收参数
aboutToAppear(): void {
const params = router.getParams() as Record<string, string>
console.log(`${JSON.stringify(params)}`)
}

build() {
NavDestination() {
Column({space: 20}) {
Blank()
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button("Back")
.onClick(() => {
router.back()
}
Blank()
}
}
}
}
import '@ohos/HARLibraryA/src/main/ets/components/MainPage'

@Component
struct Index {
build() {
...
Button("跳转到另一个HAR -- 1")
.onClick(() => {
router.pushNamedRoute({
name: "HARPage1",
params: {
'data': 'pass parameters'
}
})
})
...
}
}
}

使用单例

目前这块表现比较混乱,原因不明,发帖问了一下看看
https://developer.huawei.com/consumer/cn/forum/topic/0210157387499898499?fid=0102683795438680754

限制

没有 entry 入口, 但是可以有各种 page,可以提供各种页面供入口跳转过来

引用方式

静态引用

如同上面提及的 import { MainPage } from ‘@ohos/HARLibraryA’
称之为静态引用,在 oh-package.json5 添加 dependencies 就好

动态常量引用

相比起静态引用,好处是可以等到需要的时候再把模块加载到内存中,同样在 oh-package.json5 添加 dependencies 就好

1
2
3
4
5
6
7
8
9
Button("加载HSP")
.onClick(() => {
import('@ohos/HARLibraryA').then((ns: ESObject) => {
this.HARComponent = ns.HARComponent
console.log('DynamicImport addHar2 4 + 5 = ' + ns.addHar2(4, 5));
}).catch((error: BusinessError) => {
hilog.error(0, TAG,`show component error: ${error.code}, message is ${error.message}`)
})
})
动态变量引用

上文提到的 import(‘@ohos/HARLibraryA’)方式,在编译的时候编译器会自动加入到依赖树中
但是如果是通过变量传参,那么编译器就无法自动加入到依赖树中

1
2
3
4
5
6
7
8
9
var module = ""
if (login) {
module = "loginModule"
} else {
module = "featureModule"
}
import(module).then((ns: ESObject) => {
ns.showMainPage()
})

这时候需要去到 build-profile.json5 添加字段 runtimeOnly,再添加 packages,输入的内容跟 oh-package.json5 里面的 dependencies 名字一致

1
2
3
4
5
6
7
8
9
10
11
{
"apiType": "stageMode",
"buildOption": {
"runtimeOnly": {
"packages": [
"@ohos/HARLibraryA"
]
}
}
...
}

动态 import 还可以 import 文件路径,比如

1
2
let filePath = './Calc';
import(filePath).then(……);

如果是这种的话,build-profile.json5 的配置就加上 sources

1
2
3
4
5
6
7
8
"runtimeOnly": {
"packages": [
"@ohos/HARLibraryA"
],
"sources": [
"./Calc"
]
}

循环依赖

有时候会出现循环依赖的情况,按照以往我们会添加中间层解耦

现在官方提供了一种新的方案(仅适用于HAR和HAP之间的依赖 与 HSP与HAR之间的依赖)

这种方案的好处是,即便HAR1和HAR2中的dependencies里面没有对HAR2和HAR1的依赖配置,也可以通过动态import依赖到。
相当于:
HAR1和HAR2的依赖转移到了HAP

另外我们也可把HAR的的依赖转移到HSP,如下图,但是HAR3和HAR4和HSP的依赖不能转移到HAP

下面解释如何操作:

需要再HAP的oh-package.json5的dependencies和build-profile.json5的runtimeOnly里面同时配置两个HAR模块的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
/// oh-package.json5
"dependencies": {
"@ohos/HARLibraryA": "file:../HARLibraryA",
"@ohos/HARLibraryB": "file:../HARLibraryB"
}

/// build-profile.json5
"runtimeOnly": {
"packages": [
"@ohos/HARLibraryA",
"@ohos/HARLibraryB"
]
}

首先在HAP里面调用HAR1的方法

1
2
3
4
5
6
7
8
9
Button("调用HAR的方法")
.onClick(() => {
let har1 = '@ohos/HARLibraryA'
import(har1).then((ns: ESObject) => {
hilog.info(0, TAG, `${ns.addHar1(1, 2)}`)
}).catch((error: BusinessError) => {
hilog.error(0, TAG,`show component error: ${error.code}, message is ${error.message}`)
})
})

HAR1调用HAR2的方法,这里必须使用动态变量import,不可以import('@ohos/HARLibraryB')

1
2
3
4
5
6
7
8
9
10
11
12
export function addHar1(a:number, b:number):number {
let c = a + b;
hilog.info(0, TAG, 'DynamicImport I am har1, %d + %d = %d', a, b, c);

let harName = '@ohos/HARLibraryB';
import(harName).then((ns:ESObject) => {
hilog.info(0, TAG, 'DynamicImport addHar2 4 + 5 = ' + ns.addHar2(4, 5));
}).catch((error: BusinessError) => {
hilog.error(0, TAG,`har1 error: ${error.code}, message is ${error.message}`)
})
return c;
}

HAR2实现addHar2方法并且export

1
2
3
4
5
6
export function addHar2(a:number, b:number):number {
let c = a + b;
hilog.info(0, "INDEX_TAG", 'DynamicImport I am har2, %d + %d = %d', a, b, c);

return c;
}

效果达成,并且要注意Promise是异步的

HSP

Harmony Shared Package
类似于动态库,内存共享,按需下载

创建

对应 SharedLibrary

提供接口

方式同 HAR

页面跳转

本地引用

本地引用可以利用 HSP 的内存复用能力,但是不能使用动态下载能力,即下载到用户手机的包体还是会增大

使用 Navigation

主页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { SharedMainPage } from "@ohos/SharedLibrary"

@Entry
@Component
struct Index {
@Provide("pathStack") pathStack: NavPathStack = new NavPathStack()

@Builder
pageMap(name: string) {
if (name == 'hps') {
SharedMainPage()
}
}

build() {
Navigation(this.pathStack) {
Column({ space: 20 }) {
Blank()
Text("Ability")
.fontSize(30)
.fontWeight(FontWeight.Medium)
Button("Jump To HSP")
.onClick(() => {
const info = new NavPathInfo("hps", "params")
this.pathStack.pushPath(info,true)
})
}
}
.navDestination(this.pageMap)
.height('100%')
.width('100%')
}
}

HSP 页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { router } from '@kit.ArkUI';

@Entry
@Component
export struct SharedMainPage {
@State message: string = 'Shared MainPage';

@Consume("pathStack") navStack: NavPathStack

build() {
NavDestination() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button("跳转到HAR")
.onClick(() => {
router.pushNamedRoute({
name: "HARPage1"
})
})
}
.width('100%')
}
}
.height('100%')
}
}

使用 Router

主页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { SharedMainPage } from "@ohos/SharedLibrary"
import { router } from '@kit.ArkUI'
import '@ohos/SharedLibraryB/src/main/ets/pages/Index'

@Entry
@Component
struct Index {
build() {
Column({ space: 20 }) {
Blank()
Text("Ability")
.fontSize(30)
.fontWeight(FontWeight.Medium)
Button("Jump To HSP 2")
.onClick(() => {
router.pushNamedRoute({
name: "SharedMainPageB"
})
})
}
.height('100%')
.width('100%')
}
}

HSP 页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { router } from '@kit.ArkUI';

@Entry({routeName: "SharedMainPageB"})
@Component
export struct SharedMainPageB {
@State message: string = 'Shared MainPage B';

build() {
Row() {
Column({space: 20}) {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button("跳转到HAR")
.onClick(() => {
router.pushNamedRoute({
name: "HARPage1"
})
})
}
.width('100%')
}
.height('100%')
}
}

动态引用

动态引用既可以利用 HSP 的内存复用能力,又可以利用 HSP 的下载能力,前提是配置依赖需要配置到 dynamicDependencies 而不是 dependencies
HSP 不支持独立发布,而是跟随宿主包上传,应用市场控制安装时只下载 HAP、HAR 到用户本地,HSP 下载时机由开发者自行控制。
参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/store-moduleinstall-V5
示例:点击按钮展示动态模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Entry
@Component
struct Index {
@BuilderParam SharedModuleComponent: Function
@State isShow: boolean = false

build() {
Row() {
Column({ space: 20 }) {
Blank()
Button("加载HSP")
.onClick(() => {
this.initSharedModule('SharedLibrary', () => {
import('SharedLibrary').then((ns: ESObject) => {
this.SharedModuleComponent = ns.showSharedComponent
this.isShow = true
}).catch((error: BusinessError) => {
hilog.error(0, TAG,`show component error: ${error.code}, message is ${error.message}`)
})
})
})
if (this.isShow) {
this.SharedModuleComponent()
}
}
.width('100%')
}
.height('100%')
}
}

配置依赖:给入口的 oh-package.json5 添加 dynamicDependencies 字段

1
2
3
4
5
6
7
8
9
10
11
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dynamicDependencies": {
"SharedLibrary": "file:../SharedLibrary"
}
}

动态引用分为三步

  1. 判断是否已经安装到本地,这个方法实现了判断是否加载并且调用下载功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// 检查SharedModule是否已经加载
private initSharedModule(name: string, successCallback: Callback<void>): void {
try {
const result: moduleInstallManager.InstalledModule = moduleInstallManager.getInstalledModule(name)
if (result?.installStatus === moduleInstallManager.InstallStatus.INSTALLED) {
hilog.info(0, TAG, 'SharedLibrary is Installed')
successCallback && successCallback()
} else {
hilog.info(0, TAG, 'SharedLibrary not installed')
this.fetchModule(name, successCallback)
}
} catch (error) {
hilog.error(0, TAG, `get install module error.code is ${error.code}, message is ${error.message}`)
}
}
  1. 如果安装了则直接展示
1
2
3
4
if (result?.installStatus === moduleInstallManager.InstallStatus.INSTALLED) {
hilog.info(0, TAG, 'SharedLibrary is Installed')
successCallback && successCallback()
}
  1. 如果没安装则下载后展示,这个方法实现了下载后回调 callback 的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private fetchModule(moduleName: string, successCallback: Callback<void>) {
try {
hilog.info(0, TAG, 'handle fetch modules start')
const context = getContext(this) as common.UIAbilityContext;
const moduleInstallProvider: moduleInstallManager.ModuleInstallProvider = new moduleInstallManager.ModuleInstallProvider()
const moduleInstallRequest: moduleInstallManager.ModuleInstallRequest = moduleInstallProvider.createModuleInstallRequest(context)
if (!moduleInstallRequest) {
hilog.warn(0, TAG, 'moduleInstallRequest is empty');
return;
}
moduleInstallRequest.addModule(moduleName)
moduleInstallManager.fetchModules(moduleInstallRequest).then((data: moduleInstallManager.ModuleInstallSessionState) => {
hilog.info(0,TAG, 'succeed in fetching modules result')
if (data.code === moduleInstallManager.RequestErrorCode.SUCCESS) {
this.onListenEvents(successCallback)
} else {
hilog.info(0, TAG, `fetch modules failure ${data.code} reason ${data.desc}`)
}
}).catch((error: Error) => {
hilog.error(0, TAG, `fetchmodules onError.code is ${error.name}, message is ${error.message}`);
})
} catch (error) {
hilog.error(0, TAG, `fetchmodules onError.code is ${error.code}, message is ${error.message}`);
}
}

总结

关于包的选型,建议如下结构

关于HAR和HSP
如果需要,共享内存或者动态下载,使用HSP,否则使用HAR;如果存在单例,使用HSP。

HAP、HAR、HSP三者的功能和使用场景总结对比如下:

模块类型 包类型 说明
Ability HAP 应用的功能模块,可以独立安装和运行,必须包含一个entry类型的HAP,可选包含一个或多个feature类型的HAP。
Static Library HAR 静态共享包,编译态复用。
- 支持应用内共享,也可以发布后供其他应用使用
- 作为二方库,发布到OHPM私仓,供公司内部其他应用使用
- 作为三方库,发布到OHPM中心仓,供其他应用使用
- 多包(HAP/HSP)引用相同的HAR时,会造成多包间代码和资源的重复拷贝,从而导致应用包膨大
- 注意:编译HAR时,建议开启混淆能力,保护代码资产
Shared Library HSP 动态共享包,运行时复用。
- 当前仅支持应用内共享
- 当多包(HAP/HSP)同时引用同一个共享包时,采用HSP替代HAR,可以避免HAR造成的多包间代码和资源的重复拷贝,从而减小应用包大小

HAP、HSP、HAR支持的规格对比如下,其中“√”表示是,“×”表示否。

规格 HAP HAR HSP
支持在配置文件中声明UIAbility组件与ExtensionAbility组件 × ×
支持在配置文件中声明pages页面 ×
支持包含资源文件与.so文件
支持依赖其他HAR文件
支持依赖其他HSP文件
支持在设备上独立安装运行 × ×