查看原文
其他

iOS APP内存优化记录

第七章 小集 2022-03-15

作者 | 第七章 
来源 | http://www.zoomfeng.com

APP在运行过程中,如果内存占用过高则会引起以下几个问题:

①被操作系统的守护进程给杀掉,无论是前台还是后台;

②耗电增大,手机发热;

③系统可能会运行卡顿(不是换入换出到磁盘,而是解压缩和重压缩内存);

业务背景

因为我们APP的直播间要玩网页版的小游戏,比较耗内存,除了JS游戏本身降低内存消耗之外,native也需要释放更多的内存以提供给游戏使用,避免因为内存占用过大而被操作系统杀掉进程。

优化native本身的内存占用大体分为两部分:优化长期的内存占用和优化峰值内存占用。

理论上的技术方案

优化长期的内存占用

1.内存泄漏问题;

要做内存优化,先要分析当前占用内存较大的页面或功能。尤其是直播间。

① 开instruments来跟课,查看直播间的内存泄漏;

——需要高端一点的机器,比如iPad才能跑起来,低端机器跑不了instruments。

② 集成MLeaksFinder库到开发的target,检测内存泄漏;

——不过这个库只能检查UIViewcontroller和其对应的UIView是否有泄漏,可以方便开发的时候发现问题。

③ 集成 FBMemoryProfiler[1] 可视化工具,直接嵌入到 App 中,可以起到在 App 中直接查看内存使用情况,并筛选潜在泄漏OC对象的作用;

网上说instruments的leaks工具无法检测循环引用导致的内存泄漏,验证下是否属实;

——是真的,下图这种block和self形式的循环引用,instruments检测不出来

2.不必要的单例;是否可以改造成懒加载,用的时候才去初始化,用完可以释放掉;这个主要是去排查代码逻辑看是否合理;

3.RN页面的内存占用问题见这里[2]

① 列表的cell是否有复用,复用是否正确;

② 图片是否用了大图,能不能改成小图;

③ 进入直播间时回收掉所有RN页面,在退出直播间后重新初始化RN页面。——已采用,效果显著,每个RN页面大概占用20-40M的内存。

当然RN页面占用内存过大的问题,最彻底的解决方案还是直接用native去重写,至少一级页面用native去实现,无论是内存,滑动响应速度等等的用户体验都会好很多。只不过我们权衡之后没有这么做而已。

4.大图片的使用方式,包括动画所用图片的初始化方式,UIImage的imageWithContentWithFile可以避免使用缓存,用完即释放,但是需要把图片放在bundle的目录下,而不是XCAssets里;

——对于降低峰值内存有一定效果,但是imageNamed方式图片的缓存,在APP退到后台或者受到内存警告时,如果未被使用则会被系统自动清理。所以该方式可以采用,但是不要有太高的心理预期。

5.图片下采样。大一点的图片(比如做动画用的图片),改用ImageIO的api,而不是直接imageWithNamed的方式来创建,详情见这里[3] 这里[4];另实际显示的尺寸小于图片实际尺寸时可以降低采样率(下采样);

——下采样的方式仅针对图片尺寸比实际显示的尺寸大的情况下才会降低内存消耗。由于效果并不明显,项目中并未使用该种方式。

对一张1500 × 2668的图片做测试发现

① 使用普通的ImageWithNamed方式,APP退到后台再拉回前台,内存会被清理;

② 使用ImageWithContentWithFile的方式,每次使用后内存被释放;——直播间可以采用

这两种方式的内存变化见下图:

③ 使用下采样的方式,峰值内存降低了,但是丢弃图片后,整体占用的内存并未下降。暂不知该图片缓存释放的时机。

④ 使用按比例缩放UIGraphicsBeginImageContext(),效果跟下采样类似,图片缓存在APP退到后台依然没有释放

6.APP监听到内存警告或APP退到后台的通知时,释放一些可重建的非必要对象。

优化峰值内存占用

1.在合适的地方添加AutoreleasePool来及时释放内存,比如全局IM消息的接收和解析,视频回放的消息过滤等;

2.缓存的使用,比如系统的UIImage的imageWithNamed函数创建的对象,以及YYMemoryCache和SDImageCache来共享缓存,多个业务使用同一份内存缓存。

3.缓存释放时机,YYMemoryCache和SDImageCache以及系统的UIImage的imageWithNamed创建的图片对象,都会在APP退到后台时,或者收到内存警告时清理全部的内存缓存。

4.网络图片单张图片size过大监控,以及网络图片总内存大小限制;

5.OOM捕捉[5]

——最大的用处是用来分析短期内存增长过快的原因,同时可以获取C和C++的内存分配。需要权衡下是否引入。

