如何防止應用意外崩潰(一)

無論是全棧大神,還是普通程序猿,都不能規避掉所有的異常,所以只要是人寫的程序都有意外奔潰的可能,更何況操作系統本身就可能會包含一些意想不到的異常情況.隨着開發經驗和閱歷的增加,由於自身能力問題導致的異常情況會越來越少.但異常不可避免,而應用閃退又是一種極其糟糕的用戶體驗,所以應該儘可能地減少甚至消除這些異常導致的閃退現象.

今天重點討論一下關於

unrecognized selector sent to instance 0x......

的處理.

這是一個經常會遇到的異常,造成這個異常的情況的最終原因是調用了當前類/對象沒有實現的方法.而直接原因可能但不限於:

  • 直接強轉了本不屬於強轉類的對象,然後調用屬於強轉對象的方法;
  • 邊界條件判斷缺失導致調用不屬於當前對象的方法;
  • 遵循協議但未實現協議方法;
  • 其他導致調用了當前對象(類是一種特殊的對象)未實現的方法.

爲了消除這種異常導致的應用閃退,可以嘗試使用消息轉發機制進行處理.對於消息轉發機制時機的使用,一般會選擇在消息轉發的最後階段做處理,也就是標準消息轉發(normal forwarding)

- (void)forwardInvocation:(NSInvocation *)anInvocation;

來做處理.選擇這個時機的主要原因是:

  • 這是消息轉發機制的最後一步,能夠執行到這裏就說明這個異常並沒有在之前的時機中被處理,防止攔截到了系統將要處理的異常(系統也會利用消息轉發機制做一些系統級的異常處理,這些異常並不是我們要處理的);
  • 這個方法中包含了方法可以執行的完整信息,最方便做方法相關轉發等處理.

在進入到該時機之前系統會嘗試調用

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

如果返回了可用的方法簽名,纔會進入到標準消息轉發流程,否則會直接調用doesNotRecognizeSelector拋出異常導致閃退.這就提供了一種可以消除由於調用未實現的方法導致的應用閃退思路.

基本思路:

  1. 使用運行時替換根類的methodSignatureForSelector:方法,如果該方法可以返回有效的方法簽名,則證明調用的方法實現是存在的,直接放回方法簽名;否則就直接返回一個已經自定義的實現簽名;
  2. 由於當前對象/類並沒有實現對應的方法,所以會進入forwardInvocation:方法,這時就可以執行一個自定義的方法來防止應用閃退.

實現

定義ExceptionHandler類,用來接收未實現的方法,將未實現的方法都轉交到ExceptionHandler中處理.

@interface ExceptionHandler : NSObject
+ (void)noSelector;
- (void)noSelector;
@end

@implementation ExceptionHandler
+ (void)noSelector {}
- (void)noSelector {}
@end

定義UnrecognizedSelectorHandler類,封裝用來處理由於未實現對應方法而導致的異常.

@interface UnrecognizedSelectorHandler : NSObject
+ (void)start;
@end


