iOS底层探索之Runtime(四): 动态方法解析

1. 回顾

iOS底层探索之Runtime(一):运行时&方法的本质

iOS底层探索之Runtime(二): objc_msgSend&汇编快速查找分析

iOS底层探索之Runtime(三): lookUpImpOrForward慢速查找分析

在这里插入图片描述

在上一篇博文中,介绍了Runtime的慢速查找流程lookUpImpOrForward,本章内容主要分析动态方法解析流程。

在缓存中、自己的class_rw_t中、父类的cache中、父类的class_rw_t中都没有找到imp,就会进入objc_msgSend的第二个阶段动态方法解析

2. 动态方法解析

2.1 resolveMethod_locked

从源码中可以发现,在lookUpImpOrForward里面循环遍历没有找到imp之后就会进入下面这个判断,注释也写的很明显了。

// No implementation found. Try method resolver once.
//behavior = 3 , LOOKUP_RESOLVER = 2
// 3 & 2 = 2,就是找两个的相同值
//0011
//0010
//0010 -> 2 
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;//异或:相同为0,不同为1
        // 3 ^= 2 -> 1 ,behavior=1,下次再进来就是behavior & LOOKUP_RESOLVER -> 相当于1 & 2=0,为0这个方法也就只执行一次,相当于单例的作用
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
  • resolveMethod_locked
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

resolveMethod_locked方法里面可以看出,主要是判断是不是元类,非元类和元类两种情况,分情况进行处理。

  • 非元类:调用resolveInstanceMethod:
  • 元类:调用resolveClassMethod:

在正式开始分析之前,我们先看看下面这个例子
JPStudent的父类JPPerson类里面,声明了一个方法jp_sayHello,但是没有实现,那么JPStudent子类调用了父类的方法,会出现什么情况呢?

JPPerson *jp = [[JPPerson alloc]init];
         [jp jp_sayHello]

很显然程序是会奔溃的,奔溃信息如下:

[9441:373947] -[JPStudent jp_sayHello]: unrecognized selector sent to instance 0x100709660
[9441:373947] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[JPStudent jp_sayHello]: unrecognized selector sent to instance 0x100709660'

那么为什么会报这个unrecognized selector sent to instance经典的错误呢?我们从源码来分析
lookUpImpOrForward方法里面,如果所有的父类里面没有找到imp,就会给imp赋值forward_impbreak跳出,最后返回imp


 const IMP forward_imp = (IMP)_objc_msgForward_impcache;
  IMP imp = nil;
  
 .....代码省略......
 
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
            
 .....代码省略......
 
    return imp;

2.2 _objc_msgForward_impcache

  • _objc_msgForward_impcache

那么赋值的这个imp,也就是_objc_msgForward_impcache,到底是个什么imp呢?
在源码里面全局搜索objc_msgForward_impcache

objc_msgForward_impcache

发现objc_msgForward_impcache只有一行代码
然后再继续搜索__objc_msgForward

__objc_msgForward

__objc_msgForward里面又调用了TailCallFunctionPointer
然后再搜索TailCallFunctionPointer

.macro TailCallFunctionPointer
    // $0 = function pointer value
    braaz   $0
.endmacro

意思是要跳转$0,那么得先看x17x17就是__objc_forward_handler

__objc_forward_handler

那么再全局搜索__objc_forward_handler,发现没有找到,那么再去掉下划线搜索

__objc_forward_handler

我的天哪!原来报错信息是在这里打印的啊!这一波操作666啊!

666
 _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);

2.3 知识点补充

OC底层是不分实例方法(-)和对象方法(+)的,底层会自动给你加上-号和+号,然后按格式打印报错信息。还记得isa走位图吗?iOS底层探索之类的结构(上):ISA

isa走位

从图中,可以知道,根元类的superClass指向了根类,我相信大家都有疑问?

  • 类方法,存在元类里面,实例方法存在类里面。
  • 当调用类方法,会沿着继承链,往上找,一直找到元类都没有找到类方法,就回会去根类里面找,根类里面发现有个同名的实例方法,就会直接调用。
  • 如果根类里面没有同名的实例方法就会报错unrecognized selector sent to instance

这就解释了OC其实是不分+方法和-方法的,对于底层来说,都是消息发送,sel都一样。所以会有根元类的superClass指向了根类这么一个指针的指向。

那么如果方法没有实现,就只能程序崩溃,报错了吗?有没有什么补救措施,好让我拯救地球呢!

有的靓仔,苹果工程师会满足你的!请继续往下看!


我要拯救地球了,😁

2.4 resolveInstanceMethod

