项目常见崩溃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强引用. 这就会造成当SenderViewController
在FirstViewController
中释放的时候, 系统还持有着这个引用, 对开发者来说, 已经没有指针指向这片内存了, 所以, 当执行+ (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的模拟器进行充分的测试, 将崩溃扼杀在摇篮.