本节摘要:内存是空间账:泄漏的每一次都微不足道,累积起来就是「越用越卡、最终闪退」。本节盘点四类最常作案的泄漏(闭包持有、订阅未注销、定时器未清、大图常驻),给出各自的现场特征与修法;随后把 Hermes 引擎放回性能版图讲清它的收益与代价,最后配置一套够用的调试工具链。本章收尾,性能闭环从测量到防治就全了。
内存泄漏的定义朴素:不再需要的对象因为还被引用着而无法回收。移动端比网页端更敏感——用户切走又切回、页面进进出出,泄漏以每次几百 KB 的速度累积,最终被系统判死刑。四个惯犯各有指纹。
惯犯一:定时器不清理。 组件卸载了,定时器还在每秒执行回调,回调里的闭包把整个组件连同它抓着的数据都钉在内存里。指纹特征:内存曲线阶梯式上涨,节奏与用户操作某个页面的次数同步。修法一句话:设置与清理配对,副作用钩子的返回函数就是清理位。
惯犯二:订阅不注销。 事件总线、前后台监听、原生模块的事件流——只注册不注销,监听者名单越积越长。2.3 节的生命周期代码已经示范了标准写法,6.1 的归因实录抓的正是这个惯犯。指纹特征:事件分发越来越慢,JS 帧基线缓慢抬升。
惯犯三:闭包抓大件。 把大数组、大结果集抓进一个长期存活的回调或缓存里。比如「搜索历史」功能把每次的完整结果对象存进全局数组,本意只存关键词。指纹特征:内存台阶与某类操作强相关,涨上去就不下来。修法:缓存只存必需的最小数据,大件用完即弃。
惯犯四:大图常驻。 一张按原始尺寸解码的大图占用的内存可达其文件体积的许多倍(按像素数计),列表里几十张这样的图足以压垮低内存设备。指纹特征:内存尖峰与图片加载同步,低端 Android 机闪退集中。修法:按显示尺寸选图或降采样解码(6.2 的预取方案已覆盖),滚动离场即释放。
一个把「配对纪律」写全的通用范式,值得当成肌肉记忆:
import React from 'react'; function usePulse(onTick, ms) { React.useEffect(() => { // 设置与清理必须配对:返回函数在卸载时执行 const timer = setInterval(onTick, ms); return () => clearInterval(timer); }, [onTick, ms]); // 依赖变化时:先清旧的 再设新的 } function StockTicker({ symbol }) { const [price, setPrice] = React.useState(null); React.useEffect(() => { const ctrl = new AbortController(); fetch('https://api.example.com/quote?s=' + symbol, { signal: ctrl.signal }) .then(r => r.json()) .then(d => setPrice(d.price)) .catch(err => { if (err.name !== 'AbortError') throw err; }); // 中止在途请求:防止慢响应把旧代码写入新状态,也释放引用 return () => ctrl.abort(); }, [symbol]); usePulse(() => setPrice(p => p), 3000); return null; // 演示用:真实组件在此渲染价格 }
怀疑泄漏时按取证流程走,而不是凭感觉翻代码。第一步,确认存在:设计一组重复操作(进出页面若干次),前后各截一次内存快照对比——曲线反复阶梯上涨才算嫌疑成立,正常波动会回落。第二步,定位对象:对比两次快照里增长最多的对象类型,原生位图看位图,JS 对象看类名与构造来源,锁定「什么东西在越积越多」。第三步,追引用链:在堆快照里看增长对象被谁持有——引用链的末端通常就是那个「忘了清理的监听器、没置空的定时器、抓着大件的闭包」。第四步,修复验证:清理后重复第一步的操作脚本,两次快照对齐才算结案。这套流程与 6.1 的归因实录同构:性能工作的所有分支,最后都收敛到「测量、对比、归因、复测」四个动作。
把 2.3 的启动话题在这里补全。Hermes 是专为移动端设计的 JavaScript 引擎,它的性能主张在「装载体积」与「运行内存」两端:构建期把 JavaScript 预编译成字节码,运行时免去解析源码的开销,启动更快、内存更省、字节码体积通常还小于源码包。0.70 起它已是双端默认,多数项目无需任何操作就享受红利。要说代价:调试协议与部分依赖动态求值的写法存在兼容面,字节码按引擎版本生成,引擎升级要求包重新构建——这正是第 8 章热更新方案必须与引擎版本联动的原因。给维护老项目的读者一句提醒:还在关闭 Hermes 的项目,先测一测开启后的启动与内存曲线再决定,这是本册里少有的「可能白捡的性能」。
日常开发的够用配置有四件。React 开发工具:组件树检查、状态实时查看、渲染耗时分析,查「谁在重渲染」的主力。开发菜单内置工具:帧率监视、元素检查、网络代理开关,随手开随手关的轻量仪器。原生侧分析器(Xcode Instruments 与 Android Studio Profiler):内存堆快照、线程火焰图,定位跨语言与系统级的疑难问题,也是验证「泄漏修好了」的终审法庭。崩溃与性能上报:开发期靠本地日志,测试期起就该接入第 8 章的线上监控,让性能回归在灰度阶段就被看见。使用纪律延续 6.1:由轻到重,每次测量留下记录——性能工作最怕「改了感觉快了」,感觉不能上评审会。
背景:应用某版本上线后,低端 Android 机的闪退率异常抬升,崩溃报告指向系统杀进程,典型的内存不足特征。操作:先用线上监控锁定高发页面为图片瀑布流页;本地复现并打开原生内存分析器,滚动两分钟后截取堆快照对比——位图对象占用的份额随滚动持续上涨不回落;核对图片加载方案,发现未配置降采样,原图直接解码;再查退出页面后的快照,位图仍被一个静态缓存持有(第三方图片库的缓存配置未按页生命周期清理)。修复:启用按显示尺寸的降采样解码,页面退出时清空该页的图片内存缓存,滚动远端的条目释放位图。验证:同一操作脚本复测,内存曲线锯齿平稳,闪退率回落到基线。解读:空间账的清算套路与时间账同构——线上数据定页面、堆快照定对象、引用链定病根、配置修改定修法。变式:若堆快照显示的是 JS 对象堆积而非原生位图,侦查方向就转向闭包与订阅——两本账、四个惯犯、一套纪律,工具只是不同账本的记账笔。
性能篇收官。下一章跨过那道栅栏:亲手编写原生模块,把两片土壤的专属能力接进 JavaScript 世界。