背景
鸿蒙系统是中国首款完全自主研发的分布式操作系统,其底层技术实现完全自主可控,摆脱了对安卓和iOS的依赖。尤其在当前国际科技竞争加剧的背景下,鸿蒙的诞生降低了我国在操作系统领域对外国技术的依赖,为国家信息安全和产业安全筑起防线。
除了移动端系统,鸿蒙电脑也将在5月份发布,鸿蒙PC端应用开发,工具、环境和移动端都是一样的,可以真正实现一次开发,多端部署,所以大家了解一下还是有价值的。
1.视图
视图单位

vp类似于Android中的dp,fp类似于Android中的sp
生命周期
UIAbility生命周期
UIAbility是一种包含UI的应用组件,主要用于和用户交互,类似于Android中的Activity
如果开发者希望在任务视图中看到一个任务,建议使用“一个UIAbility+多个页面”的方式,可以避免不必要的资源加载。
如果开发者希望在任务视图中看到多个任务,或者需要同时开启多个窗口,建议使用多个UIAbility实现不同的功能。
生命周期如下:

UIAbility实例创建完成之后,在进入Foreground之前,系统会创建一个WindowStage。WindowStage创建完成后会进入onWindowStageCreate()回调,可以在该回调中设置UI入口页面(windowStage.loadContent),类似于Android中Fragment的onCreateView()

页面生命周期

页面生命周期会区分组件类型,页面入口组件和普通自定义组件生命周期是不一样的
生命周期有两类:
- 页面生命周期,即被@Entry 装饰的组件生命周期,有以下生命周期接口:
onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景。
onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。
onBackPress:当用户点击返回按钮时触发。
- 组件生命周期,即一般用@Component装饰的自定义组件的生命周期,有以下生命周期接口:
aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
onDidBuild:组件build()函数执行完成之后回调该接口,开发者可以在这个阶段进行埋点数据上报等不影响实际UI的功能。
aboutToDisappear:aboutToDisappear函数在自定义组件析构销毁之前执行。
页面入口组件:同时被@Entry @Component修饰的页面,拥有页面级和组件级双重生命周期,即上面的生命周期都能走到
普通自定义组件:仅被@Component修饰的页面,仅包含组件级生命周期,普通自定义组件无法使用 onPageShow/onPageHide/onBackPress 等页面级函数,即便是代码里都能实现生命周期函数,系统也不会回调
适配多尺寸
随着手机形态和尺寸越来越多,不知道大家有没有觉得设备条件判断越来越复杂了。
设计原理:
为了提升用户的全场景体验,需要考虑多设备体验的连续性
原则一:两个宽度相近的窗口,页面布局应相同。
原则二:高度相对宽度较小的窗口,呈现横向窗口或类方型窗口时,页面布局进行差异化设计。
设计方案:
横向和纵向维度分别代表窗口的不同特征,作为判断页面布局和交互体验的条件:
横向断点以窗口宽度值区分,代表窗口宽度,单位是vp
纵向断点以窗口高宽比区分,代表窗口相对高度,可以表示横向、方型或纵向窗口。

横向维度以应用窗口宽度为判断条件,推荐按照不同的阈值分成5个区间:

纵向维度以应用窗口的高宽比为判断条件,推荐按照不同的阈值分成3个区间:

