美文网首页
iOS 常见Crash 及解决方案

iOS 常见Crash 及解决方案

作者: 李华光 | 来源:发表于2017-03-24 12:02 被阅读0次

    一、访问了一个已经被释放的对象

    在不使用 ARC 的时候,内存要自己管理,这时重复或过早释放都有可能导致 Crash。

    例子

    NSObject * aObj = [[NSObject alloc] init];
    [aObj release];
    
    NSLog(@"%@", aObj);
    

    原因

    aObj 这个对象已经被释放,但是指针没有置空,这时访问这个指针指向的内存就会 Crash。

    解决办法

    1.使用前要判断非空,释放后要置空。

    正确的释放应该是:
    [aObj release];
    aObj = nil;
    由于ObjC的特性,调用 nil 指针的任何方法相当于无作用,所以即使有人在使用这个指针时没有判断至少还不会挂掉。

    在ObjC里面,一切基于 NSObject 的对象都使用指针来进行调用,所以在无法保证该指针一定有值的情况下,要先判断指针非空再进行调用。

    if (aObj) {
    //...
    }
    常见的如判断一个字符串是否为空:

    if (aString && aString.length > 0) {//...}

    2.适当使用 autorelease

    有些时候不能知道自己创建的对象什么时候要进行释放,可以使用 autoRelease,但是不鼓励使用。因为 autoRelease 的对象要等到最近的一个 autoReleasePool 销毁的时候才会销毁,如果自己知道什么时候会用完这个对象,当然立即释放效率要更高。如果一定要用 autoRelease 来创建大量对象或者大数据对象,最好自己显式地创建一个 autoReleasePool,在使用后手动销毁。以前要自己手动初始化 autoReleasePool,现在可以用以下写法:

    @autoreleasepool{
    for (int i = 0; i < 100; ++i) {
    NSObject * aObj = [[[NSObject alloc] init] autorelease];
    //....
    }
    }

    二、访问数组类对象越界或插入了空对象

    NSMutableArray/NSMutableDictionary/NSMutableSet 等类下标越界,或者 insert 了一个 nil 对象。

    原因

    一个固定数组有一块连续内存,数组指针指向内存首地址,靠下标来计算元素地址,如果下标越界则指针偏移出这块内存,会访问到野数据,ObjC 为了安全就直接让程序 Crash 了。

    而 nil 对象在数组类的 init 方法里面是表示数组的结束,所以使用 addObject 方法来插入对象就会使程序挂掉。如果实在要在数组里面加入一个空对象,那就使用 NSNull。

    [array addObject:[NSNull null]];

    解决办法

    使用数组时注意判断下标是否越界,插入对象前先判断该对象是否为空。

    if (aObj) {
        [array addObject:aObj];
    }
    

    可以使用 Cocoa 的 Category 特性直接扩展 NSMutable 类的 Add/Insert 方法。比如:

    @interface NSMutableArray (SafeInsert)
    -(void) safeAddObject:(id)anObject;
    @end
    
    @implementation NSMutableArray (SafeInsert)
    -(void) safeAddObject:(id)anObject {
        if (anObject) {
            [self addObject:anObject];
        }
    }
    

    @end
    这样,以后在工程里面使用 NSMutableArray 就可以直接使用 safeAddObject 方法来规避 Crash。

    三、访问了不存在的方法

    ObjC 的方法调用跟 C++ 很不一样。 C++ 在编译的时候就已经绑定了类和方法,一个类不可能调用一个不存在的方法,否则就报编译错误。而 ObjC 则是在 runtime 的时候才去查找应该调用哪一个方法。

    这两种实现各有优劣,C++ 的绑定使得调用方法的时候速度很快,但是只能通过 virtual 关键字来实现有限的动态绑定。而对 ObjC 来说,事实上他的实现是一种消息传递而不是方法调用。

    [aObj aMethod];
    这样的语句应该理解为,像 aObj 对象发送一个叫做 aMethod 的消息,aObj 对象接收到这个消息之后,自己去查找是否能调用对应的方法,找不到则上父类找,再找不到就 Crash。由于 ObjC 的这种特性,使得其消息不单可以实现方法调用,还能紧系转发,对一个 obj 传递一个 selector 要求调用某方法,他可以直接不理会,转发给别的 obj 让别的 obj 来响应,非常灵活。

    例子

    [self methodNotExists];
    调用一个不存在的方法,可以编译通过,运行时直接挂掉,报 NSInvalidArgumentException 异常:

    -[WSMainViewController methodNotExist]: unrecognized selector sent to instance 0x1dd96160
    2013-10-23 15:49:52.167 WSCrashSample[5578:907] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[WSMainViewController methodNotExist]: unrecognized selector sent to instance 0x1dd96160'

    解决方案

    像这种类型的错误通常出现在使用 delegate 的时候,因为 delegate 通常是一个 id 泛型,所以 IDE 也不会报警告,所以这种时候要用 respondsToSelector 方法先判断一下,然后再进行调用。

    if ([self respondsToSelector:@selector(methodNotExist)]) {
    [self methodNotExist];
    }

    四、多线程并发操作

    这个应该是全平台都会遇到的问题了。当某个对象会被多个线程修改的时候,有可能一个线程访问这个对象的时候另一个线程已经把它删掉了,导致 Crash。比较常见的是在网络任务队列里面,主线程往队列里面加入任务,网络线程同时进行删除操作导致挂掉。

    解决方法

    1.加锁 NSLock

    普通的锁,加锁的时候 lock,解锁调用 unlock。

    - (void)addPlayer:(Player *)player {
       if (player == nil) return;
            NSLock* aLock = [[NSLock alloc] init];
            [aLock lock];
    
            [players addObject:player];
    
            [aLock unlock];
       }
    }
    

    可以使用标记符 @synchronized 简化代码:

    - (void)addPlayer:(Player *)player {
       if (player == nil) return;
       @synchronized(players) {
          [players addObject:player];
       }
    }
    

    2.NSRecursiveLock 递归锁

    使用普通的 NSLock 如果在递归的情况下或者重复加锁的情况下,自己跟自己抢资源导致死锁。Cocoa 提供了 NSRecursiveLock 锁可以多次加锁而不会死锁,只要 unlock 次数跟 lock 次数一样就行了。

    3.NSConditionLock 条件锁

    多数情况下锁是不需要关心什么条件下 unlock 的,要用的时候锁上,用完了就 unlock 就完了。Cocoa 提供这种条件锁,可以在满足某种条件下才解锁。这个锁的 lock 和 unlock, lockWhenCondition 是随意组合的,可以不用对应起来。

    4.NSDistributedLock 分布式锁

    这是用在多进程之间共享资源的锁,对 iOS 来说暂时没用处。

    5.无锁

    放弃加锁,采用原子操作,编写无锁队列解决多线程同步的问题。酷壳有篇介绍无锁队列的文章可以参考一下:无锁队列的实现

    使用其他备选方案代替多线程:Operation Objects, GCD, Idle-time notifications, Asynchronous functions, Timers, Separate processes。

    五、Repeating NSTimer

    如果一个 Timer 是不停 repeat,那么释放之前就应该先 invalidate。非repeat的timer在fired的时候会自动调用invalidate,但是repeat的不会。这时如果释放了timer,而timer其实还会回调,回调的时候找不到对象就会挂掉。

    原因

    NSTimer 是通过 RunLoop 来实现定时调用的,当你创建一个 Timer 的时候,RunLoop 会持有这个 Timer 的强引用,如果你创建了一个 repeating timer,在下一次回调前就把这个 timer release了,那么 runloop 回调的时候就会找不到对象而 Crash。

    解决方案

    我写了个宏用来释放Timer

    /*
     * 判断这个Timer不为nil则停止并释放
     * 如果不先停止可能会导致crash
     */
    #define WVSAFA_DELETE_TIMER(timer) { \
        if (timer != nil) { \
            [timer invalidate]; \
            [timer release]; \
            timer = nil; \
        } \
    }
    

    参考:

    相关文章

      网友评论

          本文标题:iOS 常见Crash 及解决方案

          本文链接:https://www.haomeiwen.com/subject/ajlqgttx.html