OC底层探索之objc_msgSend

runtime

我们都知道大部分语言是编译时决议的,而Object-C是在运行时决议,这来源于强大的runtime。通过runtime可以动态对类各方面进行配置,还有就是消息传递。消息传递其实就是通过objc_msgSend按照sel找到函数imp的过程。

objc_msgSend

新建一个工程,在main.m文件夹内创建一个LGPerson类。在main函数内部调用[p study][p happy]

@interface LGPerson : NSObject

- (void)study;
- (void)happy;
+ (void)eat;

@end

@implementation LGPerson

- (void)study {
    NSLog(@"%s",__func__);
}
- (void)happy {
    NSLog(@"%s",__func__);
}
+ (void)eat {
    NSLog(@"%s",__func__);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LGPerson *p = [LGPerson alloc];
        [p study];
        [p happy];
        
    }
    return NSApplicationMain(argc, argv);
}

通过clang探索objc_msgSend

使用clang编译,打开该工程目录输入clang -rewrite-objc main.m,然后打开该工程目下main.cpp文件,我们可以看到[p study][p happy]被编译成了objc_msgSend()函数。第一个参数是接受者,第二个参数是方法名,系统会根据这2个参数找到方法具体实现。

LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("study"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("happy"));

我们把 [p study]直接改为编译后的方法((void (*)(id, SEL))(void *)objc_msgSend)((id)p, NSSelectorFromString(@"study")),直接运行验证,我们可以看到study方法是可以执行。

编译后的方法验证

我们在study方法加个参数str,看看clang编译文件,打开该工程目录输入clang -rewrite-objc main.m,然后打开该工程目下main.cpp文件。

加上参数的objc_msgSend

我们可以看到如果有参数的话,系统编译会自动加上相应的参数。

