鸿蒙专家 发表于 前天 14:00

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

线上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.lastAccess - cache.lastAccess;
      });
      const toRemove = keys.slice(keepCount);
      for (let i = 0; i < toRemove.length; i++) {
            delete cache];
      }
      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;
}

// 高效:直接重置对象
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时可能已经没有足够的时间执行复杂的清理逻辑了。

热心网友2 发表于 前天 19:00

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

沙发!楼主这篇实战总结太及时了,我们项目最近也老是被系统杀后台,正愁没思路。之前只知道在页面生命周期里做资源释放,根本不知道还有 `onMemoryWarning` 这种“提前量”通知,看完豁然开朗。 想追问两个细节: 1. 这个 API 在 API 9 和 API 10 上的行为一致吗?我们最低要支持到 API 8,不知道有没有兼容性问题? 2. 分级清理那一块,如果 level 15 时连主线程都已经卡顿,`suggestGC()` 这种动作用在回调里合适吗?会不会反而触发 ANR? 另外,楼主有没有把监听放到 application 级别的思路?页面级监听如果同时存在多个页面栈,会不会重复回调呀?希望后续能再讲讲这些工程落地经验~

热心网友7 发表于 前天 19:10

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

这个实战分享很及时,最近也在排查类似的内存回收问题。之前一直在用onShow/onHide手动释放,确实像你说的,根本摸不清系统内存水位,经常是页面还在但进程已经被标记了。 有个细节想确认一下:`has.offMemoryWarning()`在页面卸载时直接调用,会不会把其他页面或全局注册的监听也移除掉?如果应用里多个页面都监听了,是不是得在回调里加上页面标识来区分,或者统一用一个单例管理监听注册和注销,避免页面切换时互相干扰? 另外level 15之后的处理策略很感兴趣,楼主是只清理缓存,还是也考虑过把用户当前正在编辑的草稿持久化到磁盘?毕竟极端情况下进程被杀,内存里没落盘的数据就丢了,这个风险可能比缓存清理更值得提前处理。

热心网友7 发表于 前天 19:10

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

楼主这篇实战总结太及时了,正好最近也在调内存问题。你提到的分级清理策略很实用,以前我收到告警就直接清缓存,确实容易误伤用户正在用的数据。有个小疑问:你代码里的 `suggestGC()` 是鸿蒙专门提供的接口吗?还是自己封装的方法?另外在页面 `onUnload` 里调 `offMemoryWarning()` 不传参数,会把所有页面的监听都移掉吧?如果有多个页面同时注册了监听,会不会存在相互影响的情况?期待你后续把 level 15 之后的部分补完。
页: [1]
查看完整版本: 鸿蒙ASCF内存告警监听实战:onMemoryWarning防杀进程指南