第一次听到“鸿蒙”这两个字的人,十个里有八个会问同一句话:它是不是另一个手机系统换了个壳?这个问题问得不算错,但方向偏了——它本质上不是“再造一个手机操作系统”,而是想把手机、平板、手表、电视、车机、音箱、摄像头这些算力和形态完全不同的设备,抽象成一层可以互相调用资源的整体。我这几年在物联网和端侧应用上折腾过不少项目,从早期版本一路跟到现在,踩过的坑和看过的误解都不少。这篇就按我自己的理解路径来梳理:先把概念掰开,再拆分布式的那套底层机制,然后落到 ArkTS 和声明式 UI 的写法、工程结构与工具链,最后把我真踩过的几个坑和排查链路完整摊开。不管你是产品、运营想搞懂它到底能干什么,还是准备动手写第一行代码的开发者,或者正在做技术选型的架构师,都能从里面拿到能直接用的东西。
1. 先把概念掰开:鸿蒙到底在解决什么问题
1.1 它盯的不是“手机”,而是设备之间的那堵墙
传统做法是每个设备一套系统、每个设备一个应用,设备之间靠云端账号打通。这条路的痛点非常具体:跨设备操作要绕公网,延迟几十到几百毫秒不等;断网就彻底失效;同一个功能在不同设备上要重写一遍,体验还各不相同;权限控制只能停留在“账号级”,没法精细到“这一次调用允许访问哪个文件”。
鸿蒙的思路是把硬件能力抽象成可以被运行时按需组合的资源。打个比方,以前的 App 像一家自带厨房的餐厅,什么菜都得自己从头做;现在更像“共享中央厨房加调度系统”,哪个厨房有空、哪个厨房离用户近、哪个厨房的灶够大,就派哪个去做。同一个应用在手机上只装了一份,跑到平板上可以借用平板的键盘和手写笔,跑到电视上可以借用电视的屏幕和音响,跑到车机上可以借用车的定位和摄像头。
这个定位差异决定了它的技术重心:重点不在“把桌面做得更好看”,而在“设备发现、组网、数据同步、任务迁移、跨端安全”这几件事上。你去看它的架构文档,分布式相关的篇幅占比非常高,就是这个原因。
1.2 四组容易糊在一起的名词,分开看就通了
很多人讨论时越聊越乱,是因为把底座、发行版、应用形态、打包方式混成一锅。我习惯按下面这张表来区分:
| 名词 | 它是什么 | 谁在用 | 常见误解 |
|---|---|---|---|
| 开源底座 | 可裁剪的系统基础能力集合,包含内核、驱动框架、基础服务 | 芯片方案商、设备厂商、行业发行版 | 以为它就是消费者买到的那套系统 |
| 商业发行版 | 在底座之上补齐系统服务、应用框架、UI、应用分发的完整版本 | 终端用户 | 以为开源那部分和它完全一样 |
| 元服务 | 免安装、卡片化、可被搜索或推荐直接调起的最小服务单元 | 应用开发者 | 以为它就是个小组件 |
| HAP / HSP / HAR | 三种包形态:功能包 / 动态共享包 / 静态共享包 | 工程构建 | 以为随便选一个都行 |
分开之后问题就简单了:底座解决“能不能跑在低功耗设备上”,发行版解决“用户能不能用”,元服务解决“用户要不要先装”,包形态解决“代码怎么组织”。这是四个不同层面的问题,不能拿来互相论证。
1.3 为什么不能“拿一套现成的改改就行”
这是被问得最多的问题,我一般从三个硬约束回答。
第一是资源下限。有些设备的内存只有几百 KB 到几 MB,比如传感器节点、简易穿戴设备。一套完整的通用移动系统加上虚拟机运行时,光启动就要几十 MB,根本放不进去。所以必须有一套可以按设备规格裁剪的内核组合——大设备用功能完整的那个,小设备用轻量的那个,上层接口尽量保持一致。
第二是实时性。车控、工业控制这类场景要求调度延迟是确定的、可预期的,不能出现“大部分时候 5 毫秒、偶尔 200 毫秒”这种抖动。通用移动系统的调度策略是为吞吐和能效优化的,不保证最坏情况。这块只能从内核调度和中断处理上重做。
第三是跨设备调用的权限粒度。如果只在应用层做投屏和流转,你能控制的只有“投不投”,控制不了“投过去之后能读哪几个文件”。要做到发起方授权、接收方校验、调用链可审计,就必须有系统级的设备互信和数据分级机制。
顺带说一句,早期版本为了降低迁移门槛,确实在运行时层面做过兼容,这也让很多人形成了“它就是换壳”的印象。后续不再依赖那套运行时的版本出来之后,这个认知就明显滞后了。判断依据很简单:看它的应用包能不能直接跑旧生态的二进制产物,答案是不能。
2. 分布式软总线:一句“碰一碰就连上”背后在跑什么
2.1 发现、组网、认证、传输,四段各管一段
“碰一碰就连上”这个体验,背后至少跑四段流程,任何一段出问题都会表现为“连不上”,而错误提示往往只有一行,这是我调试时最头疼的地方。
发现阶段:靠局域网组播、蓝牙广播、Wi-Fi 点对点等多种通道并行探测,策略上是“周期性扫描 + 事件触发”结合。为什么不用单一通道?因为蓝牙省电但慢,一次完整发现要几秒;局域网组播快但要求在同一网段;点对点带宽高但握手开销大。组合使用才能在功耗和速度之间取平衡。
组网阶段:建立逻辑通道,选出一个节点做协调者,拓扑根据设备数量和角色自适应,可以是星型也可以是网状。这一步决定了后面数据走哪条路。
认证阶段:设备凭证加用户确认。用户确认的形式有 PIN 码比对、碰一碰、扫码。确认完成后建立可信关系并落库,下次再来就免确认。这里的关键是凭证要跟设备绑定,不能只跟账号绑定,否则换设备就等于重新开始。
传输阶段:根据数据类型选通道。控制信令走低延迟通道,大文件走高带宽通道,支持多路复用和加密。同一个应用可以同时开多条流,互不阻塞。
我个人的经验是:排查连接问题时,一定要先确认卡在哪一段。是根本没发现设备(发现阶段)、发现了但连不上(组网阶段)、连上了但调用被拒(认证阶段)、还是调用成功但数据不对(传输阶段)。这四类的修复方式完全不同,混在一起猜会浪费大量时间。
2.2 分布式数据、任务调度、设备管理、安全,各管一摊
| 能力 | 解决的问题 | 典型接口形态 | 我在项目里的用法 |
|---|---|---|---|
| 分布式数据管理 | 一份数据多端可见、离线可写、联网后自动收敛 | 分布式键值库、分布式对象、分布式文件 | 多端共享的草稿、待办、设备配置 |
| 分布式任务调度 | 让能力在合适的设备上启动或迁移 | 携带设备标识启动目标能力 | 手机上点一下,把播放甩到电视 |
| 分布式设备管理 | 设备上下线、可信关系、状态订阅 | 设备管理模块的发现/认证/监听接口 | 首页的设备列表与在线状态 |
| 分布式安全 | 设备互信、数据分级、调用校验 | 设备凭证、数据标签、权限矩阵 | 敏感字段只允许本机读 |
这里有一个特别容易翻车的点:分布式不等于无脑同步。多端同时修改同一条记录时,必须有明确的冲突合并策略。常见的几种是“最后写入者优先”“版本号递增”“字段级合并”“业务自定义规则”。我做过一个多端待办清单,一开始用时间戳覆盖,结果用户在平板上勾完了,手机因为时钟偏了几秒,同步回来又给取消了,用户直接投诉。后来改成版本号加操作日志,冲突时保留两边并提示用户选择,问题才消掉。
选策略的原则是看数据性质:纯展示类配置用最后写入者优先就够;有协作编辑需求的必须上操作日志或版本向量;金融、库存这类强一致要求的,分布式库根本不是合适选择,老老实实走服务端事务。
2.3 一次跨设备调用在代码里到底长什么样
流程其实不复杂,难的是配置。先拿可信设备列表,再携带设备标识发起调用:
import deviceManager from '@ohos.distributedDeviceManager'; import { common, Want } from '@kit.AbilityKit'; const dm = deviceManager.createDeviceManager('com.example.demo'); // 监听设备上下线,用于刷新界面上的设备列表 dm.on('deviceStateChange', (data: deviceManager.DeviceStateChange) => { console.info(`device ${data.device.deviceId} state -> ${data.action}`); }); // 取当前可用的可信设备 const devices = dm.getAvailableDeviceListSync(); if (devices.length === 0) { console.warn('没有可用设备,先走认证流程'); } // 在指定设备上拉起目标能力 const want: Want = { bundleName: 'com.example.target', abilityName: 'EntryAbility', deviceId: devices[0].networkId // 注意这里用的是 networkId,不是 udid }; const context = getContext(this) as common.UIAbilityContext; context.startAbility(want).then(() => { console.info('远端拉起成功'); }).catch((err: Error) => { console.error(`远端拉起失败: ${err.message}`); });具体的接口名和参数结构会随 SDK 版本变化,写之前建议对着当前版本的接口文档核一遍。但有几条是不变的,也是我踩过最多次的:
- 两端必须是同一个包名、同一份签名。不同签名的应用在系统看来就是两个应用,跨端调用必然失败。
- 目标能力必须声明为可对外导出,否则本机拉得起来,跨端拉不起来。
- 设备标识用错是最隐蔽的坑。
networkId是网络内标识,udid是设备硬件标识,跨端拉起用的是前者。 - 权限和跨端能力要在配置里声明,光写代码不够。
3. 写第一行代码之前:ArkTS 和声明式 UI 的思维转换
3.1 声明式 UI 改的不是语法,是“谁来管细节”
命令式的写法是:先拿到视图对象的引用,然后一步步告诉它“把文字改成这个”“把颜色改成这个”。状态存在两套地方——你的变量里一份,视图对象里一份,两边随时可能不一致,所以你得手动同步。
声明式反过来:你只描述“在某个状态下,界面长什么样”,框架负责比对前后两棵描述树,算出最小更新集,再去改真实视图。类比一下,命令式像自己握方向盘,每个路口都要打方向;声明式像告诉导航目的地,具体走哪条路由系统算。
代价当然有。你必须有清晰的状态边界,否则一次状态更新会触发一大片重绘,性能直接崩。而且调试方式也变了——以前可以打断点看视图属性,现在更多是看“这次更新是哪几个状态变了导致的”。我刚开始写的时候最不适应的就是这个,明明只改了一个数字,怎么整个列表都闪了一下?后来才明白是状态粒度太粗。
3.2 ArkTS 的几条硬限制,从 TypeScript 过来的人前两天基本都在跟编译器较劲
ArkTS 是 TypeScript 的严格子集,为了做静态优化,砍掉了不少动态特性。我整理了几条最常撞的:
| 限制 | 具体表现 | 应对方式 |
|---|---|---|
| 不允许 any / unknown | 编译直接报错 | 所有数据结构先定义 class 或 interface |
| 不支持结构化类型 | 长得一样但类型名不同的对象不能互相赋值 | 显式声明类型,别靠“鸭子类型”凑 |
| 对象字面量必须有明确类型 | 凭空写{a: 1}会报错 | 加类型标注或先 new 一个实例 |
| 不支持运行时动态增删属性 | 部分场景下会失效 | 需要动态字段就用 Map 或预留可选属性 |
| 不推荐使用 var | 作用域容易出问题 | 一律 let / const |
| 不支持函数动态调用写法 | apply / call 之类不可用 | 改成显式传参或策略表 |
我的做法是:先把所有数据模型定义成 class,把所有界面写成组件,最后才写交互逻辑。这样返工最少。反过来先写界面再补类型,你会发现每加一个字段都要回头改三处。
3.3 状态管理:搞清楚谁改谁,比记住装饰器名字重要
装饰器名字很好记,难的是边界。列一张我实际用下来最靠谱的对照表:
| 装饰器 | 数据流向 | 适合什么 | 我踩过的坑 |
|---|---|---|---|
| 组件内部状态 | 组件自己持有 | 开关、输入框内容、加载中标志 | 嵌套对象的深层属性变了不触发刷新 |
| 单向传入 | 父到子,值拷贝 | 子组件只负责展示 | 传大数组有拷贝开销 |
| 双向绑定 | 父子互通 | 子组件要改父的状态 | 必须由父组件的内部状态传下来 |
| 跨层提供消费 | 祖先到后代 | 深层嵌套避免层层透传 | 名字要唯一,重名会拿到意外值 |
| 全局存储 | 全局可读 | 登录态、主题、语言 | 别往里塞大对象,重建成本高 |
| 持久化存储 | 落盘 | 用户偏好 | 写入是异步的,别指望立刻读到 |
| 变化监听 | 副作用 | 状态变化后做校验、发请求 | 在里面再改被监听的变量会循环 |
关于嵌套对象不刷新的问题,补一句:当一个对象内部还有对象,只有外层属性变化才会被观察到。要让深层字段也能触发更新,需要给类加可观察标记,子组件用引用方式持有。这个坑我吃了不止一次——界面上明明数据变了,就是不刷新,加一行日志发现值确实变了,最后才定位到是观察粒度的问题。
另外,单向传入是值拷贝。列表场景下如果每一行都传一整个大对象,滚动时会有明显的拷贝开销。我现在的做法是传标识,子组件自己去数据源里取,或者用引用方式持有,效果差别很明显。
3.4 一个能跑起来的最小页面,以及那个必须写的第三个参数
@Entry @Component struct Index { @State count: number = 0; @State items: string[] = ['第一项', '第二项', '第三项']; build() { Column({ space: 12 }) { Text(`当前计数:${this.count}`).fontSize(20) Button('加一') .onClick(() => { this.count += 1; }) List({ space: 8 }) { ForEach(this.items, (item: string, idx: number) => { ListItem() { Text(`${idx + 1}. ${item}`).fontSize(16) } }, (item: string) => item) // 第三个参数:稳定唯一的键 } .height(220) .width('100%') } .padding(16) .width('100%') } }这段代码没什么玄机,重点在ForEach的第三个参数。它是键生成函数,框架靠这个键判断“这一项是新加的、还是原来那项被移动了”。如果你写成随机数,每次刷新键都变,框架会认为所有项都是新的,整个列表重建,滚动瞬间掉帧。如果不写,框架会用索引兜底,一旦发生插入或删除,后面所有项的索引都变了,同样会大面积重建。
这条规律在几乎所有声明式框架里都成立,不是什么冷知识,但真正写的时候忘掉的人特别多。我的建议是:只要列表项有可能被增删或排序,键就必须来自数据的唯一标识,不能来自索引。
3.5 元服务和卡片:把功能从应用里“抠”出来
元服务的核心是“免安装、按需加载、卡片直达”。用户不用先去应用市场下载,搜到就能用,用完就走。
适合做成元服务的场景有共同特征:功能单一、操作路径短、结果能一眼看完。查快递进度、看医院排队号、查公交到站、看日程提醒、扫码点单,都是典型。反过来,需要长时间交互、需要处理大量本地数据、需要复杂权限的场景就不适合,硬拆只会让体验更碎。
卡片开发要注意两件事。一是刷新有配额限制,定时刷新不能太频繁,超了会被系统降级。所以必须做失败降级——刷新失败时展示上一次的缓存值,而不是空白。二是卡片上的信息密度要克制,我自己定的是“一眼三个信息点”原则:一个主标题、一个关键数值、一个状态或时间。塞满四个以上,用户在桌面扫过去根本抓不住重点。
4. 工程结构与工具链:从一个空项目说起
4.1 每个目录到底管什么,一次讲清
新建一个空工程,目录看着挺多,其实职责很清晰:
| 路径 | 职责 | 我一般改什么 |
|---|---|---|
| 应用级配置 | 包名、版本号、应用图标、全局权限入口 | 改版本号和图标 |
| 模块级配置 | 能力声明、支持的设备类型、路由、权限 | 加页面路由、声明导出能力 |
| ability 目录 | 应用生命周期入口,窗口加载 | 冷启动逻辑、窗口设置 |
| pages 目录 | 页面组件 | 主要工作区 |
| resources 目录 | 字符串、颜色、图片、多语言、深色模式 | 所有文案和配色 |
| 构建配置目录 | 产物类型、签名、编译选项 | 打包和签名 |
| 依赖描述文件 | 三方库依赖 | 引库 |
这里有个特别值得强调的点:多语言和深色模式全靠resources下的限定词目录实现。所谓限定词,就是给资源目录加后缀,比如中文、英文、深色、浅色,系统会按当前环境自动选。我见过不少项目在代码里写if (language === 'zh')来判断语言,短期能跑,长期维护是灾难——加一个语种就要改一遍所有判断。正确做法是所有文案都走资源引用,代码里一个中文字符都不出现。
4.2 三种包形态,选错了后面全是麻烦
HAR 是静态共享包,编译时打进产物里,适合放工具函数、网络封装、通用组件。HSP 是动态共享包,运行时共享,适合多个模块共用的业务代码。HAP 是功能包,也就是最终会被安装或被调起的那部分。
选择的判断标准是“这份代码会不会被多个 HAP 同时使用,且希望只加载一份”。如果是,用 HSP;如果只是复用代码、不在意多一份体积,HAR 更省心。
HSP 有一个必须记住的约束:它的版本要和宿主应用对齐。升级的时候如果只升了 HSP 没升宿主,或者反过来,很容易出现“运行时找不到某个类”这种莫名其妙的错误,而且报错信息通常不会直接告诉你版本不匹配。我现在的做法是在构建脚本里把版本号统一从一处读取,避免手工改漏。
4.3 调试、签名、性能分析,三个环节的实操要点
签名。自动签名是最快的路径,在 IDE 里完成账号授权后一键生成调试证书,绝大多数开发场景够用。手动签名需要三件套:私钥文件、证书文件、描述文件,一般只在多环境或者 CI 上才需要。我第一次配手动签名花了半天,问题出在描述文件里没有包含目标设备的标识,导致装上去直接失败。
真机调试。设备要开开发者模式并完成授权确认,这一步不做的话 IDE 里能看到设备但装不上。另外真机和模拟器的行为差异比想象中大,尤其是分布式相关能力、传感器、蓝牙,模拟器基本没法验证。涉及跨设备的东西,老老实实用两台真机。
日志。别用打印函数凑合,用系统日志接口,按业务域和标签过滤。一个中型项目一天能产生几十万行日志,没有域和标签的区分,等于没有日志。
性能。帧率、内存、CPU 三件事分开看。帧率问题用性能分析器看每一帧的时间分布,能直接看出是逻辑线程超时还是渲染线程超时。内存问题看对象增长曲线,重点找有没有在生命周期回调里注册了监听但没反注册。CPU 问题用打点的方式,在关键路径前后埋标记,导出成时序图,一眼就能看出哪一段最耗时。
5. 几个真踩过的坑,以及完整的排查链路
5.1 长列表滑到中段开始掉帧
现象很典型:两百条数据的列表,前五十条滑得挺顺,越往后越卡,滑到一百多条的时候明显能感觉到顿。
我的排查顺序是这样的。先抓帧,看每帧的逻辑线程耗时,发现稳定在 20 毫秒以上,正常应该在 8 毫秒以内,说明不是渲染问题而是构建问题。然后去看ForEach的键生成函数,果然写成了随机值,改成数据自带的唯一标识。改完再测,掉帧点往后推了,但还是卡。
第三步怀疑构建方式。普通ForEach会把所有子组件一次性全部构建出来,两百条就是两百个组件实例同时在内存里。换成按需加载的列表渲染方式配合数据源接口,只构建可视区域附近的项,情况明显好转。
第四步是设置缓存数量。默认缓存范围偏小,快速滑动时会出现“还没构建好就滑到了”的白块。适当调大缓存数量能缓解,但别调太大,否则等于又回到全量构建。
第五步是给列表项组件加复用标记,让滑出屏幕的组件实例进入复用池,新滑入的项直接复用而不是新建。
四步做完,帧率从四十出头回到了接近满帧。整个过程最耗时的不是改代码,是前面定位。我的结论是:超过五十条数据的列表,按需加载不是优化项,是基本操作。写的时候直接上,别等出问题了再回头改。
// 最小可用的数据源实现,配合按需加载的列表渲染 class BasicDataSource implements IDataSource { private listeners: DataChangeListener[] = []; private data: string[] = []; totalCount(): number { return this.data.length; } getData(index: number): string { return this.data[index]; } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } // 数据变更时通知框架,避免整表重刷 notifyReload(): void { this.listeners.forEach((l) => l.onDataReloaded()); } push(item: string): void { this.data.push(item); this.listeners.forEach((l) => l.onDataAdd(this.data.length - 1)); } }5.2 跨设备调用一直超时,报错只有一行
这个坑花了我整整两天。现象是:两台设备都连着同一个网络,设备列表里也能看到对方,但一发起调用就失败,日志里只有一行“启动失败”,没有任何细节。
我按下面这个顺序一条条排除:
- 两端包名是否完全一致。不一致的话系统认为是两个应用,直接拒绝。
- 两端签名是否一致。调试签名和发布签名混用是最常见的错误,我自己就犯过——一台装的是调试包,一台装的是打出来的发布包。
- 设备之间是否已建立可信关系。只是“在同一个网络里”不够,必须走过一次认证。
- 配置里是否声明了跨端能力,以及相关权限有没有申请。权限没申请的话,调用会被静默拦截。
- 传的设备标识对不对。用成硬件标识而不是网络标识,必然失败。
- 目标能力是否声明为可对外导出。
我最后定位在第六条。因为本机自己调自己完全正常,所以一直没往配置上想。加上导出声明之后一次就通了。
这件事给我留下的教训是:跨设备相关的报错信息普遍很含糊,别指望日志告诉你原因,只能靠清单逐条核对。我现在维护了一份六项检查表,新项目接入先过一遍,能省下大量时间。
5.3 版本兼容:编译能过不等于设备能跑
编译用的 SDK 版本和目标设备的系统版本是两回事。用高版本 SDK 编译出来的代码,跑到低版本设备上,涉及新接口的地方会直接抛异常。而且这种异常往往在真机测试的后期才暴露,因为开发机上一般是最新版本。
我的处理方式是用能力可用性判断做分支,而不是用 try-catch 兜底:
import { canIUse } from '@kit.ArkUI'; if (canIUse('SystemCapability.Communication.DistributedDeviceManager')) { // 走分布式设备管理的新接口 initDistributedModule(); } else { // 降级到基础能力,或提示用户升级系统 showFallbackTip(); }用 try-catch 兜底的写法看起来更省事,但问题在于它把“能力不存在”和“能力存在但调用出错”混在一起了,日志里根本分不清。用能力判断做分支,至少能明确知道走的是哪条路。
还有一点:接口废弃是常态,每升一个大版本都会有接口标记为废弃。我的习惯是每次升级后跑一遍编译,把所有废弃警告当错误处理,一次性改完。攒着不改的话,等到必须升级那天,改起来就是几十处,还容易漏。
6. 不同起点的人,切入点完全不一样
6.1 完全没基础:先把四层结构记住,别急着看代码
我给非技术岗的建议是建立四层认知:硬件层(设备形态和算力差异)、系统层(内核、分布式连接、数据、安全)、框架层(UI 框架、应用模型、生命周期)、应用层(元服务、卡片、多端部署)。
这四层的好处是,你听到任何新名词都能立刻归位。比如听到“卡片刷新配额”,你知道这是应用层的问题;听到“软总线组网”,你知道这是系统层的问题。归位之后就不会被各种营销话术带偏。
验证自己懂没懂的方法很简单:拿一个你常用的功能,比如“手机上看视频投到电视”,把它拆到这四层里——硬件层需要两个屏幕和两条网络通道,系统层需要设备发现和媒体流传输,框架层需要任务迁移和状态同步,应用层需要播放进度续接。能拆清楚,说明概念通了。
6.2 有前端基础:三天能出页面,但要注意三件事
声明式 UI 的思维对前端来说几乎零门槛,组件化、状态驱动、单向数据流这些概念都是通的。真正要补的是三块:ArkTS 的类型约束(前面列的表格)、状态管理的观察粒度、资源限定词的组织方式。
我的建议是第一天只写静态页面,把所有组件和布局跑通;第二天接状态和交互,专门练嵌套对象的更新;第三天做多语言和深色模式适配,把所有硬编码文案清理掉。三天下来基本能独立做页面了。
6.3 有原生移动开发基础:重点不在 UI,在生命周期和分布式
UI 那套一周就能上手,真正的差异在两端。
生命周期不一样。这里的能力入口是一个带状态机的对象,创建、获得窗口、前台、后台、销毁,每个阶段能做什么有明确限制。比如窗口相关操作必须在拿到窗口之后才能做。
任务模型不一样。我一般用这张表对照记忆:
| 维度 | 传统移动端 | 鸿蒙 |
|---|---|---|
| 应用入口 | 单入口 Activity / 控制器 | 多个能力入口,按场景拆分 |
| 页面路由 | 集中式路由表为主 | 声明式路由 + 动态导航混合 |
| 跨端启动 | 基本不支持 | 携带设备标识直接拉起远端能力 |
| 数据共享 | 靠服务端或本地库 | 系统级分布式数据 + 服务端兜底 |
| 免安装 | 无 | 元服务形态原生支持 |
差异最大的是最后一条。免安装形态意味着你的应用可能被用户在完全没装过的情况下用一次,所以首屏加载时间、权限申请的时机、缓存的初始化策略都得重新设计。
6.4 三个我不建议的做法
第一个是一上来读内核源码。不是说不能读,而是投入产出比太低。绝大多数应用开发者一辈子都用不到内核层的知识,把分布式接口和 UI 框架吃透,收益高得多。
第二个是只抄示例不看配置。示例能跑是因为它的配置是全的,你抄了代码没抄配置,就会遇到各种“明明一样却不工作”。我现在的习惯是建一个新工程,先把配置文件从头到尾读一遍,理解每个字段在管什么,再往里放代码。
第三个是追着版本号跑。每次发版都跟,看起来很积极,实际上是在给自己制造兼容负担。我的做法是锁定一个稳定版本做完整个项目,中间只在遇到硬性阻塞时才升,升之前先看变更清单里有没有影响到自己的接口。稳定比新更重要,这句话在端侧开发里尤其成立。
最后分享一个我一直在用的小习惯:每接一个新能力,先写一个只有二十行的最小验证工程,只做一件事——证明这个能力在当前设备上能通。通了再往主工程里搬。这二十行代码看着多余,但它能帮你把“环境问题”“配置问题”“代码问题”彻底分开,省下的排查时间远超过写它的时间。