可以解决目前一些痛点问题:
·设备判断条件问题:
这种方法判断与设备类型无关,不需要判断机型,可减少判断条件。比如目前你想修改输入法面板上内容,需要多个判断,比如是否是折叠屏,是否是展开态,是否是小折,是否横竖屏等等,非常麻烦;
设备无关的话,同一套代码,迁移到不同厂商就会简单一些,因为屏幕分辨率接口是一样的;
·布局/皮肤复用问题:
同一个维度区域内的设备可以共用一套皮肤,减少皮肤数量;未来新增设备如果落在已适配的方格区域内,则不需要额外适配。比如现在适配了华为新出的阔折叠,如果未来再出小折就不需要再额外适配
·悬浮键盘缩放问题:
目前悬浮键盘的缩放逻辑,自由缩放就会带来很多显示问题,找一个统一的缩放算法很难,可以通过这个方案去解决,悬浮键盘调节后,用宽度和高宽比唯一确定一个形态,每个方格区域的悬浮形态使用同一个固定缩放值。
输入法高度限制
和Android输入法不同,Android端输入法Dialog可以占满全屏,但在鸿蒙系统中,输入法高度最高不能超过屏幕高度70%,输入法进程显示的内容也不能超过向系统申请的高度,比如隐私弹框等弹框的高度都不能超过输入法申请的高度
输入法生命周期
import { Want } from '@kit.AbilityKit';
import keyboardController from './model/KeyboardController';
import { InputMethodExtensionAbility } from '@kit.IMEKit';
export default class InputDemoService extends InputMethodExtensionAbility {
onCreate(want: Want): void {
keyboardController.onCreate(this.context); // 初始化窗口并注册对输入法框架的事件监听
}
onDestroy(): void {
console.log("onDestroy.");
keyboardController.onDestroy(); // 销毁窗口并去注册事件监听
}
}
// 调用输入法框架的getInputMethodAbility方法获取实例,并由此实例调用输入法框架功能接口
const inputMethodAbility: inputMethodEngine.InputMethodAbility = inputMethodEngine.getInputMethodAbility();
export class KeyboardController {
private mContext: InputMethodExtensionContext | undefined = undefined; // 保存InputMethodExtensionAbility中的context属性
private panel: inputMethodEngine.Panel | undefined = undefined;
private textInputClient: inputMethodEngine.InputClient | undefined = undefined;
private keyboardController: inputMethodEngine.KeyboardController | undefined = undefined;
constructor() {
}
public onCreate(context: InputMethodExtensionContext): void
{
this.mContext = context;
this.initWindow(); // 初始化窗口
this.registerListener(); // 注册对输入法框架的事件监听
}
public onDestroy(): void // 应用生命周期销毁
{
this.unRegisterListener(); // 去注册事件监听
if(this.panel) { // 销毁窗口
inputMethodAbility.destroyPanel(this.panel);
}
if(this.mContext) {
this.mContext.destroy();
}
}
public insertText(text: string): void {
if(this.textInputClient) {
this.textInputClient.insertText(text);
}
}
public deleteForward(length: number): void {
if(this.textInputClient) {
this.textInputClient.deleteForward(length);
}
}
private initWindow(): void // 初始化窗口
{
if(this.mContext === undefined) {
return;
}
let dis = display.getDefaultDisplaySync();
let dWidth = dis.width;
let dHeight = dis.height;
let keyHeightRate = 0.47;
let keyHeight = dHeight * keyHeightRate;
let nonBarPosition = dHeight - keyHeight;
let panelInfo: inputMethodEngine.PanelInfo = {
type: inputMethodEngine.PanelType.SOFT_KEYBOARD,
flag: inputMethodEngine.PanelFlag.FLG_FIXED
};
inputMethodAbility.createPanel(this.mContext, panelInfo).then(async (inputPanel: inputMethodEngine.Panel) => {
this.panel = inputPanel;
if(this.panel) {
await this.panel.resize(dWidth, keyHeight);
await this.panel.moveTo(0, nonBarPosition);
await this.panel.setUiContent('InputMethodExtensionAbility/pages/Index');
}
});
}
private registerListener(): void
{
this.registerInputListener(); // 注册对输入法框架服务的监听
// 注册隐藏键盘事件监听等
}
private registerInputListener(): void { // 注册对输入法框架服务的开启及停止事件监听
inputMethodAbility.on('inputStart', (kbController, textInputClient) => {
this.textInputClient = textInputClient; // 此为输入法客户端实例,由此调用输入法框架提供给输入法应用的功能接口
this.keyboardController = kbController;
})
inputMethodAbility.on('inputStop', () => {
this.onDestroy(); // 销毁KeyboardController
});
}
private unRegisterListener(): void
{
inputMethodAbility.off('inputStart');
inputMethodAbility.off('inputStop', () => {});
}
}
const keyboardController = new KeyboardController();
export default keyboardController;
与Android输入法框架不同,InputMethodExtensionAbility仅提供onCreate()和onDestroy()两个生命周期方法,其他的事件(inputStart、inputStop等)需要自己去注册监听。
2.数据
多进程
和Android端不同的是,鸿蒙输入法App端和键盘端是不一样的进程
鸿蒙目前不支持手动设置多进程,进程分配均由系统设置
通常情况下,应用中(同一Bundle名称)的所有UIAbility均是运行在同一个独立进程(主进程)中,如下图中绿色部分的“Main Process”。
应用中(同一Bundle名称)的所有同一类型ExtensionAbility均是运行在一个独立进程中,如下图中蓝色部分的“FormExtensionAbility Process”、“InputMethodExtensionAbility Process”、其他ExtensionAbility Process。
WebView拥有独立的渲染进程,如下图中黄色部分的“Render Process”。

