HarmonyOS NEXT 高级
DFX

Performance Analysis Kit
HiLog
import { hilog } from '@kit.PerformanceAnalysisKit';
hilog.isLoggable(0xFF00, "testTag", hilog.LogLevel.INFO);
hilog.info(0xFF00, "testTag", "%{public}s World %{public}d", "hello", 3);
setMinLogLevel(level: LogLevel)
setLogLevel(level: LogLevel, prefer: PreferStrategy)
- 头两个参数是domain和tag,用
isLoggable判断时,主要这两个参数也需要对上 %{public}s和%{public}d是占位符,%{public}是可选的,表示这个参数是公共的,可以被其他模块使用,%{private}表示私有- 其实就是
%s,加上了public这种隐私修饰符 - 格式化类型有
s,d/i,o/O(object的意思)
- 其实就是
hilog的日志级别有DEBUG、INFO、WARN、ERROR、FATAL,DEBUG级别最低,FATAL级别最高
HiAppEvent
- 提供事件打点机制
- 通过HiAppEvent的接口
addWatcher,开发者可以注册监听自己关注的系统事件或应用事件。- 系统事件信息的存放路径不在应用沙箱目录中,因此无法直接获取。
- 应用事件需要开发者主动触发,调用
write接口进行打点
- 订阅接口addWatcher是同步接口,涉及IO操作。对于性能有要求的模块,建议将接口的调用放到非主线程。
- 订阅接口addWatcher传入的名称name是唯一的,相同的name,后一次调用会覆盖前一次的订阅。
import { hiAppEvent, hilog } from '@kit.PerformanceAnalysisKit';
// 订阅崩溃事件
// EntryAbility.ets
hiAppEvent.addWatcher({
// 开发者可以自定义观察者名称,系统会使用名称来标识不同的观察者
name: "AppCrashWatcher",
// 订阅过滤条件,这里是订阅了系统事件中的崩溃事件
appEventFilters: [
{
domain: hiAppEvent.domain.OS,
names: [hiAppEvent.event.APP_CRASH]
}
],
// 实现onReceive回调,监听到事件后实时回调
onReceive: (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
hilog.info(0x0000, 'testTag', `domain=${domain}`);
for (const eventGroup of appEventGroups) {
hilog.info(0x0000, 'testTag', `HiAppEvent eventName=${eventGroup.name}`);
for (const eventInfo of eventGroup.appEventInfos) {
// 开发者可以获取到崩溃事件发生的时间戳
hilog.info(0x0000, 'testTag', `HiAppEvent eventInfo.params.time=${JSON.stringify(eventInfo.params['time'])}`);
// 开发者可以获取到崩溃应用的包名
hilog.info(0x0000, 'testTag', `HiAppEvent eventInfo.params.bundle_name=${JSON.stringify(eventInfo.params['bundle_name'])}`);
// 开发者可以获取到崩溃事件发生时的故障日志文件
hilog.info(0x0000, 'testTag', `HiAppEvent eventInfo.params.external_log=${JSON.stringify(eventInfo.params['external_log'])}`);
}
}
}
});
// 触发崩溃
// Index.ets
Button("appCrash")
.onClick(()=>{
let result: object = JSON.parse("");
})
订阅应用事件(如按钮点击)
// EntryAbility.ets
hiAppEvent.addWatcher({
// 开发者可以自定义观察者名称,系统会使用名称来标识不同的观察者
name: "ButtonClickWatcher",
// 开发者可以订阅感兴趣的应用事件,此处是订阅了按钮事件
appEventFilters: [{ domain: "button" }],
// 开发者可以设置订阅回调触发的条件,此处是设置为事件打点数量满足1个
triggerCondition: { row: 1 },
// 开发者可以自行实现订阅回调函数,以便对订阅获取到的事件打点数据进行自定义处理
onTrigger: (curRow: number, curSize: number, holder: hiAppEvent.AppEventPackageHolder) => {
// 如果返回的holder对象为null,表示订阅过程发生异常。因此,在记录错误日志后直接返回
if (holder == null) {
hilog.error(0x0000, 'testTag', "HiAppEvent holder is null");
return;
}
hilog.info(0x0000, 'testTag', `HiAppEvent onTrigger: curRow=%{public}d, curSize=%{public}d`, curRow, curSize);
let eventPkg: hiAppEvent.AppEventPackage | null = null;
// 根据设置阈值大小(默认为1条事件)去获取订阅事件包,直到将订阅数据全部取出
// 返回的事件包对象为null,表示当前订阅数据已被全部取出,此次订阅回调触发结束
while ((eventPkg = holder.takeNext()) != null) {
// 开发者可以对事件包中的事件打点数据进行自定义处理,此处是将事件打点数据打印在日志中
hilog.info(0x0000, 'testTag', `HiAppEvent eventPkg.packageId=%{public}d`, eventPkg.packageId);
hilog.info(0x0000, 'testTag', `HiAppEvent eventPkg.row=%{public}d`, eventPkg.row);
hilog.info(0x0000, 'testTag', `HiAppEvent eventPkg.size=%{public}d`, eventPkg.size);
for (const eventInfo of eventPkg.data) {
hilog.info(0x0000, 'testTag', `HiAppEvent eventPkg.info=%{public}s`, eventInfo);
}
}
}
});
// 事件打点
// Index.ets
Button("buttonClick")
.onClick(()=>{
// 在按钮点击函数中进行事件打点,以记录按钮点击事件
let eventParams: Record<string, number> = { "click_time": 100 };
let eventInfo: hiAppEvent.AppEventInfo = {
// 事件领域定义
domain: "button",
// 事件名称定义
name: "click",
// 事件类型定义
eventType: hiAppEvent.EventType.BEHAVIOR,
// 事件参数定义
params: eventParams
};
hiAppEvent.write(eventInfo).then(() => {
hilog.info(0x0000, 'testTag', `HiAppEvent success to write event`);
}).catch((err: BusinessError) => {
hilog.error(0x0000, 'testTag', `HiAppEvent err.code: ${err.code}, err.message: ${err.message}`);
});
hilog.info(0x0000, 'testTag', `HiAppEvent write event`);
})
- 应用内事件的
domain是字符串,并且在addWatcher里填filter里应用的也是domain,不知道能不能过滤得更细致 - 在HiLog窗口搜索“HiAppEvent”关键字可快速过滤出系统和应用事件
HiTraceMeter
- HiTraceMeter提供系统性能打点接口。开发者在关键代码位置调用这些API,能够有效跟踪进程轨迹,查看系统和应用性能。
- HiTraceMeter打点接口分为三类:同步时间片跟踪、异步时间片跟踪和整数跟踪。
- HiTraceMeter接口实现均为同步,同步和异步针对的是被跟踪的业务。
- HiTraceMeter打点接口可与HiTraceChain一起使用,进行跨设备、跨进程或跨线程的打点关联与分析。
// index.ets
import { hiTraceMeter, hilog } from '@kit.PerformanceAnalysisKit';
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.message = (this.message == 'Hello HiTrace') ? 'Hello World' : 'Hello HiTrace';
const COMMERCIAL = hiTraceMeter.HiTraceOutputLevel.COMMERCIAL;
let traceCount = 0;
// 第一个异步跟踪任务开始
hiTraceMeter.startAsyncTrace(COMMERCIAL, 'myTestAsyncTrace', 1001, 'categoryTest', 'key=value');
// 开始计数任务
traceCount++;
hiTraceMeter.traceByValue(COMMERCIAL, 'myTestCountTrace', traceCount);
// 业务流程
hilog.info(0x0000, 'testTrace', 'myTraceTest running, taskId: 1001');
// 第二个异步跟踪任务开始,同时第一个跟踪的同名任务还没结束,出现了并行执行,对应接口的taskId需要不同
hiTraceMeter.startAsyncTrace(COMMERCIAL, 'myTestAsyncTrace', 1002, 'categoryTest', 'key=value');
// 开始计数任务
traceCount++;
hiTraceMeter.traceByValue(COMMERCIAL, 'myTestCountTrace', traceCount);
// 业务流程
hilog.info(0x0000, 'testTrace', 'myTraceTest running, taskId: 1002');
// 结束taskId为1001的异步跟踪任务
hiTraceMeter.finishAsyncTrace(COMMERCIAL, 'myTestAsyncTrace', 1001);
// 结束taskId为1002的异步跟踪任务
hiTraceMeter.finishAsyncTrace(COMMERCIAL, 'myTestAsyncTrace', 1002);
// 开始同步跟踪任务
hiTraceMeter.startSyncTrace(COMMERCIAL, 'myTestSyncTrace', 'key=value');
// 业务流程
hilog.info(0x0000, 'testTrace', 'myTraceTest running, synchronizing trace');
// 结束同步跟踪任务
hiTraceMeter.finishSyncTrace(COMMERCIAL);
// 若通过HiTraceMeter性能打点接口传递的参数的生成过程比较复杂,此时可以通过isTraceEnabled判断当前是否开启应用trace捕获,
// 在未开启应用trace捕获时,避免该部分性能损耗
if (hiTraceMeter.isTraceEnabled()) {
let customArgs = 'key0=value0';
for(let index = 1; index < 10; index++) {
customArgs += `,key${index}=value${index}`
}
hiTraceMeter.startAsyncTrace(COMMERCIAL, 'myTestAsyncTrace', 1003, 'categoryTest', customArgs);
hilog.info(0x0000, 'testTrace', 'myTraceTest running, taskId: 1003');
hiTraceMeter.finishAsyncTrace(COMMERCIAL, 'myTestAsyncTrace', 1003);
} else {
hilog.info(0x0000, 'testTrace', 'myTraceTest running, trace is not enabled');
}
})
}
.width('100%')
}
.height('100%')
}
}
在DevEco Studio Terminal窗口中执行以下命令,开启应用的trace捕获。
PS D:\xxx\xxx> hdc shell
$ hitrace --trace_begin app
执行如下命令抓取trace数据,并使用“myTest”关键字过滤trace数据(示例打点接口传递的name字段前缀均为“myTest”)。
$ hitrace --trace_dump | grep myTest
成功抓取到:
e.myapplication-39945 ( 39945) [010] .... 347921.342267: tracing_mark_write: S|39945|H:myTestAsyncTrace|1001|M62|categoryTest|key=value
e.myapplication-39945 ( 39945) [010] .... 347921.342280: tracing_mark_write: C|39945|H:myTestCountTrace|1|M62
e.myapplication-39945 ( 39945) [010] .... 347921.342327: tracing_mark_write: S|39945|H:myTestAsyncTrace|1002|M62|categoryTest|key=value
e.myapplication-39945 ( 39945) [010] .... 347921.342333: tracing_mark_write: C|39945|H:myTestCountTrace|2|M62
e.myapplication-39945 ( 39945) [010] .... 347921.342358: tracing_mark_write: F|39945|H:myTestAsyncTrace|1001|M62
e.myapplication-39945 ( 39945) [010] .... 347921.342365: tracing_mark_write: F|39945|H:myTestAsyncTrace|1002|M62
e.myapplication-39945 ( 39945) [010] .... 347921.342387: tracing_mark_write: B|39945|H:myTestSyncTrace|M62|key=value
e.myapplication-39945 ( 39945) [010] .... 347921.342586: tracing_mark_write: S|39945|H:myTestAsyncTrace|1003|M62|categoryTest|key0=value0,key1=value1,key2=value2,key3=value3,key4=value4,key5=value5,key6=value6,key7=value7,key8=value8,key9=value9
e.myapplication-39945 ( 39945) [010] .... 347921.342615: tracing_mark_write: F|39945|H:myTestAsyncTrace|1003|M62
停止:
$ hitrace --trace_finish
使用DevEco Studio Profiler工具可以可视化展示HiTraceMeter日志内容,分析应用或服务的CPU使用率和线程运行状态,查看指定时间段内程序在CPU上的执行耗时。具体使用指导请参考CPU活动分析:CPU分析。
导出日志到当前文件夹
$ exit
PS D:\xxx\xxx> hdc file recv /data/local/tmp/trace.ftrace ./
可视化
- 在DevEco Studio Profiler的会话区选择“Open File”,将HiTraceMeter文本日志导入DevEco Studio。
- 通过
Smartperf_Host工具进行分析,工具下载链接:developtools_smartperf_host 发行版。
ArkTS高性能编程
基于TypeScript,但限制了一些特性
- 不支持动态属性变更,变量/参数需要有明确的类型声明和返回值声明等
- 禁用了
@ts-ignore,@ts-expect-error等屏蔽编译校验的命令 - 强制严格模式:严格判空,严格函数类型检查,严格成员初始化等
- 不支持
any,unknown等- 使用泛型,联合类型或object
- 跨语言时可使用
ESObject
冷启动过程
- 应用进程创建和初始化: 设置合适分辨率的
startWindowIcon - App和Ability初始化: 减少首页ability和page中的import
- Ability生命周期: 使用异步加载
- 加载绘制首页:延迟加载
减少卡顿丢帧
- 避免在主线程上执行耗时操作,交给
TaskPool或者Worder在后台线程中执行 - 减少渲染进程的冗余开销,用资源图代替绘制,合理使用
RenderGroup,尺寸和位置尽量使用整数等 - 减少视图嵌套层级
- 组件复用配合
LazyForEach - 精确控制状态变量关联组件数
- 对象上谨慎使用状态变量关联
@prop是深拷贝,@ObjectLink是浅拷贝,优先使用浅拷贝方案减少系统内存开销
使用性能调优工具
- ArkUlInspector:用于检查和调试应用程序页面布局的情况
- Launch Insight:录制和还原从启动应用,到显示首帧过程中的CPU、内存等资源使用情况,用于分析启动耗时长的问题。
- Frame Insight:录制卡顿过程中的关键数据,标注出应用侧、RenderService 侧卡顿帧,用于分析应用卡顿、丢帧的问题。
- Time Insight:通过周期性采集调用栈,识别CPU 耗时高的热点代码段,用于分析卡顿、CPU 占用高、运行速度慢等问题。
- Allocation Insight:录制和分析内存分配记录,用于分析内存峰值高,内存泄漏,内存不足导致应用被强杀等问题。
- Snapshot Insight:录制和分析应用程序中 ArkTS 对象的分布,通过快照方式对比 ArkTS 对象分布区别,用于分析内存泄漏问题。
- CPU Insight:录制cPU调度事件、线程运行状态、CPU核频率、Trace 等数据,可用于分析卡顿、运行速度慢、应用无响应等问题。
- Smart Perf:开源性能调优平台,支持对CPU调度、频点、进程线程时间片、堆内存、帧率等数据进行采集和展示,展示方式为泳道图
丢帧
HarmonyOS图形系统采用统一渲染模式,遵循流水线模式。
- 90Hz刷新率下,每个Vsync周期是11.1ms。
- 60Hz刷新率下,每个Vsync周期是16.7ms。
- 120Hz刷新率下,每个Vsync周期是8.3ms。
多端开发
官方描述是一次开发,多端部署,其实就是开发的时候按多平台或多屏幕大小把代码都写好了,或者利用他们提供的API提供多段代码,总之就是要增加代码量,而不是真的全自动适配。
自适应布局 & 响应式布局
- 自适应代表根据容器大小自动适配,目前提供7种能力:拉伸能力、均分能力、占比能力、缩放能力、延伸能力、隐藏能力、折行能力。
- 响应式则是预设几种布局进行有级的不连续的变化,当前响应式布局能力有3种:断点、媒体查询、栅格布局。
也就是说自适应是无级变化的,取决于容器大小,响应式是预设的几种布局进行“突变”。
系统能力
系统能力(即SystemCapability,缩写为SysCap)指操作系统中每一个相对独立的特性,如蓝牙,WIFI,NFC,摄像头等,都是系统能力之一。每个系统能力对应多个API,随着目标设备是否支持该系统能力共同存在或消失。用三个概念来实现:
- 支持能力集:设备具备的系统能力集合,在设备配置文件中配置。
- 要求能力集:应用需要的系统能力集合,在应用配置文件中配置。
- 联想能力集:开发应用时DevEco Studio可联想的API所在的系统能力集合,在应用配置文件中配置。
设备具体能能力难道不是由设备厂商决定的吗?为什么要在应用配置文件中配置?
动态判断:
要求能力里面没有配置的能力,可以通过canIUse方法判断设备是否支持该能力。
if (canIUse("SystemCapability.Communication.NFC.Core")) {
console.log("该设备支持SystemCapability.Communication.NFC.Core");
} else {
console.log("该设备不支持SystemCapability.Communication.NFC.Core");
}
或者通过import来导入,不具有该能力的话会得到undefined。
import controller from '@kit.ConnectivityKit';
try {
controller.enableNfc();
console.log("controller enableNfc success");
} catch (busiError) {
console.log("controller enableNfc busiError: " + busiError);
}
配置样例:
// syscap.json
{
"devices": {
"general": [ // 每一个典型设备对应一个syscap支持能力集,可配置多个典型设备
"default",
"tablet"
],
"custom": [ // 厂家自定义设备
{
"某自定义设备": [
"SystemCapability.Communication.SoftBus.Core"
]
}
]
},
"development": { // addedSysCaps内的sycap集合与devices中配置的各设备支持的syscap集合的并集共同构成联想能力集
"addedSysCaps": [
"SystemCapability.Communication.NFC.Core"
]
},
"production": { // 用于生成rpcid,慎重添加,可能导致应用无法分发到目标设备上
"addedSysCaps": [], // devices中配置的各设备支持的syscap集合的交集,添加addedSysCaps集合再除去removedSysCaps集合,共同构成要求能力集
"removedSysCaps": [] // 当该要求能力集为某设备的子集时,应用才可被分发到该设备上
}
}
- 先回答上面的问题,系统能力集为什么需要配置?原来是配置一下硬件平台,比如上例中的
default和tablet,而不是真的一条条写出来。即devices中指定的设备集合决定了系统能力集 addedSysCaps配置的是要求能力集,一个工程里真正请求能力的地方
- 这里没有写到数组里的,仍然可以用上一节里的动态方式在代码中使用
工程组织
- 一个应用包含一个或多个
Module(含源代码,资源文件,三方库及应用/服务配置文件) Module分为“Ability”和“Library”两种类型:- “Ability”类型的Module编译后生成
HAP包。 - “Library”类型的Module编译后生成
HAR包。
- “Ability”类型的Module编译后生成
- HarmonyOS的应用以
APPPack形式发布,其包含一个或多个HAP包。 HAP是HarmonyOS应用安装的基本单位,HAP可以分为Entry和Feature两种类型:Entry类型的HAP:应用的主模块。在同一个应用中,同一设备类型只支持一个Entry类型的HAP,通常用于实现应用的入口界面、入口图标、主特性功能等。Feature类型的HAP:应用的动态特性模块。Feature类型的HAP通常用于实现应用的特性功能,一个应用程序包可以包含一个或多个Feature类型的HAP,也可以不包含。
部署方式
- 为不同设备一次编译生成相同的
HAP,这一般是代码里写全了 - 为不同设备一次编译生成不同的
HAP,说明代码里不是把所有场景写全,而是写在不同的地方,生成不同的HAP
从屏幕尺寸、输入方式及交互距离三个维度考虑,可以将常用类型的设备分为不同泛类:
- 默认设备、平板
- 车机、智慧屏
- 智能穿戴
- ……
一般是如果为同一泛类开发,则选用模式一,在
if-else里写全,然后生成一个包即可,如果跨了泛类,则需要为不同泛类生成不同的hap
工程结构
一般采用三层结构:
- common(公共能力层):用于存放公共基础能力集合(如工具库、公共配置等)。
- common层不可分割,需编译成一个HAR包,其只可以被products和features依赖,不可以反向依赖。
- features(基础特性层):用于存放基础特性集合(如应用中相对独立的各个功能的UI及业务逻辑实现等)。
- 各个feature高内聚、低耦合、可定制,供产品灵活部署。
- 不需要单独部署的feature通常编译为HAR包,供products或其它feature使用。
- 需要单独部署的feature通常编译为Feature类型的HAP包,和products下Entry类型的HAP包进行组合部署。
- features层可以横向调用及依赖common层,同时可以被products层不同设备形态的HAP所依赖,但是不能反向依赖products层。
- products(产品定制层):用于针对不同设备形态进行功能和特性集成。
- products层各个子目录各自编译为一个Entry类型的HAP包,作为应用主入口。products层不可以横向调用。
如下所示:
/application
├── common # 可选。公共能力层, 编译为HAR包或HSP包
├── features # 可选。基础特性层
│ ├── feature1 # 子功能1, 编译为HAR包或HSP包或Feature类型的HAP包
│ ├── feature2 # 子功能2, 编译为HAR包或HSP包或Feature类型的HAP包
│ └── ...
└── products # 必选。产品定制层
├── wearable # 智能穿戴泛类目录, 编译为Entry类型的HAP包
├── default # 默认设备泛类目录, 编译为Entry类型的HAP包
└── ...
示例
// features/live/src/main/ets/view/LivePage.ets
@Component
export struct LivePage {
@StorageLink('topRectHeight') topRectHeight: number = 0;
build() {
Column() {
Header()
LiveList()
}
.width(LiveConstants.FULL_WIDTH_PERCENT)
.height(LiveConstants.FULL_HEIGHT_PERCENT)
.padding({ top: this.getUIContext().px2vp(this.topRectHeight) })
}
}
// features/live/src/main/ets/view/Header.ets
@Component
export struct Header {
build() {
Row() {
Image($r('app.media.ic_back'))
...
Text($r('app.string.live_title'))
...
}
.height($r('app.float.title_row_height'))
.width(LiveConstants.FULL_WIDTH_PERCENT)
}
}
// features/live/src/main/ets/view/LiveList.ets
@Component
export struct LiveList {
...
build() {
GridRow({
columns: { sm: LiveConstants.FOUR_COLUMN, md: LiveConstants.EIGHT_COLUMN, lg: LiveConstants.TWELVE_COLUMN },
breakpoints: { value: [LiveConstants.SMALL_DEVICE_TYPE, LiveConstants.MIDDLE_DEVICE_TYPE,
LiveConstants.LARGE_DEVICE_TYPE] },
gutter: { x: $r('app.float.grid_row_gutter') }
}) {
GridCol({
span: { sm: LiveConstants.FOUR_COLUMN, md:LiveConstants.SIX_COLUMN, lg: LiveConstants.EIGHT_COLUMN },
offset: { sm: LiveConstants.ZERO_COLUMN, md: LiveConstants.ONE_COLUMN, lg: LiveConstants.TWO_COLUMN }
}) {
Scroll(this.scroller) {
Column() {
ForEach(this.liveStreams, (item: LiveStream, index: number) => {
this.LiveItem(item, index)
}, (item: LiveStream, index: number) => index + JSON.stringify(item))
}
.width(LiveConstants.FULL_WIDTH_PERCENT)
}
...
}
...
}
...
}
...
}
上例中,断点是手动设置的(320, 600, 840),设置columns里的sm、md、lg,表示不同断点下的列数,就是不知道是不是一一对应的,比如知道320对应sm,那如果没有传够3个呢?
GridRow({
breakpoints: {
value: BreakpointConstants.BREAKPOINT_VALUE,
reference: BreakpointsReference.WindowSize
},
columns: {
sm: BreakpointConstants.COLUMN_LG,
md: BreakpointConstants.COLUMN_LG,
lg: BreakpointConstants.COLUMN_LG
}
}) {
GridCol({
span: { sm: GridConstants.SPAN_FOUR, md: GridConstants.SPAN_TWELVE, lg: GridConstants.SPAN_TWELVE }
}) {
this.CoverImage()
}
GridCol({
span: { sm: GridConstants.SPAN_EIGHT, md: GridConstants.SPAN_TWELVE, lg: GridConstants.SPAN_TWELVE }
}) {
this.CoverIntroduction()
}
GridCol({
span: { sm: GridConstants.SPAN_TWELVE, md: GridConstants.SPAN_TWELVE, lg: GridConstants.SPAN_TWELVE }
}) {
this.CoverOptions()
}
}
- 以(320, 600, 840)设置断点
- 配置大中小屏幕栅格数均为12(
COLUMN_LG) - 三列的配置是小屏幕(4,8,12),中屏幕(12,12,12),大屏幕(12,12,12),意思是小屏幕下元素1,2占1行,元素3单独占1行,中大屏幕上则是各占一行。
span就是在栅格系统里跨几列,offset就是偏移几列
Column() {
this.PlayAll()
List() {
LazyForEach(new SongDataSource(this.songList), (item: SongItem, index: number) => {
ListItem() {
Column() {
this.SongItem(item, index)
}
.padding({
left: $r('app.float.list_item_padding'),
right: $r('app.float.list_item_padding')
})
}
}, (item: SongItem, index?: number) => JSON.stringify(item) + index)
}
.width(StyleConstants.FULL_WIDTH)
.backgroundColor(Color.White)
.margin({ top: $r('app.float.list_area_margin_top') })
.lanes(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ?
ContentConstants.COL_TWO : ContentConstants.COL_ONE)
.layoutWeight(1)
- 这里配置
lanes用了条件表达式,而不再是提供{sm:xx, md:xx, lg:xx}这种对象, - 结果是大屏下
list显示两列,其它情况显示1列
breakpoint相关源码在
BreakpointSystem.ets里
在没有{sm:xx, md:xx, lg:xx}这种标准API的情况下,参考下面的例子根据断点配置不同的具体值:
xxx()
.width(new BreakpointType({
sm: $r('app.float.play_width_sm'),
md: $r('app.float.play_width_sm'),
lg: $r('app.float.play_width_lg')
}).getValue(this.currentBreakpoint))
也就是BreakpointType类,这个类可以传入一个对象,对象里配置不同断点下的值,然后通过getValue方法传入当前断点,返回对应的值。
// features/musicList/src/main/ets/components/MusicControlComponent.ets
@Component
export struct MusicControlComponent {
...
build() {
...
Row() {
if (this.isFoldFull) {
Column() {
...
GridRow({
columns: { md: BreakpointConstants.COLUMN_MD },
gutter: BreakpointConstants.GUTTER_MUSIC_X
}) {
GridCol({
span: { md: BreakpointConstants.SPAN_SM }
}) {
MusicInfoComponent()
}
.margin({
left: $r('app.float.margin_small'),
right: $r('app.float.margin_small')
})
GridCol({
span: { md: BreakpointConstants.SPAN_SM }
}) {
LyricsComponent({ isShowControl: this.isShowControlLg, isTablet: this.isTabletFalse })
}
.padding({
left: $r('app.float.twenty_four')
})
}
...
}
...
} else if (this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG) {
Column() {
...
GridRow({
columns: { md: BreakpointConstants.COLUMN_MD, lg: BreakpointConstants.COLUMN_LG },
gutter: BreakpointConstants.GUTTER_MUSIC_X
}) {
GridCol({
span: { md: BreakpointConstants.SPAN_SM, lg: BreakpointConstants.SPAN_SM },
offset: { lg: BreakpointConstants.OFFSET_MD }
}) {
Column() {
Image(this.songList[this.selectIndex].label)
.width(StyleConstants.FULL_WIDTH)
.aspectRatio(1)
.borderRadius($r('app.float.cover_radius'))
ControlAreaComponent()
}
.height(StyleConstants.FULL_HEIGHT)
.justifyContent(FlexAlign.SpaceBetween)
.margin({
bottom: $r('app.float.common_margin')
})
}
GridCol({
span: { md: BreakpointConstants.SPAN_SM, lg: BreakpointConstants.SPAN_MD },
offset: { lg: BreakpointConstants.OFFSET_MD }
}) {
LyricsComponent({ isShowControl: this.isShowControlLg, isTablet: this.isTablet })
}
}
...
}
} else {
Stack({ alignContent: Alignment.TopStart }) {
Swiper() {
MusicInfoComponent()
.margin({
top: $r('app.float.music_component_top'),
bottom: $r('app.float.music_component_bottom')
})
.padding({
left: $r('app.float.common_padding'),
right: $r('app.float.common_padding')
})
LyricsComponent({ isShowControl: this.isShowControl, isTablet: this.isTabletFalse })
.margin({
top: $r('app.float.margin_lyric')
})
.padding({
left: $r('app.float.common_padding'),
right: $r('app.float.common_padding')
})
}
...
}
.height(StyleConstants.FULL_HEIGHT)
}
}
.padding({
bottom: this.bottomArea,
top: this.topArea
})
...
}
...
}
上述代码做了更极端的例子,引入了折叠屏的布局。 不过先插一个题外话,看了这么多段代码,应该也看出来了:
- 当
Column的子组件是GridRow/GridCol 时,布局由栅格系统主导。 - 当
Column的子组件是普通视图组件或嵌套的Flex容器(如 Row、另一个 Column) 时,布局由Flexbox系统主导。 - 当然,
GridRow里面应该也只能是GridCol了吧
看回代码,
- 这是一个音乐播放的界面,截取部分是音乐信息和歌词的展示。
- 在折叠屏下,定义了一个
MD的横向栅格系统(GridRow),然后让两个组件分别占据sm的宽度 - 超大屏下,则是
sm+md的组合, 也就是说右侧的歌词组件可能更宽,当然,还是保持了一屏展示两个组件的布局。 - 中小屏幕下,则由
Swiper来切换了,一次只能看到一个。而且,甚至都没用到栅格系统
值得说的代码是,要兼顾这么多情况,只能靠if-else硬编码了,没有API能直接实现,所以所谓的响应式,不过是把所有场景都写全了。
然后,LG的场景里,配置columns的时候,传入了{md, lg},这里是没必要的,因为已经确定了此代码块内一定是lg
默认值
- 断点默认值:
{ value: ["320vp", "600vp", "840vp"], reference: BreakpointsReference.WindowSize } - 栅格数默认值:
{ xs: 2, sm: 4, md: 8, lg: 12, xl: 12, xxl: 12 }
自由流转
- 在HarmonyOS中,将跨多设备的分布式操作统称为流转;根据使用场景的不同,流转又分为跨端迁移和多端协同两种具体场景。
- 在应用开发层面,跨端迁移指在A端运行的UIAbility迁移到B端上,完成迁移后,B端UIAbility继续任务,而A端UIAbility可按需决定是否退出。
- 典型场景:媒体播控,应用接续
- 在应用开发层面,多端协同指多端上的不同UIAbility/ServiceExtensionAbility同时运行、或者交替运行实现完整的业务;或者多端上的相同UIAbility/ServiceExtensionAbility同时运行实现完整的业务。
- 典型场景:跨设备拖拽/剪贴板,跨设备互通
分布式应用开发模型
- 分布式软总线:发现,连接,组网,传输
- 分布式数据管理:
- 分布式数据库,kvstore/rdb,
- 分布式数据对象,一般保存在内存中,过程数据
- 系统公共数据访问
- 分布式硬件:硬件共享,超级终端,硬件资源池,软件定义硬件
- 分布式任务调度:提供任务跨端迁移、多端协同能力

跨端迁移
- 配置迁移功能
// src/main/resources/base/element/string.json
{
"module": {
// ...
"abilities": [
{
// ...
"continuable": true,
"launchType": "singleton"
}
],
"requestPermissions": [
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"reason": "$string:distributed_data_sync",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}
]
}
}
- 向用户申请授权
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
async checkPermissions(): Promise<void> {
const permissions: Array<Permissions> = ["ohos.permission.DISTRIBUTED_DATASYNC"];
const accessManager = abilityAccessCtrl.createAtManager();
try {
const bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION;
const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleFlags);
const grantStatus = await accessManager.checkAccessToken(bundleInfo.appInfo.accessTokenId, permissions[0]);
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_DENIED) {
accessManager.requestPermissionsFromUser(this.context, permissions);
}
} catch (err) {
Logger.error('EntryAbility', 'checkPermissions', `Catch err: ${err}`);
return;
}
}
}
这里与iOS有异,iOS下
DENIED表明用户手动禁止了,是不能再弹窗的,只有未确定或未授过权才需要弹窗
- 实现迁移
export default class EntryAbility extends UIAbility {
private localObject: distributedDataObject.DataObject | undefined = undefined;
private targetDeviceId: string | undefined = undefined;
private changeCall: (sessionId: string, fields: Array<string>) => void =
(sessionId: string, fields: Array<string>) => {
if (fields != null && fields != undefined && this.localObject != undefined) {
for (let index: number = 0; index < fields.length; index++) {
if (fields[index] === 'recipient') {
AppStorage.setOrCreate<string>('recipient', this.localObject['recipient'] as string);
} else if (fields[index] === 'sender') {
AppStorage.setOrCreate<string>('sender', this.localObject['sender'] as string);
} else if (fields[index] === 'subject') {
AppStorage.setOrCreate<string>('subject', this.localObject['subject'] as string);
} else if (fields[index] === 'emailContent') {
AppStorage.setOrCreate<string>('emailContent', this.localObject['emailContent'] as string);
}
}
}
}
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
Logger.info('EntryAbility', 'Ability onCreate');
this.restoringData(want, launchParam);
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
Logger.info('EntryAbility', 'Ability onNewWant');
this.restoringData(want, launchParam);
}
async restoringData(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
this.checkPermissions();
// Recovering migrated data from want.
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
// get user data from want params.
let sessionId: string = want.parameters?.distributedSessionId as string;
if (!this.localObject) {
let mailInfo: MailInfo = new MailInfo(undefined, undefined, undefined, undefined);
this.localObject = distributedDataObject.create(this.context, mailInfo);
this.localObject.on('change', this.changeCall);
}
if (sessionId && this.localObject) {
await this.localObject.setSessionId(sessionId);
AppStorage.setOrCreate('recipient', this.localObject['recipient']);
AppStorage.setOrCreate('sender', this.localObject['sender']);
AppStorage.setOrCreate('subject', this.localObject['subject']);
AppStorage.setOrCreate('emailContent', this.localObject['emailContent']);
}
this.context.restoreWindowStage(new LocalStorage());
}
}
onContinue(wantParam: Record<string, Object | undefined>): AbilityConstant.OnContinueResult {
try {
let sessionId: string = distributedDataObject.genSessionId();
if (this.localObject) {
this.localObject.setSessionId(sessionId);
this.localObject['recipient'] = AppStorage.get('recipient');
this.localObject['sender'] = AppStorage.get('sender');
this.localObject['subject'] = AppStorage.get('subject');
this.localObject['emailContent'] = AppStorage.get('emailContent');
this.targetDeviceId = wantParam.targetDevice as string;
this.localObject.save(wantParam.targetDevice as string)
wantParam.distributedSessionId = sessionId;
}
} catch (error) {
Logger.error('EntryAbility', 'distributedDataObject failed', `code ${(error as BusinessError).code}`);
}
return AbilityConstant.OnContinueResult.AGREE;
}
async checkPermissions(): Promise<void> {
// 见上一节的代码
}
onWindowStageCreate(windowStage: window.WindowStage) {
// Main window is created, set main page for this ability.
Logger.info('EntryAbility', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/MailHomePage', (err, data) => {
if (err.code) {
Logger.error('EntryAbility', 'Failed to load the content, ', `Catch err: ${err}`);
return;
}
Logger.info('EntryAbility', 'Succeeded in loading the content, ', `Data: ${data}`);
});
if (!this.localObject) {
let mailInfo: MailInfo = new MailInfo(undefined, undefined, undefined, undefined);
this.localObject = distributedDataObject.create(this.context, mailInfo);
}
}
async onDestroy() {
Logger.info('EntryAbility', 'Ability onDestroy');
if (this.localObject && this.targetDeviceId) {
await this.localObject.save(this.targetDeviceId).then(() => {
Logger.info('onDestroy localObject save success');
}).catch((err: BusinessError) => {
Logger.error(`Failed to save. Code:${err.code},message:${err.message}`);
});
}
}
}
如果同步/迁移的数据量少,直接将数据以键值对存入wantParam即可通过通用软总线同步,上例中只传递了sessionId,利用了分布式数据对象的特性让软总线自动同步:sessionId本质上是一个组网标识符。当多个设备上的分布式数据对象设置了相同的 sessionId,它们就自动加入了一个“同步组” 。这个 ID 本身不包含业务数据,它的核心作用是告诉目标设备应该去哪个“数据频道”拉取数据。真正的数据同步是由鸿蒙的分布式数据管理框架在背后自动完成的,它主要依赖以下几步:
- 数据持久化与推送:在源设备的 onContinue方法中,调用
this.localObject.save(wantParam.targetDevice as string)是关键 。这行代码会将当前分布式数据对象的状态持久化到分布式数据库中,并通过分布式软总线(SoftBus)自动将数据推送到目标设备 。这是一个系统级的、自动化的过程。 - 自动同步与监听:当目标设备的应用被拉起后,在 onCreate或 onNewWant中,代码通过
this.localObject.setSessionId(sessionId)加入了由 sessionId定义的同一个同步组 。一旦加入成功,分布式数据管理框架会自动将已经推送到本地的数据同步到本地的 localObject实例中。 - 数据恢复:同步完成后,框架会触发 'change'事件。上述代码中注册的
this.changeCall回调函数就会执行,从刚刚同步好的 this.localObject中读取数据(如 this.localObject['recipient']),并最终通过 AppStorage.setOrCreate将数据设置到应用全局状态,完成整个恢复流程 。
可以这么说,在distributedDataObject.DataObject的
save方法中,实现了数据的持久化(之前我理解为数据此时上云了,其实并不是,因为它只是设备间互联),在setSessionId方法中,实现了数据的同步(其实就是通过各种近场通信,也就是华为包装的软总线,实时把数据从源设备读取出来)
大文件迁移示例:
@Entry
@Component
struct MailHomePage {
aboutToAppear() {
if ((this.isContinuation === CommonConstants.CAN_CONTINUATION) && (this.appendix.length >= 1)) {
this.readFile();
}
}
build() {
// ...
}
/**
* Reading file from distributed file systems.
*/
readFile(): void {
this.appendix.forEach((item: AppendixBean) => {
let filePath: string = this.distributedPath + item.fileName;
let savePath: string = getContext().filesDir + '/' + item.fileName;
try {
while (fileIo.accessSync(filePath)) {
let saveFile = fileIo.openSync(savePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
let file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE);
let buf: ArrayBuffer = new ArrayBuffer(CommonConstants.FILE_BUFFER_SIZE);
let readSize = 0;
let readLen = fileIo.readSync(file.fd, buf, { offset: readSize });
while (readLen > 0) {
readSize += readLen;
fileIo.writeSync(saveFile.fd, buf);
readLen = fileIo.readSync(file.fd, buf, { offset: readSize });
}
fileIo.closeSync(file);
fileIo.closeSync(saveFile);
}
} catch (error) {
let err: BusinessError = error as BusinessError;
Logger.error(`DocumentViewPicker failed with err: ${JSON.stringify(err)}`);
}
});
}
/**
* Add appendix from file manager.
*
* @param fileType
*/
documentSelect(fileType: number): void {
try {
let DocumentSelectOptions = new picker.DocumentSelectOptions();
let documentPicker = new picker.DocumentViewPicker();
documentPicker.select(DocumentSelectOptions).then((DocumentSelectResult: Array<string>) => {
for (let documentSelectResultElement of DocumentSelectResult) {
let buf = new ArrayBuffer(CommonConstants.FILE_BUFFER_SIZE);
let readSize = 0;
let file = fileIo.openSync(documentSelectResultElement, fileIo.OpenMode.READ_ONLY);
let readLen = fileIo.readSync(file.fd, buf, { offset: readSize });
// File name is not supported chinese name.
let fileName = file.name;
if (!fileName.endsWith(imageIndex[fileType].fileType) ||
new RegExp("\[\\u4E00-\\u9FA5]|[\\uFE30-\\uFFA0]", "gi").test(fileName)) {
promptAction.showToast({
message: $r('app.string.alert_message_chinese')
})
return;
}
let destination = fileIo.openSync(getContext()
.filesDir + '/' + fileName, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
let destinationDistribute = fileIo.openSync(getContext()
.distributedFilesDir + '/' + fileName, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
while (readLen > 0) {
readSize += readLen;
fileIo.writeSync(destination.fd, buf);
fileIo.writeSync(destinationDistribute.fd, buf);
console.info(destinationDistribute.path);
readLen = fileIo.readSync(file.fd, buf, { offset: readSize });
}
fileIo.closeSync(file);
fileIo.closeSync(destination);
fileIo.closeSync(destinationDistribute);
this.appendix.push({ iconIndex: fileType, fileName: fileName });
}
Logger.info(`DocumentViewPicker.select successfully, DocumentSelectResult uri: ${JSON.stringify(DocumentSelectResult)}`);
}).catch((err: BusinessError) => {
Logger.error(`DocumentViewPicker.select failed with err: ${JSON.stringify(err)}`);
});
} catch (error) {
let err: BusinessError = error as BusinessError;
Logger.error(`DocumentViewPicker failed with err: ${JSON.stringify(err)}`);
}
}
}
- 第一阶段:源设备 - 文件准备与上传 (documentSelect方法),用户通过文件选择器 (DocumentViewPicker) 选择文件后,代码以只读模式打开该文件,并分片读取文件内容到一个缓冲区 (ArrayBuffer), 这一步的核心动作是 “双写”。
- 应用本地目录 (getContext().filesDir + '/' + fileName): 保证文件在当前设备本地可用。
- 应用分布式目录 (getContext().distributedFilesDir + '/' + fileName): 这是实现跨端访问的关键。鸿蒙系统会管理此目录下的文件,使其可被同一账号下组网的其他设备上的同名应用访问
- 文件成功写入后,其名称等信息会被添加到 this.appendix数组中。这个数组的作用是在迁移时,告知目标设备需要同步哪些文件。
- 第二阶段:目标设备 - 文件恢复与同步 (readFile方法)
- 触发恢复:在目标设备上,应用启动后,在 aboutToAppear生命周期中,代码会检查 this.isContinuation和 this.appendix,确认本次启动是由于迁移且带有附件,从而调用 readFile方法。
- 从分布式目录读取:readFile方法遍历 this.appendix数组中的文件列表。对于每个文件,它尝试从分布式目录 (this.distributedPath + item.fileName) 中读取。此时,源设备通过分布式软总线提供的分布式文件系统(hmdfs)使得这个文件在目标设备上“可见” 。
- 写入目标设备本地:代码将从分布式目录读取到的文件内容,写入到目标设备的应用本地目录 (getContext().filesDir + '/' + item.fileName)。这一步完成后,文件就真正“落地”到了新设备上,迁移完成
应用开发安全
- 安全设计理念-以硬件TCB作为安全信任基础,软硬结合的安全设计

- 安全设计理念-分布式基于B.L.& Biba分级安全模型
- 机密性模型:Bell-Lapadula 模型
- 完整性模型:Biba 模型
- 系统安全-软硬件结合安全加固,防护系统被篡改或利用
- 数据分级保护-保护用户的数据安全加密
- 应用权限访问控制分级-“洋葱”模型划分的分级权限管控

访问控制原则
- 以系统分配的
Access Token作为应用标识符,不以UID作为标识 - 当访问的主体应用APL大于等于客体APL时,只要在
Config中声明即可允许访问 - 当访问的主体应用APL低于客体APL时,如需使用高权限则使用必须通过
Provisioning Profile的ACL中进行授权(需审核)

安全能力

- 应用加固- 应用混淆、代码签名、应用加密和反调试
- 提供了基础的应用加固安全能力,包括混淆、加密和代码签名能力,保护代码反编译和反调试
- 应用完整性保护 -应用代码签名
- APP代码文件签名,启动APP时校验,防止篡改二进制可执行文件
- 代码段签名,只执行指定“签名”的代码,防止代码注入攻击
- 应用权限管理- 权限申请和授权,上文已提过需要在config或provison文件申请。
- 应用快速修复-热补丁需上架审核签名发布,不允许三方更新
- TEE TA应用开发-基于TEE的开发TA,高安全应用场景
- 为了帮助金融等高安全应用场景需求,基于TEE可信运行环境,开发者可以开发应用的TA,TEE隔离运行,外部无法访问TEE运行的TA,即使Root也无法获取或劫持TA应用,可用于应用的支付认证等场景
- 不公开提供SDK
- 文件加密保护 - 提供基于文件分类加密保护
- 关键资产存储(Asset)-银行卡号、Token、口令等短数据存储加密保护
- 针对关键敏感数据,为用户提供基于底层TEE级别系统安全保护;提供关键敏感数据管理API,开发者无需关注底层具体安全实现
- 密钥管理(Universal Keystore)- 基于硬件TEE运行环境的密钥加解密管理
- 设备密钥证明-基于TEE级别的设备证书的密钥公钥证明
- 应用通过设备的Attestation机制获取可信证书,云端根据预置根证书验签证明数据可信。
- 应用加解密引擎- 基于OpenSSL封装提供JS接口
- 通过对OpenSSL接口封装JS接口,实现加解密算法的北向接口
- 应用身份认证- 基于系统口令、人脸、指纹的身份认证
- 应用身份认证-基于TEE级别安全的系统人脸、指纹生态特征认证
- 申请权限:
ohos.permission.ACCESS_BIOMETRIC权限
- 申请权限:
文件加密策略: |分级加密|策略| |---|---| el4|用户锁定设备后不久(一般为10秒钟),解密的数据保护类密钥会被从内存丢弃,此类的所有数据都无法访问,除非用户再次输入密码或使用指纹或面容解锁设备。 el3|用户锁定设备后,如果文件已经被打开,则文件始终可以被继续访问,一旦文件关闭(锁), 文件将不能被再次访问,除非用户再次输入密码或使用指纹或面容解锁设备。但对于类似锁屏期间产生的数据支持写入到临时存储区 el2|用户开机后首次解锁设备后,才可对文件进行访问。这是未分配给数据保护类的所有第三方应用数据的默认数据保护类 el1|设备在直接启动模式下和用户解锁设备后均可对文件进行访问
应用如无特殊需要,应将数据存放在el2加密目录下,以尽可能保证数据安全。(默认也是el2)
获取分级数据文件路径:
getEl2Path(): void {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
context.area = contextConstant.AreaMode.EL2;
let filePath = context.filesDir + '/health_data.txt';
this.message = filePath;
}
隐私保护
- 应用使用敏感资源用户可知可控
- 应用前台显性提醒:应用前台使用敏感器件时,在右上角以小圆点显性提醒
- 应用后台使用提醒:应用后台使用敏感器件时,必须要显示的弹出悬浮窗
- 敏感器件全局开关:用户可以在SystemUI关闭敏感器件全局开关,应用在关闭状态使用全局开关时,系统弹框让用户确认是否打开
- 权限访问记录:用户可以在Setting里查看应用的权限访问记录
- 以Picker、系统控件方式访问无需向用户申请权限
- 支持应用设置隐私模式,实现防录屏截屏功能
- 隐私反跟踪:统一设备唯一标识,反跟踪
- 为了符合隐私要求,防止应用对用户的隐私追踪,设备不再对应用提供永久设备ID访问,通过统一的
OAID/AAID
- 为了符合隐私要求,防止应用对用户的隐私追踪,设备不再对应用提供永久设备ID访问,通过统一的
使用模糊定位获取位置信息
在API9以后,当应用同时申请 ohos.permission.APPROXIMATELY_LOCATION 和 ohos.permission.LOCATION 权限时,可以获取用户精准位置,精准度达到米级。如果应用仅申请 ohos.permission.APPROXIMATELY_LOCATION 权限,权限弹框仅显示模糊位置权限。

// module.json5
{
"module": {
// ...
"requestPermissions": [
{
"name": "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:location_reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
},
// ...
]
}
}
动态申请:
const accessManager = abilityAccessCtrl.createAtManager();
accessManager.requestPermissionsFromUser(this.getUIContext().getHostContext(), ['ohos.permission.APPROXIMATELY_LOCATION'])
.then((data) => {
let grantStatus: Array<number> = data.authResults;
if (grantStatus.length > 0 && grantStatus[0] === 0) {
// The user is authorized to continue to access the target operation
Logger.info('request permissions granted');
// ...
} else {
// The user rejects the authorization
Logger.info('request permissions denied');
// ...
}
Logger.info(`request permissions result: ${JSON.stringify(data)}`);
})
获取位置信息接口
let requestInfo: geoLocationManager.LocationRequest = {
'priority': geoLocationManager.LocationRequestPriority.FIRST_FIX,
'scenario': geoLocationManager.LocationRequestScenario.UNSET,
'timeInterval': 1,
'distanceInterval': 0,
'maxAccuracy': 0
};
geoLocationManager.getCurrentLocation(requestInfo).then(result => {
Logger.info(`geoLocation current location:error: ${JSON.stringify(result)}`)
// ...
}).catch((error: BusinessError) => {
Logger.error(`geoLocation getCurrentLocation: error: ${JSON.stringify(error)}`)
});
photo picker
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { BusinessError } from '@kit.BasicServicesKit';
import Logger from '../utils/Logger';
@Entry
@Component
struct PickerDemo {
@State imageUri: string = '';
build() {
RelativeContainer() {
Image(this.imageUri)
// ...
Button($r('app.string.select_picture'))
// ...
.onClick(() => {
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 5;
const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
photoViewPicker.select(photoSelectOptions).then(photoSelectResult => {
this.imageUri = photoSelectResult.photoUris[0];
Logger.info(`PhotoViewPicker.select successfully, imageUri: ${this.imageUri}`)
}).catch((err: BusinessError) => {
Logger.error(`PhotoViewPicker.select failed with err: ${JSON.stringify(err)}`)
})
})
}
// ...
}
}