线上App被系统静默回收,是很多鸿蒙应用开发者都遇到过的头疼问题。上周我这边就出了一次线上事故:用户反馈应用在连续打开多个页面、加载大量图片后直接被系统杀掉,没有任何提示。排查发现是内存占用超过阈值,被鸿蒙系统的后台管控机制回收了进程。对比iOS和Android,鸿蒙对应用内存的约束更严格,一旦超过阈值就会直接回收,几乎没有商量余地。
后来通过has.onMemoryWarning这个API解决了大部分问题。它能在系统内存紧张时提前通知应用,让开发者有机会在进程被杀之前主动清理资源,降低被回收的概率。
为什么需要内存告警监听
应用在前台运行时,系统通常不会轻易下手。但内存占用过高时,系统依然会强制执行回收策略。尤其是鸿蒙面向多设备形态,从手机到平板、智慧屏,低端设备的内存往往只有2GB到4GB,留给应用的配额非常有限。
常见的做法是在onShow/onHide里手动管理资源,但这个方案存在一个根本问题:开发者无法感知系统当前的内存水位,也不知道系统什么时候会认为应用占用过高。onMemoryWarning提供的是一个系统级的提前量,相当于考试交卷前监考老师提醒"还有5分钟",还有机会检查一遍答卷;如果老师直接抢卷子,那就什么都来不及了。
API接口说明
has.onMemoryWarning的使用非常简洁,核心方法就两个:注册监听和移除监听。
- // 注册内存告警监听
- has.onMemoryWarning(function(res) {
- console.info('内存告警等级:', res.level);
- });
复制代码
res.level是内存告警的严重程度等级,当前系统定义了三个档位:5表示内存适中(系统开始关注)、10表示内存较低(需要认真清理)、15表示内存极低(系统即将回收)。注意这些level值不是连续的递增序列,中间存在间隔。
- // 移除所有内存告警监听
- has.offMemoryWarning();
- // 移除指定的监听回调
- function myCallback(res) {
- console.info('内存告警:', res.level);
- }
- has.onMemoryWarning(myCallback);
- has.offMemoryWarning(myCallback);
复制代码
offMemoryWarning的callback参数是可选参数。不传参数时移除全部监听,传入指定函数则只移除对应的回调。
基础监听实现
一个最小的监听实现,在收到告警时记录等级并输出日志:
- Page({
- data: {
- warningLevel: 0,
- warningCount: 0,
- },
- onReady() {
- this.startMemoryMonitor();
- },
- onUnload() {
- // 页面销毁时移除监听
- has.offMemoryWarning();
- },
- startMemoryMonitor() {
- const that = this;
- has.onMemoryWarning(function(res) {
- that.setData({
- warningLevel: res.level,
- warningCount: that.data.warningCount + 1,
- });
- that.handleMemoryWarning(res.level);
- });
- },
- handleMemoryWarning(level) {
- if (level >= 15) {
- console.error('内存极度紧张,立刻释放资源');
- this.releaseAllCache();
- } else if (level >= 10) {
- console.warn('内存较低,释放非必要资源');
- this.releaseNonEssentialCache();
- } else {
- console.info('内存适中,清理部分缓存');
- this.releasePartialCache();
- }
- },
- releaseAllCache() {
- // 清空所有图片缓存、数据缓存
- },
- releaseNonEssentialCache() {
- // 清空图片缓存,保留数据缓存
- },
- releasePartialCache() {
- // 清空最近最少使用的缓存
- },
- });
复制代码
整体结构是监听回调 -> 根据level分级处理 -> 执行对应的清理策略。
分级清理策略
收到告警后怎么处理,这才是内存管理的核心。如果把所有缓存一股脑全部清掉,会直接影响用户体验:用户正在浏览的图片突然丢失、正在编辑的表单数据被清空。比较合理的做法是分三档递进清理。
level 5是内存适中的阶段,系统已经开始关注应用。这时清理一些不重要的缓存即可:
- if (level === 5) {
- // 清理过期的请求缓存
- this.clearExpiredRequestCache();
- // 清理超出数量限制的图片缓存,保留最近50张
- this.trimImageCache(50);
- }
复制代码
level 10是内存紧张阶段,需要认真清理较大的缓存对象:
- if (level === 10) {
- // 先做基础清理
- this.clearExpiredRequestCache();
- this.trimImageCache(20); // 只保留20张
- // 释放大数据缓存
- this.clearLargeDataCache();
- // 建议GC
- this.suggestGC();
- }
复制代码
level 15是内存极低阶段,系统随时可能回收进程,能清的全清:
- if (level === 15) {
- // 清空所有缓存
- this.clearAllCache();
- // 关闭非活跃的页面栈
- this.closeInactivePages();
- // 强烈建议GC
- this.suggestGC();
- }
复制代码
这里要重点强调的是:level 15时系统可能已经准备动手了,清理操作不一定来得及执行完。关键资源的释放应该在level 10时就开始,而不是等到最后一刻。
与生命周期配合使用
内存告警监听与页面生命周期配合,推荐在onReady中注册、在onUnload中移除:
- Page({
- onReady() {
- const that = this;
- has.onMemoryWarning(function(res) {
- that.handleWarning(res.level);
- });
- },
- onUnload() {
- has.offMemoryWarning();
- },
- handleWarning(level) {
- console.info('收到内存告警, level:', level);
- },
- });
复制代码
一个容易踩的坑:如果应用有多个页面都注册了监听,每个页面都会收到告警回调,导致同一个告警被重复处理。解决办法是只在主页面注册一次,或者用全局标志位做控制:
- let isMemoryWarningListening = false;
- function setupMemoryWarning(callback) {
- if (isMemoryWarningListening) return;
- has.onMemoryWarning(callback);
- isMemoryWarningListening = true;
- }
- function removeMemoryWarning() {
- has.offMemoryWarning();
- isMemoryWarningListening = false;
- }
复制代码
告警历史记录功能
把收到的内存告警记录下来,方便后期排查问题时回溯。每一轮告警记录下时间、level和对应的等级描述,并保留最近50条历史:
- Page({
- data: {
- warningHistory: [],
- logList: [],
- },
- onReady() {
- this.addLog('内存告警监听已开启');
- const that = this;
- has.onMemoryWarning(function(res) {
- const levelText = that.getLevelText(res.level);
- const record = {
- time: new Date().toLocaleTimeString(),
- level: res.level,
- levelText: levelText,
- };
- const history = that.data.warningHistory;
- history.unshift(record);
- if (history.length > 50) {
- history.pop();
- }
- that.setData({ warningHistory: history });
- that.addLog('内存告警: ' + levelText + ' (level=' + res.level + ')');
- });
- },
- onUnload() {
- has.offMemoryWarning();
- },
- getLevelText(level) {
- if (level >= 15) return '极低';
- if (level >= 10) return '低';
- if (level >= 5) return '适中';
- return '未知(' + level + ')';
- },
- addLog(msg) {
- const time = new Date().toLocaleTimeString();
- const logList = this.data.logList;
- logList.unshift('[' + time + '] ' + msg);
- if (logList.length > 20) {
- logList.pop();
- }
- this.setData({ logList: logList });
- },
- clearLog() {
- this.setData({ logList: [], warningHistory: [] });
- this.addLog('日志已清空');
- },
- });
复制代码
调试时的事件日志非常有价值。内存告警不像键盘高度变化那样有直观的视觉反馈,不记录日志几乎无法感知触发时机。
- <view class="log-list" if="{{logList.length > 0}}">
- <text class="log-item" for="{{logList}}">{{$item}}</text>
- </view>
- <text class="log-empty" if="{{logList.length === 0}}">暂无日志</text>
复制代码
模拟告警的几种方式
开发阶段触发真实的内存告警并不容易,总不能在开发机上把内存撑爆。尝试过的模拟方式有:
- // 制造内存压力
- function simulateMemoryPressure() {
- let bigArray = [];
- for (let i = 0; i < 1000; i++) {
- bigArray.push(new Array(10000).fill('test'));
- }
- console.info('已分配大量内存,等待告警...');
- return bigArray; // 保持引用,防止被GC
- }
复制代码
但这种方式的触发效果取决于设备总内存和当前空闲内存:在8GB设备上可能无论如何分配都触发不了,在2GB设备上很快就会出现告警。DevEco Studio的Profiler工具可以模拟内存压力,但需要连接真机或模拟器,纯代码层面不太好模拟。
另一种思路是在Demo中放一个"模拟告警"按钮,手动触发不同等级的告警逻辑,方便查看UI效果和验证清理策略是否符合预期。真实的系统告警只能在实际内存紧张时才能触发。
实际场景:图片列表页的内存管理
图片列表是内存消耗最大的场景之一。瀑布流页面随着用户不断下滑,图片越来越多,内存持续上升。接入onMemoryWarning后可做如下优化:
- Page({
- data: {
- imageList: [],
- fullList: [],
- imageCache: {},
- cacheSize: 0,
- },
- onReady() {
- const that = this;
- has.onMemoryWarning(function(res) {
- if (res.level >= 10) {
- // 内存紧张,只保留当前可见区域的图片缓存
- that.trimCache();
- }
- });
- },
- onUnload() {
- has.offMemoryWarning();
- this.clearAllCache();
- },
- trimCache() {
- const cache = this.data.imageCache;
- const keys = Object.keys(cache);
- const keepCount = 20; // 只保留20条
- if (keys.length <= keepCount) return;
- // 按最后访问时间排序,删除最旧的
- keys.sort(function(a, b) {
- return cache[b].lastAccess - cache[a].lastAccess;
- });
- const toRemove = keys.slice(keepCount);
- for (let i = 0; i < toRemove.length; i++) {
- delete cache[toRemove[i]];
- }
- this.setData({
- imageCache: cache,
- cacheSize: keepCount,
- });
- },
- clearAllCache() {
- this.setData({ imageCache: {}, cacheSize: 0 });
- },
- });
复制代码
核心思路是:收到level 10告警就开始缩减缓存,不要等到level 15。清理策略遵循LRU思路,保留最近使用的,删除最旧的。
几个容易踩的坑
告警回调里的this指向问题
function(res)形式的回调中,this并不会指向Page实例。直接写this.setData会导致页面无法更新,而且控制台不会报错。需要在外部先用const that = this保存引用:
- // 错误写法
- has.onMemoryWarning(function(res) {
- this.setData({ level: res.level }); // this不是Page实例
- });
- // 正确写法
- const that = this;
- has.onMemoryWarning(function(res) {
- that.setData({ level: res.level });
- });
复制代码
清理操作本身也耗内存
清理过程中,遍历大缓存并逐条删除的操作本身就占用内存。在遍历过程中内存可能不降反升。一种更高效的方式是直接清空整个缓存对象:
- // 低效:逐条删除
- for (let key in cache) {
- delete cache[key];
- }
- // 高效:直接重置对象
- 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是否存在:
- if (has.onMemoryWarning) {
- has.onMemoryWarning(function(res) {
- // 处理告警
- });
- } else {
- console.warn('当前版本不支持onMemoryWarning');
- }
复制代码
后台任务场景的额外注意点
如果应用有后台任务(音乐播放、定位上报等),内存告警的处理需要更谨慎。后台任务被系统回收的影响远比前台页面大得多。之前做过一个带后台定位的应用,因为内存告警处理不当导致后台定位被杀,用户反馈"走到一半导航断了",排查很久才发现是内存告警释放策略没有覆盖后台场景。
总结
onMemoryWarning这个API很小,只有两个方法,但背后的内存管理思路才是关键。收到告警后清理什么、什么时候清理、清理到什么程度,都需要根据实际业务形态确定。最重要的一点是:不要等到level 15才开始处理。level 5时做基础清理,level 10时做大清理,level 15时可能已经没有足够的时间执行复杂的清理逻辑了。 |