iOS大师养成之路--方法的旅程

1,167 阅读15分钟

1. 关于对OC方法调用开始

1.1 前期的准备工作

1.1.1 准备用于测试的类和方法

我在工程里准备了这么一个类LCHero,有一个对象方法throwSkill, 继承至LCPerson。 LCPerson里面有一个对象方法attack, 一个类方法defence,LCPerson 继承至NSObject. 我在NSObject的一个分类里准备了一个测试方法test 代码如下:

#import <Foundation/Foundation.h>
#import "LCHero.h"
#import <objc/message.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LCHero *hero = [LCHero new];
        [hero throwSkill];
    }
    return 0;
}
/******************************************/
@interface LCPerson : NSObject

- (void)attack;
+ (void)defence;
- (void)revival;

@end

@implementation LCPerson

- (void)attack {
    NSLog(@"%s -->  开始进攻!",__func__);
}

+ (void)defence {
    NSLog(@"%s ||--  开始防御! ",__func__);
}

@end
/******************************************/
@interface LCHero : LCPerson
- (void)throwSkill;
@end

@implementation LCHero
- (void)throwSkill {
    NSLog(@"%s --> 释放终极技能!",__func__);
}

@end

/******************************************/
@interface NSObject (test)
- (void)test;
@end

@implementation NSObject (test)
- (void)test {
    NSLog(@"%s, 测试一下!",__func__);
}
@end

1.1.2 Clang 编译命令对main.m 编译一下看看.cpp文件对应的内容

使用的命令如下:

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk main.m

我们看到的对应的cpp文件中对main文件中我们写的内容的c++编译内容

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LCHero *hero = ((LCHero *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCHero"), sel_registerName("new"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)hero, sel_registerName("throwSkill"));
    }
    return 0;
}

如果把方法的类型以及类型转换去掉,就是如下的样式。我们发现底层调用的是一个objc_msgSend方法,我们调用的方法被转成了SEL。

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LCHero *hero = (LCHero *)objc_msgSend(objc_getClass("LCHero"), sel_registerName("new"));
        (void *)objc_msgSend(hero, sel_registerName("throwSkill"));
    }
    return 0;
}

2. 方法旅程的第一站--> objc_msgSend一日游

2.1 objc_msgSend第一个景点--> objc_msgSend汇编流程

既然我们知道了底层吊起的是objc_msgSend,那么我们在方法调用之前打一个调试断点,当断点来了之后我们按住control键 + step into 一步一步点击看看它会怎么走。

按住点击几下之后,来到了这里。居然是汇编!!

我们配置好相应的汇编源码,去找下这个流程。

2.1.1 通过isa获取类

来到这一步之后,我们想既然方法这些都在类里面,而类和对象是通过isa联系起来的,是不是要找isa呢?然后通过isa找到对应的类,答案是肯定的。

2.1.2 查找缓存cache_t

找到class之后开始查找类中的方法缓存。

找到缓存直接返回,这是最佳的也是最快的方式,为什么要用汇编走这个流程呢?因为执行效率更高。

没有找到缓存开始进入到下一流程,方法表查询

到此,快速的汇编查找方法流程告一段落,我们进入下一景点:慢速查找

2.2 objc_msgSend第二个景点--> 在c、c++中慢速查找imp

2.2.1 _class_lookupMethodAndLoadCache3

通过上面那个景点我们看到了_class_lookupMethodAndLoadCache3方法,源码中如下:发现它直接调起一个下层方法lookUpImpOrForward,_class_lookupMethodAndLoadCache3只是起到中间连接作用

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

2.2.2 lookUpImpOrForward

