iOS消息机制(发送、动态解析、转发)

iOS开发经常碰到以下两个问题:
一: unrecognized selector sent to instance,这是因为调用了不存在的方法导致的(比如字典当做数组来用,使用了下标。 数组当做字典来用,调用了键值对取值,调用外部传递的sel等),在所有的崩溃中占有相当大的比例。

二: 还有一种占比比较高的闪退就是NULL,本来和后端说好的数据类型不会错,但是呢,总是不如意啊。尤其是php当没有数据的时候字典就变成数组了,这样的话客户端解析一不小心就JJ了。

想要彻底解决以上两个问题需要了OC的方法调用机制。

一. 先看整个消息机制流程图

1. objc_msgSend代码执行流程图
objc_msgSend执行流程 – 源码解析
2. 消息发送(方法查找)
消息发送
3. 动态消息解析
动态方法解析
4.消息转发流程
消息转发流程

消息解析、转发流程图:

消息转发流程
从上边一系列流程图中可以看出调用一个方法大致分为两个大的步骤:
1.消息发送
2.动态消息处理
使用动态消息处理就可以做到当调用了一个不存在的方法的时候程序不会崩溃
下面先来解决第一种情况

1,首先在ViewController类中创建对象以及调用person的sendMessage方法

 @implementation ViewController
 - (void)viewDidLoad { 
     [super viewDidLoad];
      Person*p = [[Person alloc] init];
     [p performSelector:NSSelectorFromString(@"sendMessage") withObject:nil];    
 }
Person
 @interface Person : NSObject 
 @end 

 @implementation Person 
 //在.m中实现这两个方法:
 -(void)noObjMethod{   
     NSLog(@"未实现这个实例方法");
 }
 
 +(void)noClassMethod{   
     NSLog(@"未实现这个类方法");
 }
 
 // 返回值是什么没有切实的意义,通过源码可以看到只是作为log打印了出来。但是如果实现了添加方法最好返回YES,否则返回NO。
 // 实例方法是存在于当前对象对应的类的方法列表中
 +(BOOL)resolveInstanceMethod:(SEL)sel{   
     SEL aSel = NSSelectorFromString(@"noObjMethod");   
     Method aMethod = class_getInstanceMethod(self, aSel);   
     class_addMethod([self class], sel, method_getImplementation(aMethod), method_getTypeEncoding(method));   
     return YES;
 }

// 如果是动态添加类方法class_addMethod第一个参数应该使用object_getClass(self)
 +(BOOL)resolveClassMethod:(SEL)sel{   
     SEL aSel = NSSelectorFromString(@"noClassMethod");   
     Method aMethod = class_getInstanceMethod(self, aSel);   
     class_addMethod(object_getClass(self), sel, method_getImplementation(aMethod), method_getTypeEncoding(method));   
     return YES;
 }

- (id)forwardingTargetForSelector:(SEL)aSelector { 
     if(aSelector ==@selector(sendMessage:)) { 
         return [Student new]; 
     }else{ 
         return  [super forwardingTargetForSelector:aSelector]; 
     } 
 }

 - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {
     if(aSelector ==@selector(sendMessage:)) { 
         return [NSMethodSignature signatureWithObjCTypes:"V@:@"];
     } 
     return nil; 
 }
 
 - (void)forwardInvocation:(NSInvocation*)anInvocation {
     SEL sel = [anInvocation selector]; 
     Student *stu = [Student new];
     if([stu respondsToSelector:sel]) { 
         [anInvocation invokeWithTarget:stu];
     }else{ 
         [super forwardInvocation:anInvocation]; 
     } 
}
 @end
Student类:
 @interface Student :NSObject
 @end
 
 @implementation Student
- (void)sendMessage:(NSString *)str {
    NSLog(@"Student--%@", str);
}

+ (void)sendMessage:(NSString *)str {
    NSLog(@"Student++%@", str);
}
 @end

上述示例调用了sendMessage方法,这个方法不存在,所以通过一系列的方法查找还是没有找到指定方法的时候会进入动态消息解析阶段。

二. 动态方法解析

