iOS底层分析之类的探究-cache之 insert、objc_msgSend

承接文章iOS底层分析之类的探究-cache篇继续:

前面我们说了cache方法缓存数据通过insert方法来插入的,但从始自终,这都是我们的猜测以及一些推导,我们并没有看到明确的流程走向,接下来我们借助objc源码,查看整个cache从写入到读取的构成,了解整个cache的完整链走向。由于我们目前的切入点只有insert方法,我们也不知道到底是谁调用了insert,所以我们干脆在insert方法打个断点,然后通过bt指令打印堆栈信息:

01.png

02.png

我们发现了一个跟cache有关系的方法,复制并项目里全局搜索一下“log_and_fill_cache”,看看这个方法是不是我们insert的上一层调用者:

03.png
果不其然,通过comand+单机insert可以确定,log_and_fill_cache就是insert的上层调用者,接下来再搜一下“log_and_fill_cache”找到它的上层调用者:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    ...
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    ...
};
复制代码

lookUpImpOrForward这个里面就涉及到消息的转发知识点,在那之前我们要先知道,我们平时发起消息,有三种方法:oc层面的发起、runtime API层面的发起、framework层面的发起,

06.png
针对以上三种,下面我们来一一测试下:

第一种:

void test02(void){
    
    Student *stu = [Student alloc];
    
    [stu run];//实例方法
}

int main(int argc, char * argv[]) {
    test02();
    ...
};
复制代码

首先,终端或iTerm里cd到main.m所在目录;然后输入以下命令
$xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
目的是在目录下生成一个main.cpp文件,然后我们打开它,全局搜索“test02(void)”:

void test02(void){

    Student *stu = ((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc"));

    ((void (*)(id, SEL))(void *)objc_msgSend)((id)stu, sel_registerName("run"));
}
复制代码

我们可以看到[Student alloc]在底层变成了

((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc"))
复制代码

分析可以得到,方法在底层的调用是通过objc_msgSend进行消息的发送,而它需要两个参数:
1、消息接收者
2、方法名

我们有时候会将一些通用的方法,写在父类里,子类在需要的时候直接调用父类方法就好了,又或者调用一些在子类声明却未实现,但在父类里有实现的方法,又会是什么情况呢?

@interface StudentChild:Student
- (void)run;
- (void)sleep;
- (void)SwimmingWithSpeed:(NSInteger)speed sex:(NSInteger)sex;
+ (void)eat;
@end
@implementation StudentChild
- (void)sleep{
    NSLog(@"StudentChild--sleep");
}
- (void)SwimmingWithSpeed:(NSInteger)speed sex:(NSInteger)sex{
    NSLog(@"Swimming Speed is %ld ;sex is %@",(long)speed,sex==0?@"boy":@"girl");
}
@end


void test03(void){
    
    StudentChild *stuChild = [StudentChild alloc];
    
    [stuChild SwimmingWithSpeed:100 sex:0];//实例方法,    有声明 已经实现
    
    [stuChild run];//实例方法,      有声明 未实现
    
    [stuChild dump];//实例方法       未声明 未实现
    
    [StudentChild eat];//类方法       有声明 未实现
}
复制代码

打印结果如下

image.png

我们执行同上面一样的xcrun命令,可能会报警高,因为我们没有在StudentChild里实现方法提,暂时不必理会警告,我们继续查看main.cpp文件:

void test03(void){

    StudentChild *stuChild = ((StudentChild *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("StudentChild"), sel_registerName("alloc"));

    ((void (*)(id, SEL, NSInteger, NSInteger))(void *)objc_msgSend)((id)stuChild, sel_registerName("SwimmingWithSpeed:sex:"), (NSInteger)100, (NSInteger)0);

    ((void (*)(id, SEL))(void *)objc_msgSend)((id)stuChild, sel_registerName("run"));

    ((void (*)(id, SEL))(void *)objc_msgSend)((id)stuChild, sel_registerName("dump"));

    ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("StudentChild"), sel_registerName("eat"));
}
复制代码

分析得出,方法的调用依旧是objc_msgSend消息发送,内部参数按照:接收者、方法名、参数1、参数2…依次追加。

第二种

既然我们已经知道了方法的发送在底层是消息发送,那么我们何不直接调用objc_msgSend发送消息,来模拟方法的调用呢?

记得引入头文件:#import <objc/message.h>

void test04(void){
    StudentChild *stuChild = [StudentChild alloc];
    objc_msgSend(stuChild,sel_registerName("run"));
    objc_msgSend(stuChild,sel_registerName("SwimmingWithSpeed:sex:"),98,1);
}
复制代码

如果你们没有对工程进行设置,应该会编译报错:

Too many arguments to function call, expected 0, have 2
复制代码

image.png
需要设置一下 target > Build Settings > Enable Strict Checking of objc_msgSend Calls 修改为 No,默认情况下Yes.
运行结果如下

image.png

第三种

void test05(void){
    StudentChild *stuChild = [StudentChild alloc];
    objc_msgSend(stuChild,@selector(run));
    objc_msgSend(stuChild,@selector(SwimmingWithSpeed:sex:),66,1);
}
复制代码

image.png

我们在查看main.cpp文件,发现除了objc_msgSend方法,还有一个objc_msgSendSuper方法,顾名思义是给父类发送方法,那么我们也来测试下吧:

//找到objc_msgSendSuper定义,发现有两个参数objc_super结构体指针,以及SEL
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
复制代码

点击查看objc_super定义如下:

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 */
};
复制代码

当前是OBJC2环境,简化代码如下:

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};
复制代码

所以测试代码如下:

//测试objc_msgSendSuper
void test06(void){
    StudentChild *stuChild = [StudentChild alloc];//实例化对象
    ///结构体
    struct objc_super ssjobjc;
    ssjobjc.receiver = stuChild;
    ssjobjc.super_class = Student.class;
    
    objc_msgSendSuper(&ssjobjc,@selector(sleep));
}
复制代码

打印结果:

image.png

接下来我们看一下objc_msgSend里面是如何走的

打开
Debug->Debug workflow 选中always show Disassembly

image.png
然后运行

image.png

会进入这个界面
image.png

由此我们得知objc_msgSend要去到objc源码去看:

image.png

按上图操作,左边的方法列表就被折叠起来了。我们知道,objc_msgSend是用汇编写的,所以需要关注后缀是.s的几个文件,又因为我们主要研究的环境是iPhone真机,所以我们需要关注的是arm64相关名字的.s文件,我们发现还是不能定位哪个objc_msgSend方法,但是我们留意到有个ENTRY _objc_msgSend

image.png
没错,这里就是_objc_msgSend相关代码,我们来分析一下:

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
        /**
            cmp:compare,比较的意思
            p0:p0寄存器,存放的是第一个参数,也就是消息接收者
            #0:常量0
            意思就是p0和0进行比较,判断消息接收者是否为nil
        */
	cmp	p0, #0			// nil check and tagged pointer check
        
        /**
        tagged pointer:为了节省内存和提高执行效率,苹果提出的概念,感兴趣可自行查阅。
        如果支持tagged pointer,就会进入b.le	LNilOrTagged,否则return 空。
        */
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
        /**
        ldr	p13, [x0] 解释如下:
            从x0开始读取,把它对应的执行码给p13;
        x0即消息接收者,即消息接收者首地址,即消息接收者的isa.
        */
	ldr	p13, [x0]		// p13 = isa
        //关于GetClassFromIsa_p16,请继续往下看~
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
    LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
        
复制代码

当前文件搜索“GetClassFromIsa_p16”,找到一个宏定义:

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
//---- 此处SUPPORT_INDEXED_ISA用于watchOS,不用看它
#if SUPPORT_INDEXED_ISA
	...
        
/**我们分析一下__LP64__*/
#elif __LP64__

.if \needs_auth == 0 // _cache_getImp takes an authed class already
    /**
    1、前面调用 GetClassFromIsa_p16 p13, 1, x0,所以src存放的就是isa;
    2、mov	p16, \src的意思就是把src传给p16,所以p16就是isa;
    */
	mov	p16, \src
.else
	// 64-bit packed isa
        /**
        ExtractISA p16, \src, \auth_address根据ExtractISA定义,可以理解为:
        and	$0, $1, #ISA_MASK
        也就是把$1与上ISA_MASK地址,结构给$0
        即p16 = isa &  ISA_MASK,也就是p16存的是Class地址
        */
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro
复制代码

objc搜索”ExtractISA“:

.macro ExtractISA
	and	$0, $1, #ISA_MASK
复制代码

看到这里,隐约间有了这么一个概念:objc_msgSend通过消息接收者,去获取它的Class类。那么为什么要这么做呢?首先,我们知道objc_msgSend之后,会有一个cache_t里面的insert方法,而cache_t存在于Class类里面。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享