[转载] 使用火山引擎 APMPlus 优化 iOS 内存性能的全套指南

原文地址

前言

本文面向 iOS 研发,不会涉及复杂的底层原理,而是直接告诉 iOS 研发答案,即怎么做,只需要花半小时阅读本文,就可以在开发需求的时候,知道如何更好利用内存来提升用户体验,同时避免稳定性相关问题给业务带来负向的用户体验;同时本文作者的初心是希望这篇文章能成为研发同学的一个”字典”,可以在一些特定场景或者感觉可能会踩内存坑的时候翻阅,快速找到最佳的编码规范。

为什么需要合理使用内存资源

在编程中有一个经常使用到优化技巧:空间换时间。对于 iOS 研发人员来说,平时的开发工作中同样也会遇到空间换时间,并且一般还是拿内存空间换时间(少部分还涉及到磁盘空间,本文只讨论内存),例如提前下载、解码一张图片,在需要时候直接展示在屏幕上,避免临时解码不能给用户带来极致的用户体验。

内存资源对于我们设计良好的策略以提升用户体验很有帮助,我们应当尽量充分利用手机内存资源,为用户带来最优的用户体验。同时我们也需要注意到,不合理的,无节制的使用内存资源,是不能给用户带来最佳体验的,因为设备资源有限,使用了太多内存,首先会带来的是性能问题,如果超过了限制,系统就会
Kill 我们的应用
。下表是各种内存大小的手机,最多可用的内存大小:

descript

部分研发人员可能在这里会有误解,认为自己的业务逻辑使用更多内存,肯定对自己业务体验是没有负向的。但是可用内存变少后,系统为了缓解内存压力,系统层面进行内存压缩、数据重加载等,应用整体性能下降,会出现卡顿甚至卡死等体验问题;同时增加了设备能耗,造成设备发热;极端情况系统则会
kill 应用。这是一个多输局面,所以我们需要知道如何合理的利用内存,保证最佳的用户体验。

了解 OOM 崩溃

不合理使用内存,带来最严重后果就是 OOM 崩溃,下面简单介绍 OOM
最基本两个概念:

什么是 OOM 崩溃

OOM 其实是 Out Of Memory 的简称,指的是在 iOS 设备上当前应用因为内存占用过高而被操作系统强制终止,在用户侧的感知就是 App 一瞬间的闪退,与普通的 Crash 没有明显差异。

OOM 的监控原理

由于我们不能直接监控到 OOM 崩溃,所以业界目前监控方式都是排除法,简单来说,我们排除所有已知的原因,那剩下的未知异常退出,就认为是 OOM 崩溃。

Jetsam 日志

当我们在调试阶段遇到这种崩溃的时候,从设备设置->隐私->分析与改进中是找不到普通类型的崩溃日志,只能够找到 Jetsam 开头的日志,这种形式的日志其实就是 OOM 崩溃之后系统生成的一种专门反映内存异常问题的日志。

descript

上图是截取一份 Jetsam 日志中最关键的一部分。关键信息解读:

  • pageSize:指的是当前设备物理内存页的大小:16KB。
  • states:当前应用的运行状态,对于 Inhouse 这个应用而言是正在前台运行的状态,这类崩溃我们称之为 FOOM(ForegroundOut Of Memory);与此相对应的也有应用程序在后台发生的 OOM 崩溃,这类崩溃我们称之为 BOOM(Background Out Of Memory)。
  • rpages:是 resident pages 的缩写,表明进程当前占用的内存页数量,该进程占用内存=内存页数量*16KB。

通过 APMPlus 内存监控发现异常问题解决方案

1.Block 里面混用 weakSelf 和 self

为了避免混用 weakSelf 和 self,推荐使用 weakify 和 strongify。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

__weak typeof(self) weakSelf = self;
[self doSomethingWithCompletion:^(){
[weakSelf foo];
[self bar];//一般是添加的新代码,不知道前面有weakSelf
}];


@weakify(self);
[self doSomethingWithCompletion:^(){
@strongify(self);
[self foo];
[self bar];
}];

2.嵌套的 Block,每个 Block 都需要注意循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

@weakify(self);
[self doSomethingWithCompletion:^(){
@strongify(self);
[self fooWithCompletion:^(){
//嵌套block,如果存在循环引用,也需要用weak来解耦
[self foo];//self refers to the original self pointer
}];
[self bar];
}];


@weakify(self);
[self doSomethingWithCompletion:^(){
@strongify(self);
[self fooWithCompletion:^(){
@strongify(self);
[self foo];
}];
[self bar];
}];