OOM暂未支持bugly集成,获取的堆栈日志暂时没地方存放。且因为都用了fishhook,会跟 FBRetainCycleDetector[6] 冲突,暂不引入。

6.了解mmap,测试其在图片映射的内存降低数据;

SDWebImage使用NSData的dataWithContentOfFile的方式来读取图片到内存;

mmap主要是可以省略从内核空间拷贝到用户空间的这一步操作,其他省不掉。FastImage额外省略了图片解码,但是却是通过把解码后的图片写磁盘来实现的(解码后图片增大,读取的IO耗时也会增大)。另FastImage可以使图片字节对齐,避免CoreAnimation在渲染时做一次额外的拷贝操作。

7.vmmap分析Xcode抓到的memgraph内存信息,看看有什么收获;

——没必要,原理跟FBMemoryProfiler[1]类似,还不如直接用FBMemoryProfiler[1]

ps:Facebook三件套 FBRetainCycleDetector[6],FBAllocationTracker和FBMemoryProfiler。

命令行有不少工具可以用来配合分析内存,比如vmmap,leaks和heap,见这里[7]

在我们项目中的实际应用

内存泄漏

1.Instruments 的部分捕获结果

①AFNetworking的session

②JSBridge的内存泄漏

③第三方库的泄漏

原因见下图,是第三方的库里边,create出来的CF对象没有release掉:

2.MLeaksFinder发现的部分内存问题

①巡堂和动画的view内存泄漏,block互相持有导致,已fixed。

②IAP的Header内存泄漏,动画的delegate强引用导致,在开始做动画的时候设置delegate,在动画结束后的回调里把delegate置空可以解决这个问题。

内存峰值优化

1.视频回放的业务,拖动进度条会有大量的string被分配内存,占据了快100M

解决方案:在拖曳进度条时会过滤消息,在过滤消息的函数里加自动释放池,及时释放局部变量的内存。

2.另一块是由于IM群组的设计,很多用户不需要知道的别的小班的消息也会一起发过来,数据量会很大,在子线程进行高度计算时会产生大量的临时变量,此处也加一个自动释放池用来降低内存峰值。同时之前在另一篇文章里曾经针对消息数据的过滤做过优化

之后的instruments内存监控如下:

第一张图里红圈的string内存占用已经不再出现。

3.SSZPicMemoryTool优化网络图片的内存占用问题(通过SD下载的,包括native和RN),

——主要是通过SSZPicMemoryTool接管了native通过SDWebImage使用的网络图片,以及RN页面使用的网络图片。所以可以及时在使用时监控到内存超过阈值的单张图片。只需要不同的业务页面走查一遍即可发现。除了单张图片的最大size超出监控之外,还直接把YYCache里的LRU淘汰算法拿来用,用于避免网络图片占用的峰值内存过大。

原理见下图,SSZPicMemoryTool接管了图片内存缓存的管理,包括读取,写入和淘汰。

业务逻辑优化和时间空间置换策略

直播间玩H5的小游戏会比较吃内存,所以可以采用一些策略,在用户玩游戏的时候,先释放掉一些内存出来供游戏使用。我们采用的两个方案,一个是用户玩游戏时,把直播的视频画面给移除掉,仅播放音频,因为此时本身视频画面也是被游戏画面挡住的,所以没有太大必要。另一个就是由于RN页面消耗的内存比较大,采用进入直播间就释放掉外围的RN页面,等退出直播间再恢复。

H5游戏本身也做了一轮内存的优化,优化前后降低了一半左右的内存消耗,效果还是很明显的。

1.游戏时关闭视频,以及进入直播间后回收RN的内存优化效果

测试环境:iPhone8P + 无其他课堂互动(消息等) + 进入直播后稳定后采集数据,直接跳转到直播间,不经过试讲和课程页

游戏中剔除视频可以优化部分内存:剔除视频渲染后直播间整体内存可以降低25~30M(占进入直播间后增加的 40~50%),接收解码等操作因为SDK没有屏蔽接口,可能需要obs端做改造,但剩余可优化空间不足了,预计最多可优化10~20M。

直播间外部的内存有优化空间:剔除RN后,进入首屏和进入直播间的内存降低了约 50M,RN占用的内存比较大,首屏39M也有待细化,这两部分可以跟进优化。

——进入APP后会占用约40M的内存,经过测试,这部分无法继续降低了,一个空的工程也需要消耗掉这么多的内存。

回收RN页面

重新加载RN页面

2.直播间游戏自身的内存占用优化(主要是JS做的)

WKWebView 是运行在另一个进程里,Network Loading 以及 UI Rendering 在其它进程中执行。在 WKWebView 的进程里当总体的内存占用比较大的时候,WebContent Process 会 crash,从而出现白屏现象。