在Person.m中实现一下动态解析相关方法就可以做到不闪退了.

但是这样做不通用,我们可以搞一个NSObject的分类,将这些动态解析方法写在分类里边,但是这样做太危险了,最好不要这样做。

resolveInstanceMethod

v@:中, v表示返回值void, @表示对象self, :表示SEL

Person.m实现以下方法
 //在.m中实现这两个方法:
 -(void)noObjMethod{   
     NSLog(@"未实现这个实例方法");
 }
 
 +(void)noClassMethod{   
     NSLog(@"未实现这个类方法");
 }
 
 // 返回值是什么没有切实的意义,通过源码可以看到只是作为log打印了出来。但是如果实现了添加方法最好返回YES,否则返回NO。
 // 实例方法是存在于当前对象对应的类的方法列表中
 +(BOOL)resolveInstanceMethod:(SEL)sel{   
     SEL aSel = NSSelectorFromString(@"noObjMethod");   
     Method aMethod = class_getInstanceMethod(self, aSel);   
     class_addMethod([self class], sel, method_getImplementation(aMethod), method_getTypeEncoding(method));   
     return YES;
 }

// 如果是动态添加类方法class_addMethod第一个参数应该使用object_getClass(self)
 +(BOOL)resolveClassMethod:(SEL)sel{   
     SEL aSel = NSSelectorFromString(@"noClassMethod");   
     Method aMethod = class_getInstanceMethod(self, aSel);   
     class_addMethod(object_getClass(self), sel, method_getImplementation(aMethod), method_getTypeEncoding(method));   
     return YES;
 }

三. 消息转发
1. 如果resolveInstanceMethod 没有实现class_addMethod,会执行- (id)forwardingTargetForSelector:(SEL)aSelector,此方法会将消息转发给Student类实现
 - (id)forwardingTargetForSelector:(SEL)aSelector { 
     if(aSelector ==@selector(sendMessage:)) { 
         return [Student new]; 
     }else{ 
         return  [super forwardingTargetForSelector:aSelector]; 
     } 
 }

如果上述代码做修改,不是返回实例Student对象,而是返回Student类对象也是可以的,如下:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(sendMessage:)) {
        return [Student class];
    } else {
        return [super forwardingTargetForSelector:aSelector];
    }
}

Student.m需要实现类方法

+ (void)sendMessage:(NSString *)str {
    NSLog(@"Student++%@", str);
}

完成以上操作就可以成功将消息转发到Student的类方法

2. 如果- (id)forwardingTargetForSelector:(SEL)aSelector返回值为nil,则会调用以下方法
 - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {
     if(aSelector ==@selector(sendMessage:)) { 
         return [NSMethodSignature signatureWithObjCTypes:"V@:@"];
     } 
     return nil; 
 }
 
 - (void)forwardInvocation:(NSInvocation*)anInvocation {
     SEL sel = [anInvocation selector]; 
     Student *stu = [Student new];
     if([stu respondsToSelector:sel]) { 
         [anInvocation invokeWithTarget:stu];
     }else{ 
         [super forwardInvocation:anInvocation]; 
     } 
}

a) methodSignatureForSelector实现以后,forwardInvocation里边可以实现任意内容。
b) - (void)forwardInvocation:(NSInvocation*)anInvocation 可以将消息转发给多个对象实现。
c) - (void)forwardInvocation:(NSInvocation*)anInvocation里边可以无任何实现或者只是打印log都不会有任何问题。
d) forwardingTargetForSelectormethodSignatureForSelectorforwardInvocation这些方法都有对应的类方法,只是敲代码的时候没有提示。

如果以上三种重载都没执行消息,此时会调用- (void)doesNotRecognizeSelector:(SEL)aSelector方法,此时程序会崩溃

 - (void)doesNotRecognizeSelector:(SEL)aSelector {
     NSLog(@"doesNotRecognizeSelector"); 
 }

从流程图可以看出,越是往后开销越大,所以在早期做出预防处理是最好的选择 所以直接在父类中重写resolveInstanceMethod方法,就可以做到程序不会崩溃了

