背景
根据Apple官方WWDC的回答,减少内存可以让用户体验到更快的启动速度,不会因为内存过大而导致Crash,可以让APP存活的更久。
对于高德地图来说,根据线上数据的分析,内存过高会导致导航过程中系统强杀OOM。尤其区别于其他APP的地方是,一般APP只需要关注前台内存过高的系统强杀FOOM,高德地图有不少用户使用后台导航,所以也需要关注后台的内存过高导致的系统强杀BOOM,且后台强杀较前台强杀更为严重。为了提升用户体验,内存治理迫在眉睫。
原理剖析
OOM
OOM是Out of Memory的缩写。在iOS APP中如果内存超了,系统会把APP直接杀死,一种另类的Crash,且无法捕获。发现OOM时,我们可以从设备->隐私->分析与改进->分析数据中找到以JetsamEvent开头的日志,日志里面记录了很多信息:手机设备信息、系统版本、内存大小、CPU时间等。
Jetsam
Jetsam是iOS系统的一种资源管理机制。不同于MacOS、Linux、Windows等,iOS中没有内存交换空间,所以在设备整体内存紧张时,系统会将一些优先级不高或者占用内存过大的直接Kill掉。
通过iOS开源的XNU内核源码可以分析到:
- 每个进程在内核中都存在一个优先级列表,JetSam在受到内存压力时会从优先级列表最低的进程开始尝试杀死,直到内存水位恢复到正常水位。
- Jetsam是通过get_task_phys_footprint获取到phys_footprint的值,来决定要不要杀掉应用。
Jetsam机制清理策略可以总结为以下几点:
- 单个APP物理内存占用超过上限会被清理,不同的设备内存水位线不一样。
- 整个设备物理内存占用受到压力时,优先清理后台应用,再清理前台应用。
- 优先清理内存占用高的应用,再内存占用低的应用。
- 相比系统应用,会优先清理用户应用。
Android端为Low Memory Killer:
- 根据APP的优先级和使用总内存的多少,系统会在设备内存吃紧情况下强杀应用。
- 内存吃紧的判断取决于系统RSS(实际使用物理内存,包含共享库占用的全部内存)的大小。
- 关键参数有3个:
1)oom_adj:在Framework层使用,代表进程的优先级,数值越高,优先级越低,越容易被杀死。
2)oom_adj threshold:在Framework层使用,代表oom_adj的内存阈值。Android Kernel会定时检测当前剩余内存是否低于这个阀值,若低于则杀死oom_adj ≥该阈值对应的oom_adj中,数值最大的进程,直到剩余内存恢复至高于该阀值的状态。
3)oom_score_adj:在Kernel层使用,由oom_adj换算而来,是杀死进程时实际使用的参数。
数据分析
phys_footprint获取iOS应用总的物理内存,具体可以参考官方说明iOS Memory Deep Dive.
std::optional memoryFootprint()
{
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 std::nullopt;
return static_cast(vmInfo.phys_footprint);
}
Instruments-VM Tracker可以用来分析具体内存分类,比如Malloc部分是堆内存,Webkit Malloc部分是JavaScriptCore占用的内存等。需要注意的是每个分类的内存值 = Dirty Size + Swapped。
通过Instruments VM Tracker抓取导航中内存分布进行对比分析。导航前台静置时,高德地图的总内存数值非常高,其中IOKit、WebKit Malloc和Malloc堆内存为内存占用大头。
在分析过程中可以使用的工具很多,各有优缺点,需要配合使用,相互弥补。我们在分析的过程中主要用到Intruments VM Tracker、Allocations、Capture GPU Frame、MemGraph、dumpsys meminfo 、Graphics API Debugger、Arm Mobile Studio、AJX 内存分析工具、自研Malloc分析工具等。
- IOKit内存为地图渲染显存部分。
- WebKit Malloc内存为AJX JS业务内存。
- Malloc堆内存,我们通过Hook Malloc分配内存的API,通过抓取堆栈分析具体内存消费者。
治理优化
根据上面的数据分析,很容易做出从大头开始抓起的思路。我们在治理过程中的大体思路:
- 分析数据:从内存大头开始,分析各内存归属业务,以便业务进一步分析优化。
- 内存治理:优化技术方案减少内存开销、高低端机功能分级和智能容灾(即内存告警时通过功能降级等策略释放内存)。
分而治之
据数据分析,高德地图三大内存消耗分别是地图渲染(Graphic显存)、功能业务(JavaScriptCore)和通用业务(Malloc)。我们也主要从这三个方面入手优化。
地图Graphic显存优化
Xcode自带Debug工具Capture GPU Frame,可以分析出具体显存占用,显存主要分为纹理Texture部分和Buffer部分,通过详细的地址信息分析具体消耗。Android端类似分析显存工具可以用Google的Graphics API Debugger。
根据分析,Texture部分我们通过FBO绘制方式调整、矢量路口大图背景优化、图标跨页面释放、文字纹理优化、低端机关闭全屏抗锯齿等减少显存消耗。Buffer部分通过开启低显存模式、关闭四叉树预加载、切后台释放缓存资源等。
Webkit Malloc优化
高德地图使用的是自研的动态化方案,依赖于iOS系统提供的框架JavaScriptCore,使用的业务内存消耗大多会被系统归类到WebKit Malloc,从系统工具Instruments上的VM Tracker可以看出。此处有两个思路,一个是业务自身优化内存消耗,第二个是动态化引擎和框架优化内存消耗。
业务自身优化,动态化方案的IDE提供内存分析工具可以清晰的输出具体业务内存消耗在什么地方,便于业务同学分析是否合理。
动态化引擎和框架优化,我们通过优化对系统库JavaScriptCore的使用方式,即多个JSContextRef上下文共享同一份JSContextGroupRef的方式。多个页面可以共享一份框架代码,从而减少内存开销。
Malloc堆内存优化
iOS端堆内存分配基本上使用的libmalloc库,其中包含以下几个内存操作接口:
// c分配方法
void *malloc(size_t __size) __result_use_check __alloc_size(1);
void *calloc(size_t __count, size_t __size) __result_use_check __alloc_size(1,2);
void free(void *);
void *realloc(void *__ptr, size_t __size) __result_use_check __alloc_size(2);
void *valloc(size_t) __alloc_size(1);
// block分配方法
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
通过hook内存操作API记录下内存分配的堆栈、大小,即可分析内存使用情况。
同时源码中还存在一个全局钩子函数malloc_logger ,可输出Malloc过程中的日志,定义如下:
// We set malloc_logger to NULL to disable logging, if we encounter errors
// during file writing
typedef void(malloc_logger_t)(uint32_t type,
uintptr_t arg1,
uintptr_t arg2,
uintptr_t arg3,
uintptr_t result,
uint32_t num_hot_frames_to_skip);
extern malloc_logger_t *malloc_logger;
iOS堆内存分析方案,可通过hook malloc系列API,也可以设置malloc_logger的函数实现,即可记录下堆内存使用情况。
此方案有几个难点问题,每秒钟内存分配的量级大、内存有分配有释放需要高效查询和堆栈反解聚合。为此我们设计了一套完整的Malloc堆内存分析方案,来满足快速定位堆内存归属,以便分发到各自业务Owner分析优化。
统一管理
随着业务的增长给高德地图这个超级APP带来了极大资源压力,因此我们沉淀了一套自适应资源管理框架,来满足不同业务场景在有限资源下能够做到功能和体验极致均衡。主要的设计思路是通过监测用户设备等级、系统状态、当前业务场景以及用户行为,利用调度算法进行实时推算,统一管理协调APP当前资源状态分配,对用户当前不可见的内存等资源进行回收。
自适应资源管理框架-内存部分
可以根据不同的设备等级、业务场景、用户行为和系统状态来管理资源。各业务都可以很容易的接入此框架,目前已经应用到多个业务场景,均有不错的收益。
数据验收
通过三个版本的连续治理,前后台导航场景均有50%的收益,同时Abort率也有10%~20%的收益。整体收益算是比较乐观,但是随之而来的挑战是我们该如何守住成果。
长线管控
所谓打江山容易守江山难,如果没有长线管控的方案,随着业务的版本迭代,不出三五个版本就会将先前的优化消耗。为此我们构建了一套APM性能监控平台,在研发测试阶段发现并解决问题,不把问题带上线。
APM性能监控平台
为了将APP的性能做到日常监控,我们建设了一套线下「APM性能监控平台」,平台能够支持常规业务场景的性能监控,包括:内存、CPU、流量等,能够及时的发现问题并进行报警。再配合性能跟进流程,为客户端性能保障把好最后一关。
内存分析工具
Xcode memory gauge:在Xcode的Debug navigator中,可以粗略查看内存占用的情况。
Instruments - Allocations:可以查看虚拟内存占用、堆信息、对象信息、调用栈信息、VM Regions信息等。可以利用这个工具分析内存,并针对地进行优化。
Instruments - Leaks:用于检测内存泄漏。
Instruments - VM Tracker:可以查看内存占用信息,查看各类型内存的占用情况,比如dirty memory的大小等等,可以辅助分析内存过大、内存泄漏等原因。
Instruments - Virtual Memory Trace:有内存分页的具体信息,具体可以参考WWDC 2016 - Syetem Trace in Depth。
Memory Resource Exceptions:从Xcode 10开始,内存占用过大时,调试器能捕获到EXC_RESOURCE RESOURCE_TYPE_MEMORY异常,并断点在触发异常抛出的地方。
Xcode Memory Debugger:Xcode中可以直接查看所有对象间的相互依赖关系,可以非常方便的查找循环引用的问题。同时,还可以将这些信息导出为memgraph文件。
memgraph + 命令行指令:结合上一步输出的memgraph文件,可以通过一些指令来分析内存情况。vmmap可以打印出进程信息,以及VMRegions的信息等,结合grep可以查看指定VMRegion的信息。leaks可追踪堆中的对象,从而查看内存泄漏、堆栈信息等。heap会打印出堆中所有信息,方便追踪内存占用较大的对象。malloc_history可以查看heap指令得到的对象的堆栈信息,从而方便地发现问题。
总结:malloc_history ===> Creation;leaks ===> Reference;heap & vmmap ===> Size。
MetricKit:iOS 13新推出的监控框架,用于收集和处理电池和性能指标。当用户使用APP的时候,iOS会记录各项指标,然后发送到苹果服务端上,并自动生成相关的可视化报告。通过Window -> Organizer -> Metrics可查,包括电池、启动时间、卡顿情况、内存情况、磁盘读写五部分。也可以MetricKit集成到工程里,将数据上传到自己的服务进行分析。
MLeaksFinder:通过判断UIViewController被销毁后其子view是否也都被销毁,可以在不入侵代码的情况下检测内存泄漏。
Graphics API Debugger:Google开源的一系列的Graphics调试工具,可以检查、微调、重播应用对图形驱动的API调用。
Arm Mobile Studio: 专业级GPU分析工具。