问题背景
随着功能迭代,项目越来越庞大,需要一套内存监控体系,来及时的上报线上内存问题,帮助定位问题原因。
oom
out-of-memory 简称。iOS app中内存占用超出一定上限后,系统会把app直接杀死。oom发生时从现象上看,与普通的Crash 并不区别,不过设备的设置-隐私-分析与改进中生成的并不是普通类型的Crash日志,而是JetsamEvent 开头的日志。
foom
JetsamEvent发生时,可能会杀死多个app,杀死的app可能在前台也可能在后台。发生在前台的oom,简称为foom。一般而言,前台运行的app闪退时会极大的影响到用户体验,所以我们需要筛选出FOOM。
LPOOMDetector 核心功能及原理简介
LPOOMDetector 的核心功能主要参考腾讯开源库Tencent/OOMDetector,在落地项目之后,也在不断的功能增加和完善中,后续不排除采用其他的监控方案。
- FOOM 发生检测
- 问题堆栈聚集上报
- 大内存上报
- NSObject存活对象个数上报
- 其他功能
原理简介
foom发生检测原理
苹果官方没有提供检测 foom 发生的方法,所以并没有统一的检测方式。目前采用的是FB的排除法。每次 APP 冷启动时,判断上次 APP 运行过程中记录的特定值,排除掉其他崩溃原因后,得出上次发生了FOOM 的结论。
问题堆栈聚集上报原理
问题堆栈聚集上报是一个笼统的概念,简单理解就是上报有问题的内存分配时的堆栈信息。
首先看两个 libmalloc 源码中stack_logging_disk.c定义的两个接口
1 | // We set malloc_logger to NULL to disable logging, if we encounter errors |
当malloc_logger和__syscall_logger函数指针不为空时,malloc/free、vm_allocate/vm_deallocate等内存分配/释放通过这两个指针通知上层。
根据这两个指针可以记录到对象的内存分配信息(地址,大小,类型),同时配合backtrace函数捕获当前的堆栈信息。(这里捕获到的地址是虚拟内存地址,不能从符号表dsym解析符号,还需要记录每个image加载时的偏移slide,堆栈地址减去slide就是符号表能解析的地址)
malloc_logger
在 libmalloc库中的以下关于内存相关的方法 malloc_zone_malloc, malloc_zone_calloc, malloc_zone_valloc, malloc_zone_realloc, malloc_zone_free, malloc_zone_free_definite_size, malloc_zone_memalign等函数内部都会调用malloc_logger,所以我们只需要监控malloc_logger就可以实现对于内存的alloc与free的监控。
__syscall_logger
__syscall_logger可以实现对于vm_allocate, vm_deallocate, mmap, munmap的监控。但是__syscall_logger是私有方法,只能在debug 环境下使用
malloc_logger 和 __syscall_logger 这里举两个简单的例子:NSArray的创建及CALayer 渲染
有了系统底层的函数回调后,加上backtrace 获取到的堆栈地址,我们就可以根据这些参数设计一套堆栈聚集上报体系。问题堆栈聚集上报总的来说就是当内存分配的堆栈相同,且同堆栈的内存大小累积值达到设定好的阈值时,写入本地mmap文件,下次冷启动时,判断是否发生了FOOM,确认发生FOOM 时 上报该堆栈。
核心数据流向如下图:
大内存上报
大内存是指单次开辟的内存大于设定好的阈值。
大内存的监控方式共用问题堆栈聚集上报的逻辑,通过malloc_logger 和 __syscall_logger 拿到内存分配信息,通过backtrace获取到堆栈详情。
NSObject 存活对象个数上报
项目的编程语言是OC,记录NSObject的存活对象,可以帮助我们更加细致的排查可疑代码,尤其是ViewController的上报,针对聊天室这样的特定页面可以清晰的观察是否存在内存泄漏。
NSObject 存活对象的数据采集通过 hook alloc 和 dealloc方法,记录方式同堆栈上报记录方式基本一致,内存中通过ClassName的hash值来写入自实现的字典中,对于达到特定条件的数据写入mmap文件中,等待上报。
SDK 其他功能
内存占用足迹
SDK 会在控制器的生命周期方法 viewDidLoad 及 viewDidApper 记录APP 此刻占用的内存,但是 SDK只会保留最后记录的20个页面,其余的可在用户日志中查询。同时,如果下次冷启判断上次APP运行有发生foom时,会用最后记录到的ViewController当做 foom的reason,并且JIRA会创建foom 任务分配给该页面所属模块的负责人
SVGA 内存占用
当前的 SVGA 播放库会通过UIImage 缓存播放过的文件,且存在内存占用特别大的SVGA动画,故增加该项的数据监控。针对内存占用过大的文件,可以和UI部门沟通,优化对应的动效
iOS 14以上 Metric 性能事件上报(cpu 异常,I/O异常,无响应异常,crash)
通过排除法判断上次是否发生foom会有一个漏洞。当页面发生卡死时,watchdog主动杀死APP或者用户手动杀掉APP,均会被误判为foom。(线上也有很多的上报数据显示,虽然记录到有发生foom,但是APP崩溃时占用的内存并不高,并且也没有内存警告)
这里通过metric上报无响应异常的数据,先尝试确认该种场景的发生次数,后续版本再单独把卡死场景标识上报。
内存优化常见方案
当前整理的内存优化方案主要是三个方向:
1、避免内存泄漏
2、移除未使用的内存。例如消息列表中已不可见的消息数据
3、优化图片使用
这里着重列举下避免内存泄漏 和 优化图片使用:
避免内存泄漏
内存泄漏是指申请的内存空间使用完毕之后未回收。
造成内存泄漏的原因有很多,各个场景也不尽相同。这里列举聊天室页面的相关内存泄漏处理作为范例:
1、常见的block 内直接使用了self 导致的循环引用及隐含的循环引用。
2、driver的某些关闭房间逻辑释放的资源不够全面,收归了一下,把离开/关闭/关闭房间并关闭当前页面 等场景,最终都调同一个,避免不规范导致有些资源释放,有的资源不放。
3、子线线程的runloop没有被停掉,子线程一直存活,后面在stop的时候,同时停止runloop从而使得线程正常退出。
优化图片使用
1、 避免将图片放在内存里
(1)解码后的UIImage 占用的内存比较大,如果当前不需要显示时,可以不放在内存里
(2)同一个页面使用重复资源时,避免重复创建(YYImage)
(3)批量使用图片的场景,推荐使用autoreleasepool等方式及时释放内存
2、图片裁剪
(1)大多情况下业务场景需要显示的图片尺寸小于图片的原始尺寸
3、图片绘制及缩放使用 UIGraphicsImageRenderer 和 ImageIO
备注:
常见的UIGraphicsBeginImageContextWithOptions绘图方式会有两个问题:
1、默认是 SRGB 的格式,也就是说每个像素需要占 4 个 bytes 的空间,对于一些黑白或者仅有 alpha 通道的数据来说是没有必要的。
2、需要将原图片完全解码后渲染出来,原图片的解码会造成内存占用的高峰。
iOS 12之后,使用UIGraphicsImageRenderer绘图。系统会自动选择合适的颜色格式,避免不必要的内存消耗。
对于原方法需要解码原图造成内存暴涨的问题,可以考虑用ImageIO来解决。ImageIO可以直接读取图像大小和元数据信息,不会带来额外的内存开销.
总结
SDK已经能够通过上报的数据来帮助分析解决一些内存问题,但是存在的问题还是比较多,比如SDK的核心思路是基于OOMDetector的内存信息聚集,这种方式导致SDK在运行时本身需要常驻内存且对CPU有较大的消耗。此外通用的内存堆栈上报(比如这边上报的YYImage 相关的堆栈)无法准确的定位出实际的内存使用场景,且很多低内存情况下的崩溃也无法提供任何帮助。
未来展望:
1、内存监控的方案还在不断的探索中,不排除后续会采用其他的监控方式
2、APP 卡死导致的 数据误报应当筛选出,单独解决