3.Block 里使用了 Super

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

[self doSomethingWithCompletion:^(){
[super foo];//super 是个编译器指令,也会引用到self,所以需要分析是否存在循环引用环;
//并且这里没法用Weakself来解耦
}];


@weakify(self);
[self doSomethingWithCompletion:^(){
@strongify(self);
[self xxMethod];
}];

用方法包一层,间接调用super
- (void)xxMethod {
[super foo];
}

4.Block 里使用的外面对象都需要分析是否存在循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14


//不能认为Block里没有引用self,就不需要分析是否有引用环
[v bk_whenTapped:^{
v.backgroundColor = [UIColor redColor];
}];
//v -> tap_block -> v 导致循环引用


@weakify(v);
[v bk_whenTapped:^{
@strongify(v);
v.backgroundColor = [UIColor redColor];
}];

5.Block 里面使用了宏,而宏定义里隐式引用了 self

1
2
3
4
5
6
7
8
9
10
11
12
13
14


RACSignal *signal3 = [anotherSignal flattenMap:^(NSArrayController *arrayController) {
// Avoids a retain cycle because of RACObserve implicitly referencing self.
return RACObserve(arrayController, items);
}];


@weakify(self);
RACSignal *signal3 = [anotherSignal flattenMap:^(NSArrayController *arrayController) {
// Avoids a retain cycle because of RACObserve implicitly referencing self.
@strongify(self);
return RACObserve(arrayController, items);
}];

6.避免循环导致的 AutoreleasePool 内存堆积

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28


for (...) { //如果循环长度不确定,就需要用autoreleasepool包起来
@autoreleasepool {
...
// -[NSString stringWithFormat:] is a typical method that returns an autorelase object.
NSString *foo = [NSString stringWithFormat:@"bar %d", i];
...
}
}

或使用enumerateObjectsUsingBlock: / enumerateKeysAndObjectsUsingBlock: 代替for循环遍历

//array is an NSArray
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
...
// -[NSString stringWithFormat:] is a typical method that returns an autorelase object.
NSString *foo = [NSString stringWithFormat:@"bar %d", i];
...
}];

//dictionary is an NSDictionary
[dictionary enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
...
// -[NSString stringWithFormat:] is a typical method that returns an autorelase object.
NSString *foo = [NSString stringWithFormat:@"bar %d", i];
...
}];

7.避免串行队列导致的 AutoreleasePool 内存堆积

对于串行队列,如果应用性能遇到问题,或异步到队列任务太多,都会导致串行队列 AutoreleasePool 里的对象不能及时释放,强烈建议使用 xxx_dispatch_async_autorelease 来代替 dispatch_async。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

dispatch_async(queue, ^{
//stuff you want to do
...
});

✅ Highly recommended
在xxxMacros.h封装一层
NS_INLINE void xxx_dispatch_async_autorelease(dispatch_queue_t _Nonnull queue, dispatch_block_t _Nonnull block)
{
dispatch_async(queue, ^{
@autoreleasepool {
block();
}
});
}

#import <xxxMacros.h>
使用xxx_dispatch_async_autorelease替换dispatch_async
xxx_dispatch_async_autorelease(queue, ^{
//stuff you want to do
...
});

8.注意 KVOController 造成的循环引用

KVOController 会持有 observee,所以当 observe self 的时候,就需要判断是否会造成循环引用。原因参考:https://www.jianshu.com/p/22c5024cc3c0。

1
2
3
4
5
6
7
8
9
10
11
12
13


[self.KVOController observe:self keyPath:@"foo" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *, id> *_Nonnull change) {
...
}];
//self(observer) -> self.KVOController -> self(observee) ,造成循环引用


- (void)setFoo:(FooClass *)foo
{
_foo = foo;
//do your own stuff
}

9.Runtime 相关函数导致的内存泄漏

例如 class_copyPropertyList 函数:

descript

这部分函数非常多,Xcode
在每个函数注释里都标注了需要释放,下面只列出函数,具体可看函数注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

// 建议:调用runtime相关函数,一定要看下函数注释!
class_copyPropertyList
method_copyReturnType
class_copyMethodList
property_copyAttributeList
objc_copyClassList
class_copyIvarList
class_copyProtocolList
method_copyArgumentType
property_copyAttributeList
objc_copyProtocolList
protocol_copyMethodDescriptionList
protocol_copyPropertyList2
protocol_copyProtocolList
objc_copyImageNames

10.RACSubject 内存泄漏

