最近一段时间,我把不少业余时间砸在了鸿蒙HarmonyOS开发上。起因倒也没什么惊天动地,就是想在真机上跑一个自用的跨端待办应用,顺便看看这个系统的分布式能力到底是不是宣传里那么好用。折腾了大概两三个周末之后,我最大的感受是:鸿蒙真正需要重学的不是语法,而是看App的方式。这篇随笔不打算写成一本教程目录,我想按自己的实际开发顺序,把那些值得记录的架构理解、实操细节、踩坑经验、性能观察和生态判断一次聊透。如果你准备从Android、iOS或前端方向转过来,或者只是好奇鸿蒙开发到底怎么回事,这篇应该能帮你少走几步弯路。
1. 为什么突然想聊鸿蒙?——我一年来最真实的使用体感
先说一个可能很多人有同感的困惑:看了不少发布会和宣传稿,感觉鸿蒙很厉害,但真到自己上手时,文档翻得眼花缭乱,不知道从哪开始。我以前做移动端开发,主力是跨平台那套东西,对系统级特性一直有点“敬而远之”。直到我打算做一个自己用的待办工具,想要手机、平板、手表之间无缝接力,才发现传统跨平台方案做这种场景特别拧巴,要么靠云同步轮询,要么自己搭设备间通信协议。于是我开始认真看鸿蒙到底提供了什么。
真正跑起来之后,有几个体感特别明显。
第一,构建和部署链路比我想象中顺。DevEco Studio新建工程、选择模板、自动签名、真机运行,整个流程跟现代IDE的体验差不多,没有传说中那么高的门槛。第二,ArkTS和ArkUI的上手曲线比预期平滑。我本身写过TypeScript,再看ArkTS基本是“带约束的TS”,声明式UI写起来很像在写一种更收敛的Compose。第三,也是最颠覆的一点:在鸿蒙里,应用不是一个“装在手机里的整体”,而是由一个个能被调度的能力组成的。
为了让你对后面要聊的东西有个整体画面,我先放一张我脑内整理过的技能地图。它不是官方路线,只是我这个阶段认为值得掌握的内容。
| 方向 | 核心内容 | 我为什么要学 |
|---|---|---|
| 语言基础 | ArkTS、TypeScript语法约束 | 写页面、写逻辑、写卡片都要用 |
| UI框架 | ArkUI声明式组件、状态管理 | 所有界面呈现和交互的基础 |
| 应用模型 | Ability、Stage模型、HAP包结构 | 理解应用怎么被系统拉起、怎么流转 |
| 分布式协同 | 跨端迁移、多设备协同 | 鸿蒙区别于传统系统最大的亮点 |
| 元服务与卡片 | 免安装形态、桌面卡片 | 轻交互入口,增量场景很值得做 |
| 工程与发布 | 签名、打包、上架审核 | 再好的代码过不了审核也白搭 |
| 性能调优 | 并发模型、启动优化、状态保存 | 做到能用和做到好用之间差在这里 |
这张表看着内容不少,但实际上每一块都有比较清晰的官方路径。接下来我从架构认知讲起,因为如果不先理解鸿蒙的底层思路,后面写代码时总会带着旧系统的惯性。
2. 架构认知:鸿蒙和传统移动系统最不一样的地方
2.1 超级终端背后的分布式软总线思维
先聊一个我感触最深的概念:分布式软总线。听起来很玄乎,其实可以理解成系统为设备之间铺了一条“隐形的USB线”。手机、平板、手表、智慧屏、车机,在用户授权之后可以互相发现、连接、组合,像一个整体一样协作。
我第一次在真机上体验设备协同是这么个场景:手机上的视频应用正在播放,随手往平板上轻轻一碰,视频就“滑”到了平板上继续播。这不是投屏,因为远程设备上打开的是同一个应用服务,会话状态也迁移过去了。这个体验很直觉,但背后的工作量非常大:设备发现、连接建立、数据传输、状态同步、安全校验,全都要做。
对开发者来说,分布式软总线最大的价值是:我们不用自己写一套设备发现和通信协议,系统已经提供了统一通道。但代价也很明显——你的应用必须能“接得住”这种协同。也就是说,不能把一个页面当成一个孤岛,数据和状态得随时可以被序列化、被传输、被另一个设备上的实例接管。
我刚开始写待办应用时完全没考虑这些,代码里到处都是直接读写全局变量的页面逻辑。后来要做跨端续接,才发现这套设计根本没法迁。最后只能重构:把业务状态抽到可序列化的数据层,UI层只做渲染和交互。这也让我意识到,在鸿蒙里,“一个应用只活在一个设备”的想法,从架构上就已经过时了。
2.2 Stage模型、HAP包与元能力:应用被重新拆解了
如果你以前写过Android,应该很熟悉Activity那个概念:一个页面就是一个Activity,应用启动就是拉起一个Activity,返回键就是销毁它。iOS那边则是ViewController栈,本质也差不多。但在鸿蒙里,这套模型被重新抽象成了Ability。
当前主流的Stage模型下,一个应用由多个Module组成,编译产物叫HAP。每个HAP里可以有多种能力:UIAbility负责有界面的交互,ServiceExtensionAbility负责后台服务,FormExtensionAbility负责桌面卡片,还有DataShareExtensionAbility这类处理数据共享的能力。
让我用一个生活类比来解释这个变化。以前的应用像个只能整租的临街铺面,所有功能都挤在一个大招牌底下,想单独使用某个功能,得先打开整间铺子。而鸿蒙的元能力更像是一个综合市场里的独立摊位,每个摊位可以有自己的招牌、营业时间和接待方式。系统拉起一个“摊位”,不一定需要整个“市场”开门。
这意味着应用不再是一个铁板一块的整体。用户可以只通过卡片使用你的关键功能,系统也可以主动拉起某个元能力来响应语音指令或跨端请求。对开发者的实际影响是:别再把所有逻辑都塞进一个页面级的Ability里,要从“服务边界”去思考拆分。
我当时重构待办应用时,就把“输入待办”和“展示待办列表”拆成了不同的能力入口。桌面卡片点一下可以直接进入输入态,而不是先打开主页面再点新建按钮。这种体验上的变化,用户会觉得“这个应用很顺手”,背后其实就是Ability的合理拆分。
2.3 从页面思维到能力思维:一次开发,多端部署
很多人看到“一次开发,多端部署”这句口号,会下意识以为是响应式布局,一套代码自适应屏幕就行。我刚开始也这么想,结果发现这只说对了一半。
响应式布局解决的只是“相同功能在不同尺寸屏幕上的呈现”,但鸿蒙更强调的是“同一能力在不同设备间流转”和“不同设备上的能力差异化组合”。比如手机上的待办应用,在手表上可能不需要完整编辑能力,只需要展示今天的待办、打勾完成;在平板上则可以充分展开编辑区、时间轴和统计面板。这不是同一个页面在缩放,而是同一个账号体系、同一份业务数据,在不同的设备上呈现不同的能力边界。
所以如果你要入坑鸿蒙,我建议尽早放弃“一个App对应一套页面”的固有套路,开始用“能力 + 数据 + 多设备适配”的角度来建模。这比学任何API都重要。我甚至觉得,哪怕你暂时不碰分布式特性,只把应用按能力拆开设计,后续做卡片、做流转都会少很多重构痛苦。
3. 开发入门:ArkTS与ArkUI的实操记录
3.1 搭建环境与创建第一个HarmonyOS工程
很多刚接触鸿蒙的人最头疼的其实是环境准备。我按自己的步骤整理一下,照着走基本不会卡壳。
第一步,下载DevEco Studio。安装时注意勾选SDK和模拟器组件,省得后面要用时再补。版本选择上,建议直接用当前稳定的Release版本,不要追Beta,不然有些插件和调试功能会踩到还没修复的问题。
第二步,新建工程。选择“Empty Ability”模板,语言选ArkTS。工程创建好之后,先看一眼目录结构。重点看entry模块下的src/main/ets,里面是入口Ability和页面文件;module.json5是模块配置文件;resources目录放字符串、颜色、图片等资源。
第三步,配置签名。本地调试推荐用自动签名,真机调试要登录开发者账号。开启真机的开发者模式后,连接电脑,在IDE里选择设备。第一次跑真机时,手机会弹信任调试证书的提示,记得点允许。
第四步,运行。模拟器启动速度在可接受范围内,但涉及分布式协同、卡片拉起这些能力,模拟器支持有限,建议尽早拿真机测试。我当时就是图省事一直用模拟器,结果做卡片时怎么拉都不显示,换真机一次就好了。
跑通后默认工程是经典的Hello World。我照着改了一个计数器页面,大概长这样:
@Entry @Component struct Index { @State count: number = 0 build() { Column({ space: 16 }) { Text(`点击了 ${this.count} 次`) .fontSize(24) .fontWeight(FontWeight.Bold) Button('加一') .onClick(() => { this.count++ }) } .padding(24) .width('100%') .height('100%') } }这段代码的写法很典型:build方法描述UI结构,Column类似线性布局的容器,@State装饰的count是响应式状态。你不需要手动去findViewById,也不需要调用setText,只要改count的值,UI会自动刷新。这种声明式UI的思维,和现在流行的跨平台框架是比较接近的,写起来比想象中顺手。
3.2 状态管理:装饰器到底该怎么用
ArkUI的状态管理是新手最容易困惑的地方,因为装饰器太多了。我把常用的整理成一张表,照着选基本不会错。
| 装饰器 | 作用 | 典型场景 |
|---|---|---|
| @State | 组件内部的可变状态 | 页面内部的计数、开关、输入框内容 |
| @Prop | 父组件向子组件单向同步 | 父传子,子组件展示数据 |
| @Link | 父子组件双向同步 | 父子需要共同修改同一个状态 |
| @Provide / @Consume | 跨层级共享状态 | 祖先组件向深层子组件提供数据 |
| @Observed / @ObjectLink | 观察对象或数组内部变化 | 复杂对象字段级更新 |
我实际开发中踩过最典型的坑是:数组或对象里面的某个字段改了,UI没刷新。原因往往是只给类加了@Observed,但子组件接收对象属性时没配合@ObjectLink,导致ArkUI观察不到内部变化。
比如我有这样一个任务对象:
@Observed class Task { title: string = '' done: boolean = false }父组件里声明@State tasks: Task[] = [],子组件接收单个任务时如果只是普通参数,改done属性不会触发子组件刷新。正确做法是子组件里用@ObjectLink task: Task来接收。这个细节如果没搞懂,排查起来真的很耗时间。
我的经验是:复杂业务不要把所有状态都堆在组件里,尽早引入统一的状态管理方案,把数据和UI彻底解耦。否则组件一多,@Link传引用传得云里雾里,改一个字段影响一片页面,调试起来非常痛苦。
3.3 组件化重构与几个编译坑
ArkUI里组件不是class,而是struct,不能继承。这跟面向对象习惯完全不同。我刚开始总想抽一个BaseComponent做公共逻辑,折腾半天发现路子不对。正确的做法是用组合:把公共UI抽成@Builder函数或自定义组件,逻辑差异通过属性传参和事件回调解决。
举个例子,我想做一个通用的空状态组件,只需要接收提示文案和图标资源,点击重新加载的事件由父组件传进来。子组件完全不关心父组件怎么重新加载,只负责触发回调。这样的设计,职责清晰,也符合声明式UI的直觉。
编译坑方面,ArkTS对TypeScript做了很多收敛,比如不允许使用any、有些复杂的类型体操也不支持。最常遇到的就是装饰器修饰的属性类型推断失败,表现为编译报错信息指向某个表达式,但实际原因是类型不匹配。解决思路很简单:把装饰器修饰的属性显式标注类型,不要靠推导。我当时整了一下午一个诡异报错,最后只是把@State list = []改成@State list: Task[] = []就好了,属于看着离谱但很常见的坑。
4. 元服务与卡片:轻交互的想象力所在
4.1 为什么我看好元服务
如果说分布式能力是鸿蒙的技术底座,那元服务和卡片就是把这种能力送到用户面前的形态。元服务可以理解为“免安装、即用即走”的应用分身,但它比传统的小程序拥有更多的系统级入口和跨设备流转能力。
我自己做一个工具类应用,最大的感受是:用户根本不想为一个“只看今天天气/只查一个快递/只记一条待办”的需求去下载一个完整App。元服务让用户能通过桌面卡片、小艺建议、碰一碰等入口直接触达功能,用完就走,没有安装负担。这种交互对高频轻量场景特别友好。
很多团队在考虑鸿蒙适配时,第一反应是“我要把现有App完整移植过来”,但完整移植的成本和收益未必划算。我更建议的思路是:先把App里最有价值的一两个功能抽出来,做成元服务和卡片,验证用户是否买单,再做深度适配。这种渐进式打法,投入小、见效快,也符合鸿蒙生态目前的发展节奏。
4.2 卡片:从桌面直达服务的入口
卡片是元服务最典型的表现形态。用户不需要打开应用,在桌面上就能看到信息、进行交互。我做的待办应用里,第一个卡片就是“今日待办”卡片,直接列出前三项任务,可以点击打勾。
开发卡片需要三个部分:一个FormExtensionAbility负责卡片生命周期和逻辑,一个卡片UI文件负责渲染,一个form_config.json负责配置。卡片UI的写法和普通页面类似,但限制更多,不能使用所有组件和API。我的卡片UI大致是这样:
export default class EntryFormAbility extends FormExtensionAbility { onAddForm(want: Want) { return { formConfigName: 'widget', data: { text: '今天有三件事待办' } } } }对应的卡片配置:
{ "forms": [ { "name": "widget", "displayName": "$string:widget_name", "description": "$string:widget_desc", "src": "./ets/entryformability/EntryFormAbility.ets", "uiSyntax": "arkts", "window": { "designWidth": 720, "autoDesignWidth": true }, "isDefault": true, "updateEnabled": true, "scheduledUpdateTime": "12:00", "updateDuration": 1 } ] }有一个很重要的认知:卡片进程随时可能被系统回收,所以不能在卡片里维护复杂的内存状态。每次刷新时,都要通过onUpdateForm等方法重新准备数据。我一开始把卡片数据缓存在全局变量里,结果卡片刷新时偶尔拿到旧数据。后来改成每次刷新都从数据源拉取,问题就消失了。做卡片,一定要把“无状态”当作默认假设。
4.3 跨端流转的落地姿势
跨端流转是我觉得鸿蒙最有魅力但也最考验设计能力的功能。以我的待办应用为例,我希望用户在手机上记录待办后,到了工位能直接流转到平板上继续编辑。方向没问题,但落地时要解决几件事。
首先是数据模型要可序列化。流转不是简单地把页面截图搬到另一个设备,而是要传递业务状态。我的Task对象如果包含不可序列化的字段,流转时就会丢失数据。所以跨端流转前,一定要规划好哪些数据需要跟着走,哪些数据应该从服务端重新拉取。
其次是登录态和权限问题。两个设备如果登录的不是同一个账号,或者没有授权协同,流转就无从谈起。应用内做单设备登录判断很容易,但多设备场景下,最好在设计阶段就考虑“账号-设备-数据”的关系模型,否则做一半会发现权限体系撑不住。
最后是大局观:不要一开始就做“整个应用流转”。最靠谱的做法是先跑通一个最小闭环,比如一个文档详情页,从手机流转到平板,确认数据一致、UI适配、能正常返回,再逐步扩展。我见过很多团队雄心勃勃想一步到位,最后被各种边界情况拖垮,反而失去了信心。
4.4 开发和上架踩过的坑
签名的坑最隐蔽。本地调试用自动签名没问题,但上线发布必须用正式签名。我遇到过一次CI构建时打包失败,排查了半天才发现是签名证书过期,而当时的构建脚本没有做证书过期检查。建议把签名配置和证书有效期固话进CI校验流程,宁可构建前失败,也不要打包完才发现。
还有一个坑是卡片不显示。代码和配置都对,但桌面长按应用看不到卡片入口。最后发现是module.json5里没有声明对应的extensionAbility。这类问题不是因为代码难,而是因为配置分散在多个文件里,容易遗漏。我的检查习惯是:改完配置先看构建日志有没有解析对应文件,再跑一遍安装,避免在真机上反复试错。
元服务上架审核也有一些细节和普通应用不一样。比如图标尺寸、卡片样式、隐私声明这些,如果之前没有经验,很容易被打回。我建议在开发早期就去翻一遍官方审核规范,不要等开发完了再对照,否则返工成本很高。
5. 性能调优:从慢到稳的真实体验
5.1 包体积与启动速度:方舟编译器带来的变化
很多文章喜欢吹方舟编译器,但实际感受如何呢?我的一个体感是:用ArkTS写的HAP包确实比同功能的应用包要小。主要原因有几个:语言层面更精简、构建时做了静态优化、系统框架内置了大量公共能力,不需要把重复的库打进包里。
启动速度方面,方舟编译器通过AOT(预编译)机制,把一部分工作放到了构建阶段,运行时减少了逐行解释的开销。我用几个小工具做过对比,冷启动体感明显快一些,但也没有夸张到“秒开”的程度。真正影响启动速度的还是应用本身的逻辑:首屏是否依赖同步网络请求、是否有大量初始化任务、图片加载是否合理。编译器优化只是基础,开发者自己的优化才是大头。
我优化启动速度时常用的招数很朴素:把非必要的初始化任务放到子线程或延迟加载,首屏只用本地数据渲染,图片裁剪到合适尺寸再展示。这些经验在任何系统上都适用,但鸿蒙上因为组件模型和线程模型的差异,写法会有些不同。
5.2 并发与异步:TaskPool要这么用才稳
ArkTS里的并发模型和传统的线程池不太一样,它基于Actor模型,每个任务有独立的执行上下文,通过消息传递协作。我在做待办应用时,需要处理一个比较耗时的统计计算,用TaskPool跑起来很顺手。基本写法是这样:
import { taskpool } from '@kit.ArkTS' function heavyCalculate() { let sum = 0 for (let i = 0; i < 10000000; i++) { sum += i } return sum } let task = new taskpool.Task(heavyCalculate) taskpool.execute(task).then((res) => { console.info(`计算结果是 ${res}`) })TaskPool的好处是不用手动管理线程生命周期,系统会根据设备负载动态调度。但用起来有几个注意事项。第一,任务函数不能捕获外部的大对象,参数和返回值需要能够序列化。第二,不是所有任务都适合塞进TaskPool,高频小任务反而会有调度开销,得不偿失。第三,I/O类任务(比如网络请求、文件读写)应该用系统提供的异步接口,而不是用TaskPool模拟阻塞执行。
我个人的使用原则是:CPU密集型的计算任务、图片处理任务、需要并行处理的大数据任务,才考虑TaskPool。其他场景,能用异步接口解决的绝不多开一个任务。
5.3 状态保存:别把应用存活当成理所当然
移动端开发有个很常见的错觉:用户切到后台一会儿,应用应该还在原地等我们。但在鸿蒙里,系统根据内存和功耗会灵活回收后台应用,开发者无法预测系统什么时候会销毁你的Ability实例。
我踩过最痛的一次坑是:用户在一个表单页填了一半内容,切到后台回个消息,再回来时整个页面被重建,输入内容全没了。从那以后,凡是涉及用户输入的数据,我一律进入持久化,至少也要在关键节点保存现场。
这里的核心原则是:把“保存现场”当成和“渲染UI”一样的基础能力来设计。页面在onPageHide或Ability进入后台时保存草稿,页面重新可见时检查是否有未提交的数据并恢复。跨端流转场景更是如此,因为你连目标设备都不确定是哪一个,唯一能依赖的就是已经持久化或同步过的数据。
6. 生态观察:现阶段值不值得投入
6.1 我看到的生态现状
从设备量上看,鸿蒙已经是一个不可忽视的系统。但客观讲,第三方应用的生态成熟度还是参差不齐的。大部分常见应用已经有了鸿蒙版,但很多还停留在“能用”的阶段,真正把卡片、分布式、元服务这些原生特性充分利用的应用,确实还比较少。
这其实是一个很微妙的窗口期。说它不成熟,是因为系统的能力边界还在快速变化,文档和工具也在不断迭代;说它是机会,是因为越是这种时候,越容易做出差异化体验。等生态完全成熟再入场,你做的就不是创新,而是同质化竞争。
我自己的观察是,用户其实不关心系统底层叫什么,只关心“这个应用在手表上能不能用”“这个卡片能不能不打开App就看到信息”“这个应用能不能跨设备接续”。谁能把这些体验做顺,谁就能在这一轮生态建设里拿到红利。
6.2 不同背景的开发者的学习路线建议
我身边有各种背景的朋友问过我同一个问题:我想学鸿蒙开发,应该从哪开始?我的回答往往取决于他们的既有经验。
| 背景 | 建议路径 | 预期时间 |
|---|---|---|
| Android/iOS开发 | 直接看Ability模型与生命周期、ArkTS与ArkUI差异、状态管理;重点是卸载旧页面思维 | 1到2周可写简单页面 |
| 前端/跨平台开发 | 先补TypeScript基础,理解ArkTS的约束;再学声明式UI思维和状态管理;做卡片会顺手 | 2到3周可上手项目 |
| 零基础 | 按官方入门路线走:TypeScript、ArkUI、Ability、工程发布;不要一上来就搞分布式 | 建议给足1到2个月 |
不管哪种背景,最有效的学习方式都是做一个小而完整的闭环。不需要大而全,一个待办应用、一个倒计时卡片、一个跨端阅读器都行。从创建工程到上真机,到做成卡片,再到跨端流转,完整走一遍之后,你才能建立真正的系统认知。
6.3 我最看好的三个投入方向
如果说要在这个时间点押注,我会优先看三个方向。
第一个是元服务和卡片。工具类、提醒类、资讯类应用特别适合这种轻交互形态,用户触达成本低,开发成本也比完整App低。这是当前最值得尝试的方向。
第二个是多设备协同应用。办公、健康、家庭场景里,“手机连着平板用”“手表接力手机”的体验一旦做顺,会有很强的用户粘性。这个方向技术门槛高,但竞争还不激烈,适合有积累的团队。
第三个是IoT和智能设备联动。手机天然适合做控制中心和状态中枢,配合鸿蒙的设备连接能力,能做很多以前需要专门App才能实现的事情。这个方向需要一个核心场景打透,而不是做一堆控制开关的浅层聚合。
我不建议现在就指望鸿蒙生态给你带来巨大流量,更不赞成只为了“蹭热点”而硬做一个不接地气的应用。更好的姿势是:先从自己熟悉的领域里找到一个小场景,把体验做到比别人顺手,再考虑放大。
写到这里,我其实更想说一点个人体验。鸿蒙开发真正难的不是语法,而是习惯被打破。过去我们习惯一个应用一个包、一个页面一个入口,现在则要习惯把应用拆成能力、把设备拼成一张网。我在真机上第一次实现手机与平板接力打开同一个待办时,确实有一种痛快感,那是纯页面开发很难给到的正反馈。如果这篇随笔对你有帮助,我建议你今晚就装好DevEco Studio,随便写一个能点按钮计数的页面,然后试着把它变成桌面卡片。哪怕只做到这里,你也会比我第一次上手时少走很多弯路。