本文面向 iOS 研发,不会涉及复杂的底层原理,而是直接告诉 iOS 研发答案,即怎么做,只需要花半小时阅读本文,就可以在开发需求的时候,知道如何更好利用内存来提升用户体验,同时避免稳定性相关问题给业务带来负向的用户体验;同时本文作者的初心是希望这篇文章能成为研发同学的一个“字典”,在一些特定场景或者感觉可能会踩内存坑的时候可以翻阅,快速找到最佳的编码规范。
为什么需要合理使用内存资源
在编程中有一个经常使用到优化技巧:空间换时间。对于 iOS 研发人员来说,平时的开发工作中同样也会遇到空间换时间,并且一般还是拿内存空间换时间(少部分还涉及到磁盘空间,本文只讨论内存),例如提前下载、解码一张图片,在需要时候直接展示在屏幕上,避免临时解码不能给用户带来极致的用户体验。
内存资源对于我们设计良好的策略以提升用户体验很有帮助,我们应当尽量充分利用手机内存资源,为用户带来最优的用户体验。同时我们也需要注意到,不合理的,无节制的使用内存资源, 是不能给用户带来最佳体验的 ,因为设备资源有限,使用了太多内存,首先会带来的是性能问题,如果超过了限制,系统就会 kill 我们的应用。 下表是各种内存大小的手机,最多可用的内存大小:
设备内存大小 | 内存报警阈值 | 最多可用内存 |
---|---|---|
1GB | ~ 594 MB | ~ 645MB |
2GB | ~ 1343 MB | ~ 1443MB |
3GB | ~ 1733 MB | ~ 1843MB |
4GB | ~1987 MB | ~ 2070MB |
6GB | ~ 2943 MB | ~ 3070MB |
部分研发人员可能在这里会有误解,认为自己的业务逻辑使用更多内存,肯定对自己业务体验是没有负向的。但是可用内存变少后,系统为了缓解内存压力,系统层面进行内存压缩、数据重加载等,应用整体性能下降,会出现卡顿甚至卡死等体验问题;同时增加了设备能耗,造成设备发热;极端情况系统则会 kill 应用。这是一个多输局面,所以我们需要知道如何合理的利用内存,保证最佳的用户体验。
了解 OOM 崩溃
不合理使用内存,带来最严重后果就是 OOM 崩溃,下面简单介绍 OOM 最基本两个概念:
什么是 OOM 崩溃
OOM 其实是 Out Of Memory 的简称,指的是在 iOS 设备上当前应用因为内存占用过高而被操作系统强制终止,在用户侧的感知就是 App 一瞬间的闪退,与普通的 Crash 没有明显差异。
OOM 的监控原理
由于我们不能直接监控到 OOM 崩溃,所以业界目前监控方式都是排除法,简单来说,我们排除所有已知的原因,那剩下的未知异常退出,就认为是 OOM 崩溃。
Jetsam 日志
当我们在调试阶段遇到这种崩溃的时候,从设备设置->隐私->分析与改进中是找不到普通类型的崩溃日志,只能够找到 Jetsam 开头的日志,这种形式的日志其实就是 OOM 崩溃之后系统生成的一种专门反映内存异常问题的日志。
上图是截取一份 Jetsam 日志中最关键的一部分。关键信息解读:
- pageSize:指的是当前设备物理内存页的大小:16KB。
- states:当前应用的运行状态,对于 Inhouse 这个应用而言是正在前台运行的状态,这类崩溃我们称之为 FOOM(Foreground Out Of Memory);与此相对应的也有应用程序在后台发生的 OOM 崩溃,这类崩溃我们称之为 BOOM(Background Out Of Memory)。
- rpages:是 resident pages 的缩写,表明进程当前占用的内存页数量,该进程占用内存=内存页数量*16KB。
当通过 APMPlus 内存监控发现各种异常问题该如何解决
1.lock 里面混用 weakSelf 和 self
为了避免混用 weakSelf 和 self,推荐使用 weakify 和 strongify。
❌
__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 都需要注意循环引用
❌
@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
❌
[self doSomethingWithCompletion:^(){
[super foo];//super 是个编译器指令,也会引用到self,所以需要分析是否存在循环引用环;
//并且这里没法用Weakself来解耦
}];
✅
@weakify(self);
[self doSomethingWithCompletion:^(){
@strongify(self);
[self xxMethod];
}];
用方法包一层,间接调用super
- (void)xxMethod {
[super foo];
}
4.Block 里面使用到的任何外面对象,都需要分析是否存在循环引用
❌
//不能认为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
例如 RACObserve 隐式引用了 self ,详情见:NSObject+RACPropertySubscribing.h 文件
❌
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 内存堆积
✅
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。
❌
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。
❌
[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 函数:
这部分函数非常多,Xcode 在每个函数注释里都标注了需要释放,下面只列出函数,具体可看函数注释。
// 建议:调用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 操作,那么一定要发送完成信号,不然会内存泄漏。
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。
❌
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.其它延迟执行导致的内存泄漏
❌
- (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 不匹配导致的内存泄漏
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 对象
如果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 是苹果官方提供的缓存类,使用该类有如下优点:
- NSCache 是一个类似 NSDictionary 一个可变的集合。
- 提供了可设置缓存的数目与内存大小限制的方式。
- 保证了处理的数据的线程安全性。
- 缓存使用的 key 不需要是实现 NSCopying 的类。
- 当内存警告时内部自动清理部分缓存数据。
具体使用可参考:https://developer.apple.com/documentation/foundation/nscache
16.单例对象不要持有大内存
单例对象不会被释放,如果持有了例如图片等大内存,会导致应用内存水位在整个生命周期都会升高。
17.Model 对象强持有 image
如果 model 强持有 image,会导致应用在内存紧张的时候不能及时释放内存;model 只需要强持有 url,model 对应的 view,可以强持有 image,在 view 展示时候,持有的 image 不会释放,当 view 被释放了,image 会被 XXWebimage(图片库一般都有缓存管理) 管理,在内存充足时候,这些 image 都会被缓存起来,只有应用快 OOM 时候,缓存才可能被释放。这种策略既保证了用户体验,也避免了 OOM 崩溃。
❌
@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函数,申请的内存需要释放
分配内存时需要记住在分配完成后释放内存。
19.getifaddrs 和 freeifaddrs 函数需要配套使用
getifaddrs() 返回的数据是动态分配的,当不再需要时应使用 freeifaddrs() 进行释放。
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 约定
Methods 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。
Memory Graph
1.接入指南:接入APMPlus iOS Memory Graph
2.在 OOM 崩溃中过滤有无 Memory Graph 文件,如果有的话点击进入 Issue 详情后,可以跳转至单设备内存详情进行分析。
或者直接点击菜单单设备内存详情,查看上报的所有的 MemoryGraph 文件,点击查看详情进入详情页分析内存。
3.Memory Graph 分析方法参考:Memory Graph最佳实践
关于 APMPlus
APMPlus 是火山引擎下的应用性能监控产品,通过先进的数据采集与监控技术,为企业提供全链路的应用性能监控服务,助力企业提升异常问题排查与解决的效率。基于海量数据的聚合分析,平台可帮助客户发现多类异常问题,并及时报警,做分配处理。目前,APMPlus 已服务了抖音、今日头条等多个大规模移动 App,以及教育、健身、图书、物流等多个行业的外部客户。
APMPlus APP 端监控中的 iOS 方案提供了崩溃、卡死、OOM 崩溃、Extension 崩溃等不同的异常类别监控,以及启动、页面、卡顿等流畅性监控,还包括内存、CPU、磁盘、MetricKit 等资源消耗问题的监控。此外,APMPlus 提供的网络耗时和异常监控,拥有强大的单点分析和日志回捞能力。在数据采集方面,提供灵活的采样和开关配置,以满足客户对数据量和成本控制的需求,只按事件量收费,不限制用户数。针对跨平台方案,其提供了 WebView 页面的监控。丰富的能力满足客户对 App 全面性能监控的诉求。
iOS 方案亮点
- MemoryGraph 提供 应用内存全景,准确定位内存问题,引用关系、泄漏 对象 、 大对象 一目了然 。
- 强大的工具箱助力解决线上疑难崩溃问题:野指针归因让 OC 野指针无处遁形、GWPAsan 通过记录内存分配和释放堆栈协助高效分析内存踩踏、Coredump 还原崩溃现场数据为崩溃分析提供全面的上下文相关信息。
- 高性能日志库,做到数据稳定性强、性能好,保障了现场业务信息的高度还原。
- 结合系统的 MetricKit 数据,磁盘、CPU、流量等数据全面收集,真正做到监控无死角。