JPPerson类的.m文件下,实现resolveInstanceMethod方法

resolveInstanceMethod

程序运行起来报错了,但是我们发现,在报错奔溃之前
走了resolveInstanceMethod方法,那么也就是说,我们可以提前处理,不让程序奔溃。


@implementation JPPerson

- (void)jp_sayNB {
    NSLog(@"%@,%s",self,__func__);
}

+(BOOL)resolveInstanceMethod:(SEL)sel {
    // 方法匹配
    NSString *methodName = NSStringFromSelector(sel);

    if ([methodName isEqualToString:@"jp_sayHello"]) {

        IMP sayNBImp = class_getMethodImplementation(self, @selector(jp_sayNB));
        Method method = class_getClassMethod(self, @selector(jp_sayNB));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, sayNBImp, type);//添加方法
    }
    return [super resolveInstanceMethod:sel];
}

程序跑起来,看看结果如何

添加方法

程序并没有奔溃,而是走了我们添加的方法里面去,这就是对象方法的动态解析。

2.5 resolveClassMethod

那么我们再来看看,类方法的动态解析。

resolveClassMethod
从图中可以看出,在调用了resolveClassMethod方法,也调用了resolveInstanceMethod方法,这是怎么回事呢?
resolveClassMethod

从底层代码来看,处理流程差不多,那么我们现在去实现类方法的动态解析。

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

class_getClassMethod方法,是获取元类的实例方法。在类里面实现resolveClassMethod,相当于元类里面以对象方式存在

JPPerson类调用没有实现方法[JPPerson jp_sayHappy],在JPPerson中实现resolveClassMethod方法解析动态解析

+ (void)jp_say666 {
    NSLog(@"%@,%s",self,__func__);
}

// 相当于 元类中的对象方法
+(BOOL)resolveClassMethod:(SEL)sel {
    //获取元类的对象方法
    NSString *methodName = NSStringFromSelector(sel);

    if ([methodName isEqualToString:@"jp_sayHappy"]) {

        IMP sayNBImp = class_getMethodImplementation(objc_getMetaClass("JPPerson"), @selector(jp_say666));
        Method method = class_getClassMethod(objc_getMetaClass("JPPerson"), @selector(jp_say666));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("JPPerson"), sel, sayNBImp, type);//添加方法
    }
    return [super resolveClassMethod:sel];
}
resolveClassMethod运行结果

动态方法解析,么得问题,jp_sayHappy方法并没有实现,但是在动态方法解析的时候,可以添加jp_say666方法来防止崩溃。上面提到了,resolveInstanceMethod方法还走了一次,这是因为类方法,存在元类里面,但是同时也可以以对象方法的形式存在,就有两条路径,上面isa的走位图,也验证了这个说法,其实也可以解释底层是不分+/-方法的。

但是又暴露出了一个问题,就是每个类都得写一次resolveInstanceMethodresolveClassMethod,那要是有多个类呢?都写一遍就太麻烦了。那么给NSObject写一个分类NSObject+JPResolver就可以很好的解决这种问题。

+(BOOL)resolveInstanceMethod:(SEL)sel {
        // 方法匹配
    NSString *methodName = NSStringFromSelector(sel);

    if ([methodName isEqualToString:@"jp_sayHello"]) {

        IMP sayNBImp = class_getMethodImplementation(self, @selector(jp_sayNB));
        Method method = class_getClassMethod(self, @selector(jp_sayNB));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, sayNBImp, type);//添加方法
        
    }else if ([methodName isEqualToString:@"jp_sayHappy"]) {

        IMP sayNBImp = class_getMethodImplementation(objc_getMetaClass("JPPerson"), @selector(jp_say666));
        Method method = class_getClassMethod(objc_getMetaClass("JPPerson"), @selector(jp_say666));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("JPPerson"), sel, sayNBImp, type);//添加方法
    }
    return NO;
}

那要是上面两种情况,程序员都不处理呢?
苹果:给你机会,你得珍惜啊!你不处理,我也不能崩溃啊!那就再来一次吧!

那么再来一次是什么机会呢?请听下回分解

iOS底层探索之Runtime(五): 消息转发

3.总结

  • lookUpImpOrForward慢速查找imp没有找到会进入动态方法解析流程
  • resolveInstanceMethod实例方法解析,可以动态添加方法,防止崩溃。
  • resolveClassMethod类方法解析,也会调用resolveInstanceMethod,根据isa走位图可以知道,类方法是存在元类里面,但是也会以对象方法形式存在在类中,主要是因为底层不区分+-

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 觉得学习到了的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

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

推荐阅读更多精彩内容