我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

先把狠话放在前头:性能大多数时候不是被 CPU“拖垮”的,而是被内存慢慢掏空的——页面退了对象却没退、图片解了缓存却没限、定时器和监听像藤蔓一样缠着组件……久而久之,曲线一路向上,用户体验一路向下。别怂,这篇我就按你的大纲来一把梳顺:内存模型 → 泄漏定位 → 工具使用。核心工具锁定 DevEco Studio 的 Memory ProfilerHeap Analyzer(堆快照分析),再配几把“硬扳手”——ASan/HWASan、HiDumper。全程工程化视角,能复用、能落地、能闭环。走起!💥

一、内存模型:先把“水系”摸清楚

1) 三个你必须会读的指标:PSS / RSS / USS

  • PSS(Proportional Set Size):独占 + 共享库按比例分摊。最像“这进程真正占了多少”。
  • RSS(Resident Set Size):独占 + 共享库全算。偏大一些。
  • USS(Unique Set Size):只看独占。最能反映“自己胖没胖”。

快速体感

  • USS 台阶式上升且回不去 → 大概率泄漏。
  • PSS 突增 → 可能刚加载大资源/大库。
  • RSS 长期高位 → 缓存/大图没有按时回收。

读曲线的目的:先发现“异常走势”,再进入下一步“是谁涨、为什么涨”。

2) 双堆现实:ArkTS 堆 × Native 堆

HarmonyOS 原生应用常见混合场景

  • ArkTS/JS 堆:页面状态、控件树、闭包、定时器等。
  • Native 堆:NAPI/C++、图像解码器、音视频缓冲、第三方库。

结论:两边都要看。仅盯 ArkTS,可能漏过 PixelMap/解码缓冲;只盯 Native,又可能忽略了未解绑的 JS 监听导致的“整棵组件树被引用”。

3) 系统视角的“望远镜”

  • HiDumperhdc shell hidumper --mem <pid> 快速看进程内存概况;配合 --zip 打包拉回做基线对比。
  • AppAnalyzer(体检):批量跑用例抓 profiler 轨迹 & 堆快照,一键回到 DevEco Profiler继续深挖。

二、泄漏定位:从“怀疑”到“实锤”的闭环打法

总流程:趋势监控 → 分配追踪 → 堆快照对比 → 引用链复盘 → 修复回归。别跳步!

步骤 A|用 Memory 泳道看“趋势”

  1. 复现实景:例如“进入商品页 → 滑动浏览 → 返回列表”,循环 3~5 次。
  2. USS/PSS 曲线:退出页面后是否回落到近似基线?如果次次都留一块,那基本坐实“泄漏趋势”。

步骤 B|切 Allocation 看“谁在涨”

  • ArkTS Allocation:对象类型、分配点、线程;找“只增不减”的类型。
  • Native Allocationmalloc/mmap 残留块、分配栈、按 SO 聚合;常见大户是图片解码、第三方库缓存。
  • 技巧:在时间轴上框选问题区间,看“Created & Existing”对“Created & Released”的比例。

步骤 C|抓两张堆快照“对比实锤”

  1. A(进页面稳定后),再抓 B(退页面 5~10s)

  2. 打开 Heap Analyzer/堆快照视图

    • 对象数量/保留大小 的差值;
    • 展开 Retainers(持有者链),谁在“攥着不放”;
    • 关注“主导对象(Dominators)”——它们通常是一串全局监听、未清理的闭包、无上限缓存,或是 PixelMap/Buffer。

需要离线?把设备的 .rawheaprawheap_translator 转成 .heapsnapshot,DevEco/Chrome Memory 都能打开,继续看对象图引用链


三、典型“真凶”与可复制修复(ArkTS × Native)

A|ArkTS 侧高频坑

1) 事件监听未解绑 / 订阅漏清

症状:页面退后 USS 不降;堆快照里,页面 ViewModel/State 被全局事件回调链持有。
修复:生命周期成对注册/注销;一次性监听用 once;封装“可自动解绑”的小工具。

// ❌ 全局事件只上不下
eventBus.on('user:update', this.onUser);

// ✅ 封装作用域型订阅
class ScopedSubs {
  private cleanups: (()=>void)[] = [];
  on<T>(bus: any, topic: string, fn: (v:T)=>void) {
    bus.on(topic, fn);
    this.cleanups.push(()=>bus.off(topic, fn));
  }
  dispose(){ this.cleanups.forEach(fn => fn()); this.cleanups = []; }
}

@Entry
@Component
struct UserPage {
  private subs = new ScopedSubs();
  aboutToAppear() { this.subs.on(eventBus, 'user:update', this.onUser); }
  aboutToDisappear() { this.subs.dispose(); } // ✅
}
2) 定时器/Worker 未停
// ❌ 定时器抓着闭包,页面没了它还在跑
let timer: number | undefined;
aboutToAppear() { timer = setInterval(() => doTick(), 1000); }
aboutToDisappear() { if (timer) { clearInterval(timer); timer = undefined; } } // ✅

// Worker 同理:worker.terminate() + 断消息通道
3) 大图 / PixelMap 没释放
  • ImageReceiver/解码器拿到的 PixelMap 用完要 release()
  • 列表中按目标尺寸解码,别拿 4K 图填 200px 卡片;
  • 长列表滚动务必做复用/采样/预取去抖
4) 无上限缓存(Map/Set)当 LRU 用
  • 真正的缓存要 容量上限 + 淘汰策略
  • 统计命中率,过期清理;别让 Map 变“黑洞”。

B|Native / NAPI 侧高频坑