使用 RACSubject,如果进行了 map 操作,那么一定要发送完成信号,不然会发生内存泄漏。

1
2
3
4
5
6
7
8
9

RACSubject *subject = [RACSubject subject];
[[subject map:^id(NSNumber *value) {
return @([value integerValue] * 3);
}] subscribeNext:^(id x) {
NSLog(@"next = %@", x);
}];
[subject sendNext:@1];
[subject sendCompleted]; //✅一定要发送完成信号,不然会内存泄漏(或调用了sendError:函数)

11.dispatch_after 延迟执行导致的内存泄漏

虽然 dispatch_after 不是循环引用,但是也会造成 self 在 1000s 后才释放,一般情况下不会使用 dispatch_after delay 1000s,但是在复杂的业务场景中可能存在复杂的 dispatch_after 嵌套等情况。解决办法是使用 weakify(self), 如果 self 已经释放就直接进行 return。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

NSTimeInterval longTime = 1000.f;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(longTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self doSomething];
});


NSTimeInterval longTime = 1000.f;
@weakify(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(longTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self);
if (!self) {
return;
}
[self doSomething];
});

12.其他延迟执行导致的内存泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14

- (void)viewDidLoad {
NSTimeInterval longTime = 1000.f;
[super viewDidLoad];
[self performSelector:@selector(xxMethod) withObject:nil afterDelay:longTime];
}
当执行[self performSelector:@selector(xxMethod) withObject:nil afterDelay:longTime];代码的时候会对self进行一个捕获,当前self的引用计数进行+1直到延迟方法执行后才会进行-1操作。


方案一:提前主动取消调用
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(xxMethod) object:nil];


方案二:使用定时器,并且不持有self方式来调用

13.dispatch_group_enter 和 dispatch_group_leave 不匹配导致的内存泄漏

1
2
3
4
5
6
7
8
9
10
11
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[self fetchXXMethod:^(UIImage *image) {
...
//如果这里的dispatch_group_leave得不到调用,就会出现内存泄漏
//dispatch_group_leave(group);
}];

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
xxx
});

14.不要忘记释放 Core Foundation 对象

1
2
3
4
5
6
7
8
9
10
11

如果Core Foundation是从函数名里有“create”或“copy”的函数创建得到的,你有责任释放这个对象

CFStringRef str = CFStringCreateWithCString(NULL, "Hello World", kCFStringEncodingASCII);
...
CFRelease(str); //不再需要后,需要手动释放


CGImageRef imageRef = CGImageCreateWithImageInRect(self.CGImage, rect);
UIImage *image = [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation];
CGImageRelease(imageRef); //不再需要后,需要手动释放

15.使用 NSCache 去代替 NSDictionary / NSMutableDictionary 缓存对象**

NSCache 是苹果官方提供的缓存类,使用该类有如下优点:

  1. NSCache 是一个类似 NSDictionary 一个可变的集合。
  2. 提供了可设置缓存的数目与内存大小限制的方式。
  3. 保证了处理的数据的线程安全性。
  4. 缓存使用的 key 不需要是实现 NSCopying 的类。
  5. 当内存警告时内部自动清理部分缓存数据。

具体使用可参考developer.apple.com/nscache

16.单例对象不要持有大内存

单例对象不会被释放,如果持有了例如图片等大内存会导致应用内存水位在整个生命周期都会升高。

17.Model 对象强持有 image

如果 model 强持有 image,会导致应用在内存紧张的时候不能及时释放内存;model 只需要强持有 url,model 对应的 view,可以强持有 image,在 view 展示时候,持有的 image 不会释放,当 view 被释放了,image 会被 XXWebimage(图片库一般都有缓存管理) 管理,在内存充足时候,这些 image 都会被缓存起来,只有应用快 OOM 时候,缓存才可能被释放。这种策略既保证了用户体验,也避免了 OOM 崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

@interface xxDataModel : NSObject
@property (nonatomic, strong, nullable) UIImage *image;
...
@end

// Highly Recommended
@interface xxDataModel : NSObject
@property (nonatomic, strong, readonly, nullable) NSURL *imageURL;
...
@end

// OK (if applicable)
@interface xxDataModel : NSObject
@property (nonatomic, weak, readonly, nullable) UIImage *wkimage;
...
@end

18.Swift 的 func allocate函数,申请的内存需要释放

分配内存时需要记住在分配完成后释放内存。

descript

19.getifaddrs 和 freeifaddrs 函数需要配套使用

getifaddrs() 返回的数据是动态分配的,当不再需要时应使用 freeifaddrs()
进行释放。