@implementation UnrecognizedSelectorHandler
//保存類方法的methodSignatureForSelector原始實現
NSMethodSignature * (*ori_meta_methodSignatureForSelector)(id self, SEL _cmd, SEL aSelector);
//保存實力方法的methodSignatureForSelector原始實現
NSMethodSignature * (*ori_methodSignatureForSelector)(id self, SEL _cmd, SEL aSelector);
NSMethodSignature * methodSignatureForSelector(id self, SEL _cmd, SEL aSelector) {
    Class class = [self class];
    if (class_isMetaClass(class)) {
        //類方法
        NSMethodSignature *signature = ori_meta_methodSignatureForSelector(self, _cmd, aSelector);
        if (signature) {
            //若類方法已經實現則直接返回
            return signature;
        }
    } else {
        //實例方法
        NSMethodSignature *signature = ori_methodSignatureForSelector(self, _cmd, aSelector);
        if (signature) {
            //若實例方法已經實現則直接返回
            return signature;
        }
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

//保存元類中的forwardInvocation實現
void (*ori_meta_forwardInvocation)(id self, SEL _cmd, NSInvocation * anInvocation);
//保存普通類中的forwardInvocation實現
void (*ori_forwardInvocation)(id self, SEL _cmd, NSInvocation * anInvocation);
void ds_forwardInvocation(id self, SEL _cmd, NSInvocation * anInvocation) {
    Class class = [anInvocation.target class];
    BOOL existImp = class_respondsToSelector(anInvocation.class, anInvocation.selector);
    if (class_isMetaClass(class)) {
        if (existImp && ori_meta_forwardInvocation) {
            ori_meta_forwardInvocation(self, _cmd, anInvocation);
        } else {
            //元類,類方法
            anInvocation.target = [ExceptionHandler class];
            anInvocation.selector = @selector(noSelector);
            [anInvocation invoke];
        }
    } else {
        if (existImp && ori_forwardInvocation) {
            ori_forwardInvocation(self, _cmd, anInvocation);
        } else {
            id obj = [[ExceptionHandler alloc] init];
            anInvocation.target = obj;
            anInvocation.selector = @selector(noSelector);
            [anInvocation invoke];
        }
    }
}



+ (void)start {
    //1. 處理類方法異常
    Class class = [NSObject class];
    Method method = class_getInstanceMethod(class, @selector(methodSignatureForSelector:));
    ori_methodSignatureForSelector = (NSMethodSignature *(*)(id,SEL, SEL))method_setImplementation(method,(IMP)methodSignatureForSelector);
    
    method = class_getInstanceMethod(class, @selector(forwardInvocation:));
    ori_forwardInvocation = (void(*)(id,SEL,NSInvocation *))method_setImplementation(method, (IMP)ds_forwardInvocation);


    //2. 處理實例方法異常
    class = object_getClass(class);
    Method method_meta = class_getInstanceMethod(class, @selector(methodSignatureForSelector:));
    ori_meta_methodSignatureForSelector = (NSMethodSignature *(*)(id,SEL, SEL))method_setImplementation(method_meta,(IMP)methodSignatureForSelector);
    method_meta = class_getInstanceMethod(class, @selector(forwardInvocation:));
    ori_meta_forwardInvocation = (void(*)(id,SEL,NSInvocation *))method_setImplementation(method_meta, (IMP)ds_forwardInvocation);

}
@end

爲了在儘可能早地時機攔截處理異常,可以將這個方法放置在應用啓動方法中:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    //註冊異常處理
    [UnrecognizedSelectorHandler start];
    return YES;
}

來來來,開始測試啦:

//爲了方便測試創建Person類
@interface Person : NSObject
//以下所有方法只聲明不做實現
+ (void)sayHello;
+ (NSString *)getContent;
+ (Person *)sendContent:(NSString *)content;

- (void)sayHello;
- (NSString *)getContent;
- (Person *)sendContent:(NSString *)content;

@end

@implementation Person

@end

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    //註冊異常處理,一定要儘可能早地註冊才能儘可能多地攔截到異常
    [UnrecognizedSelectorHandler start];
    
    NSString *str = [NSNull null];
    if (str.length > 0) {
        NSLog(@"str合法可用");
    } else {
        NSLog(@"str不合法可用");
    }
    
    [Person sayHello];
    NSString *classContent = [Person getContent];
    NSLog(@"class content == %@", classContent);
    Person *classPerson = [Person sendContent:@"Hello world"];
    NSLog(@"class person == %@", classPerson);
    
    
    Person *person = [[Person alloc] init];
    [person sayHello];
    NSString *content = [person getContent];
    NSLog(@"instance content == %@", content);
    Person *instancePerson = [person sendContent:@"Hello world"];
    NSLog(@"class person == %@", instancePerson);
    
    return YES;
}

可以看到,在註冊完成異常處理類UnrecognizedSelectorHandler的start方法之後,凡是unrecognized selector類的異常都被很好地攔截處理,從而使得應用不再發生閃退.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章