系列文章《鸿蒙开发从入门到专家》第 8 篇(高级篇)
前言
组件库搭好了,界面也漂亮了,但用户不会为你的"好看"买单——打开 App 要等 3 秒,滑动列表会卡顿,用一会儿手机就开始发烫,这样的应用用户会直接卸载。
性能优化是高级开发者的分水岭。本文不聊虚的理论,直接聚焦鸿蒙应用最常见的三个性能瓶颈:启动慢、列表卡、内存涨。每个问题都给测量方法、根因分析和具体的优化代码。
一、启动速度优化
1.1 测量启动时间
在 DevEco Studio 的 Profiler → Launch 中可以精确测量冷启动时间。一个良好的目标是:
1.2 启动流程拆解
鸿蒙应用的冷启动流程:
点击图标 → 加载 HAP → 创建进程 → 加载 Ability → onCreate → onWindowStageCreate → loadContent → 首帧渲染
优化思路是并行化和延迟化:把不影响首屏显示的工作挪到后面去做。
1.3 AppStartUp 框架:官方推荐的启动优化方案
HarmonyOS NEXT 引入了 AppStartUp 框架,让开发者在应用启动时有条理地初始化各个组件:
// startup/DatabaseInitializer.ets
import { StartupTask } from '@kit.AbilityKit'
export default class DatabaseInitializer implements StartupTask {
async init(): Promise<void> {
// 初始化数据库连接
await TodoDatabase.getInstance()
console.info('数据库初始化完成')
}
dependencies(): string[] {
// 声明依赖:数据库初始化依赖于配置加载
return ['ConfigInitializer']
}
}
// startup/ConfigInitializer.ets
export default class ConfigInitializer implements StartupTask {
async init(): Promise<void> {
// 加载应用配置
let prefs = await AppPreferences.getInstance()
let theme = await prefs.getString('theme', 'light')
console.info(`配置加载完成,当前主题: ${theme}`)
}
dependencies(): string[] {
return [] // 无依赖,最先执行
}
}
在 module.json5 中注册启动任务:
{
"module": {
"startupTasks": [
{
"name": "ConfigInitializer",
"srcEntry": "./ets/startup/ConfigInitializer.ets"
},
{
"name": "DatabaseInitializer",
"srcEntry": "./ets/startup/DatabaseInitializer.ets"
}
]
}
}
AppStartUp 会自动按依赖关系拓扑排序并并行执行无依赖的任务,比把所有初始化堆在 onCreate 里快得多。
1.4 延迟加载非首屏内容
首屏不需要的数据,用 setTimeout 或 requestIdle 延迟加载:
aboutToAppear() {
// 立即加载首屏数据
this.loadCriticalData()
// 延迟加载非关键数据
setTimeout(() => {
this.loadRecommendations()
this.loadNotifications()
}, 500)
}
1.5 减少 HAP 包体积
包体积直接影响加载速度:
图片资源用
webp格式替代png按需加载本地资源,不要一股脑放进
resources使用 ProGuard/R8 类似的代码压缩(DevEco Studio 构建时自动处理)
分包加载:非首屏模块做成动态 HSP
二、列表渲染优化
列表卡顿是用户感知最明显的性能问题。ArkUI 提供了多种手段来保障长列表的流畅度。
2.1 LazyForEach 的正确用法
前面文章提到过,这里强调几个容易忽略的细节:
List({ space: 8 }) {
LazyForEach(
this.dataSource, // 数据源
(item: ItemType) => { // 子组件构建
ListItem() {
ItemComponent({ item: item })
}
},
(item: ItemType) => item.id // key 生成器:必须唯一且稳定
)
}
key 生成器的黄金法则:
必须唯一:同一列表中不能有两个相同的 key
必须稳定:同一个数据项的 key 不能变来变去
不要用
index做 key:数据插入/删除时会导致大量不必要的重渲染
2.2 组件复用:@Reusable
对于结构相同、只有数据不同的列表项,使用 @Reusable 让 ArkUI 复用组件实例:
@Reusable
@Component
struct NewsListItem {
@State item: NewsItem = { id: '', title: '', summary: '', time: '' }
aboutToReuse(params: Record<string, object>): void {
// 复用时更新数据
this.item = params.item as NewsItem
}
build() {
Row({ space: 12 }) {
Image(this.item.coverUrl)
.width(100)
.height(75)
.objectFit(ImageFit.Cover)
.borderRadius(6)
Column({ space: 6 }) {
Text(this.item.title)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.item.summary)
.fontSize(13)
.fontColor('#999999')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.item.time)
.fontSize(11)
.fontColor('#CCCCCC')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.padding(16)
.backgroundColor(Color.White)
}
}
使用复用组件:
LazyForEach(this.newsList, (item: NewsItem) => {
ListItem() {
NewsListItem({ item: item })
}
}, (item: NewsItem) => item.id)
@Reusable 的效果:1000 条数据的列表,实际上只创建了屏幕可视区域 + 缓冲区的组件实例(大约 20 个),滑动时复用这些实例,只更新数据。
2.3 避免 build() 中的复杂计算
build() 方法会被频繁调用,任何复杂计算都会拖慢渲染:
// ❌ 错误:每次 build() 都重新计算
build() {
let filteredList = this.allItems.filter(item => item.isActive).sort((a, b) => b.time - a.time)
List() {
ForEach(filteredList, (item) => { ... })
}
}
// ✅ 正确:计算结果缓存到状态变量
@State filteredList: ItemType[] = []
aboutToAppear() {
this.updateFilteredList()
}
private updateFilteredList() {
this.filteredList = this.allItems
.filter(item => item.isActive)
.sort((a, b) => b.time - a.time)
}
build() {
List() {
ForEach(this.filteredList, (item) => { ... })
}
}
2.4 图片加载优化
图片是列表卡顿的头号元凶:
Image(item.coverUrl)
.width(100)
.height(75)
.objectFit(ImageFit.Cover)
.borderRadius(6)
.alt($r('app.media.placeholder')) // 占位图
.error($r('app.media.error_image')) // 加载失败图
.syncLoad(false) // 异步加载,不阻塞 UI
如果图片来自网络,建议加一层本地缓存:
async function loadImageWithCache(url: string): Promise<string> {
let cacheKey = md5(url)
let cachePath = `${getContext().cacheDir}/${cacheKey}.jpg`
let file = fileIo.openSync(cachePath, fileIo.OpenMode.READ_ONLY)
if (file) {
fileIo.closeSync(file)
return cachePath // 命中本地缓存
}
// 下载并缓存
let httpRequest = http.createHttp()
let response = await httpRequest.request(url)
// ... 写入缓存文件
httpRequest.destroy()
return cachePath
}
三、内存优化
3.1 测量内存占用
DevEco Studio Profiler 提供内存分析工具:
Memory Profiler:实时查看堆内存、Native 内存占用
Heap Snapshot:抓取堆快照,分析对象引用关系
Allocation Tracker:追踪内存分配热点
3.2 常见内存泄漏场景
场景 1:HTTP 请求未销毁
// ❌ 错误
async function fetchData() {
let request = http.createHttp()
let response = await request.request('https://api.example.com/data')
// 忘记 destroy()!
}
// ✅ 正确
async function fetchData() {
let request = http.createHttp()
try {
let response = await request.request('https://api.example.com/data')
return response
} finally {
request.destroy() // 确保一定释放
}
}
场景 2:事件监听未移除
aboutToAppear() {
// 注册监听
window.getLastWindow(getContext()).then(win => {
win.on('windowSizeChange', this.onWindowSizeChange)
})
}
aboutToDisappear() {
// ❌ 容易遗漏:窗口监听不会随组件销毁自动移除
window.getLastWindow(getContext()).then(win => {
win.off('windowSizeChange', this.onWindowSizeChange)
})
}
场景 3:闭包引用导致组件无法回收
// ❌ 错误:回调持有外部引用
setInterval(() => {
this.updateTimer() // 闭包引用了 this,组件销毁后定时器仍在运行
}, 1000)
// ✅ 正确:组件销毁时清理定时器
private timerId: number = -1
aboutToAppear() {
this.timerId = setInterval(() => {
this.updateTimer()
}, 1000)
}
aboutToDisappear() {
if (this.timerId !== -1) {
clearInterval(this.timerId)
this.timerId = -1
}
}
3.3 大数据量的处理策略
当需要处理大量数据时(如日志分析、本地搜索),不要放在主线程:
@Concurrent
function processLargeData(data: string[]): ProcessedResult {
// 大数据处理逻辑
let result: ProcessedResult = { items: [], count: 0 }
for (let item of data) {
// 复杂计算...
}
return result
}
// UI 线程中调用
async function handleData() {
let task = new taskpool.Task(processLargeData, this.rawData)
let result = await taskpool.execute(task) as ProcessedResult
this.processedData = result
}
四、性能优化检查清单
□ 启动优化
□ 使用 AppStartUp 框架并行初始化
□ 非首屏内容延迟加载
□ 减少 HAP 包体积
□ 列表优化
□ 长列表使用 LazyForEach + 稳定 key
□ 列表项使用 @Reusable
□ build() 内无复杂计算
□ 图片使用占位图 + 异步加载 + 本地缓存
□ 内存优化
□ HTTP 请求用完即 destroy
□ 事件监听在 aboutToDisappear 中移除
□ 定时器/动画在组件销毁时清理
□ 大数据处理放入 Taskpool
小结与下篇预告
性能优化没有银弹,但有清晰的优先级:
启动速度用 AppStartUp 框架并行化初始化,延迟非关键加载
列表流畅度靠 LazyForEach + @Reusable + 图片缓存三件套
内存稳定关键是养成"谁创建谁销毁"的习惯,事件、定时器、HTTP 请求都要记得清理
下一篇(第 9 篇)是鸿蒙最独特的分布式能力开发:跨设备迁移与数据流转。这是鸿蒙区别于 Android/iOS 的杀手级特性,也是很多开发者最感兴趣的部分。