1
2
3
4
struct ifaddrs *addrs;
int retval = getifaddrs(&addrs);
// do something
freeifaddrs(addrs); //don't forget to free memory

20.异常导致内存泄漏

其实异常不止导致内存泄漏,还会导致各种资源泄漏,甚至导致死锁发生,由于通常情况下异常本身发生的概率很低,所以除非在该路径有大内存泄漏需要特别注意,一般情况下不需要特别关注。反而是加解锁等操作需要注意,因为一般在加解锁操作中发生异常很容易造成死锁发生。

解决方案:
RAII(Resource Acquisition Is Initialization)是 C++ 之父 Bjarne
Stroustrup 在设计 C++ 异常时,为解决资源管理的异常安全性提出的一种技术:使用局部对象来管理资源。这里的资源指:内存、锁、网络套接字、fd、数据库句柄等,简而言之是任何需要释放的计算机资源。

RAII 要求资源的有效期与持有资源的对象生命周期严格绑定:即对象的构造函数完成资源的分配(获取),同时对象的析构函数完成资源的释放。那么这样后,就只需要正确管理对象的生命周期,就不会出现资源管理问题(特别是异常安全性可以得到保证)。

21.方法命名违反 ARC 约定

参考

1
2
3
4
5
6
7
8
9
10
11
12
ethods in the alloc, copy, init, mutableCopy, 
and new families are implicitly marked __attribute__((ns_returns_retained)).
then the caller expects to take ownership of a +1 retain count.

- (EMProduct *)newProduct {
...
}

NSObject *obj = [NSObject performSelector:@selector(newXXMethod)];


如果不是预期引用计数+1,函数名中不要包含alloc, copy, init, mutableCopy, new 这些字符串。

当然,除了上述 21 条规则外,还有很多内部 SDK 使用的编码规范,例如图片、网络等 SDK,都是容易导致内存问题的 SDK,这些规则就不在这里列举了。

APMPlus iOS 内存监控相关功能

OOM 崩溃

通过在崩溃趋势中筛选 OOM 崩溃,或直接在内存优化模块下打开 OOM 趋势,可查询 OOM 崩溃相关的指标以及具体的 Issue。

descript

Memory Graph

接入指南:https://www.volcengine.com/docs/6431/1175759#%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96

 在 OOM 崩溃中过滤有无 Memory Graph 文件 ,如果有的话点击进入 Issue 详情后可以跳转至单设备内存详情进行分析。

descript

descript

或者直接点击菜单单设备内存详情,查看上报的所有 MemoryGraph 文件,点击查看详情进入详情页分析内存。

descript

Memory Graph 分析方法参考:https://www.volcengine.com/docs/6431/68858#%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5

关于 APMPlus

APMPlus

是火山引擎下的应用性能监控产品,通过先进的数据采集与监控技术,为企业提供全链路的应用性能监控服务,助力企业提升异常问题排查与解决的效率。基于海量数据的聚合分析,平台可帮助客户发现多类异常问题,并及时报警做分配处理。目前,APMPlus
已服务了抖音、今日头条等多个大规模移动
App,以及教育、健身、图书、物流等多个行业的外部客户。

descript

APMPlus APP 端监控中的 iOS 方案提供了崩溃、卡死、OOM 崩溃、Extension
崩溃等不同的异常类别监控
,以及启动、页面、卡顿等流畅性监控,还包括内存、CPU、磁盘、MetricKit
等资源消耗问题的监控。此外,APMPlus 提供的网络耗时和异常监控,拥有强大的单点分析和日志回捞能力。在数据采集方面,提供灵活的采样和开关配置,以满足客户对数据量和成本控制的需求,只按事件量收费,不限制用户数。针对跨平台方案,其提供了 WebView 页面的监控。丰富的能力满足客户对 App 全面性能监控的诉求。

iOS 方案亮点

  • MemoryGraph提供应用内存全景,准确定位内存问题,引用关系泄漏对象、大对象一目了然。
  • 强大的工具箱助力解决线上疑难崩溃问题野指针归因让 OC 野指针无处遁形、GWPAsan 通过记录内存分配和释放堆栈协助高效分析内存踩踏、Coredump 还原崩溃现场数据为崩溃分析提供全面的上下文相关信息。
  • 高性能日志库,做到数据稳定性强、性能好,保障了现场业务信息的高度还原。
  • 结合系统的 MetricKit数据,磁盘、CPU、流量等数据全面收集,真正做到监控无死角。

descript

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道