鸿蒙迎来纯血版本,你还不来学习一下

前言

最近移动开发圈子里,鸿蒙可谓出尽了风头,先是宣布即将正式发布的 Harmony OS Next 将完全剥离 Android 代码,也就是不再兼容 Android,化身为纯血的鸿蒙,紧接着又启动了鸿蒙生态千帆启航,伴随着的是众多大厂已经启动原生鸿蒙适配,包括支付宝、京东、美团等等。

作为一个整天被内卷的客户端开发,不得不加入了。

本文基于 Harmony OS Api 9,如果文章内容与新版本有不一致的地方,一切以新版本为准,望见谅。

熟悉概念

在正式开始前,我们先了解一下鸿蒙开发相关的概念,打开鸿蒙官网,首先看到的就是这几个套件

image01.png

下面我们来简单认识下

  • DevEco Studio

    面向 HarmonyOS 应用及元服务开发者提供的集成开发环境(IDE), 助力高效开发。

    也就是鸿蒙开发的 IDE,类似 Android Studio 和 Xcode。

  • ArkTS

    ArkTS 是鸿蒙生态的应用开发语言。它在保持 TypeScript (简称TS)基本语法风格的基础上,对 TS 的动态类型特性施加更严格的约束,引入静态类型。同时,提供了声明式 UI、状态管理等相应的能力,让开发者可以以更简洁、更自然的方式开发高性能应用。

    TypeScript 曾经风靡一时,弥补了 JavaScript 不支持强类型的缺点,作为一名 Java 开发者,更习惯强类型语言。至于 ArkTS 和 TypeScript 的区别可以暂时不用关心,把它当做标准的 TS 用就行。

  • ArkUI

    ArkUI 是一套构建分布式应用界面的声明式 UI 开发框架。它使用极简的 UI 信息语法、丰富的 UI 组件、以及实时界面预览工具,帮助您提升 HarmonyOS 应用界面开发效率30%。您只需使用一套 ArkTS API,就能在多个 HarmonyOS 设备上提供生动而流畅的用户界面体验。

    可以理解为用 TS 实现的一套 UI 组件库,包括常用的组件和布局等,类似于 Google 的 material 组件库和 Swift UI。

  • ArkCompiler

    ArkCompiler 是华为自研的统一编程平台,包含编译器、工具链、运行时等关键部件,支持高级语言在多种芯片平台的编译与运行,并支撑应用和服务运行在手机、个人电脑、平板、电视、汽车和智能穿戴等多种设备上的需求。

    用于将 TS 代码编译为鸿蒙系统可执行指令,俗称编译打包工具。

初识鸿蒙

准备IDE

按照官方教程,我们需要下载鸿蒙开发 IDE,也就是 DevEco Studio,和 AndroidStudio 一样,都是基于 IntelliJ IDEA 二次开发,上手没有难度。

首次启动,需要安装 Node.js、ohpm、Harmony OS SDK 等依赖,根据引导一直下一步即可。

完整依赖如下

screenshot-20240228-163242.png

可能有些同学还不太熟悉这些概念,我们简单介绍下

  • Node.js

    Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,我们从上面的概念中了解到,鸿蒙使用 TS 作为开发语言,而 TS 实际上就是 JS,因此需要 JS 运行环境也很正常。

  • ohpm

    大家应该都听说过 npm,是一个前端比较常用的包管理工具,ohpm 全称应该是 Open Harmony Package Manager,也就是鸿蒙上使用的包管理工具,类似于 Java 上的 Maven 仓库。

  • Harmony OS SDK

    这个应该很好理解,开发鸿蒙需要的软件开发工具包,相当于 Android 上的 Android SDK。

image03.png

有一点需要注意,IDE 仅支持特定版本的 Node.js,我本地由于安装了高版本的 Node.js,因此无法继续下一步,建议大家先卸载本地安装的 Node.js,通过 IDE 重新安装支持的版本,因为我自己安装的低版本也会报错🤩

创建项目

