查看: 110|回复: 0

鸿蒙ASCF内存告警监听实战:onMemoryWarning防杀进程指南

[复制链接]
发表于 半小时前 | 显示全部楼层 |阅读模式
线上App被系统静默回收,是很多鸿蒙应用开发者都遇到过的头疼问题。上周我这边就出了一次线上事故:用户反馈应用在连续打开多个页面、加载大量图片后直接被系统杀掉,没有任何提示。排查发现是内存占用超过阈值,被鸿蒙系统的后台管控机制回收了进程。对比iOS和Android,鸿蒙对应用内存的约束更严格,一旦超过阈值就会直接回收,几乎没有商量余地。

后来通过has.onMemoryWarning这个API解决了大部分问题。它能在系统内存紧张时提前通知应用,让开发者有机会在进程被杀之前主动清理资源,降低被回收的概率。

为什么需要内存告警监听

应用在前台运行时,系统通常不会轻易下手。但内存占用过高时,系统依然会强制执行回收策略。尤其是鸿蒙面向多设备形态,从手机到平板、智慧屏,低端设备的内存往往只有2GB到4GB,留给应用的配额非常有限。

常见的做法是在onShow/onHide里手动管理资源,但这个方案存在一个根本问题:开发者无法感知系统当前的内存水位,也不知道系统什么时候会认为应用占用过高。onMemoryWarning提供的是一个系统级的提前量,相当于考试交卷前监考老师提醒"还有5分钟",还有机会检查一遍答卷;如果老师直接抢卷子,那就什么都来不及了。

API接口说明

has.onMemoryWarning的使用非常简洁,核心方法就两个:注册监听和移除监听。
  1. // 注册内存告警监听
  2. has.onMemoryWarning(function(res) {
  3.     console.info('内存告警等级:', res.level);
  4. });
复制代码

res.level是内存告警的严重程度等级,当前系统定义了三个档位:5表示内存适中(系统开始关注)、10表示内存较低(需要认真清理)、15表示内存极低(系统即将回收)。注意这些level值不是连续的递增序列,中间存在间隔。
  1. // 移除所有内存告警监听
  2. has.offMemoryWarning();
  3. // 移除指定的监听回调
  4. function myCallback(res) {
  5.     console.info('内存告警:', res.level);
  6. }
  7. has.onMemoryWarning(myCallback);
  8. has.offMemoryWarning(myCallback);
复制代码

offMemoryWarning的callback参数是可选参数。不传参数时移除全部监听,传入指定函数则只移除对应的回调。

基础监听实现

一个最小的监听实现,在收到告警时记录等级并输出日志:
  1. Page({
  2.     data: {
  3.         warningLevel: 0,
  4.         warningCount: 0,
  5.     },
  6.     onReady() {
  7.         this.startMemoryMonitor();
  8.     },
  9.     onUnload() {
  10.         // 页面销毁时移除监听
  11.         has.offMemoryWarning();
  12.     },
  13.     startMemoryMonitor() {
  14.         const that = this;
  15.         has.onMemoryWarning(function(res) {
  16.             that.setData({
  17.                 warningLevel: res.level,
  18.                 warningCount: that.data.warningCount + 1,
  19.             });
  20.             that.handleMemoryWarning(res.level);
  21.         });
  22.     },
  23.     handleMemoryWarning(level) {
  24.         if (level >= 15) {
  25.             console.error('内存极度紧张,立刻释放资源');
  26.             this.releaseAllCache();
  27.         } else if (level >= 10) {
  28.             console.warn('内存较低,释放非必要资源');
  29.             this.releaseNonEssentialCache();
  30.         } else {
  31.             console.info('内存适中,清理部分缓存');
  32.             this.releasePartialCache();
  33.         }
  34.     },
  35.     releaseAllCache() {
  36.         // 清空所有图片缓存、数据缓存
  37.     },
  38.     releaseNonEssentialCache() {
  39.         // 清空图片缓存,保留数据缓存
  40.     },
  41.     releasePartialCache() {
  42.         // 清空最近最少使用的缓存
  43.     },
  44. });
复制代码

整体结构是监听回调 -> 根据level分级处理 -> 执行对应的清理策略。

分级清理策略

收到告警后怎么处理,这才是内存管理的核心。如果把所有缓存一股脑全部清掉,会直接影响用户体验:用户正在浏览的图片突然丢失、正在编辑的表单数据被清空。比较合理的做法是分三档递进清理。

level 5是内存适中的阶段,系统已经开始关注应用。这时清理一些不重要的缓存即可:
  1. if (level === 5) {
  2.     // 清理过期的请求缓存
  3.     this.clearExpiredRequestCache();
  4.     // 清理超出数量限制的图片缓存,保留最近50张
  5.     this.trimImageCache(50);
  6. }
复制代码

