项目常见崩溃iOS开发好东西

项目常见崩溃6(陆续更新)

2017-06-02  本文已影响105人  bigParis

很多崩溃都是源于低版本的系统, 可能这是系统的bug, 但作为开发人员, 即便是系统的bug, 我们也应该找到崩溃的原因, 并解决掉它.

崩溃重现

FirstViewController中创建一个controller, 并作为自己的子controller
SenderViewController *controller = [[SenderViewController alloc] init];
controller.view.frame = self.view.bounds;
[self addChildViewController:controller];
self.secondVC = controller;
发送一个通知
[self.secondVC removeFromParentViewController];
self.secondVC = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:@"doCancle" object:nil];
在SenderViewController中
- (void)dealloc {
    NSLog(@"SenderViewController dealloc");
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(autoSkipMic) object:nil];
}
- (void)viewDidLoad {
    // 添加取消自动过麦的Observer
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(autoSkipMic)
                                                 name:@"doCancle"
                                               object:nil];

    // 做10秒的延时自动过麦操作
    [self performSelector:@selector(autoSkipMic) withObject:nil afterDelay:10];
}

- (void)autoSkipMic {
    NSLog(@"autoSkipMic before cancel executed");
    // 收到自动过麦通知后, 取消延时执行中的操作.
    [NSObject cancelPreviousPerformRequestsWithTarget:self
                                             selector:_cmd
                                               object:nil];
    NSLog(@"autoSkipMic after cancel executed");
    // 崩溃了.
    NSLog(@"%@", self.view);
    NSLog(@"pm will fuck dogs");
}
问题分析

当我们使用- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;做一个延时操作的时候, 系统会对self强引用. 这就会造成当SenderViewControllerFirstViewController中释放的时候, 系统还持有着这个引用, 对开发者来说, 已经没有指针指向这片内存了, 所以, 当执行+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument后, 系统会取消对self的强引用, 这时候self变成野指针.

出错信息如下

*** -[SenderViewController view]: message sent to deallocated instance 0x7f9a18c6f5a0

这时候, 我们可能会有点懵逼, autoSkipMic既然能进来, 怎么可能是野指针呢, 本代码不存在多线程的问题, 都是在主线程执行的.
再看下控制台的输出日志

2017-06-02 10:15:10.990 14-Notification[1363:46855] autoSkipMic before cancel executed
2017-06-02 10:15:10.990 14-Notification[1363:46855] SenderViewController dealloc
2017-06-02 10:15:10.990 14-Notification[1363:46855] autoSkipMic after cancel executed

OH, My God, 我们看到了, 当cancelPreviousPerformRequestsWithTarget后, 就dealloc了, 一个方法中的代码居然在不同的runLoop执行! 上面的是iOS8.X的行为. 下面我们看下iOS8以上的行为.

2017-06-02 10:16:29.684 14-Notification[1475:49640] autoSkipMic before cancel executed
2017-06-02 10:16:29.685 14-Notification[1475:49640] autoSkipMic after cancel executed
2017-06-02 10:16:29.688 14-Notification[1475:49640] <UIView: 0x7fb125c060c0; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x600000222760>>
2017-06-02 10:16:29.689 14-Notification[1475:49640] pm will fuck dogs
2017-06-02 10:16:29.689 14-Notification[1475:49640] SenderViewController dealloc

这才是符合我们预期的结果, 既然进到autoSkipMic, 就把这里的代码都执行完, 确保本方法内的代码在同一个runLoop执行. So, 我们很快就找到了解决的办法.

解决办法1
    dispatch_async(dispatch_get_main_queue(), ^{
        [NSObject cancelPreviousPerformRequestsWithTarget:self
                                                 selector:_cmd
                                                   object:nil];
    });

这次控制台的日志一样了. 但是我们还是不放心, 因为, 并不是所有的使用者都会像我一样知道用dispatch, 他们可能还会按照原来的方式写, 我又不可能一行一行review每个人的代码, 因为, 这里写了一个分类hook了performSelector
分类的代码如下

解决办法2
#import "NSObject+performDelayHook.h"
#import <objc/runtime.h>

@interface YYWeakObject : NSObject

@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL sel;

@end

@implementation YYWeakObject

- (void)onTimeout:(NSTimer *)timer {
    if (_target ) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
        [_target performSelector:_sel withObject:self];
#pragma clang diagnostic pop
    }
}

@end

@implementation NSObject (performDelayHook)
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL selA = @selector(performSelector:withObject:afterDelay:);
        SEL selB = @selector(myPerformSelector:withObject:afterDelay:);
        Method methodA = class_getInstanceMethod(self,selA);
        Method methodB = class_getInstanceMethod(self, selB);
        
        BOOL isAdd = class_addMethod(self, selA, method_getImplementation(methodB), method_getTypeEncoding(methodB));
        if (isAdd) {
            class_replaceMethod(self, selB, method_getImplementation(methodA), method_getTypeEncoding(methodA));
        }else{
            method_exchangeImplementations(methodA, methodB);
        }
    });
}

- (void)myPerformSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay {
    YYWeakObject *weakObject = [[YYWeakObject alloc] init];
    weakObject.target = self;
    weakObject.sel = aSelector;
    [NSTimer scheduledTimerWithTimeInterval:delay target:weakObject selector:@selector(onTimeout:) userInfo:anArgument repeats:NO];
}
@end

又是黑魔法, 这里用timer代替系统的调用, 并防止timer对self强引用, 这里只是一个demo, 如果真要做还要把相关的performSelector都hook掉, 并且要把参数处理好, 这里就不展开细说了, 有兴趣的同学自己去实现以下吧, 这里要注意测试, 因为hook的是全局的, 一些不是你调用的performSelector也被hook了, 可能会造成意想不到的其它问题.

总结

1 如果单纯解决问题, 这2种方式其实都可以, 在网上搜也会有网友提供了retain-release dance和weak的方案-->传送门, 如果是为了更优雅的解决问题, 显然方法2是一种一劳永逸的方案
2 对于系统API, 我们在使用的时候尤其要注意低版本上的表现, 尤其是NSObject和NSRunLoop里面的方法, 在开发时候多使用iOS8的模拟器进行充分的测试, 将崩溃扼杀在摇篮.

上一篇下一篇

猜你喜欢

热点阅读