IDE 配置完成后,我们新建一个项目,IDE 默认提供了很多模板,值得一提的是,把光标放上去还会提示你支持哪些设备

image04.png

这里出现了 Ability 的概念,看起来和 Android 中的 Activity 很像,也就是一个页面,我们暂且认为它就是一个页面,后面我们再详细介绍。

除了空页面之外,还支持关于、分类、网格、列表、登录、闪屏等模板,还支持创建 C++ 原生项目。

支持的完整模板如下

image05.png

我们选择 Empty Ability 下一步

image06.png

接下来到了项目配置页面,支持的配置如下

  • Project name: 项目名

  • Bundle name: 项目唯一标志,类似于 Android 的 Package name

  • Save location: 保存位置

  • Compile SDK: 编译使用的 SDK 版本

  • Model: 应用模型,有 StageFA 可选,官网推荐使用 Stage,我们就先用这个

  • Enable Super Visual: 官方的解释是启用低代码开发,先不管它

  • Language: 开发语言,Stage 模型只能选 ArkTS,FA 模型还可以选 JS

  • Compatible SDK: 字面意思是兼容的的 SDK 版本,暂时不清楚是最低支持版本(minSdk)还是适配的版本(targetSdk),看起来更像是前者,如果是这样的话鸿蒙走的应该是类似苹果的路线,即新版本出来后开发者必须要适配,否则高版本可能用不了

  • Device type: 支持的设备类型

点击 Finish 即可完成创建

项目结构

创建完的项目是这样的

image07.png

完整目录如下:

├── AppScope // app 默认配置
│  ├── resources // 资源
│  │  └── base
│  │      ├── element // 文案、颜色等资源
│  │      │  └── string.json
│  │      └── media // 图片资源
│  │          └── app_icon.png
│  └── app.json5 // app 配置,包括 名称、图标、bundleName、版本号等
├── entry // entry 文件夹,相当于 Android 项目中的 app module
│  ├── src
│  │  ├── main
│  │  │  ├── ets // 源代码,相当于 Android 项目中的 java 目录
│  │  │  │  ├── entryability
│  │  │  │  │  └── EntryAbility.ts // entry 中的页面,一个 entry 可以有多个 Ability
│  │  │  │  └── pages
│  │  │  │      └── Index.ets
│  │  │  ├── resources
│  │  │  │  ├── base // 和语言无关的通用资源
│  │  │  │  │  ├── element // 文案、颜色等资源
│  │  │  │  │  │  ├── color.json
│  │  │  │  │  │  └── string.json
│  │  │  │  │  ├── media // 图片资源
│  │  │  │  │  │  └── icon.png
│  │  │  │  │  └── profile
│  │  │  │  │      └── main_pages.json // entry 中包含的 pages 路径,相当于前端项目中 app.json 中的 pages
│  │  │  │  ├── en_US // 英文下使用的资源,可以用来配置多语言
│  │  │  │  ├── rawfile // 应该是存放二进制文件的目录,相当于 Android 项目中的 res/raw 目录,暂时还没用到
│  │  │  │  └── zh_CN // 同 en_US
│  │  │  └── module.json5 // entry 内部配置文件,包括名称、包含的 Abilities、pages,默认 Ability 等
│  │  └── ohosTest // 测试代码
│  ├── build-profile.json5 // entry 构建配置,包括应用模型、支持的系统类型等
│  ├── hvigorfile.ts // 暂时不清楚
│  └── oh-package.json5 // entry 对外配置文件,包括名称、版本、许可证、依赖项等
├── hvigor // 暂时不清楚,看着像是 Android 项目中的 grade 目录
│  ├── hvigor-config.json5
│  └── hvigor-wrapper.js
├── oh_modules // 项目依赖的三方库,相当于前端项目中的 node_modules
├── build-profile.json5 // app 构建配置,包括 app 签名、编译 SDK 版本、兼容 SDK 版本,以及包含的 entry 列表
├── hvigorfile.ts // 暂时不清楚
├── hvigorw // 暂时不清楚
├── hvigorw.bat
├── local.properties // 本地配置
├── oh-package.json5 // 项目配置,包括名称、版本、许可证、依赖项等
└── oh-package-lock.json5

