系列文章《鸿蒙开发从入门到专家》第 57 篇 · 高级
第 8 篇讲性能优化时提到内存优化是"三大战役"之一,但那篇只给了原则。这篇讲具体怎么查、怎么治。
鸿蒙应用最常见的内存问题不是 OOM(系统有 LMK 兜底),而是内存泄漏导致的列表卡顿和反复 GC。一个被泄漏的图片对象占 2MB,10 个就 20MB,GC 频繁触发,帧率掉到 40fps 以下用户就能感知到。
一、ArkTS 内存模型
ArkTS 运行在 JS 引擎上,依赖 GC(垃圾回收)管理内存。和 Android 的 JVM 不同,JS 引擎的 GC 策略是分代的:
新生代(Young Generation):短命对象,Minor GC 频繁回收。大部分临时对象在这里被回收。
老生代(Old Generation):存活时间长的对象,Major GC 不定期回收。如果对象被引用持有,会从新生代晋升到老生代,长期驻留。
内存泄漏的本质:对象本该被回收,但被某个长期存活的引用持有,无法被 GC 标记为可回收。
二、常见泄漏模式
2.1 闭包捕获
最隐蔽的泄漏。闭包持有外部作用域的变量引用,如果闭包被长期持有,变量引用的对象也不会被回收。
// 泄漏:定时器回调持有组件引用
@Component
struct TimerLeakDemo {
private timerId: number = -1;
@State count: number = 0;
aboutToAppear(): void {
this.timerId = setInterval(() => {
// 这个箭头函数捕获了 this
// 只要定时器没清除,this(组件)就不会被 GC
this.count++;
}, 1000);
}
// 忘了清除定时器
aboutToDisappear(): void {
// 必须清除!
if (this.timerId !== -1) {
clearInterval(this.timerId);
this.timerId = -1;
}
}
}
这在列表项组件中尤其危险。列表项被回收时,如果 aboutToDisappear 没有清理定时器、事件监听器或异步回调,组件实例就泄漏了。
2.2 全局单例持有
// 泄漏:全局事件总线持有组件引用
class EventBus {
private static instance: EventBus;
private listeners: Map<string, Function[]> = new Map();
static getInstance(): EventBus {
if (!EventBus.instance) {
EventBus.instance = new EventBus();
}
return EventBus.instance;
}
on(event: string, callback: Function): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)!.push(callback);
}
emit(event: string, data: any): void {
this.listeners.get(event)?.forEach(cb => cb(data));
}
off(event: string, callback: Function): void {
const callbacks = this.listeners.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index >= 0) {
callbacks.splice(index, 1);
}
}
}
}
@Component
struct EventListenerComponent {
private handler = (data: any) => {
// 箭头函数捕获 this
console.info('Received: ' + data);
};
aboutToAppear(): void {
EventBus.getInstance().on('dataUpdate', this.handler);
}
aboutToDisappear(): void {
// 必须反注册!
EventBus.getInstance().off('dataUpdate', this.handler);
}
}
2.3 LazyForEach 闭包
第 52 篇提到过:LazyForEach 的 itemGenerator 是一个闭包,如果在里面引用外部大对象,即使列表项被回收,大对象也无法被 GC。
// 泄漏:闭包捕获大数组
const largeData = loadLargeData(); // 100MB 数据
LazyForEach(source, (item) => {
// largeData 被闭包持有
// 即使列表项被回收,largeData 也不会被 GC
processWith(largeData, item);
});
// 正确做法:只传需要的数据
const processedItems = source.map(item => extractNeeded(largeData, item));
// largeData 可以被释放
三、DevEco Profiler 内存分析
DevEco Studio 内置了 Profiler 工具,可以抓取内存快照。
3.1 录制内存快照
打开 DevEco Studio → Run → Profile
选择 Memory 标签页
点击 Record 开始录制
操作应用(进入页面 → 退出页面 → 再进入 → 再退出)
点击 Stop 停止录制
录制结果会显示内存曲线。正常曲线:进入页面内存上升,退出页面内存回落。如果退出后内存不回落,说明有泄漏。
3.2 分析堆快照
抓取堆快照后,按对象类型分组查看:
重点看:
组件实例(
@Component生成的对象)数量是否随页面退出而减少。图片 Image 对象的保留大小是否异常。
闭包(Closure)的保留大小。
3.3 代码定位
发现泄漏对象后,通过引用链定位到代码。典型的引用链:
GC Root
→ setInterval 的回调队列
→ 箭头函数闭包
→ Component 实例(泄漏)
→ @State 数据
→ 图片对象
这条链告诉你:定时器没清除,导致组件实例和它持有的数据全部泄漏。
四、生命周期治理
泄漏排查是亡羊补牢,生命周期治理是预防。
4.1 资源清理清单
每个组件的 aboutToDisappear 应该清理以下资源:
4.2 统一清理基类
// base/DisposableComponent.ts
type CleanupFn = () => void;
export abstract class DisposableComponent {
private cleanups: CleanupFn[] = [];
private disposed: boolean = false;
protected registerCleanup(fn: CleanupFn): void {
this.cleanups.push(fn);
}
// 子类在 aboutToDisappear 中调用
protected dispose(): void {
if (this.disposed) return;
this.disposed = true;
// 逆序执行清理
for (let i = this.cleanups.length - 1; i >= 0; i--) {
try {
this.cleanups[i]();
} catch (e) {
console.error('Cleanup failed: ' + e);
}
}
this.cleanups = [];
}
}
使用:
@Component
struct SafeTimerComponent extends DisposableComponent {
@State count: number = 0;
aboutToAppear(): void {
const timerId = setInterval(() => {
if (this.disposed) return; // 防止已销毁后回调
this.count++;
}, 1000);
this.registerCleanup(() => clearInterval(timerId));
// 事件监听也注册
const handler = (data: any) => { /* ... */ };
EventBus.getInstance().on('update', handler);
this.registerCleanup(() => EventBus.getInstance().off('update', handler));
}
aboutToDisappear(): void {
this.dispose();
}
}
4.3 异步回调保护
异步操作(网络请求、Promise)的回调可能在组件销毁后触发,导致操作已销毁的 @State 变量。
@Component
struct AsyncSafeComponent {
@State data: string = '';
private isAlive: boolean = true;
aboutToAppear(): void {
this.fetchData();
}
private async fetchData(): Promise<void> {
const result = await httpClient.get('/api/data');
// 检查组件是否还活着
if (!this.isAlive) return;
this.data = result;
}
aboutToDisappear(): void {
this.isAlive = false;
}
}
简单但有效。对于 TaskPool 的任务,用 @Sendable 的方式传递 isAlive 标志不可行(跨线程无法共享引用),需要在任务完成后通过消息回传时检查标志。
五、图片内存优化
图片是内存消耗大户。一张 1080p 的图片解码后占 8MB(1920×1080×4 bytes),10 张就 80MB。
5.1 按需加载尺寸
import { image } from '@kit.ImageKit';
async function loadThumbnail(context: Context, uri: string,
targetWidth: number, targetHeight: number): Promise<PixelMap> {
const imageSource = image.createImageSource(uri);
// 只解码需要的尺寸
const decodingOptions: image.DecodingOptions = {
editable: false,
desiredSize: { width: targetWidth, height: targetHeight }
};
const pixelMap = await imageSource.createPixelMap(decodingOptions);
imageSource.release();
return pixelMap;
}
desiredSize 让解码器只生成目标尺寸的位图,而不是解码原图再缩放。一张 4032×3024 的照片(48MB 解码后)按 200×200 解码只占 160KB。
5.2 LRU 图片缓存
class ImageCache {
private cache: Map<string, PixelMap> = new Map();
private maxSize: number = 50;
get(key: string): PixelMap | null {
const item = this.cache.get(key);
if (item) {
// 移到队尾(LRU 策略)
this.cache.delete(key);
this.cache.set(key, item);
}
return item || null;
}
put(key: string, value: PixelMap): void {
if (this.cache.size >= this.maxSize) {
// 淘汰最久未使用的
const oldestKey = this.cache.keys().next().value;
const oldest = this.cache.get(oldestKey);
if (oldest) {
oldest.release();
}
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
clear(): void {
this.cache.forEach(item => item.release());
this.cache.clear();
}
}
release() 是关键——PixelMap 持有 native 内存,不调用 release 等于泄漏。
六、内存水位监控
运行时监控内存水位,超过阈值时主动清理缓存:
import { hidumper } from '@kit.PerformanceAnalysisKit';
class MemoryMonitor {
private threshold: number = 200; // 200MB
private checkInterval: number = 30000; // 30秒检查一次
start(): void {
setInterval(() => {
const used = this.getMemoryUsage();
if (used > this.threshold) {
console.warn(`Memory high: ${used}MB, clearing cache`);
ImageCache.getInstance().clear();
}
}, this.checkInterval);
}
private getMemoryUsage(): number {
// 通过系统接口获取当前进程内存
// 实际项目中用 Profiler API 或系统服务
return 0; // 示例简化
}
}
小结
内存管理的三道防线:预防(生命周期治理 + 资源清理基类)、监控(Profiler 堆分析 + 运行时水位)、治理(按需加载图片 + LRU 缓存 + 主动清理)。泄漏排查的关键是看引用链——找到谁持有不该持有的引用,然后在该清理的地方补上清理代码。
下篇讲主题与暗黑模式。颜色令牌怎么设计、动态换肤怎么实现。
参考阅读:
第 8 篇:鸿蒙性能优化实战(内存优化部分)
第 52 篇:高效列表渲染(LazyForEach 闭包泄漏)
官方文档:DevEco Profiler、image 模块