接下来解决第二种情况,就是NULL的情况

这时候可以搞一个分类,如下:

 #import <Foundation/Foundation.h> 
 @interface NSNull (Exception) 
 @end
 
 #import "NSNull+Exception.h" 
 #import <objc/runtime.h>
 
 @implementation NSNull (Exception)
 
 #define pLog
 
 #define JsonObjects @[@"",@0,@{},@[]]
 
 //在.m中实现这两个方法:
 -(void)noObjMethod{ 
     NSLog(@"未实现这个实例方法");
 }
 
 +(void)noClassMethod{ 
     NSLog(@"未实现这个类方法"); 
 }
 
 +(BOOL)resolveInstanceMethod:(SEL)sel{ 
     for(id jsonObj in JsonObjects) { 
         if([jsonObj respondsToSelector:sel]) { 
 #ifdef pLog 
             NSLog(@"NULL出现啦!这个对象应该是是_%@",[jsonObj class]); 
 #endif 
         } 
     } 
     SEL aSel = NSSelectorFromString(@"noObjMethod"); 
     Method aMethod = class_getInstanceMethod(self, aSel); 
     class_addMethod([self class], sel, method_getImplementation(aMethod), "v@:");
     return YES;
 }

如果此时在 ViewController.m中调用一下错误的字典 则不会引起崩溃

 NSDictionary* dict = [[NSNull alloc] init]; 
 [dict objectForKey:@"123"];

控制台会打印出以下信息:

 **2019-03-02 09:44:30.905674+0800 Test[28763:7642018] NULL****出现啦!这个对象应该是是****___NSDictionary0** 
 **2019-03-02 09:44:30.905790+0800 Test[28763:7642018] ****未实现这个实例方法**

现在就可以随意折腾了

image

好了,至此咱们的APP就可以减少大部分闪退问题了

四. 附录

在源码里边找不到__forwarding____forwarding__clean.c,这里整理了一份别人写的伪代码供参考。

  1. forwarding.c
// 伪代码
int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // 调用 forwardingTargetForSelector:
    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget && forwardingTarget != receiver) {
            if (isStret == 1) {
                int ret;
                objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
                return ret;
            }
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // 僵尸对象
    const char *className = class_getName(receiverClass);
    const char *zombiePrefix = "_NSZombie_";
    size_t prefixLen = strlen(zombiePrefix); // 0xa
    if (strncmp(className, zombiePrefix, prefixLen) == 0) {
        CFLog(kCFLogLevelError,
              @"*** -[%s %s]: message sent to deallocated instance %p",
              className + prefixLen,
              selName,
              receiver);
        <breakpoint-interrupt>
    }

    // 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        if (methodSignature) {
            BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
            if (signatureIsStret != isStret) {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.  Signature thinks it does%s return a struct, and compiler thinks it does%s.",
                      selName,
                      signatureIsStret ? "" : not,
                      isStret ? "" : not);
            }
            if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
                NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

                [receiver forwardInvocation:invocation];

                void *returnValue = NULL;
                [invocation getReturnValue:&value];
                return returnValue;
            } else {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
                      receiver,
                      className);
                return 0;
            }
        }
    }

    SEL *registeredSel = sel_getUid(selName);

    // selector 是否已经在 Runtime 注册过
    if (sel != registeredSel) {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
              sel,
              selName,
              registeredSel);
    } // doesNotRecognizeSelector
    else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
        [receiver doesNotRecognizeSelector:sel];
    }
    else {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
              receiver,
              className);
    }

    // The point of no return.
    kill(getpid(), 9);
}

  1. __forwarding__clean.c
int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // 调用 forwardingTargetForSelector:
    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget && forwardingTarget != receiver) {
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
            NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

            [receiver forwardInvocation:invocation];

            void *returnValue = NULL;
            [invocation getReturnValue:&value];
            return returnValue;
        }
    }

    if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
        [receiver doesNotRecognizeSelector:sel];
    }

    // The point of no return.
    kill(getpid(), 9);
}

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

推荐阅读更多精彩内容