鸿蒙中支持多种 Module,常用的有以下两种:

  • Entry

    相当于 Android 中的 Application Module,每个 Entry 都可以独立运行。

  • Library

    相当于 Android 中的 Library Module,仅可被依赖,无法独立运行。

Stage 模型

Stage模型概念图

image08.png

  • UIAbility组件

    UIAbility 组件是一种包含 UI 界面的应用组件,主要用于和用户交互。UIAbility 组件是系统调度的基本单元,为应用提供绘制界面的窗口;一个 UIAbility 组件中可以通过多个页面来实现一个功能模块。每一个 UIAbility 组件实例,都对应于一个最近任务列表中的任务。

  • WindowStage

    每个 UIAbility 类实例都会与一个 WindowStage 类实例绑定,该类提供了应用进程内窗口管理器的作用。它包含一个主窗口。也就是说 UIAbility 通过 WindowStage 持有了一个窗口,该窗口为 ArkUI 提供了绘制区域。

  • Context

    Context 是应用中对象的上下文,其提供了应用的一些基础信息,例如 resourceManager(资源管理)、applicationInfo(当前应用信息)、dir(应用开发路径)、area(文件分区)等,以及应用的一些基本方法,例如 createBundleContext()、getApplicationContext()等。UIAbility 组件和各种 ExtensionAbility 派生类组件都有各自不同的 Context 类。分别有基类Context、ApplicationContext、AbilityStageContext、UIAbilityContext、ExtensionContext、ServiceExtensionContext等Context。

    image09.png

  • Page

    Ability 是一个窗口,不包含内容,而 Page 则是真正显示内容的载体,Page 需要依附于 Ability 才能显示。

上手

熟悉了上面的这些概念,我们就可以尝试开发一个鸿蒙上的 App 了。

之前学习 Compose 的时候做了一个「玩 Android」App,为了方便使用现成接口和 UI,我们来做一个鸿蒙版的「玩 Android」,顺便对比下鸿蒙和 Compose 在 UI 层的差异。

UI开发

和 Compose、SwiftUI 类似,鸿蒙的 ArkUI 也采用声明式开发范式,这也是现代UI开发的共识,相比命令式UI,声明式UI更加简洁高效。

ArkUI 提供了官方组件和布局,基本能够满足常见 UI 界面的开发

image10.png

组件的详细使用这里不再赘述,我们以文章 Item 布局作为示例,来对比下鸿蒙和 Compose 的实现方式

先看下展示效果

image11.png

代码对比

image12.png

可以看出,语法的相似度非常高,以至于我后来直接把 Compose 的代码复制来过改一下就能用了🤣

简单介绍下差异

  • 鸿蒙使用 struct 定义组件,而 Compose 使用方法

  • 鸿蒙仅支持在 ArkUI 中使用除组件外的特定操作符,包括 if-else、ForEach 等,而 Compose 比较自由,可以在 UI 方法中使用任意代码

  • 鸿蒙通过组件对象提供的方法设置属性,如 Text("").fontSize(10),而 Compose 则是在构造函数中设置,如 Text(text = "", fontSize = 10.sp)

由于鸿蒙和 Compose 都是声明式 UI,因此状态更新也比较相似,都是状态驱动 UI 更新,此外,鸿蒙和 Compose 都支持通过 Diff 算法完成差量更新。

页面路由

在项目结构部分提到,鸿蒙上 UI 界面的单位包括 Ability 和 page。

一般来说一个功能只需要一个 Ability 呢 就够了,那什么时候需要多个 Ability 呢?

举个🌰,如果你当前开发的是通讯录应用,当其他应用需要选择联系人时,需要打开通讯录的选人页面,而打开外部应用页面的最小单元为 Ability,因为 page 无法独立显示,这时你就需要创建多个 Ability。

