iOS底层探索八(方法本质上)

 前言


    相关文章:   
       iOS底层探索一(底层探索方法)       

       iOS底层探索二(OC 中 alloc 方法 初探)

       iOS底层探索三(内存对齐与calloc分析)  

iOS底层探索四(isa初探-联合体,位域,内存优化)     

iOS底层探索五(isa与类的关系)  

iOS底层探索六(类的分析上)

iOS底层探索七(类的分析下)

iOS底层探索九(方法的本质下objc_msgSend慢速及方法转发初探)

iOS底层探索十(方法的本质下-消息转发流程)

    相关代码:
      objc4_752源码 方法的本质    方法分发

    温馨提示

这里先需要和大家解释一下,相关代码每次里面都只有objc4_752,因为这里探索的时候这份代码一直在github上,我这边会把每次探索中,需要对应的类会进行添加相应的注释,其中如果,有的人下载下来代码后,发现运行后结果不一样,例如XZPerson类中可能当前文章中需要里面有属性,成员变量,实例方法,类方法 等,会直接添加上,但是后续文章可能不需要,所以下载下来源码后,建议大家可以结合博文进行对应阅读,其中README.md文件中,也会添加上对应博文地址,后续文章会根据不同文章直接新建不同的target工程,避免影响。

前几篇文章,我们分析了,alloc方法,对象的本质,类的本质,这篇文章我们对方法的本质进行探索;

方法的本质

1.方法底层调用

首先写一个简单Demo 

#import <Foundation/Foundation.h>
#import "XZPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        XZPerson *p = [XZPerson alloc];
         
        [p sayHappy];
    }
    return 0;
}


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface XZPerson : NSObject
-(void)sayHappy;

@end

NS_ASSUME_NONNULL_END

#import "XZPerson.h"

@implementation XZPerson
-(void)sayHappy{
    NSLog(@"say happy")
}
@end

可以看出,很简单,只是在main函数中调用了一个XZPerson类中的一个方法,下面我们通过clang,对main文件进行编译;使用终端进入main文件目录:clang -rewrite-objc main.m -o main1.cpp  ,同级目录下生成编译后cpp文件

直接查看main函数

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        XZPerson *p = ((XZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XZPerson"), sel_registerName("alloc"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHappy"));
    }
    return 0;
}

我们将类型强转去掉方便我们对代码进行分析

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        XZPerson *p = (objc_msgSend)(objc_getClass("XZPerson")
                                    ,sel_registerName("alloc"));

        (objc_msgSend)(p, sel_registerName("sayHappy"));
    }
    return 0;
}

这样我们就可以很清晰的看出来,方法的本质,其实就是底层通过objc_msgSend方法进行发送消息其中有2个参数

  • 参数一:消息接收者

  • 参数二:sel类型的方法编号

这里的方法编号sel 就可以联系到上篇文章中的cache中,通过sel在找到imp 并在cache中进行方法查找,并进行返回

我们在main中写一个C语言的方法

#import <Foundation/Foundation.h>
#import "XZPerson.h"

void run(){
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        XZPerson *p = [XZPerson alloc];
         
        [p sayHappy];

        run();
    }
    return 0;
}

再次进行clang :这次输出main2.m文件,编译完成后查看源码


void run(){
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_vl_y9rmzqq165ngxyd_yz2dmz0h0000gn_T_main_784d77_mi_0,__func__);
}

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        XZPerson *p = ((XZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XZPerson"), sel_registerName("alloc"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHappy"));

        run();
    }
    return 0;
}

我们可以看到C语言方法,直接进行调用,而我们的OC方法需要使用objc_msgSend方法进行查找方法进行调用,这就说明

objc_msgSeng 方法其实就是OC方法进行查找底层实现的方法实现的过程并进行调用,而C语言函数实现可以直接找到C语言函数名就是函数指针直接找到地址进行调用;

2.方法几种情况

当前类方法由objc_msgSend,进行底层查找,父类使用objc_msgSendSuper方法进行底层查找

实例方法:objc_msgSend -->person对象-->找到对应sel -->对应imp

类方法:objc_msgSend-->Person类  -->找到sel  -->对应imp

我们先看一下objc_msgSend 方法底层是怎么声明的:

需要一个消息接收这,和一给方法编号
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

再看一下objc_msgSendSuper 是怎么声明的

需要声明一给super结构体 和一个方法编号
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

结构体中有一下属性
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

我们使用objc_msgSend,和objc_msgSendSuper代码演示下怎么调用方法

首先XZPerson继承NSObject ,XZTeacher 继承XZPerson;在main函数中来进行编码,

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface XZPerson : NSObject
- (void)sayHello;
+ (void)sayNB;

@end
#import "XZPerson.h"

