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

很多崩溃都是源于低版本的系统, 可能这是系统的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的模拟器进行充分的测试, 将崩溃扼杀在摇篮.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容