Ability 跳转

1. 在 EntryAbility 中,通过调用 startAbility() 方法启动 UIAbility,want 为 UIAbility 实例启动的入口参数,其中 bundleName 为待启动应用的 Bundle 名称,abilityName 为待启动的 UIAbility 名称,moduleName 在待启动的 UIAbility 属于不同的 Module 时添加,parameters 为自定义信息参数。

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) => {
    // ...
})

2. 在 FuncAbility 的生命周期回调文件中接收 EntryAbility 传递过来的参数。

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;
        // ...
    }
}

3. 如需要停止当前UIAbility实例,通过调用 `terminateSelf()` 方法实现。

 
// context为需要停止的UIAbility实例的AbilityContext
this.context.terminateSelf((err) => {
    // ...
});

同时,鸿蒙也提供了 startAbilityForResult 来实现启动 Ability 并获取返回结果,详细使用可以看官方文档。

可以看出,Ability 和 Android 中的 Activity 不仅在形态上相似,在使用上也基本一致,唯一不同的是,Activity 可以直接显示内容,而 Ability 则是一个容器,需要依赖 page 显示内容。

page 跳转

鸿蒙提供了 Router 模块,通过 url 地址完成 page 之间的路由。

Router 模块提供了两种跳转模式,分别是 router.pushUrl()router.replaceUrl(),这两种模式决定了目标页是否会替换当前页。

  • router.pushUrl():目标页不会替换当前页,而是压入页面栈。这样可以保留当前页的状态,并且可以通过返回键或者调用 router.back() 方法返回到当前页。

  • router.replaceUrl():目标页会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页。

