赞
踩
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
每个 UIAbility 类实例都会与一个 WindowStage 类实例绑定,该类提供了应用进程内窗口管理器的作用。它包含一个主窗口。也就是说 UIAbility 通过 WindowStage 持有了一个窗口,该窗口为 ArkUI 提供了绘制区域。
Context 是应用中对象的上下文,其提供了应用的一些基础信息,例如 resourceManager(资源管理)、applicationInfo(当前应用信息)、dir(应用开发路径)、area(文件分区)等,以及应用的一些基本方法,例如 createBundleContext()、getApplicationContext()等。UIAbility 组件和各种 ExtensionAbility 派生类组件都有各自不同的 Context 类。分别有基类Context、ApplicationContext、AbilityStageContext、UIAbilityContext、ExtensionContext、ServiceExtensionContext等Context。
Ability 是一个窗口,不包含内容,而 Page 则是真正显示内容的载体,Page 需要依附于 Ability 才能显示。
熟悉了上面的这些概念,我们就可以尝试开发一个鸿蒙上的 App 了。
之前学习 Compose 的时候做了一个「玩 Android」App,为了方便使用现成接口和 UI,我们来做一个鸿蒙版的「玩 Android」,顺便对比下鸿蒙和 Compose 在 UI 层的差异。
和 Compose、SwiftUI 类似,鸿蒙的 ArkUI 也采用声明式开发范式,这也是现代UI开发的共识,相比命令式UI,声明式UI更加简洁高效。
ArkUI 提供了官方组件和布局,基本能够满足常见 UI 界面的开发。
组件的详细使用这里不再赘述,我们以文章 Item 布局作为示例,来对比下鸿蒙和 Compose 的实现方式。
先看下展示效果:
代码对比:
可以看出,语法的相似度非常高,以至于我后来直接把 Compose 的代码复制来过改一下就能用了。
简单介绍下差异:
由于鸿蒙和 Compose 都是声明式 UI,因此状态更新也比较相似,都是状态驱动 UI 更新,此外,鸿蒙和 Compose 都支持通过 Diff 算法完成差量更新。
在项目结构部分提到,鸿蒙上 UI 界面的单位包括 Ability 和 page。
一般来说一个功能只需要一个 Ability 呢 就够了,那什么时候需要多个 Ability 呢?
举个例子,如果你当前开发的是通讯录应用,当其他应用需要选择联系人时,需要打开通讯录的选人页面,而打开外部应用页面的最小单元为 Ability,因为 page 无法独立显示,这时你就需要创建多个 Ability。
let wantInfo = {
deviceId: ‘’, // deviceId为空表示本设备
bundleName: ‘com.example.myapplication’,
abilityName: ‘FuncAbility’,
moduleName: ‘module1’, // moduleName非必选
parameters: { // 自定义信息
info: ‘来自EntryAbility Index页面’,
},
}
// context为调用方UIAbility的AbilityContext
this.context.startAbility(wantInfo).then(() => {
// …
}).catch((err) => {
// …
})
import UIAbility from ‘@ohos.app.ability.UIAbility’;
import Window from ‘@ohos.window’;
export default class FuncAbility extends UIAbility {
onCreate(want, launchParam) {
// 接收调用方UIAbility传过来的参数
let funcAbilityWant = want;
let info = funcAbilityWant?.parameters?.info;
// …
}
}
terminateSelf()
方法实现。// context为需要停止的UIAbility实例的AbilityContext
this.context.terminateSelf((err) => {
// …
});
同时,鸿蒙也提供了 startAbilityForResult 来实现启动 Ability 并获取返回结果,详细使用可以看官方文档。
可以看出,Ability 和 Android 中的 Activity 不仅在形态上相似,在使用上也基本一致,唯一不同的是,Activity 可以直接显示内容,而 Ability 则是一个容器,需要依赖 page 显示内容。
鸿蒙提供了 Router 模块,通过 url 地址完成 page 之间的路由。
Router 模块提供了两种跳转模式,分别是 router.pushUrl() 和 router.replaceUrl(),这两种模式决定了目标页是否会替换当前页。
同时,Router 模块提供了两种实例模式,分别是 Standard 和 Single。这两种模式决定了目标 url 是否会对应多个实例。
function onJumpClick(): void {
router.pushUrl({
url: ‘pages/Detail’, // 目标url
params: {id: 123} // 添加params属性,传递自定义参数
}, (err) => {
if (err) {
console.error(Invoke pushUrl failed, code is ${err.code}, message is ${err.message}
);
return;
}
console.info(‘Invoke pushUrl succeeded.’);
})
}
在目标页中,可以通过调用 Router 模块的 getParams()
方法来获取传递过来的参数。
const params = router.getParams(); // 获取传递过来的参数对象
const id = params[‘id’]; // 获取id属性的值
page 的设计思路和前端比较接近,相比 Android 的 Activity 更加轻量,相比 Fragment 使用起来更加简单。
和 Fragment 不同的是,page 是全屏展示,而 Fragment 则比较自由,展示大小可以自定义。
其实很多年前 Android 上就有单 Activity 多 Fragment 的架构设计,最近 Google 也推出了 Jetpack Navigation 库,专门用来做 Fragment 路由,然而用起来并不友好,网上吐槽不少。
单从页面架构设计来看,鸿蒙确实遥遥领先~
在互联网时代,网络请求是 App 中最常用的功能之一。
鸿蒙中的网络开发比较简单,官方提供了 Http 模块,可以快速发送网络请求,为了减少重复代码,我们可以封装一个统一的请求方法。
async function requestSync(path: string, method: http.RequestMethod, extraData?: Object): Promise<Response> {
return new Promise<Response>((resolve, reject) => {
let url = BASE_URL + path;
let uri = parseUri(url);
let header = {};
if (method === http.RequestMethod.POST) {
header[“Content-Type”] = “application/x-www-form-urlencoded”;
if (!extraData) {
// POST 必须有请求体,否则会报 Parameter error
extraData = {};
}
}
let httpRequest = http.createHttp();
hilog.info(0, TAG, start request, path: ${path}, method: ${method}, extraData:
+ JSON.stringify(extraData));
httpRequest.request(
url,
{
method: method,
expectDataType: http.HttpDataType.OBJECT,
header: header,
extraData: extraData
},
(err, data) => {
let res = new Response()
if (!err && data.responseCode === 200) {
Object.assign(res, data.result)
hilog.info(0, TAG, request success, path: ${path}, result: ${JSON.stringify(res)}
)
} else {
hilog.error(0, TAG, request error, path: ${path}, error: ${JSON.stringify(err)}
)
res.errorCode = data?.responseCode??-1
res.errorMsg = err?.message??“”
}
resolve(res);
}
)
})
}
同时提供统一的返回结构 Response。
export class Response {
errorCode: number = 0
errorMsg: string = “”
data: T = null
isSuccess(): boolean {
return this.errorCode === 0
}
isSuccessWithData(): boolean {
return this.errorCode === 0 && !!this.data
}
}
当需要发请求的时候,一行代码就能搞定。
async getHomeArticleList(page: number): Promise<Response> {
return requestSync(/article/list/${page}/json
, http.RequestMethod.GET);
}
得益于 TS 中的 Promise,我们可以将繁琐的异步回调转为同步调用,和 Kotlin 中的协程有异曲同工之妙,强烈推荐!
App 的状态保存,免不了要使用持久化数据,持久化主要分为 KV 键值对、SQL 数据库,而鸿蒙上 KV 键值对又分为 Preference 和 KVStore。
Preferences
用户首选项为应用提供Key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。
设计上同样是内存 + 磁盘缓存,首次使用时会将全量数据读取到内存中,因此不是和存储大量数据。
接口如下:
用法基本和 Android 中的 SharedPreference 一致,鸿蒙还提供了 on 和 off 来订阅数据变更,遥遥领先~
不过,还是要吐槽一下,目前 DevEcoStudio 3.1.1 正式版每次运行会清楚 App 数据,导致持久化保存的数据全部被清空,希望新版本能优化下。
KV-Store
相比 Preferences,KV-Store 在数据大小上没有太多限制,猜测底层实现应该和 MMKV 一样,通过共享内存完成磁盘同步,因此性能要高不少。
接口如下:
鸿蒙直接把 Android 上三方库的功能给内置了,再次领先~
数据库
当我们需要存储复杂数据结构时,就需要用到数据库了,鸿蒙也提供了便捷操作数据库的接口。
这么方便的操作数据库,在没有 Room、GreenDAO 等开源库之前,简直不敢想,Android 原生的数据库操作,简直是噩梦, 鸿蒙仍然领先~
在移动应用中,免不了要处理各种后台任务,比如接口请求、数据存储、埋点上报等等,因此多线程的性能,直接影响了应用的流畅度。
鸿蒙提供了 TaskPool 和 Worker 两种并发能力。
看到第一行,线程间内存不共享的时候,我震惊了,操作系统课堂上老师不是说线程共享进程****的资源吗?难道物理学不存在了?
内存不共享的好处是不用担心对象被并发修改,但是坏处是线程间通信必须复制对象,对于多线程同步大量数据的场景,性能应该会有不小的影响。网上搜了下,也没找到鸿蒙这么设计的原因。
既然如此,我们先看下怎么使用吧。
TaskPool
TaskPool 支持在主线程封装任务抛给任务队列,系统选择合适的工作线程,进行任务的分发及执行,再将结果返回给主线程,支持任务的执行、取消,工作线程数量上限为 4。
使用:
import taskpool from ‘@ohos.taskpool’;
@Concurrent
function imageProcessing(dataSlice: ArrayBuffer) {
// 步骤1: 具体的图像处理操作及其他耗时操作
return dataSlice;
}
function histogramStatistic(pixelBuffer: ArrayBuffer) {
// 步骤2: 分成三段并发调度
let number = pixelBuffer.byteLength / 3;
let buffer1 = pixelBuffer.slice(0, number);
let buffer2 = pixelBuffer.slice(number, number * 2);
let buffer3 = pixelBuffer.slice(number * 2);
let task1 = new taskpool.Task(imageProcessing, buffer1);
let task2 = new taskpool.Task(imageProcessing, buffer2);
let task3 = new taskpool.Task(imageProcessing, buffer3);
taskpool.execute(task1).then((ret: ArrayBuffer[]) => {
// 步骤3: 结果处理
});
taskpool.execute(task2).then((ret: ArrayBuffer[]) => {
// 步骤3: 结果处理
});
taskpool.execute(task3).then((ret: ArrayBuffer[]) => {
// 步骤3: 结果处理
});
}
和 Java 中的 ThreadPool 比较类似,通过任务队列 + 线程池实现多任务并发,相比 ThreadPool,TaskPool 的接口比较简单,内部自动分派线程,不支持配置线程类型和数量,此外,TaskPool 的最大线程数为4,感觉放在现在动辄 8 核的处理器上,好像差了点意思,似乎不能完全发挥处理器性能?
Worker
创建 Worker 的线程称为宿主线程(不一定是主线程,工作线程也支持创建 Worker 子线程),Worker 自身的线程称为 Worker 子线程(或 Actor 线程、工作线程)。每个 Worker 子线程与宿主线程拥有独立的实例,包含基础设施、对象、代码段等。Worker 子线程和宿主线程之间的通信是基于消息传递的,Worker 通过序列化机制与宿主线程之间相互通信,完成命令及数据交互。
使用:
import worker from ‘@ohos.worker’;
const workerInstance = new worker.ThreadWorker(‘entry/ets/workers/MyWorker.ts’);
例如向 Worker 线程发送训练和预测的消息,同时接收 Worker 线程发送回来的消息。
// 接收Worker子线程的结果
workerInstance.onmessage = function(e) {
// data:Worker线程发送的信息
let data = e.data;
console.info(‘MyWorker.ts onmessage’);
}
workerInstance.onerror = function (d) {
// 接收Worker子线程的错误信息
}
// 向Worker子线程发送训练消息
workerInstance.postMessage({ ‘type’: 0 });
// 向Worker子线程发送预测消息
workerInstance.postMessage({ ‘type’: 1, ‘value’: [90, 5] });
例如在Worker线程中定义预测模型及其训练过程,同时与主线程进行信息交互。
import worker, { ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from ‘@ohos.worker’;
let workerPort: ThreadWorkerGlobalScope = worker.workerPort;
// 定义训练模型及结果
let result;
// 定义预测函数
function predict(x) {
return result[x];
}
// 定义优化器训练过程
function optimize() {
result = {};
}
// Worker线程的onmessage逻辑
workerPort.onmessage = function (e: MessageEvents) {
let data = e.data
// 根据传输的数据的type选择进行操作
switch (data.type) {
case 0:
// 进行训练
optimize();
// 训练之后发送主线程训练成功的消息
workerPort.postMessage({ type: ‘message’, value: ‘train success.’ });
break;
case 1:
// 执行预测
const output = predict(data.value);
// 发送主线程预测的结果
workerPort.postMessage({ type: ‘predict’, value: output });
break;
default:
workerPort.postMessage({ type: ‘message’, value: ‘send message is invalid’ });
break;
}
}
此外,Worker 的创建和销毁耗费性能,需要自行管理已创建的 Worker 并重复使用。Worker 空闲时也会一直运行,因此当不需要 Worker 时,需要调用 terminate() 接口或 parentPort.close() 方法主动销毁 Worker。Worker 存在数量限制,支持最多同时存在 8 个 Worker。
可以看出,Worker 更接近 Java 中使用 new Thread() 原生创建线程的方式,不同的是 Worker 一旦创建,就会一直运行,占用 CPU 资源,只能手动停止,而 Java 中的线程则更加“智能”,一旦线程中的逻辑执行完,即自动进入 TERMINATED 状态,释放 CPU 资源,并自动销毁。
因此,推荐大家使用 TaskPool。
看起来鸿蒙上使用多线程不太领先,存在各种限制,比如线程间内存不共享,线程数量存在上限。
在完整开发完一个 App 后,简单总结下鸿蒙原生开发的优缺点。
优点
虽然目前 Compose 和 SwiftUI 也支持声明式,但推出这么久之后,真正在线上大规模使用的产品寥寥无几,顶多在新业务上尝试使用,所以鸿蒙在 UI 上有天然优势。
以发送网络请求为例,在 Android 上如果用原生的方式,各种配置用起来头大,而鸿蒙则是通过一个方法即可发起请求,并将常用配置都作为参数开放出来,其他 API 也是类似,鸿蒙的系统 API 用起来更加便捷。
对于有前端开发经验的同学来说,几乎是0成本,即使没用过 TS/JS,上手也非常简单。
首先 IDE 使用基本一致,其次系统架构和 API 定义也非常相似,可以说是一一对应,如果你刚好又用过 Compose 或 Flutter,那 ArkUI 也不成问题。
在使用系统 API 的时候,有时想看下内部实现,习惯性的点击方法查看源码,发现看不了,如果遇到系统 API 有问题,就很难定位了。
在开发中遇到一些bug,比如模拟器上 WebView 无法滚动,而真机正常,还有一些体验不太好的地方,比如每次重新运行会先卸载再安装,导致重新运行后数据丢失,在社区咨询后得知后期都会修复。
目前比较活跃的社区只有官方的开发者社区,但是目前看下来干活不多,大多是来咨询问题或者反馈bug的,而且很多反馈没人回应,可能是问题太多了忙不过来。
官方维护了一个开源库列表,刚接触鸿蒙时,里面的开源库还寥寥无几,没想到几个月后的现在已经非常丰富了,包括和 OKHttp 能力基本一致的网络库 httpclient,但是稳定性还需要大家去验证。
在开发的过程中,我一直在想一个问题,鸿蒙使用 TS 作为开发语言,那么运行时到底是通过 JS Runtime 还是其他方式呢?如果是 JS Runtime,那和 WebApp 有什么区别呢?如果是通过转译的方式,那虚拟机最终执行的代码是什么形式的?
这些问题可能只有完全了解了 ArkCompiler 才能回答,不过目前官网对 ArkCompiler 的介绍仅局限于短短一页,我们只能尝试分析打包产物,看能否看出一些端倪。
先解包 hap 看下目录结构:
resources 和 module.json、pack.info 是资源和配置信息,主要看 ets 目录。
先看 modules.abc:
是一个纯二进制文件,我在网上没有找到 abc 格式相关的信息,可能是鸿蒙自己定义的,暂时不清楚这里保存了什么信息。
接着看 sourceMaps.map 和 symbolMap.map:
可以看出这两个文件保存了所有 TS 文件的原始信息和 mappings 映射,mappings 是不是很眼熟?
没错,这里可能是将 TS 源码进行了混淆,在安装或运行时,虚拟机再通过某种方式获取到真正要执行的 TS 代码。官网上的介绍也能佐证。
到这里,我们对鸿蒙运行时有了一个大致的轮廓:虚拟机执行的仍然是原始 TS 代码,只是在编译时被加密了,以此保证源码安全。
我大胆猜测,鸿蒙虚拟机内置了 JS Runtime,用来解释执行 TS 代码,至于 UI 渲染,则是 ArkUI 将 TS 代码映射为相应的 UI 控件,并进行布局和渲染。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
机再通过某种方式获取到真正要执行的 TS 代码。官网上的介绍也能佐证。
到这里,我们对鸿蒙运行时有了一个大致的轮廓:虚拟机执行的仍然是原始 TS 代码,只是在编译时被加密了,以此保证源码安全。
我大胆猜测,鸿蒙虚拟机内置了 JS Runtime,用来解释执行 TS 代码,至于 UI 渲染,则是 ArkUI 将 TS 代码映射为相应的 UI 控件,并进行布局和渲染。
[外链图片转存中…(img-w3Ij6aLN-1715796794366)]
[外链图片转存中…(img-ZEvYOsak-1715796794366)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。