这个方法里面的内容有点多同时也很重要,我就把判断、断言、赋值的代码都删掉了,保留一些关键的方法和注释。大家先过个眼隐。我们再一一分析下。

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{

    if (!cls->isRealized()) {  //先判断类有没有加载到内存,如果没有,先加载类
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {//判断是否实现了initialize,如果有实现,先调用initialize
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }
    
 retry:    

    // Try this class's cache.

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
        //从类的方法表里查询,如果有就返回imp 顺便存一份到类的cache里
    
    // Try superclass caches and method lists.
         // Found the method in a superclass. Cache it in this class.
    
    // No implementation found. Try method resolver once.
    
    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();
    return imp;
}
2.2.2.1 方法查找前的准备-->继承链上的类以及分类都得准备
  • cls->isRealized()判断类是否加载了,realizeClass(cls)这是一个递归操作,所有继承链上的类都会被加载。父类-->根类-->根元类,直到cls为空才退出递归。
  • supercls = realizeClass(remapClass(cls->superclass)); metacls = realizeClass(remapClass(cls->ISA()));加载父类元类一直递归加载,为的就是方便方法查找。
  • 连接关联的子类以及父类
  • methodizeClass(cls)分类方法加载我们来看看他们做了什么,主线流程我在代码块中介绍了
先看注释,我英文不太好但是大概意思明白了,把方法、协议、属性安排好,然后把外面的分类也添加进来 --> 感觉看到了美景 ><
/***********************************************************************
* methodizeClass
* Fixes up cls's method list, protocol list, and property list.
* Attaches any outstanding categories.
* Locking: runtimeLock must be held by the caller
**********************************************************************/

我们把这个方法的主要内容拆开说明一下
1. 把方法、属性、协议从类的ro 拷贝到 rw中来,为啥有个1?-->是因为把数组的首地址传进去组成了一个二维数组

  method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }
    
2. 判断是否是根元类,如果是根元类需要把方法加载一下,确保在分类替换之前就已经加载好了 --> ? 下面的注释也有,
就是说如果根类调用了一个它自己没有的方法,它会往根元类中找。
我的根元类要是有相关方法我要把他添加到我的类的方法表里面,它才能找的得到,而且要早于分类方法添加之前。
为什么是在分类替换之前呢?我在这里只能大胆猜想一下,方法表设计的可能是一个类似栈的表,
如果有分类在后面添加之后那么我就找imp的时候就先找到分类的imp就返回了,就出现了替换了类的方法的这么个现象
    // Root classes get bonus method implementations if they don't have 
    // them already. These apply before category replacements.
    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }
    
3. 添加分类方法
 // Attach categories.
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);

2.2.2.2 再次尝试cache_getImp(cls, sel)

我们前面已经找过缓存了,为什么还要找缓存呢?原因有2:

  • 可能是多线程找,万一有线程有返回了呢是不是可以直接调用。
  • 提升效率,有就立马调用有助于提升性能-->毕竟是苹果爸爸写的代码
2.2.2.3 没有缓存就往类、递归父类的方法表里找
    Method meth = getMethodNoSuper_nolock(cls, sel);//找类的方法表
    log_and_fill_cache(cls, meth->imp, sel, inst, cls);//找到就缓存一份
    imp = meth->imp;
    goto done
    
    for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
    imp = cache_getImp(curClass, sel); //递归找父类的方法表
    log_and_fill_cache(cls, meth->imp, sel, inst, cls);//找到就缓存一份
    imp = meth->imp;
    goto done
2.2.2.4 遍历了类、递归了父类之后还没有找到

如果我们找了类的方法表,同时递归找了父类都没有找到,由于我们传递的resolver默认是YES同时triedResolver也没有进行从新赋值还是NO,我们会走下到下一站方法决议_class_resolveMethod,具体源码在下面,

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

3. 方法旅程的第二站--> method resolver 动态方法决议

在经历了类->父类->元类->根元类->...->NSObject ,分类等一系列的查找之后没有找到,那然后怎么办?我们的旅程还要继续啊!苹果爸爸还是很心疼我们的,给你个机会处理一下吧,我不直接让它崩溃。于是我们看到了下一站的风景_class_resolveMethod(cls, sel, inst)

3.1 _class_resolveMethod

我们来看下这个方法里有啥东西,具体代码在下面代码块。步骤在下面分析

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