1) 忘记 napi_delete_reference / 引用计数不对
napi_ref ref;
napi_create_reference(env, jsObj, 1, &ref);
// ... 使用结束:
napi_delete_reference(env, ref); // ✅
2) new/malloc 对不齐、异常路径泄漏
  • RAIIunique_ptr/自定义 deleter)托底;
  • 大对象分层所有权明确,避免“大家都以为别人会 free”。
3) C/C++ 越界或悬挂 → 堆看不出,ASan/HWASan 能一锤定音
  • 开启 ASan/HWASan 运行关键场景,第一时间抓越界/Use-After-Free;
  • 记得它们与其他 Sanitizer 互斥,按需选择。

四、工具使用:一条顺滑的实战链路

1) Memory Profiler(趋势 + 分配)

如何开:DevEco Studio → 底部 Profiler → 选择设备/进程 → Allocation 模板 → Create Session。
怎么用

  • Memory(PSS/RSS/USS) 泳道找“台阶”;
  • ArkTS/Native Allocation 泳道里,按时间框选,查看对象类型/分配点/分配栈、Created & Existing vs Created & Released
  • 对 Native 侧,按 SO/库 汇总,常能“一眼认亲”。

2) Heap Analyzer / Snapshot Insight(堆快照对比)

抓快照:场景前后抓两张(A 进场、B 退场 N 秒后)。
分析法

  • 差值排序看“保留大小(Retained Size)”最大的一批;
  • 展开 Retainers 查谁在持有(全局单例?事件?闭包?缓冲?);
  • 锁定“主导对象”之后,再回代码补齐 解绑/释放/限流

离线场景:.rawheaprawheap_translator.heapsnapshot 导入分析。

3) ASan / HWASan(原生“踩内存”克星)

  • Debug 构建开启后跑关键路径,日志会标出可疑堆块与调用栈;
  • 常配合 Native Allocation 一起用:一个看“谁没释放”,一个抓“怎么踩坏的”。

4) HiDumper(CI 友好的“卫星视角”)

  • hidumper --mem <pid> 做基线巡检;
  • 异常构建自动打包报告、回传对比;
  • 一旦发现上涨,再回 Profiler“显微镜”。

5) AppAnalyzer(体检一条龙)

  • 一键跑场景 → 自动收集 trace/内存会话/堆快照 → 报告里点击回跳 Profiler 继续定位;
  • 很适合团队周检/回归。

五、30 分钟击破“页面退出不降内存”的实操剧本

  1. 录制 Allocation(10 min)

    • 进入页面稳定 10 s → Start → 操作 1~2 分钟 → 返回列表 → Stop;
    • 看 USS 曲线:退出后不回落 → 判定“存在泄漏趋势”。
  2. ArkTS vs Native 定界(5 min)

    • ArkTS Allocation:是否有某类组件/状态对象只增不减;
    • Native Allocation:是否有 PixelMap/Buffer 残留、某 SO 的分配一直增长。
  3. 堆快照对比(10 min)

    • 抓 A(进场)、B(退场) → Heap Analyzer 对比;
    • 看保留大小最大的一批 → 展开 Retainers:十有八九是“事件未解绑/定时器未清/PixelMap 未 release/缓存无上限”。
  4. 修复&验证(5 min)

    • 补上解绑/清理/释放/限流;
    • 重跑 Allocation,确认曲线回落、快照差值消失,贴标签“已闭环”。

六、团队可复用“自检清单”(贴墙上就能用)

ArkTS

  • 监听/订阅成对 on/off,页面 aboutToDisappear/onPageHide 清理。
  • 定时器/Worker 成对 clearInterval/terminate;Promise 回调别强绑 UI。
  • PixelMap/大图用完 release();按目标尺寸解码;列表滚动做复用/采样。
  • 缓存设上限(LRU),统计命中率,定期淘汰。
  • 单例里只放数据,不放活对象(尤其 UI/上下文)。

Native / NAPI

  • napi_ref 有借有还:统一封装引用生命周期。
  • new/deletemalloc/free 成对;异常路径用 RAII。
  • 大块缓冲明确“所有权”与“释放点”。
  • ASan/HWASan 开启一次关键路径跑全量。

自动化

  • CI 每周跑 HiDumper 基线,异常自动报警。
  • 体检(AppAnalyzer)固定节奏执行;报告条目回链 Profiler 二次定位。
  • 重要版本保留 .rawheap,统一转换为 .heapsnapshot 归档,沉淀泄漏图谱

七、两个“能抄走”的最小修复片段

片段 1|作用域型订阅管理(防监听泄漏)

class ScopedSubs {
  private list: (()=>void)[] = [];
  add(unsub: () => void) { this.list.push(unsub); }
  on(bus: any, topic: string, fn: (...args:any[])=>void) {
    bus.on(topic, fn);
    this.add(() => bus.off(topic, fn));
  }
  dispose() { this.list.forEach(f => f()); this.list = []; }
}

片段 2|PixelMap 生命周期包一层(防图片泄漏)

class PixelMapBox {
  private pm?: image.PixelMap;
  adopt(pm: image.PixelMap) { this.dispose(); this.pm = pm; }
  get() { return this.pm; }
  dispose() { if (this.pm) { this.pm.release(); this.pm = undefined; } }
}
// 使用:new PixelMapBox().adopt(pm); 页面消失时统一 dispose()

结语

性能优化不要“靠感觉”,要“看证据”。趋势(Memory)→ 分配(Allocation)→ 快照(Heap)→ 引用链(Retainers)→ 修复回归,这就是你对付泄漏的五段式。下次曲线再悄悄上扬,先别烦,问自己:“它为什么不走?” ——然后,打开 Profiler,把真凶一根一根揪出来。😉

(未完待续)

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