WebContent Process 因为整机内存过大被系统kill掉的时候不可控,所以一般会在crash白屏的回调加reload的逻辑,故而出现游戏不断reload问题。

Webview的CPU和内存优化交给前端自己做,只需要关注native本身的优化即可。WebKit进程的内存消耗可以用Instruments的Activity Monitor组件看到。

Instrument Activity Monitor + Allocations ,忽略IM和互动其他情况,只在音视频+测评、游戏的情况分析:

关于内存统计

APP使用内存统计可以直接如下的函数,参考来自这里

+ (NSInteger)getResidentMemory
{
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if (result == KERN_SUCCESS) {
return (NSInteger)vmInfo.phys_footprint; ///< 应用使用的物理内存大小
} else {
return -1;
}
}

shared memory

共享内存可以提供跨进程访问的能力,不过如果你的App使用了别的进程创建的共享内存,那么Debug Navigator是不会将它计入你自己的内存总量的,不过vmmap会将它加入TOTAL中,所以可能会导致vmmap计算的内存量会大于Debug Navigator统计内存量。

Debug Navigator其实就是统计了当前进程的所有虚拟内存的Dirty Size + Swapped Size,当然还要剔除掉对第三方共享内存的使用量,当我们发现Debug Navigator的内存量飙高时,不仅仅要去关注Heap上的内存用量,更要关注VM Tracker中那些大Dirty Size的VM Region,这样才能更透彻的了解你的App究竟是怎样使用内存的。

其他

内存泄漏工具的使用

介绍

在iOS的Dev的包里会集成腾讯的MLeaksFinder库和Facebook的FBMemoryProfiler三件套的库,作用都是用来检测内存泄漏的。

不同点在于,MLeaksFinder只用于检查页面和页面相关的view的内存泄漏,当发现有内存泄漏时,会用一个弹窗来做出提示。比如最常见的,你退出一个页面,2秒后如果该页面的内存还未释放,则会提示内存泄漏。

FBMemoryProfiler则可以检测所有类型的内存泄漏,原理是hook了系统的alloc和dealloc函数,跟instruments的功能类似,只不过更加轻量化,可以在APP运行时实时看到内存分配的情况,如果有对象内存泄漏,则会标红表示。

MLeaksFinder使用指南

很简单,在退出一个页面后,如果有弹出下图的弹窗,则说明该页面有内存泄漏,并且列出了具体的内存泄漏的对象。

备注:这个列表是一个内存泄漏的链表指针,不一定每个对象都会泄漏,但至少有一个是发生了内存泄漏的,通常可能是最后一个,也可能是多个,比如上图其实XunTangTipView和LOTAnimationView都有内存泄漏。

另外就是默认的判断时间是2秒,实际测试发现有时候会有误伤,所以可以把时间间隔调大到3秒或者更大一些。

FBMemoryProfiler使用指南

在Dev的pod里集成该库后,Dev的包,顶部会有一个小浮窗显示当前的总内存,点击后可以展开大图查看更多信息,如下图所示。

当你认为某个时刻,可能有内存泄漏,或者想看看是否有内存泄漏时,可以点击下图的Mark Gen按钮,此时会生成一个当前内存状态相比前一个generation时的快照,点击Expand按钮,会展开一个列表,把所有的对象都列出来,红色的一栏则表示该对象有内存泄漏。

左上方有一个输入款,可以用来进行过滤。

点击红色的那一栏,就能看到具体的内存泄漏的原因,如下图,nats库的实例跟timer互相强引用导致。

参考

[1]https://github.com/facebook/FBMemoryProfiler 
[2]http://bbs.reactnative.cn/topic/5550/rn%E5%86%85%E5%AD%98%E9%97%AE%E9%A2%98-%E5%86%85%E5%AD%98%E6%8C%81%E7%BB%AD%E5%A2%9E%E9%95%BF-%E7%9B%B4%E5%88%B0%E8%A2%AB%E7%B3%BB%E7%BB%9F%E6%9D%80%E6%AD%BB 
[3]https://techblog.toutiao.com/2018/06/19/untitled-42/ 
[4]https://blog.csdn.net/TuGeLe/article/details/81137995 
[5]https://github.com/Tencent/OOMDetector/issues 
[6]https://github.com/facebook/FBRetainCycleDetector 
[7]https://juejin.im/post/5b23dafee51d4558e03cbf4f



推荐阅读
• iOS调试Block引用对象无法被释放的一个小技巧
• XCode启动参数和环境变量
• iOS代码瘦身实践:删除无用的方法
• Swift 游戏开发之「能否关个灯」


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存