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

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

1.1 前期的准备工做

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

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

#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++编译内容objective-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找到对应的类,答案是确定的。bash

2.1.2 查找缓存cache_t

找到class以后开始查找类中的方法缓存。 多线程

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

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

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

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

2.2.1 _class_lookupMethodAndLoadCache3

经过上面那个景点咱们看到了_class_lookupMethodAndLoadCache3方法,源码中以下:发现它直接调起一个下层方法lookUpImpOrForward,_class_lookupMethodAndLoadCache3只是起到中间链接做用app

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

2.2.2 lookUpImpOrForward

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

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

相关文章
相关标签/搜索