@implementation XZPerson
- (void)sayHello{
    NSLog(@"%s",__func__);
}

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

@end

#import "XZPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface XZTeacher : XZPerson
- (void)sayCode;

@end

NS_ASSUME_NONNULL_END
@implementation XZTeacher
- (void)sayCode{
    NSLog(@"%s",__func__);
}

@end

 在main函数中编码调用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        XZTeacher *t = [XZTeacher new];
        [t sayCode];
        // 方法调用底层编译
        // 方法的本质: 消息 : 消息接受者 消息编号 ....参数 (消息体)
        objc_msgSend(t, sel_registerName("sayCode"));
        
        // 类方法编译底层
        //        id cls = [XZTeacher class];
        //        void *pointA = &cls;
        //        [(__bridge id)pointA sayNB];
        objc_msgSend(objc_getClass("XZTeacher"), sel_registerName("sayNB"));
        
        // 向父类发消息(对象方法)
        struct objc_super xzSuper;
        xzSuper.receiver = t;
        xzSuper.super_class = [XZPerson class];
        objc_msgSendSuper(&xzSuper, @selector(sayHello));
        
        //向父类发消息(类方法)
        struct objc_super myClassSuper;
        myClassSuper.receiver = [t class];
        myClassSuper.super_class = class_getSuperclass(object_getClass([t class]));// 元类
        objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));
         
    }
    return 0;
}

我们可以看到输出结果:

按照这种调用方式,方法是可以正常被调用并进行输出打印的,这里需要注意的是,如果要实验objc_msgSend 方法,我们需要在工程的bulid Setting 找到下面属性并置为NO ,否则clang替我们检查语法会直接报错的;

看来现在所有问题都集中在objc_msgSend方法,那我们先看一下objc_msgSend方法在那个库中

3.objc_msgSend所在库探索

我们还是在main函数中写个方法,打断点进行调用,我们打开Xcode栈地址调试模式Debug->DebugWorkflow->Always Show Disassembly,在调用[person sayHello],方法处打断点看下;

结果

看到objc_msgSend方法:我们进入这个方法,

这里我们可以看到是libobjc.A.dylib库中的objc_msgSend方法,对于这个libobjc.A.dylib库我们还是了解的objc4_752中就有这个库,我们可以进入继续源码的探索了;

4.objc_msgSend源码探索

在objc_msgSeng查找方法分为2种情况

1.objc_msgSend汇编写的《快速查找流程》

     1.1:因为RunTime底层是有C,C++和汇编阻证的一套API,而C,C++是一种静态语言

            1.2:是因为在C,C++ 语言中没有必要通过写一个函数来保留位置的参数,并且跳转到任意的函数指针,C,C++语言没有满足做这个事情的必要性

           1.3:是objc_msgSend必须要快,快的话,汇编语言肯定比C,C++速度要快

       2.objc_msgSend慢速查找流程使用C语言进行实现的

4.1objc_msgSend快速查找

  由于快速查找为汇编语言,本人能力有限,看了个大概,告诉大家位置,可以自行查看,大概梳理了下流程做了部分注释

  我们主要看的是,因为我们手机为arm64架构,所以只需要看这个objc-msg-arm64.s类即可

  1. ENTRY _objc_msgSend
  2. 对消息接受者进行判断处理 (id self, sel _cmd)
  3. taggedPointer 判断处理
  4. GetClassFromisa_p16 isa指针处理 —>找到class
  5. CacheLookup 查找缓存 
  6. ’_cache_t’处理’bucket‘ 以及内存哈希处理(这里其实就是上篇文章中cachet进行处理)
  7.  __objc_msgSend_uncached 告诉我们找不到缓存的 ’imp‘
  8.  ’STATIC_ENTRY’ __objc_msgSend_uncached;
  9. MethodTableLookup:方法查找  
  10. .save parameter register 参数进行准备就绪self和_cmd进行准备
  11. 调用C方法_class_lookupMethodAndCache3调用

今天有点事情,就先写这么多吧,下篇文章对objc_msgSend慢速流程进行详细分析;

总结:

此篇文章对方法的本质进行了初步探索,发现了方法是指是RunTime底层调用objc_msgSend 进行消息转发,并对快速转发流程进行了稍微梳理;如果有错误的地方还请指正,大家一起讨论,开发水平一般(文章中有错误后,我发现会第一时间对博文进行修改),还望大家体谅,欢迎大家点赞,关注我的CSDN,我会定期做一些技术分享!

写给自己

喋喋不休不如观心自省,埋怨他人不如即听即忘。能干扰你的,往往是自己的太在意,能伤害你的,往往是自己的想不开,未完待续...

 

原创文章 88 获赞 21 访问量 2万+

猜你喜欢

转载自blog.csdn.net/ZhaiAlan/article/details/104813887