同时,Router 模块提供了两种实例模式,分别是 StandardSingle。这两种模式决定了目标 url 是否会对应多个实例。

  • Standard:标准实例模式,也是默认情况下的实例模式。每次调用该方法都会新建一个目标页,并压入栈顶。

  • Single:单实例模式。即如果目标页的url在页面栈中已经存在同 url 页面,则离栈顶最近的同 url 页面会被移动到栈顶,并重新加载;如果目标页的 url 在页面栈中不存在同 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<T>(path: string, method: http.RequestMethod, extraData?: Object): Promise<Response<T>> {
  return new Promise<Response<T>>((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<T>()
        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<T> {
  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<ArticleList>> {
  return requestSync(`/article/list/${page}/json`, http.RequestMethod.GET);
}

得益于 TS 中的 Promise,我们可以将繁琐的异步回调转为同步调用,和 Kotlin 中的协程有异曲同工之妙,强烈推荐!

持久化

App 的状态保存,免不了要使用持久化数据,持久化主要分为 KV 键值对、SQL 数据库,而鸿蒙上 KV 键值对又分为 Preference 和 KVStore。

  • 用户首选项(Preferences):通常用于保存应用的配置信息。数据通过文本的形式保存在设备中,应用使用过程中会将文本中的数据全量加载到内存中,所以访问速度快、效率高,但不适合需要存储大量数据的场景。

  • 键值型数据库(KV-Store):一种非关系型数据库,其数据以“键值”对的形式进行组织、索引和存储,其中“键”作为唯一标识符。适合很少数据关系和业务关系的业务数据存储,同时因其在分布式场景中降低了解决数据库版本兼容问题的复杂度,和数据同步过程中冲突解决的复杂度而被广泛使用。相比于关系型数据库,更容易做到跨设备跨版本兼容。

  • 关系型数据库(RelationalStore):一种关系型数据库,以行和列的形式存储数据,广泛用于应用中的关系型数据的处理,包括一系列的增、删、改、查等接口,开发者也可以运行自己定义的SQL语句来满足复杂业务场景的需要。

Preferences

用户首选项为应用提供Key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。

设计上同样是内存 + 磁盘缓存,首次使用时会将全量数据读取到内存中,因此不是和存储大量数据。

接口如下

image13.png

用法基本和 Android 中的 SharedPreference 一致,鸿蒙还提供了 on 和 off 来订阅数据变更,遥遥领先~

不过,还是要吐槽一下,目前 DevEcoStudio 3.1.1 正式版每次运行会清楚 App 数据,导致持久化保存的数据全部被清空,希望新版本能优化下。

KV-Store

相比 Preferences,KV-Store 在数据大小上没有太多限制,猜测底层实现应该和 MMKV 一样,通过共享内存完成磁盘同步,因此性能要高不少。

接口如下

image14.png

鸿蒙直接把 Android 上三方库的功能给内置了,再次领先~

数据库

当我们需要存储复杂数据结构时,就需要用到数据库了,鸿蒙也提供了便捷操作数据库的接口

image15.png

这么方便的操作数据库,在没有 Room、GreenDAO 等开源库之前,简直不敢想,Android 原生的数据库操作,简直是噩梦😭 鸿蒙仍然领先~

多线程

在移动应用中,免不了要处理各种后台任务,比如接口请求、数据存储、埋点上报等等,因此多线程的性能,直接影响了应用的流畅度。

鸿蒙提供了 TaskPoolWorker 两种并发能力

image16.png

看到第一行,线程间内存不共享的时候,我震惊了,操作系统课堂上老师不是说线程共享进程的资源吗?难道物理学不存在了?

内存不共享的好处是不用担心对象被并发修改,但是坏处是线程间通信必须复制对象,对于多线程同步大量数据的场景,性能应该会有不小的影响。网上搜了下,也没找到鸿蒙这么设计的原因。

既然如此,我们先看下怎么使用吧

TaskPool

TaskPool 支持在主线程封装任务抛给任务队列,系统选择合适的工作线程,进行任务的分发及执行,再将结果返回给主线程,支持任务的执行、取消,工作线程数量上限为 4。

image17.png

使用:

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 通过序列化机制与宿主线程之间相互通信,完成命令及数据交互。

image18.png

使用:

  1. 在主线程中通过调用 ThreadWorker 的 constructor() 方法创建 Worker 对象,当前线程为宿主线程。

 
import worker from '@ohos.worker';

const workerInstance = new worker.ThreadWorker('entry/ets/workers/MyWorker.ts');

  1. 在宿主线程中通过调用 onmessage() 方法接收 Worker 线程发送过来的消息,并通过调用 postMessage() 方法向 Worker 线程发送消息。

例如向 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] });
  1. 在 Worker 线程中通过调用 onmessage() 方法接收宿主线程发送的消息内容,并通过调用 postMessage() 方法向宿主线程发送消息。

例如在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 后,简单总结下鸿蒙原生开发的优缺点。

优点

  • 原生支持声明式 UI

    虽然目前 Compose 和 SwiftUI 也支持声明式,但推出这么久之后,真正在线上大规模使用的产品寥寥无几,顶多在新业务上尝试使用,所以鸿蒙在 UI 上有天然优势。

  • 系统 API 封装性高

    以发送网络请求为例,在 Android 上如果用原生的方式,各种配置用起来头大,而鸿蒙则是通过一个方法即可发起请求,并将常用配置都作为参数开放出来,其他 API 也是类似,鸿蒙的系统 API 用起来更加便捷。

  • TS 学习成本低

    对于有前端开发经验的同学来说,几乎是0成本,即使没用过 TS/JS,上手也非常简单。

  • Android 同学上手快

    首先 IDE 使用基本一致,其次系统架构和 API 定义也非常相似,可以说是一一对应,如果你刚好又用过 Compose 或 Flutter,那 ArkUI 也不成问题。

缺点

  • 系统不开源

    在使用系统 API 的时候,有时想看下内部实现,习惯性的点击方法查看源码,发现看不了,如果遇到系统 API 有问题,就很难定位了。

  • 配套工具还未完善

    在开发中遇到一些bug,比如模拟器上 WebView 无法滚动,而真机正常,还有一些体验不太好的地方,比如每次重新运行会先卸载再安装,导致重新运行后数据丢失,在社区咨询后得知后期都会修复。

  • 社区不够成熟

    目前比较活跃的社区只有官方的开发者社区,但是目前看下来干货不多,大多是来咨询问题或者反馈bug的,而且很多反馈没人回应,可能是问题太多了忙不过来。

  • 开源框架较少

    官方维护了一个开源库列表,刚接触鸿蒙时,里面的开源库还寥寥无几,没想到几个月后的现在已经非常丰富了,包括和 OKHttp 能力基本一致的网络库 httpclient,但是稳定性还需要大家去验证。

深入了解

在开发的过程中,我一直在想一个问题,鸿蒙使用 TS 作为开发语言,那么运行时到底是通过 JS Runtime 还是其他方式呢?如果是 JS Runtime,那和 WebApp 有什么区别呢?如果是通过转译的方式,那虚拟机最终执行的代码是什么形式的?

这些问题可能只有完全了解了 ArkCompiler 才能回答,不过目前官网对 ArkCompiler 的介绍仅局限于短短一页,我们只能尝试分析打包产物,看能否看出一些端倪。

先解包 hap 看下目录结构

image19.png

resources 和 module.json、pack.info 是资源和配置信息,主要看 ets 目录

先看 modules.abc(857KB)

image20.png

abc 的全称是 Ark byte code,即方舟字节码,暂时不清楚这里保存了什么信息,以及它的运行原理。

接着看 sourceMaps.map(828KB) 和 symbolMap.map(287KB)

image21.png

image22.png

可以看出这两个文件保存了所有 TS 文件的原始信息和 mappings 映射,mappings 是不是很眼熟?

没错,这里可能是将 TS 源码进行了混淆,在安装或运行时,虚拟机再通过某种方式获取到真正要执行的 TS 代码。官网上的介绍也能佐证

image23.png

到这里,我们对鸿蒙运行时有了一个大致的轮廓:虚拟机执行的仍然是原始 TS 代码,只是在编译时被加密了,以此保证源码安全。

我大胆猜测,鸿蒙虚拟机内置了 JS Runtime,用来解释执行 TS 代码,至于 UI 渲染,则是 ArkUI 将 TS 代码映射为相应的 UI 控件,并进行布局和渲染。

More? ArkUI-X

你以为鸿蒙只是一个独立于 Android 和 iOS 的移动操作系统吗?鸿蒙的野心不止于此!

要知道,鸿蒙剥离 Android 之后,需要各个应用开发商重新基于鸿蒙开发一次应用,这个成本绝对不可忽视,尤其对于头部大型 App 来说,业务极其复杂,开发成本也非常大。

因此,除了积极和开发商沟通外,鸿蒙也在考虑如何降低开发成本,当大家还在考虑能否将 Android/iOS 代码转为鸿蒙时,华为悄悄的发布了 ArkUI-X

简单来说,使用 ArkUI 完成鸿蒙应用开发后,可以快速构建 Android 和 iOS 平台的应用,相当于鸿蒙版的 Flutter。

image24.png

这里不再具体介绍,感兴趣的可以看下这篇文章:华为发布了跨平台开发的ArkUI-X,我不允许你学不会!

项目源码

最后推荐下我做的鸿蒙版「玩 Android」,有兴趣的同学可以点击查看源码

screenshot.jpg

总结

本文主要介绍了鸿蒙相关的概念,以及如何上手开发一个鸿蒙原生应用,通过开发一个鸿蒙版的「玩 Android」,带领大家熟悉 ArkUI 和常用 API 的使用,基于开发体验总结了现阶段鸿蒙开发的优势和存在的问题,通过对 hap 包的简单分析了解了鸿蒙运行时的大致轮廓,最后介绍了鸿蒙上的跨平台开发框架 ArkUI-X,希望读完本文对大家有帮助。

作者:王晨彦
链接:https://juejin.cn/post/7340307629891616808
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值