一.代码质量总结
在几周的稳定性工作中, 我对现有内涵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 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操作,但是调用了super。而super 是个编译器指令,当调用[super loadMoreData]的时候,它告诉编译器到父类中去找方法,但super和self其实是一个,因此super造成了强引用,只需要改成self就可以解决了。
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__ : @"" ; \
[[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为空就return
-
performSelector
[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的稳定性。