level 10是内存紧张阶段,需要认真清理较大的缓存对象:
  1. if (level === 10) {
  2.     // 先做基础清理
  3.     this.clearExpiredRequestCache();
  4.     this.trimImageCache(20); // 只保留20张
  5.     // 释放大数据缓存
  6.     this.clearLargeDataCache();
  7.     // 建议GC
  8.     this.suggestGC();
  9. }
复制代码

level 15是内存极低阶段,系统随时可能回收进程,能清的全清:
  1. if (level === 15) {
  2.     // 清空所有缓存
  3.     this.clearAllCache();
  4.     // 关闭非活跃的页面栈
  5.     this.closeInactivePages();
  6.     // 强烈建议GC
  7.     this.suggestGC();
  8. }
复制代码

这里要重点强调的是:level 15时系统可能已经准备动手了,清理操作不一定来得及执行完。关键资源的释放应该在level 10时就开始,而不是等到最后一刻。

与生命周期配合使用

内存告警监听与页面生命周期配合,推荐在onReady中注册、在onUnload中移除:
  1. Page({
  2.     onReady() {
  3.         const that = this;
  4.         has.onMemoryWarning(function(res) {
  5.             that.handleWarning(res.level);
  6.         });
  7.     },
  8.     onUnload() {
  9.         has.offMemoryWarning();
  10.     },
  11.     handleWarning(level) {
  12.         console.info('收到内存告警, level:', level);
  13.     },
  14. });
复制代码

一个容易踩的坑:如果应用有多个页面都注册了监听,每个页面都会收到告警回调,导致同一个告警被重复处理。解决办法是只在主页面注册一次,或者用全局标志位做控制:
  1. let isMemoryWarningListening = false;
  2. function setupMemoryWarning(callback) {
  3.     if (isMemoryWarningListening) return;
  4.     has.onMemoryWarning(callback);
  5.     isMemoryWarningListening = true;
  6. }
  7. function removeMemoryWarning() {
  8.     has.offMemoryWarning();
  9.     isMemoryWarningListening = false;
  10. }
复制代码

告警历史记录功能

把收到的内存告警记录下来,方便后期排查问题时回溯。每一轮告警记录下时间、level和对应的等级描述,并保留最近50条历史:
  1. Page({
  2.     data: {
  3.         warningHistory: [],
  4.         logList: [],
  5.     },
  6.     onReady() {
  7.         this.addLog('内存告警监听已开启');
  8.         const that = this;
  9.         has.onMemoryWarning(function(res) {
  10.             const levelText = that.getLevelText(res.level);
  11.             const record = {
  12.                 time: new Date().toLocaleTimeString(),
  13.                 level: res.level,
  14.                 levelText: levelText,
  15.             };
  16.             const history = that.data.warningHistory;
  17.             history.unshift(record);
  18.             if (history.length > 50) {
  19.                 history.pop();
  20.             }
  21.             that.setData({ warningHistory: history });
  22.             that.addLog('内存告警: ' + levelText + ' (level=' + res.level + ')');
  23.         });
  24.     },
  25.     onUnload() {
  26.         has.offMemoryWarning();
  27.     },
  28.     getLevelText(level) {
  29.         if (level >= 15) return '极低';
  30.         if (level >= 10) return '低';
  31.         if (level >= 5) return '适中';
  32.         return '未知(' + level + ')';
  33.     },
  34.     addLog(msg) {
  35.         const time = new Date().toLocaleTimeString();
  36.         const logList = this.data.logList;
  37.         logList.unshift('[' + time + '] ' + msg);
  38.         if (logList.length > 20) {
  39.             logList.pop();
  40.         }
  41.         this.setData({ logList: logList });
  42.     },
  43.     clearLog() {
  44.         this.setData({ logList: [], warningHistory: [] });
  45.         this.addLog('日志已清空');
  46.     },
  47. });
复制代码

调试时的事件日志非常有价值。内存告警不像键盘高度变化那样有直观的视觉反馈,不记录日志几乎无法感知触发时机。
  1. <view class="log-list" if="{{logList.length > 0}}">
  2.     <text class="log-item" for="{{logList}}">{{$item}}</text>
  3. </view>
  4. <text class="log-empty" if="{{logList.length === 0}}">暂无日志</text>
复制代码

模拟告警的几种方式

开发阶段触发真实的内存告警并不容易,总不能在开发机上把内存撑爆。尝试过的模拟方式有:
  1. // 制造内存压力
  2. function simulateMemoryPressure() {
  3.     let bigArray = [];
  4.     for (let i = 0; i < 1000; i++) {
  5.         bigArray.push(new Array(10000).fill('test'));
  6.     }
  7.     console.info('已分配大量内存,等待告警...');
  8.     return bigArray; // 保持引用,防止被GC
  9. }
复制代码

