代码质量以及内存泄露排查总结
2017-07-28 16:13
531 查看
想体验一把 CentOS 系统玩一下命令行?试试腾讯云上实验室吧 https://cloud.tencent.com/developer/labs
原文链接:http://www.jianshu.com/p/4e447f1d8ffa
在几周的稳定性工作中, 我对现有内涵iOS代码进行了一次初步的review过程,主要是针对一些非必现性crash的审查。 —— 由Moon同学分享
众所周知iOS Crash类型分为Objective-C Exception 和 Signal。其中Objective-C 的 Exception 是比较好处理的,在 Crash 的时候会有详细的描述信息,而错误case也相对集中一些,比如未加保护而任意的使用MutableArray && MutableDictionary 导致添加一个nil对象引起Crash,比如下面这样的代码
初步review了下,发现addObject以及setObject:forKey:两个方法,几乎完全没有安全保护机制,这样的代码是非常不严谨的同时也是容易crash的。这里目前我们设置了安全容器类,使用姿势:
而对于Signal类的错误,通常是由于内存访问出错引起,例如常见的 [EXC_BAD_ACCESS // SIGSEGV // SIGBUS]都是这些错误:
这是平时开发中最常碰到的问题,通常指访问了无效或者已释放的内存,一般情况下可以通过开启 Zombie Objects 和 Address Sanitizer 来在调试时获取更多的 Crash 信息。
但是随着业务的不断开发,又由于缺乏有效的UnitTest,一些新的case不可能全部被覆盖,同时由于一些历史原因,部分旧代码中不规范又模糊的写法,又继续被后来接手的开发人员所延续,最终导致了整个代码不可维护。部分代码由于主观的经验主义的错误,导致了一些潜在的crash,比较深刻的有下面几种:
针对这种三种写法,没有明确的对与错的界限,也就是说只要理解了每种写法的case,怎么使用都可以。但是,我认为既然选定了一种方式,就尽量统一来写,一般场景下不要三种混用,一是混用会导致代码脏乱不堪,二是会带来一些潜在的bug。同时,我个人认为在业务场景中尽可能的使用self.property方式能让代码更佳具有维护性。主要原因有:
self.property 的形式,实质是调用了property的getter与setter方法,虽然在ARC场景下,几乎99%的场景不需要我们关心内存问题,但是为了那1%的场景我们还是得需要了解下ARC的处理机制的。比如下面这种场景
大概场景是这样的:
其实分析一些这个crash场景,就是向一个野指针中写入了数据「访问野指针不会crash」,那么是哪个成野指针了呢,很明显,MemoryTest变成了野指针,那么为什么呢?明明MemoryTest在ViewController中是weak属性,这里就要怪ARC的坑了:
因为在被调用方中使用了self做为传参,同时self在被调方法中被置空,相当于调用了一次release,而其中self会被clang解析成unsafe_unretained类型,那么下面再继续使用self的话,由于unsafe_unretained的不会自动给释放对象置nil,因此野指针了。因此代码真实的样子是这样:
这样再来看下现在代码是不是非常后怕,那么解决途径有哪些?
通常的解决方法简单省事:
首先不讨论这样是否是最好的解决办法,暂时留个悬念,我们先来分析一下,为什么会出现这种颠覆我们三观的crash。先来做个对比
对比一些似乎就是取getter事例变量方法的时候有区别,继续来分析getter方法有哪些问题:
熟悉autoreleasepool的同学,都知道一次方法调用后返回值会被objc_retainAutoreleasedReturnValue再局部进行一次强引用,因此有意思的事出现了:
会使danger这个事例变量的retain+1,因此在
中即使
会使danger触发一次release,也不会使其retainCount为0,所以不会发生crash。
然而回到刚才那个问题,那个外面使用
来解决这种问题到底是不是最好的解决方案呢?
在我看来不是,因为这样只能做为一个约束,如果从SDK角度来看,SDK提供方并不能强制约束外部调用者的 代码习惯性 问题,比如 UITableView,因此更因该把这个安全性处理放到业务提供方内部,比如这样
实质就是在局部做一次retain+1操作,后续的操作其实也可以直接使用self
因此对于SDK提供方以delegate形式提供的话,需要非常注意是否会发生类似的crash。
一段简单的KVO block操作,但是使用MLeakfinder或用Instrument检测、或在dealloc中打断点检测的话,就会知道这里发生了内存泄漏。
这是因为接访问实例变量(_nameLabel), 导致weak-strong dance无效, 最终导致循环引用。
如果由于重写了getter方法,只是想用实例变量的话可以这样
如果考虑到多线程场景下,大部分同学应该都会这样写「假定propertyA长度小于设备地址总线长度」)
然后大部分开发同学可能或多或少地看过不少iOS关于原子性的操作的文章,也都知道atomic修饰符只是在存取值的时候是原子性,其他操作不是。然后回头看了一下这个Lazy load
嗯,读写的时候似乎没有问题,不需要加锁,然后上线总是有几个诡异的Bug。
究其原因,在这个例子中,多个线程同时访问时,_propertyA 可能会被赋值多次,导致后续调用过程中,内存被释放,从而引起crash。
那么简单,是不是加个锁就OK了呢。比如这样
OK,上线一段时间发现,似乎crash率略有下降,但是还是有点小异常,那么是不是锁的打开姿势不对呢。考虑一个场景,
self很可能会被外部对象访问,被用作key来生成一锁,类似上述代码中的@synchronized (objectA)。两个公共锁交替使用的场景就容易出现死锁,因此我的建议是不要传self来做为synchronized的key!
这部分主要介绍下面两点:
这里首先,根据code review过程中发现的几个典型例子来做整理「隐藏了实际代码,以case形式出现」
一个隐蔽的循环引用,里面没有出现任何的self操作,但是调用了<font color=red size=4 >super</font>。而super 是个编译器指令,当调用<font color=red size=4 >[super loadMoreData]</font>的时候,它告诉编译器到父类中去找方法,但<font color=red size=4 >super</font>和<font
color=red size=4 >self</font>其实是一个,因此<font color=red size=4 >super</font>造成了强引用,只需要改成<font color=red size=4 >self</font>就可以解决了。
同样的block中没有出现self,也没有出现super,但是依然内存泄漏了,究其原因,可能是对循环引用的理解有出入
当然了,解决办法也很简单:
检测一下,又是一个隐藏的内存泄漏,为什么呢,在于KVOController的使用姿势不对,可以看下这个 issue ,KVOController的原理也非常简单,网上有很多分析,可以参考下之前写过的这篇 KVOController分享,简单来说:
只要 observee 反过来强引用 observer 就会造成循环引用, weak-strong dance都没用, 本例中是它的一种特殊情况(observee 就是 observer), 所以要使用方多注意。那么,解决途径是什么呢?
推荐两种:
(1)打死还用KVOController
可能有同学搜到用下面方法也可以
但是这个放到设计场景中并不是一个通用的解决办法,经常容易因忘记解绑而crash
(2)使用RACObserve
由于项目中已经使用了RAC来作为支撑,因此直接简单粗暴的使用RACObserve
不在需要关心是否会观察自己,不管是MVC还是MVVM都可以。
那么再次回到刚才留的问题,为什么KVOController在MVVM架构下会比较适合呢,这是因为MVVM架构的核心是拆分View「View&& ViewController」放到ViewModel中,那么反应到代码中,基本就是对ViewModel的各种KVO了。
这样就可以把FBKVOController和self隔离开了。
很明显,由于timer使用不当, self -> self.timer -> self 循环引用, 也就是说dealloc永远调用不到.
那么这里来说这个问题主要是提供一种新的方式来解决,可能现有的解决方案无非下面两种
但这里我比较喜欢使用RAC的方式
非常直观,也不需要在dealloc中解除timer。
这个不是循环引用, 但是会造成VC在1000s后才释放, 虽然一般不会用dispatch_after delay 1000s, 但是在复杂的业务场景中, 可能存在复杂的dispatch_after嵌套等情况.解决办法: weakify(self), 然后如果self已经释放, 就直接return.
这段代码来看似乎没有用到任何的self,只是简单地做了个router跳转,但是使用MLeaksFinder的话,会非常奇怪的出现一个memory leak的AlertView「更奇怪的是Debug模式下出现,Release下不出现」,这里我们来解析一些这段代码,主要可疑点很明显是 NSAssert(x, @""),那么展开看下 NSAssert是什么呢
碰到了最喜欢的
继续观察里面有一段可疑的self
因此答案很明显了。那么有什么解决方式,当然可以使用weak-strong dance,但是在一个没有self的场景下似乎显得有点啰嗦。这里推荐使用不带self的NSCAssert,这样就可以避免在开发时期被误杀了
一般来说 weak-strong dance 可以避免大部分循环引用问题, 但是也不能盲目的使用.
简单介绍下weak-strong dance, 老司机可以跳过. 原始的写法是:
其中weak打破了循环引用, 在self释放时, weakSelf自动置空, 至于为什么又用strong的原因是为了防止block中的代码执行一半, self释放了, weakself也就是nil了. block中代码执行一半就半途而废了.
后来我们引入libextobjc中的 @weakify 和 @strongfiy 来简写. 其原理还是一样的.
需要注意的是, 在嵌套的blocks中, 只需@weakify(self)一次, 但必须在每个blocks里都@strongify(self), 可以参考这个issue
前面说weak-strong dance并不是万能的, 我们从block的使用来具体分析一下.
block的使用可以分成三种:
1 临时性的,只用在栈当中,不会存储起来
比如数组的enumerateObjectsUsingBlock方法比如masonry的mas_makeConstraints直接执行block, 不曾持有block
在这些情况下, 不需要weak-strong dance.可以看到mas_makeConstraints实现就是拿了block直接用, 没有持有
2 需要存储起来,但只会调用一次,或者有一个完成时期
比如一个 UIView 的动画, 动画完成之后, 需要使用 block 通知外面, 一旦调用 block 之后, 这个 block 就可以释放了.再比如网络库的successBlock, 它会在网络请求结束释放该block.
再比如GCD的相关方法, 排队执行完就释放该block.在这些情况下, 有时需要weak-strong dance
比如网络超时设为30s, 那个有可能网络库在30s后再会释放block里的对象. 造成资源的浪费. 这时候就需要weak-strong dance. 再比如例子5也需要weak-strong dance.
比如UIView的动画, 0.3s后动画完成, 0s延迟, 就不需要weak-strong dance.
3 需要存储起来,可能会调用多次
比如按钮的点击事件,假如采用 block 实现,这种 block 就需要长期存储,并且会调用多次。调用之后,block 也不可以删除,可能还有下一次按钮的点击。
在这些情况下, 都需要weak-strong dance. 并且有可能还不够
performSelector
解决办法: 在dealloc中调用
NSOperationQueue解决办法: 在dealloc中调用[queue cancelAllOperations]
block和performSelector等的使用一定要考虑到对象的生命周期,block等会延长对象的生命,延迟释放,由此可能会造成逻辑上时序的问题.
后, 在dealloc中调用
无效, 原因是
的observer不再是self了, 而是
的observer. 所以正确移除的办法是保留observer的引用然后移除. 在具体的使用中, weak-strong dance之后, 并不会造成VC的无法释放, 只会造成observer空跑, 影响不是很大. 但还是建议使用RACObserve来避免这个问题.
FLWeakProxy 只持有self的weak引用, 并通过OC的消息转发机制将消息转发给self处理, 这样timer就不会强引用self, dealloc里的[self.timer invalidate]就可以得到调用.
原文链接:http://www.jianshu.com/p/4e447f1d8ffa
在几周的稳定性工作中, 我对现有内涵iOS代码进行了一次初步的review过程,主要是针对一些非必现性crash的审查。 —— 由Moon同学分享
一.代码质量总结
在几周的稳定性工作中, 我对现有内涵iOS代码进行了一次初步的review过程,主要是针对一些非必现性crash的审查。众所周知iOS Crash类型分为Objective-C Exception 和 Signal。其中Objective-C 的 Exception 是比较好处理的,在 Crash 的时候会有详细的描述信息,而错误case也相对集中一些,比如未加保护而任意的使用MutableArray && MutableDictionary 导致添加一个nil对象引起Crash,比如下面这样的代码
- (void)addAccount:(AccountInfoBase*)account { [accountDict setObject:account forKey:[account keyName]]; [accountArray addObject:account]; }
初步review了下,发现addObject以及setObject:forKey:两个方法,几乎完全没有安全保护机制,这样的代码是非常不严谨的同时也是容易crash的。这里目前我们设置了安全容器类,使用姿势:
@interface NSArray<__covariant ObjectType> (NHSSecurityUtil) - (ObjectType)NH_safe_objectAtIndex:(NSUInteger)index; @end @interface NSMutableArray< ObjectType> (NHSSecurityUtil) - (void)NH_safe_addObject:(ObjectType)anObject; @end @interface NSMutableDictionary (NHSSecurityUtil) - (id)objectForKey:(id)aKey ofClass:(Class)aClass; - (NSString *)stringForKey:(id)aKey; - (void)NH_safeSetObject:(id)anobject forKey:(NSString *)akey; - (void)NH_safeRemoveObjectForKey:(NSString *)aKey; @end
而对于Signal类的错误,通常是由于内存访问出错引起,例如常见的 [EXC_BAD_ACCESS // SIGSEGV // SIGBUS]都是这些错误:
The process attempted to access invalid memory, or it attempted to access memory in a manner not allowed by the memory's protection level (e.g, writing to read-only memory).
这是平时开发中最常碰到的问题,通常指访问了无效或者已释放的内存,一般情况下可以通过开启 Zombie Objects 和 Address Sanitizer 来在调试时获取更多的 Crash 信息。
但是随着业务的不断开发,又由于缺乏有效的UnitTest,一些新的case不可能全部被覆盖,同时由于一些历史原因,部分旧代码中不规范又模糊的写法,又继续被后来接手的开发人员所延续,最终导致了整个代码不可维护。部分代码由于主观的经验主义的错误,导致了一些潜在的crash,比较深刻的有下面几种:
1)self.property VS _property
在review代码过程中,发现了大量的self.property与_property大面积混用的情况,可能由于个人习惯问题,不同的开发者主要集中在下面三种写法:[self.property method]; [_property method]; [self->_property method];
针对这种三种写法,没有明确的对与错的界限,也就是说只要理解了每种写法的case,怎么使用都可以。但是,我认为既然选定了一种方式,就尽量统一来写,一般场景下不要三种混用,一是混用会导致代码脏乱不堪,二是会带来一些潜在的bug。同时,我个人认为在业务场景中尽可能的使用self.property方式能让代码更佳具有维护性。主要原因有:
1.ARC中的坑
是在实际业务代码中我们经常会出现这样的代码[self.property method]
self.property 的形式,实质是调用了property的getter与setter方法,虽然在ARC场景下,几乎99%的场景不需要我们关心内存问题,但是为了那1%的场景我们还是得需要了解下ARC的处理机制的。比如下面这种场景
@class MemoryTest; @protocol MemoryTestDelegate <NSObject> - (void)testMemoryTest:(MemoryTest *)obj; @end @interface MemoryTest :NSObject @property(nonatomic, weak) id <MemoryTestDelegate> dangerDelegate; @property(nonatomic, copy) NSString *name; @end @implementation MemoryTest - (void)dangerStart { if ([self.dangerDelegate respondsToSelector:@selector(testMemoryTest:)]) { [self.dangerDelegate testMemoryTest:self]; } [self.name stringByAppendingString:@"crash"]; } @end @interface ViewController () <MemoryTestDelegate> @property(nonatomic, strong) MemoryTest *danger; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; self.danger = [[MemoryTest alloc] init]; self.danger.dangerDelegate = self; [_danger dangerStart];//crash } #pragma mark - MemoryTestDelegate - (void)testMemoryTest:(MemoryTest *)obj { self.danger = nil; } @end
大概场景是这样的:
1.ViewController 持有类型为 MemoryTest 的property :danger; 2.属性danger将自己的delegate设置为其持有者(ViewController); 3.在Dangerdelegate代理方法 testMemoryTest: 中,ViewController将其属性danger置为nil; 4.ViewController通过调用方法[_danger dangerStart], 5.在[self.name stringByAppendingString:@"crash"];发生 EXC_BAD_ACCESS Crash
其实分析一些这个crash场景,就是向一个野指针中写入了数据「访问野指针不会crash」,那么是哪个成野指针了呢,很明显,MemoryTest变成了野指针,那么为什么呢?明明MemoryTest在ViewController中是weak属性,这里就要怪ARC的坑了:
因为在被调用方中使用了self做为传参,同时self在被调方法中被置空,相当于调用了一次release,而其中self会被clang解析成unsafe_unretained类型,那么下面再继续使用self的话,由于unsafe_unretained的不会自动给释放对象置nil,因此野指针了。因此代码真实的样子是这样:
- (void)dangerStart { const __unsafe_unretained MemoryTest *self; if ([self.dangerDelegate respondsToSelector:@selector(testMemoryTest:)]) { [self.dangerDelegate testMemoryTest:self]; } [self.name stringByAppendingString:@"crash"]; }
这样再来看下现在代码是不是非常后怕,那么解决途径有哪些?
通常的解决方法简单省事:
[self.danger dangerStart]; //not [_danger dangerStart];
首先不讨论这样是否是最好的解决办法,暂时留个悬念,我们先来分析一下,为什么会出现这种颠覆我们三观的crash。先来做个对比
1.使用[_danger dangerStart]方式调用,直接取_danger事例变量,做dangerStart消息转发; 2.使用[self.danger dangerStart],首先调用danger的getter方法,然后默认取到了事例变量,然后再做dangerStart消息转发。
对比一些似乎就是取getter事例变量方法的时候有区别,继续来分析getter方法有哪些问题:
熟悉autoreleasepool的同学,都知道一次方法调用后返回值会被objc_retainAutoreleasedReturnValue再局部进行一次强引用,因此有意思的事出现了:
[self.danger dangerStart]
会使danger这个事例变量的retain+1,因此在
- (void)dangerStart { const __unsafe_unretained MemoryTest *self; if ([self.dangerDelegate respondsToSelector:@selector(testMemoryTest:)]) { [self.dangerDelegate testMemoryTest:self]; } [self.name stringByAppendingString:@"crash"]; }
中即使
[self.dangerDelegate testMemoryTest:self];
会使danger触发一次release,也不会使其retainCount为0,所以不会发生crash。
然而回到刚才那个问题,那个外面使用
[self.danger dangerStart]
来解决这种问题到底是不是最好的解决方案呢?
在我看来不是,因为这样只能做为一个约束,如果从SDK角度来看,SDK提供方并不能强制约束外部调用者的 代码习惯性 问题,比如 UITableView,因此更因该把这个安全性处理放到业务提供方内部,比如这样
- (void)safeStart { MemoryTest *strongSelf = self; if ([strongSelf.dangerDelegate respondsToSelector:@selector(testMemoryTest:)]) { [strongSelf.dangerDelegate testMemoryTest:strongSelf]; } [strongSelf.name stringByAppendingString:@"crash"]; }
实质就是在局部做一次retain+1操作,后续的操作其实也可以直接使用self
因此对于SDK提供方以delegate形式提供的话,需要非常注意是否会发生类似的crash。
2.Useless Case of Weak-strong Dance
先来看下面这段代码- (void)setKVO { [[[RACObserve(self, name) ignore:nil] deliverOnMainThread] subscribeNext:^(NSString * _Nullable x) { _nameLabel.text = x; }]; }
一段简单的KVO block操作,但是使用MLeakfinder或用Instrument检测、或在dealloc中打断点检测的话,就会知道这里发生了内存泄漏。
这是因为接访问实例变量(_nameLabel), 导致weak-strong dance无效, 最终导致循环引用。
如果由于重写了getter方法,只是想用实例变量的话可以这样
- (void)setKVO { @weakify(self); [[[RACObserve(self, name) ignore:nil] deliverOnMainThread] subscribeNext:^(NSString * _Nullable x) { @strongify(self); self->_nameLabel.text = x; }]; }
2)Lazy load VS Mutil thread
由于代码习惯大部分开发同学都喜欢用Lazy load的形式来重写property的getter方法,比如- (id)propertyA { if (!_propertyA) { _propertyA = [SomeClass new]; } return _propertyA; }
如果考虑到多线程场景下,大部分同学应该都会这样写「假定propertyA长度小于设备地址总线长度」)
@property (atomic, strong) id *propertyA;
然后大部分开发同学可能或多或少地看过不少iOS关于原子性的操作的文章,也都知道atomic修饰符只是在存取值的时候是原子性,其他操作不是。然后回头看了一下这个Lazy load
- (id)propertyA { if (!_propertyA) { _propertyA = [SomeClass new]; } return _propertyA; }
嗯,读写的时候似乎没有问题,不需要加锁,然后上线总是有几个诡异的Bug。
究其原因,在这个例子中,多个线程同时访问时,_propertyA 可能会被赋值多次,导致后续调用过程中,内存被释放,从而引起crash。
那么简单,是不是加个锁就OK了呢。比如这样
- (id)propertyA { @synchronized (self) { if (!_propertyA) { _propertyA = [SomeClass new]; } return _propertyA; } }
OK,上线一段时间发现,似乎crash率略有下降,但是还是有点小异常,那么是不是锁的打开姿势不对呢。考虑一个场景,
//class A @synchronized (self) { [_sharedLock lock]; NSLog(@"code in class A"); [_sharedLock unlock]; } //class B [_sharedLock lock]; @synchronized (objectA) { NSLog(@"code in class B"); } [_sharedLock unlock];
self很可能会被外部对象访问,被用作key来生成一锁,类似上述代码中的@synchronized (objectA)。两个公共锁交替使用的场景就容易出现死锁,因此我的建议是不要传self来做为synchronized的key!
二.内存泄露排查
谈到稳定性工作,不得不说内存泄露,因为目前IES这边存在大量的视音频内容,而这些又都是内存大户, 一旦出现VC泄漏这样的大问题, 对稳定性影响是非常大的, 所以在review code过程对内存问题极其关注。这部分主要介绍下面两点:
1. 几个循环引用的例子, 均是从项目中直接拿出来的实际例子. 2. 可能引起内存问题的情况大总结 不但会谈到什么情况下会有循环引用的问题, 还会谈到什么情况下不会发生循环引用的问题.不但会谈到什么时候weak-strong dance有用, 还会谈到什么时候weak-strong dance没用.
这里首先,根据code review过程中发现的几个典型例子来做整理「隐藏了实际代码,以case形式出现」
(1)Cases of Memory Leaks
1. The trick of super
[[[[RACObserve(self, testArray) skip:1] distinctUntilChanged] deliverOnMainThread] subscribeNext:^(id _Nullable x) { [super loadMoreData]; }];
一个隐蔽的循环引用,里面没有出现任何的self操作,但是调用了<font color=red size=4 >super</font>。而super 是个编译器指令,当调用<font color=red size=4 >[super loadMoreData]</font>的时候,它告诉编译器到父类中去找方法,但<font color=red size=4 >super</font>和<font
color=red size=4 >self</font>其实是一个,因此<font color=red size=4 >super</font>造成了强引用,只需要改成<font color=red size=4 >self</font>就可以解决了。
2.Not only self
[self.view.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull v, NSUInteger idx, BOOL * _Nonnull stop) { [v bk_whenTapped:^{ v.backgroundColor = [UIColor redColor]; }]; }];
同样的block中没有出现self,也没有出现super,但是依然内存泄漏了,究其原因,可能是对循环引用的理解有出入
v, v -> tap_block -> v 导致循环引用
当然了,解决办法也很简单:
[self.view.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull v, NSUInteger idx, BOOL * _Nonnull stop) { @weakify(v); [v bk_whenTapped:^{ @strongify(view); v.backgroundColor = [UIColor redColor]; }]; }];
3.Incorrect usage of KVOController
KVOController是FaceBook的一个开源库,官方说法是一个简单安全的 KVO工具,其实看一下issue就知道这个东西并不安全,是一种相对的安全,或是说只是适用于MVVM架构下的安全。为什么这么说呢,看一下下面这段代码__weak __typeof(&*self)weakSelf = self; self.fbKVO= [FBKVOController controllerWithObserver:self]; [self.fbKVO observe:self keyPath:@"ugcPublishBarName" options: NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) { __strong __typeof(weakSelf)strongSelf = weakSelf; NSString *barName = change[NSKeyValueChangeNewKey]; dispatch_async(dispatch_get_main_queue(), ^{ [strongSelf refreshBarName:barName]; }); }];
检测一下,又是一个隐藏的内存泄漏,为什么呢,在于KVOController的使用姿势不对,可以看下这个 issue ,KVOController的原理也非常简单,网上有很多分析,可以参考下之前写过的这篇 KVOController分享,简单来说:
KVOController会retain observee, 造成 所以形成 self(observer) -> self.KVOController -> self(observee) 的循环引用
只要 observee 反过来强引用 observer 就会造成循环引用, weak-strong dance都没用, 本例中是它的一种特殊情况(observee 就是 observer), 所以要使用方多注意。那么,解决途径是什么呢?
推荐两种:
(1)打死还用KVOController
__weak __typeof(&*self)weakSelf = self; NSObject *kvo = [[NSObject alloc] init]; [kvo.KVOController observe:self keyPath:@"ugcPublishBarName" options: NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) { __strong __typeof(weakSelf)strongSelf = weakSelf; NSString *barName = change[NSKeyValueChangeNewKey]; dispatch_async(dispatch_get_main_queue(), ^{ [strongSelf refreshBarName:barName]; }); }];
可能有同学搜到用下面方法也可以
[self.KVOControllerNonRetaining observe:self keyPath:@"ugcPublishBarName" options: NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) { __strong __typeof(weakSelf)strongSelf = weakSelf; NSString *barName = change[NSKeyValueChangeNewKey]; dispatch_async(dispatch_get_main_queue(), ^{ [strongSelf refreshBarName:barName]; }); }];
但是这个放到设计场景中并不是一个通用的解决办法,经常容易因忘记解绑而crash
(2)使用RACObserve
由于项目中已经使用了RAC来作为支撑,因此直接简单粗暴的使用RACObserve
@weakify(self); [[[RACObserve(self, ugcPublishBarName) skip:1] deliverOnMainThread] subscribeNext:^(id _Nullable x) { @strongify(self); [self refreshBarName:x]; }];
不在需要关心是否会观察自己,不管是MVC还是MVVM都可以。
那么再次回到刚才留的问题,为什么KVOController在MVVM架构下会比较适合呢,这是因为MVVM架构的核心是拆分View「View&& ViewController」放到ViewModel中,那么反应到代码中,基本就是对ViewModel的各种KVO了。
@weakify(self); [self.KVOController observe:self.viewModel keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) { @strongify(self); self.nameLabel.text = nil; }];
这样就可以把FBKVOController和self隔离开了。
4. NSTimer
说到NSTimer,其实也是老生常谈了,但是在这里提出来主要是想说一个新的思路去解决,先来看一段线上代码- (void)setAutoScroll:(BOOL)autoScroll { _autoScroll = autoScroll; if (autoScroll) { if (!self.autoScrollTimer || !self.autoScrollTimer.isValid) { self.autoScrollTimer = [NSTimer scheduledTimerWithTimeInterval:DISCOVERY_BANNER_SCROLLINTERVAL target:self selector:@selector(handleScrollTimer:) userInfo:nil repeats:YES]; } } else { if (self.autoScrollTimer && self.autoScrollTimer.isValid) { [self.autoScrollTimer invalidate]; self.autoScrollTimer = nil; } } }
很明显,由于timer使用不当, self -> self.timer -> self 循环引用, 也就是说dealloc永远调用不到.
那么这里来说这个问题主要是提供一种新的方式来解决,可能现有的解决方案无非下面两种
1.手工打破循环, 在viewWillDisapear时调用timer的invalidate方法 2.使用 GCD Timer,比如MSWeakTimer.
但这里我比较喜欢使用RAC的方式
- (void)setAutoScroll:(BOOL)autoScroll { _autoScroll = autoScroll; if (autoScroll) { if (!self.autoScrollTimer || !self.autoScrollTimer.isValid) { @weakify(self); [[RACSignal interval:DISCOVERY_BANNER_SCROLLINTERVAL onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) { @strongify(self); [self handleScrollTimer:nil]; }]; } } else { if (self.autoScrollTimer && self.autoScrollTimer.isValid) { [self.autoScrollTimer invalidate]; self.autoScrollTimer = nil; } } }
非常直观,也不需要在dealloc中解除timer。
5.No retain-cycle but issue
先来看一段代码NSTimeInterval largeTime = 1000.f; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(largeTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self doSomething]; });
这个不是循环引用, 但是会造成VC在1000s后才释放, 虽然一般不会用dispatch_after delay 1000s, 但是在复杂的业务场景中, 可能存在复杂的dispatch_after嵌套等情况.解决办法: weakify(self), 然后如果self已经释放, 就直接return.
NSTimeInterval largeTime = 1000.f; @weakify(self); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(largeTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ @strongify(self); if (!self) { return; } [self doSomething]; });
6.Debug memory links
随着MLeaksFinder的出现,检测循环引用,只需要简单的一句话就可以了,但是在Debug模式开发的话,经常会碰到一些“误报”的问题,比如[[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler] withLeeway:0] subscribeNext:^(NSDate * _Nullable x) { NSAssert(x, @""); [MGJRouter openUrl:kNHDetailRouterUrl]; }];
这段代码来看似乎没有用到任何的self,只是简单地做了个router跳转,但是使用MLeaksFinder的话,会非常奇怪的出现一个memory leak的AlertView「更奇怪的是Debug模式下出现,Release下不出现」,这里我们来解析一些这段代码,主要可疑点很明显是 NSAssert(x, @""),那么展开看下 NSAssert是什么呢
#define NSAssert(condition, desc, ...) \ do { \ __PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \ if (!(condition)) { \ NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \ __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \ [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \ object:self file:__assert_file__ \ lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \ } \ __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \ } while(0) #endif
碰到了最喜欢的
do{ }while(NO);
继续观察里面有一段可疑的self
[[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \ object:self file:__assert_file__ \ lineNumber:__LINE__ description:(desc), ##__VA_ARGS__];
因此答案很明显了。那么有什么解决方式,当然可以使用weak-strong dance,但是在一个没有self的场景下似乎显得有点啰嗦。这里推荐使用不带self的NSCAssert,这样就可以避免在开发时期被误杀了
2. Summary of Memory Leaks
1) weak-strong dance 和 block
weak-strong dance使用情况的分析:一般来说 weak-strong dance 可以避免大部分循环引用问题, 但是也不能盲目的使用.
简单介绍下weak-strong dance, 老司机可以跳过. 原始的写法是:
__weak typeof(self) weakSelf = self; __strong typeof(weakSelf) strongSelf = weakSelf;
其中weak打破了循环引用, 在self释放时, weakSelf自动置空, 至于为什么又用strong的原因是为了防止block中的代码执行一半, self释放了, weakself也就是nil了. block中代码执行一半就半途而废了.
后来我们引入libextobjc中的 @weakify 和 @strongfiy 来简写. 其原理还是一样的.
需要注意的是, 在嵌套的blocks中, 只需@weakify(self)一次, 但必须在每个blocks里都@strongify(self), 可以参考这个issue
前面说weak-strong dance并不是万能的, 我们从block的使用来具体分析一下.
block的使用可以分成三种:
1 临时性的,只用在栈当中,不会存储起来
比如数组的enumerateObjectsUsingBlock方法比如masonry的mas_makeConstraints直接执行block, 不曾持有block
在这些情况下, 不需要weak-strong dance.可以看到mas_makeConstraints实现就是拿了block直接用, 没有持有
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; }
2 需要存储起来,但只会调用一次,或者有一个完成时期
比如一个 UIView 的动画, 动画完成之后, 需要使用 block 通知外面, 一旦调用 block 之后, 这个 block 就可以释放了.再比如网络库的successBlock, 它会在网络请求结束释放该block.
再比如GCD的相关方法, 排队执行完就释放该block.在这些情况下, 有时需要weak-strong dance
比如网络超时设为30s, 那个有可能网络库在30s后再会释放block里的对象. 造成资源的浪费. 这时候就需要weak-strong dance. 再比如例子5也需要weak-strong dance.
比如UIView的动画, 0.3s后动画完成, 0s延迟, 就不需要weak-strong dance.
3 需要存储起来,可能会调用多次
比如按钮的点击事件,假如采用 block 实现,这种 block 就需要长期存储,并且会调用多次。调用之后,block 也不可以删除,可能还有下一次按钮的点击。
在这些情况下, 都需要weak-strong dance. 并且有可能还不够
2) delegate 用 strong 修饰
虽然最最低级的错误, 几乎不会有人再犯了,使用"内存泄露"做关键词搜索主客的commit记录时, 我发现曾经有七处地方写错然后被修正过来了. 还是很恐怖的.可以在主客里面使用strong) id<.*?>搜索3) Toll-Free Bridging
在CF对象和NS对象之间转换时, 我们会使用bridge来桥接, 除了bridge_transfer会将CF对象交给ARC管理, bridge和bridge_retained都需要手工管理CF对象.具体可以参考Mika Ash老师的这篇文章.4) 可能造成延迟dealloc的情况
dispatch_after:1000sblock里面使用weakself, 判断weakself为空就returnperformSelector
[self performSelector:@selector(method1:) withObject:nil afterDelay:1000];
解决办法: 在dealloc中调用
[NSObject cancelPreviousPerformRequestsWithTarget:self]
NSOperationQueue解决办法: 在dealloc中调用[queue cancelAllOperations]
block和performSelector等的使用一定要考虑到对象的生命周期,block等会延长对象的生命,延迟释放,由此可能会造成逻辑上时序的问题.
5) NSNotificationCenter
需要注意的是 NSNotificationCenter 需要 removeObserver 不是由于循环引用的问题,通知中心维护的是观察者是unsafe_unretained 引用, 类似于assgin, 不是weak, 不会自动置空, 使用unsafe_unretained的原因是兼容老版本的系统, 所以要及时removeObserver, 否则可能造成访问野指针crash.另外, 在VC中使用addObserverForName:object:queue:usingBlock:
后, 在dealloc中调用
[[NSNotificationCenter defaultCenter] removeObserver:self];
无效, 原因是
addObserverForName:object:queue:usingBlock:
的observer不再是self了, 而是
id observer = addObserverForName:object:queue:usingBlock:
的observer. 所以正确移除的办法是保留observer的引用然后移除. 在具体的使用中, weak-strong dance之后, 并不会造成VC的无法释放, 只会造成observer空跑, 影响不是很大. 但还是建议使用RACObserve来避免这个问题.
6) WeakProxy
NSTimer 或者 [self xxx_observe:self forKeyPath:xxx]等这些会强引用observer的API, 在dealloc中去释放是没有用的, 在上面例子4已经提到了. 还有一种方法解决这个问题, 就是WeakProxy, FLAnimatedImage里面有一个实现, 用法是:FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self]; self.timer = [NSTimer scheduledTimerWithTimeInterval:.01 target:weakProxy selector:@selector(scanAnimation) userInfo:nil repeats:YES];
FLWeakProxy 只持有self的weak引用, 并通过OC的消息转发机制将消息转发给self处理, 这样timer就不会强引用self, dealloc里的[self.timer invalidate]就可以得到调用.
总结
可能每个人都有自己的代码风格,但是不同的风格应该是建立在代码稳定性与可用性基础之上的,因此抛开架构的宏观大层次,我们更应该注重一些小的细节,比如内存释放引起的Crash问题「P.S:这类是不会有Crash上报的, 并且Crash上报中一些无线索的Crash很有可能是内存问题造成的, 很难排查」 。希望这次分享可以帮到大家, 一起加强APP的稳定性。相关文章推荐
- 共享内存总结以及实现代码
- Netty堆外内存泄露排查与总结
- Netty堆外内存泄露排查与总结
- 项目问题总结:Block内存泄露 以及NSTimer使用问题
- android 内存泄露原因以及排查和解决方案
- Java虚拟机垃圾回收(四) 总结:内存分配与回收策略 方法区垃圾回收 以及 JVM垃圾回收的调优方法
- 最近在SPARK上定位的几个内存泄露问题总结
- 内存泄露查找方法总结
- 提高C++代码质量 - [085]了解指针参数传递内存中的玄机
- 编写内存泄露检测器的方法选择以及实现方式 c++
- Linux环境定位C程序内存泄露以及非法访问的方法
- android 内存泄露和内存溢出总结
- 关于android view属性的归属以及如何在代码中设置相关属性总结
- ProGuard总结和混淆Android代码中遇到的问题的解决方法以及寻找getSomething游戏
- 提高ios代码质量 细节总结1
- Android内存泄露问题总结
- 提高代码质量及字节码如何防止内存错误
- matlab 优化内存使用以及优化代码运行速度
- 浅谈一个线程通信代码的内存泄露及解决方案
- JVM内存分配以及存储总结