代码质量以及内存泄露排查总结

想体验一把 CentOS 系统玩一下命令行?试试腾讯云上实验室吧  https://cloud.tencent.com/developer/labs
原文链接: 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  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 持有类型为 MemoryTestproperty :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],首先调用dangergetter方法,然后默认取到了事例变量,然后再做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]的时候,它告诉编译器到父类中去找方法,但superself其实是一个,因此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的场景下似乎显得有点啰嗦。这里推荐使用不带selfNSCAssert,这样就可以避免在开发时期被误杀

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的稳定性。

你可能感兴趣的:(iOS)