3.1.1 如果当前传入的类不是元类--> 对象方法

  • 为什么要判断?--> 对象方法在类里面,类方法在元类里面

  • 注释中写try [cls resolveInstanceMethod:sel] ?--> 我们的类里面没有怎么try? 是不是系统帮我们实现了这个方法,不知道,继续往下看

  • 先判断是否没有实现这个方法,如果没有就直接返回。我自己写的类里面没有这个方法,如果我们自己没有实现的话是不是系统帮我们实现?

  • 在objc的源码中找到了这个方法,默认返回的是NO.

  • 现在我们思考一下,如果我能让这个方法执行下去势必要找到一个imp返回回去。如果我们重写这个方法然后添加一个imp到类里面是不是就解决这个问题了呢?我们去文档中查一下这个方法,果然验证了我们的想法。

  • 不管我们有没有处理lookUpImpOrNil都会调起,然后再回到lookUpImpOrForward,因为已经retry过了这个结果已经保存了,如果找到imp直接到done流程,如果还是没找到就会来到imp = (IMP)_objc_msgForward_impcache.

  • 我们搜一下,结果这个家伙在汇编里面调用的是__objc_msgForward

  • 我们再看看__objc_msgForward,它里面调用的有两个很像的方法,再搜一下就发现在objc的源码中有实现,这里的打印内容我们好熟悉哦,我们来试下在我们开始准备的main里面调用LCHero没有实现的对象方法- (void)revival看看错误输出 --> 没错就是没找到方法的报错输出

3.1.2 如果当前传入的类是元类--> 类方法

类方法和对象方法处理有点不太一样调用的是resolveClassMethod,添加imp是往元类里面添加,只是和对象方法类似的处理流程只不过调用的方法不一样,只是调用完类方法决议之后居然还走了对象方法的决议。我们就猜想为什么还要走这步。

  • 类方法在元类里面,我们程序员一般不能直接操作元类,是很有可能找不到类方法。
  • 不管找不找得到,一旦查找了类方法的动态决议之后就会lookUpImpOrNil再次查找一下如果还是没有就会走一次对象方法决议_class_resolveInstanceMethod
  • 也就意味着如果调用的类方法没有会一直往父类的继承链中找直到NSObject,NSObject类我们不能直接改,但是可以写一个分类拓展方法,如果我们再NSObject分类中进行相应类方法的动态决议就能截获这类崩溃。
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }

但是就这样结束了么?我们再来看看奔溃时候的堆栈是不是还有我们没了解过的方法,明显还有。

但是我们实在跟不进去流程了怎么办?我们记得在父类以及元类递归找方法的时候有个log_and_fill_cache方法,除了fill_cache 还有log,不妨一看究竟。发现调用了logMessageSend,再往里面看看发现了一个类似log开关控制的参数

这个开关参数是怎么控制的呢?在👇

  • 这里我们不妨大胆地玩一下,因为根据我们在log_and_fill_cache的流程中发现它有个打印log的方法,还会写到一个文件里,这个文件里根据它的注释说会给我们一些方法有关的线索。我们把这个开关在我们准备的工程中拓展一下使用范围在奔溃前后都调用了啥方法?

  • 再运行一下,我们在/tmp/msgSends 找有没有类似的文档记录log

  • 打开一看,OMG, 啥!! 都打印出来的确有两个我们没跟出来的流程一个是forwardingTargetForSelector,另一个是methodSignatureForSelector。忽然发现我们还想逛的还要很多,我们都想看看后面两站都是什么风景!!

4. 方法旅程的第三站--> forwardingTargetForSelector

表面意思是传递一个对象,什么意思?原来我方法调用时传入的对象不要了么? 我们来看看源码是不是NSObject也实现了只是跟上一个站点一样没有做处理额?

  • 我自己没有,父类没有,元类也没有。-->是不是别人有也可以呢如果交个有这个方法的对象处理原则上也是OK的嘛。

卧槽,果然实现了只是直接返回了一个nil,这里就引发了我的另一个猜想,如果返回一个实现了这个方法的对象呢是不是就解决问题了。我们试一试哈

  • 我们新建一个类,LCEnemy,继承之LCPerson 然后实现- (void)revival