输入法目前会存在3个进程,App端进程(UIAbility),键盘端进程(InputMethodExtensionAbility),换机克隆进程(BackupExtensionAbility)
进程间通信通过公共事件机制(commonEventManager) 实现,只能通过异步消息通信,无法实现Android中类似AIDL的同步通信
独立沙箱


和Android一样,有全局的App Context,也有页面级别的UI Context,不同的Context获取的资源目录不一样:

基础模式/完整模式
这个是输入法键盘端独有的设置,用户可以在系统设置项控制,仅控制键盘端,App端不受影响,相当于在系统级别实现了纯净模式
基础模式下,输入法扩展(InputMethodExtensionAbility)进程无法拉起其他UIAbility或ExtensionAbility。
基础模式下,输入法扩展会受到系统管控,不能使用涉及访问或泄漏用户个人数据的各种接口,同时无法将数据传递出进程。管控功能包括但不限于:网络、短信、电话、麦克风、定位、相机、蓝牙、壁纸、支付、日历、游戏、扬声器、Wi-Fi、剪切板、多媒体、联系人、公共事件、系统账号、健康数据、地图服务、推送服务、融合搜索、共享内存、分布式特性、广告设备标识、振动等。
共享沙箱

为了方便基础模式下,键盘端和App端能共享数据,专门设计了共享沙箱。比如基础模式下,设置项的开关,键盘端需要读取
基础模式下,输入法键盘端对共享沙箱只读,对输入法键盘端自己的独立沙箱可读写;App端可以对共享沙箱可读写。
共享沙箱的使用,需要向鸿蒙申请group id,并在应用的profile里面配置data-group-ids和在InputMethodExtensionAbility所在的module.json5里面配置dataGroupIds实现
代码中,如果需要和基础模式的键盘端共享数据,需要在创建Preference和数据库的时候,传入dataGroupId,才能和基础模式键盘共享数据;如果需要共享文件,需要将文件放在getGroupDir目录。
3.性能
async/await:
async/await 是 JavaScript 的语法特性,它是 ES2017(ES8)中引入的异步编程语法糖,旨在简化基于 Promise 的异步操作,使代码更接近同步风格
鸿蒙系统功能接口一般会提供两个接口,一个是同步的,一个是异步的,这样会让人误以为异步接口不会影响性能,其实async不切换线程,本质还是在同一个线程执行
类似协程机制,仅用来调度任务,不会切换线程,如果耗时任务全部丢在主线程async函数中,同样会导致App卡死
多线程:
为了避免主线程卡顿问题,我们还是需要多线程
与Java共享内存模型不一样,鸿蒙使用消息传递模型,线程间不共享内存
并发模型的差异:
共享内存模型:

线程间内存共享,访问共享内存时需要先获取锁
消息传递模型/Actor模型:

同一个进程的线程间不共享内存,避免了锁竞争,但会增加内存消耗,鸿蒙限制最多同时运行的Worker子线程数量为64个,代码上也会导致主线程的全局变量和SDK无法在子线程共享使用,需要重新初始化