但这种方式的触发效果取决于设备总内存和当前空闲内存:在8GB设备上可能无论如何分配都触发不了,在2GB设备上很快就会出现告警。DevEco Studio的Profiler工具可以模拟内存压力,但需要连接真机或模拟器,纯代码层面不太好模拟。

另一种思路是在Demo中放一个"模拟告警"按钮,手动触发不同等级的告警逻辑,方便查看UI效果和验证清理策略是否符合预期。真实的系统告警只能在实际内存紧张时才能触发。

实际场景:图片列表页的内存管理

图片列表是内存消耗最大的场景之一。瀑布流页面随着用户不断下滑,图片越来越多,内存持续上升。接入onMemoryWarning后可做如下优化:
  1. Page({
  2.     data: {
  3.         imageList: [],
  4.         fullList: [],
  5.         imageCache: {},
  6.         cacheSize: 0,
  7.     },
  8.     onReady() {
  9.         const that = this;
  10.         has.onMemoryWarning(function(res) {
  11.             if (res.level >= 10) {
  12.                 // 内存紧张,只保留当前可见区域的图片缓存
  13.                 that.trimCache();
  14.             }
  15.         });
  16.     },
  17.     onUnload() {
  18.         has.offMemoryWarning();
  19.         this.clearAllCache();
  20.     },
  21.     trimCache() {
  22.         const cache = this.data.imageCache;
  23.         const keys = Object.keys(cache);
  24.         const keepCount = 20; // 只保留20条
  25.         if (keys.length <= keepCount) return;
  26.         // 按最后访问时间排序,删除最旧的
  27.         keys.sort(function(a, b) {
  28.             return cache[b].lastAccess - cache[a].lastAccess;
  29.         });
  30.         const toRemove = keys.slice(keepCount);
  31.         for (let i = 0; i < toRemove.length; i++) {
  32.             delete cache[toRemove[i]];
  33.         }
  34.         this.setData({
  35.             imageCache: cache,
  36.             cacheSize: keepCount,
  37.         });
  38.     },
  39.     clearAllCache() {
  40.         this.setData({ imageCache: {}, cacheSize: 0 });
  41.     },
  42. });
复制代码

核心思路是:收到level 10告警就开始缩减缓存,不要等到level 15。清理策略遵循LRU思路,保留最近使用的,删除最旧的。

几个容易踩的坑

告警回调里的this指向问题

function(res)形式的回调中,this并不会指向Page实例。直接写this.setData会导致页面无法更新,而且控制台不会报错。需要在外部先用const that = this保存引用:
  1. // 错误写法
  2. has.onMemoryWarning(function(res) {
  3.     this.setData({ level: res.level }); // this不是Page实例
  4. });
  5. // 正确写法
  6. const that = this;
  7. has.onMemoryWarning(function(res) {
  8.     that.setData({ level: res.level });
  9. });
复制代码

清理操作本身也耗内存

清理过程中,遍历大缓存并逐条删除的操作本身就占用内存。在遍历过程中内存可能不降反升。一种更高效的方式是直接清空整个缓存对象:
  1. // 低效:逐条删除
  2. for (let key in cache) {
  3.     delete cache[key];
  4. }
  5. // 高效:直接重置对象
  6. this.setData({ cache: {} });
复制代码

level值不是连续整数

level的取值为5、10、15,中间有间隔。不要写类似if (level > 5 && level < 10)这样的区间判断——虽然当前文档只定义了三个等级,但系统可能传其他值。使用>=做边界判断更安全。

页面切换的监听残留

如果在页面A注册监听后没有在onUnload中移除,跳到页面B后页面A的回调依然生效。每次收到告警,页面A的回调都会执行,但此时页面A的上下文可能已经销毁,容易引发异常。注册和移除必须成对出现。

版本兼容性判断

has.onMemoryWarning的起始版本是1.0.16。如果应用需要兼容更早的系统版本,调用前需要先判断API是否存在:
  1. if (has.onMemoryWarning) {
  2.     has.onMemoryWarning(function(res) {
  3.         // 处理告警
  4.     });
  5. } else {
  6.     console.warn('当前版本不支持onMemoryWarning');
  7. }
复制代码

后台任务场景的额外注意点

如果应用有后台任务(音乐播放、定位上报等),内存告警的处理需要更谨慎。后台任务被系统回收的影响远比前台页面大得多。之前做过一个带后台定位的应用,因为内存告警处理不当导致后台定位被杀,用户反馈"走到一半导航断了",排查很久才发现是内存告警释放策略没有覆盖后台场景。

总结

onMemoryWarning这个API很小,只有两个方法,但背后的内存管理思路才是关键。收到告警后清理什么、什么时候清理、清理到什么程度,都需要根据实际业务形态确定。最重要的一点是:不要等到level 15才开始处理。level 5时做基础清理,level 10时做大清理,level 15时可能已经没有足够的时间执行复杂的清理逻辑了。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-8-26 14:37 , Processed in 0.023036 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部