((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("study:"), (NSString *)&__NSConstantStringImpl__var_folders_mx_2ljwkcpn0kg_4m1bgs1507f00000gn_T_main_5b862f_mi_3);

objc_msgSend的种类

我们查看main.cpp文件的顶部。我们发现有5种objc_msgSend

  • objc_msgSend : 发消息给本类
  • objc_msgSendSuper : 发消息给父类
  • objc_msgSend_stret : 发消息返回值是一个结构体
  • objc_msgSendSuper_stret : 发消息给父类,返回值是一个结构体
  • objc_msgSend_fpret : 发消息返回值是一个浮点型的
    objc_msgSend的种类

objc_msgSendSuper

首先我们来个经典的面试题,新建一个LGPerson,还是申明一个study方法,再新建一个LGTeacher继承自LGPerson,在LGTeacher的init方法里面打印[self class][super class]

@interface LGPerson : NSObject

-(void)study;

@end
@interface LGTeacher : LGPerson

@end
@implementation LGTeacher

-(instancetype)init {
    if (self = [super init]) {
        NSLog(@"%@",[self class]);
        NSLog(@"%@",[super class]);
    }
    return self;
}
@end

初始化一个LGTeacher的实例对象,我们通常会认为第一个打印是LGTeacher,第二的打印位LGPerson。看看是不是啊,运行。

初始化LGTeacher实例对象

我们可以看到运行结果2个打印都是LGTeacher。这是为什么呢?使用clang编译,打开该工程目录输入clang -rewrite-objc LGTeacher.m,打开LGTeacher.cpp文件,找到init函数,然后我们可以看到了[super class]其实就是调用了objc_msgSendSuper函数。
运行结果

objc_msgSendSuper

那我看苹果官方文档是怎么解释objc_msgSendSuper这个函数的,首先打开Xcode菜单栏的help然后点击Developer Documentation,选择Objective-C,然后点击搜索框搜索objc_msgSendSuper
Developer Documentation

objc_msgSendSuper官方文档

查看objc_msgSendSuper官方文档。

  • Parameters
    super
    A pointer to an objc_super data structure. Pass values identifying the context the message was sent to, including the instance of the class that is to receive the message and the superclass at which to start searching for the method implementation.
    op
    A pointer of type SEL. Pass the selector of the method that will handle the message.
    ...
    A variable argument list containing the arguments to the method.

  • Return Value
    The return value of the method identified by op.

  • Discussion
    When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.

  • 参数
    父类
    指向 objc_super 数据结构的指针。 传递标识消息发送到的上下文的值,包括要接收消息的类的实例和开始搜索方法实现的超类。
    操作
    SEL 类型的指针。 传递将处理消息的方法的选择器。
    ...
    包含方法参数的变量参数列表。

  • 返回值
    op 标识的方法的返回值。

  • 讨论
    当遇到方法调用时,编译器生成对函数 objc_msgSend、objc_msgSend_stret、objc_msgSendSuper 或 objc_msgSendSuper_stret 之一的调用。 发送到对象超类的消息(使用 super 关键字)使用 objc_msgSendSuper 发送; 其他消息使用 objc_msgSend 发送。 将数据结构作为返回值的方法使用 objc_msgSendSuper_stret 和 objc_msgSend_stret 发送。
    我们查看苹果官方文件可以查看到(使用 super 关键字)使用 objc_msgSendSuper 发送,所以说我们前文[super class]是实际是调用objc_msgSendSuper函数的,接受者是类的实例。
    我们可以看到main.app内部[super class],其实接受者还是self一个LGTeacher的实例对象,所以说消息的接受者还是LGTeacher的实例对象,所以[super class]输出为LGTeacher

    main.app

重写objc_msgSendSuper

我们在子类LGTeacher重写父类LGPersonstudy方法,然后不进行调用,然后我们重写objc_msgSendSuper。查看objc_super结构体我们可以发现它需要传一个receiver(接受者)和super_class(父类)。receiver还是LGTeachersuper_classLGPerson.class

    struct objc_super lg_objc_super;
    lg_objc_super.receiver = self;
    lg_objc_super.super_class = LGPerson.class;

    void* (*objc_msgSendSuperTyped)(struct objc_super *self,SEL _cmd) = (void *)objc_msgSendSuper;
    objc_msgSendSuperTyped(&lg_objc_super,@selector(study));
重写objc_msgSendSuper方法

子类调用study方法

objc_super结构体

运行我们可以发现是可以调用LGPerson的study方法。

study方法调用

那我们把super_class改为NSObject.class试试;

lg_objc_super.super_class = NSObject.class;

运行,我们可以看到是找不到这个study方法的,因为使用objc_msgSendSuper时候它会直接从它的super_class直接找相应的方法实现,我们设置的是NSObject.classNSObject没有实现study方法,所以就直接就找不到study方法的实现了。

找不到方法

方法的快速查找

我们是结合objc4-838进行探索的,我们全局搜索objc_msgSend,可以看到有很多,我们找到真机arm64架构下的,objc_msgSend是由汇编写的,为什么会采用汇编呢,是因为汇编效率性能高,可以节约不少的时间。

objc_msgSend

我们新建个工程,连上真机,在[t study]处打上断点,然后再加上objc_msgSend符号断点。运行至断点处打开objc_msgSend符号断点。我们就可以看到汇编了。
objc_msgSend符号

然后在加上符号

进入objc_msgSend函数

  • objc_msgSend汇编解析
//进入objc_msgSend流程
    ENTRY _objc_msgSend
//流程开始,无需frame
    UNWIND _objc_msgSend, NoFrame

//判断p0(消息接受者)是否存在,不存在则重新开始执行objc_msgSend
    cmp p0, #0          // nil check and tagged pointer check

//如果支持小对象类型。返回小对象或空
#if SUPPORT_TAGGED_POINTERS
//b是进行跳转,b.le是小于判断,也就是小于的时候LNilOrTagged
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
//等于,如果不支持小对象,就LReturnZero
    b.eq    LReturnZero
#endif
//通过p13取isa
    ldr p13, [x0]       // p13 = isa
//通过isa取class并保存到p16寄存器中
    GetClassFromIsa_p16 p13, 1, x0  // p16 = class
//LGetIsaDone是一个入口
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
//进入到缓存查找或者没有缓存查找方法的流程
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// nil check判空处理,直接退出
    b.eq    LReturnZero     // nil check
    GetTaggedClass
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

我们可以看到是和objc4-838中objc_msgSend函数汇编是一样的。

  1. 首先进入objc_msgSend流程,cmp p0,判断p0是否存在,我们读取寄存器看看,可以看到x0是一个LGTeacher的实例对象,x1study方法。
    读取寄存器x0 x1

    2.ldr p13, [x0]通过p13isa指针地址,我们可以打印x13地址为0x000021a1047b96e1,然后我们通过x0打印对象的isa,可以看到x13就是对象的isa指针。
    isa

    3.GetClassFromIsa_p16 p13, 1, x0:通过isa&掩码(0xffffffff8)获取class并保存到p16寄存器中,我们打印x16可以看到x16是类对象地址。
    image.png

    4.CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached开始找方法,取出x16class移到x15,通过x16cache
CacheLookup

5.ldr x11, [x16, #0x10]x16通过内存平移可以得到x11,x11就是之前cache详解内的第一个8字节成员_bucketsAndMaybeMask

验证x11

6.and x10, x11, #0xfffffffffffex11&0xfffffffffffe就得到buckets的首地址x10。然后在cache进行方法查找。如果找到了会进行CacheHit(缓存命中),找不到的话会调用_objc_msgSend_uncached函数。
CacheHit

_objc_msgSend_uncached函数

总结:
当调用objc_msgSend(receiver, sel)时:

  • receiver是否存在
  • 通过receiverisa指针获取类对象
  • 通过类对象内存平移获取cache
  • 通过cache找到buckets
  • 根据buckets找相应的sel
  • 如果有相应的selCacheHit函数
  • 如果没有相应的sel会调用_objc_msgSend_uncached函数

方法的慢速查找

方法在cache内找不到就是调用_objc_msgSend_uncached函数,我们在_objc_msgSend_uncached函数内部可以看到它会调用MethodTableLookup函数,MethodTableLookup函数会调用_lookUpImpOrForward_lookUpImpOrForward函数在汇编内是看不到,那我们直接在源码内搜索lookUpImpOrForward

_objc_msgSend_uncached

MethodTableLookup

我们可以看到lookUpImpOrForward函数内部首先会判断cache内部有没有方法,因为在多线程环境下现在cache可能有该方法。
判断cache内部有没有方法

如果本类没有的话会调用getMethodNoSuper_nolock函数,然后依次调用search_method_list_inlinefindMethodInSortedMethodList函数。我们可以看到findMethodInSortedMethodList是采用二分查找来获取方法的。
lookUpImpOrForward

getMethodNoSuper_nolock
search_method_list_inline

findMethodInSortedMethodList

当我们在本类里面找到方法时,它会进行goto done,然后会进入log_and_fill_cache函数,可以看到log_and_fill_cache函数内部调用了cache.insert()方法,也就是加入缓存里面了。需要注意的是哪个类调用就会加入哪个类的cache里面,也就是如果子类调用父类的方法,之后该方法会缓存到子类的cache
done

log_and_fill_cache

如果本类也没有方法,通过curClass = curClass->getSuperclass(),把curClass转换成父类,然后找父类的cachecache如果没有在通过二分查找找方法列表。如果再没有把curClass转换成父类的父类依次查找。
curClass->getSuperclass()

总结:
消息的慢速查找流程:

  • 调用lookUpImpOrForward
  • 看本类的cache里面有没有该方法
  • 如果没有看本类的methodList有没有该方法(二分查找)
  • 如果没有看父类的cache里面有没有该方法
  • 如果没有看父类的methodList有没有该方法(二分查找)
  • 然后逐级向上查找
  • 当父类是nil的时候也就是查找到NSObject的时候,都没有的话就会进入消息转发流程。

你可能感兴趣的:(OC底层探索之objc_msgSend)