@implementation LCEnemy
- (void)revival {
    NSLog(@"%s --> 哈哈哈 本魔王又活了!",__func__);
}
@end
  • 然后LCHero重写forwardingTargetForSelector,返回一个LCEnemy对象。
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return [NSClassFromString(@"LCEnemy") alloc];
}

  • 运行再看结果。 >< 惊不惊喜意不意外 --> 居然调用了LCEnemy方法。
    可是还有个方法我还没看到啊,下一站我们很期待啊,没毛病 --> 赶紧上车去往下一站

4. 方法旅程的第三站--> methodSignatureForSelector

到这个流程之后也就意味着:

  • 我自己没有对应的方法处理
  • 继承链也没有谁能处理
  • 也没有动态方法决议处理
  • 别人也没有这个能力处理

那这个时候我是不是要在网上发给求助帖-->看看哪个好心人能处理。 但是总得知道你这个方法是什么格式吧,不然别人怎么知道能不能处理?-->方法调用必须签名和SEL 相匹配才会被调用。 我们来看看官方文档怎么解释的

这个方法要想有效地防止崩溃的话有个使用前提就是要实现另外一个方法- (void)forwardInvocation:(NSInvocation *)anInvocation; 那么我们来试一下:

  • 我们还是使用之前准备的工程实现这两个方法,只是把上一步注释掉。
// 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(revival)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 消息转发 -- 开始祈祷谁来处理一下
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    NSLog(@"%@",anInvocation);
}
  • 调用查看一下结果,走了forwardInvocation方法,但是没有崩溃直接走完了。--> 意味着如果我发出的方法签名没有人处理这个方法调用流程就结束了,相当于一个无效的方法,没有任何响应。

5. 方法旅程的第四站--> doesNotRecognizeSelector

如果我们调用没有实现的方法,既没有动态决议、也没有转发给其他对象处理同时也没有写求助帖🙏好心人来处理此时苹果爸爸也帮不了你,只好结束你的方法旅程,给你一张红色的回程票doesNotRecognizeSelector --> 熟悉的崩溃

6. 写在最后的回顾和思考

6.1方法的流程回顾

objc_msgSend汇编流程 -->从类缓存中快速查找imp
    _class_lookupMethodAndLoadCache3 --> 开始进入c、c++慢速查找
        lookUpImpOrForward --> 继承链上的类的方法表里遍历查找,找到了缓存一份然后返回imp
            _class_resolveMethod --> 开始查看是否有动态决议,如果有给到imp,重新lookUpImpOrForward
                forwardingTargetForSelector --> 自己没有处理,是否有交给别人代理处理。
                    methodSignatureForSelector + forwardInvocation --> 如果也没有代理者,请按照规范写求助信
                        doesNotRecognizeSelector --> 如果什么都不做,你太懒了 苹果爸爸表示上帝也救不了你。

6.2方法旅程的思考

经过这段方法探索的旅程我领悟了一些东西

  • 崩溃是可以有效预防的,甚至是可以自己搜集的。--> 原来那些崩溃统计的SDK也是从这个思路去做的
  • 之前觉得很神奇的组件化、路由啥的,经过这次旅程已经被我揭掉了神秘面纱。--> 调用方法并不一定需要引入相应的类的头文件
  • 苹果对方法调用防崩溃的策略还是很丰富的。--> 给了开发人员3次挽救处理的机会
  • 方法在下层调用匹配是操作的是SEL,是一个数值而不是字符串比较。而且方法是有签名的,只有SEL和签名匹配成功才会认为是可以调用的方法。
  • 其实在每次方法调用的时候都传了一个id(调用方法的对象),这也是为什么我们能在任何方法里面能轻易地拿到self
  • 一些控制打印的东西能很好地帮助我们进行Bug分析,如void instrumentObjcMessageSends(BOOL flag),在合适的时候拓展作用域就能跟踪一些方法调用的线索。

感谢大家的阅读,如果你觉得写得还可以请动动你们的小手给我点个赞。我会更有动力给大家分享一下好东西。下一次计划更新关于类的加载的文章。有兴趣交流学习的可以加我QQ:578200388