-
准备工作
- objc4-781可编译源码 github.com/Bore-TuDou/…
- 熟悉汇编常用指令
;数据处理指令 MOV X1,X0 将寄存器X0的值传送到寄存器X1。MOV:从另一个寄存器、被移位的寄存器或将一个立即数加载到目的寄存器。 ;算术运算:ADD SUB MUL … 等加减乘除运算 ADD X0,X1,X2 寄存器X1和X2的值相加后传送到X0 SUB X0,X1,X2 寄存器X1和X2的值相减后传送到X0 MUL add x14, x4, x27, lsl #1 算术运算也可以与逻辑位移运算一起用,意思是把 (x27 << 1) + x4 = x14; ;扩展位数运算:有 zero extend(高位补0) 和 sign extend(高位填充和符号位一致,一般有符号数用这个)。 一般用来补齐位数。常和算术运算配合一起. add w20, w30, w20, uxth 算术运算也可以与扩展位数运输算一起,意思是取 w20的低16位,无符号补齐到32位后再进行 w30 + w20的运算 ;逻辑运算指令 LSL 逻辑左移 LSR 逻辑右移 ASR 算术右移 ROR 循环右移 AND X0,X0,#0xF 与。X0的值与0xF相位与后的值传送到X0 ORR X0,X0,#9 或。X0的值与9相位或后的值传送到X0 EOR X0,X0,#0xF 异或。X0的值与0xF相异或后的值传送到X0 ;寄存器加载/存储指令 LDR X5,[X6,#0x08] ld(load): X6寄存器加0x08的和的地址值内的数据传送到X5 LDP x29, x30, [sp, #0x10] ldp(load pair):是ldr 的变种指令,可以同时操作两个寄存器,从指定内存处读取两个数据到寄存器 STR X0, [SP, #0x8] st:store, str:往内存中写数据(偏移值为正); X0寄存器的数据传送到SP+0x8地址值指向的存储空间 STUR w0, [x29, #-0x8] 往内存中写数据(偏移值为负) STP x29, x30, [sp, #0x10] stp(store pair):是str 的变种指令,可以同时操作两个寄存器,将一对寄存器中的值,入栈,存放到指定内存处 ADR ;将一个立即值与 pc 值相加,并将结果写入目标寄存器 ADRP ;以页为单位的大范围的地址读取指令,这里的P就是page的意思。取得page的基地址存入寄存器 示例: adrp x0, l_.str@PAGE ;将符号l.str所在的page基址读入x0 add x0, x0, l_.str@PAGEOFF ;x0 = x0 + l.str所在page中的偏移量 ;跳转和控制指令 CBZ ;比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令) CBNZ ;比较,如果结果非零(Non Zero)就转移(只能跳到后面的指令) CMP ;比较指令,相当于SUBS,影响程序状态寄存器CPSR,关于CPSR的几个状态值,前面寄存器节已经讲过 B{条件} 目标地址 ;跳转指令,可带条件跳转与cmp配合使用。一般是本方法内的跳转,如while循环,if else等。 BL ;带返回的跳转指令, 返回地址保存到LR(X30)。存了LR也就意味着可以返回到本方法继续执行。一般用于不同方法之间的调用 RET ;子程序返回指令,返回地址默认保存在LR(X30) ;异常产生指令 SWI(Software Interrupt) 软件中断指令。用于产生软中断,从而实现处理器从用户模式变换到管理模式,CPSR保存到管理模式的SPSR中,执行转移到SWI向量,在其他模式下也可以使用SWI指令,处理器同样切换到管理模式。 BKPT(BreakPoint) 断点中断指令。产生一个预取异常(prefetch abort),它常被用来设置软件断点,在调试程序时十分有用。当系统中存在调试硬件时,该指令被忽略。 -
runtime浅析
- 编译时
顾名思义就是正在编译的时候. 那啥叫编译呢?就是编译器帮你把 源代码翻译成机器能识别的代码. (当然只是⼀般意义上这么说,实际上可能只是翻译成某个中间状态的语⾔.) 编译时就是简单的作⼀些翻译⼯作 ,⽐如检查你有没有粗⼼写错啥关键字了啊.有啥词法分析,语法分析之类的过程. 就像个⽼师检查学⽣的作⽂中有没有错别字和病句⼀样.如果发现啥错误编译器就告诉你.如果你⽤微软的VS的话, 点下build.那就开始编译,如果下⾯有errors或者warning信息,那都是编译器检查出来的.所谓这时的错误就叫编译时错误,这个过程中做的啥类型检查也就叫编译时类型检查,或静态类型检查(所谓静态嘛就是没真把代码放内存中运⾏起来, ⽽只是把代码当作⽂本来扫描下). 所以有时⼀些⼈说编译时还分配内存啥的肯定是错误的说法. - 运行时
就是代码跑起来了.被装载到内存中去了。
(你的代码保存在磁盘上没装⼊内存之前是个死家伙.只有跑到内存中才变成活的).⽽运⾏时类型检查就与前⾯讲的编译时类型检查(或者静态类型检查)不⼀样.不是简单的扫描代码.⽽是在内存中做些操作,做些判断。 - runtime使用方式以及与编译层和底层的关系图
- 通过OC代码,例如方法的调用,@selector()
- 通过NSObject方法,例如isKindOfClass
- 通过Runtime API,例如class_getInstanceSize
- 关系图
compiler就是我们了解的编译器,即LLVM,例如OC的alloc 对应底层的objc_alloc, runtime system libarary 就是底层库
- 编译时
-
探索方法的本质
- 将上述给到的源码中的main文件编译成.cpp文件,看底层的方法调用的实现
不难发现方法的调用其实就是消息转发的一个过程通过调用runtime objc_msgSend方法实现消息的转发
- 验证上述结论
为了验证上述说法的正确性我们可以直接使用objc_msgSend来实现方法的调用,再看执行结果要调用objc_msgSend首先要做如下两步准备:- 引入runtime头文件#import <objc/message.h>
- 需要将target --> Build Setting -->搜索msg -- 将enable strict checking of obc_msgSend calls由YES 改为NO,将严厉的检查机制关掉,否则objc_msgSend的参数会报错
可以发现通过实例调用方法和objc_msgSend转发消息得到的结果是一致的,从而印证的上述的结论
- 类方法实现
- 第一步按照上述步骤实现一下发现
使用objc_msgSend转发消息的时候传入TDPerson的实例会直接报错提示找不到方法
- 第二步将objc_msgSend传参修改一下
将objc_msgSend参数中的中的TDPerson的实例换成TDPerson再试一下发现类方法的调用如果使用objc_msgSend实现只需要类为传参传递过去就好了,同时也验证了类方法存储在类中的说法
- 第一步按照上述步骤实现一下发现
- 子类调用父类方法
TDStudent中是没有sayNB方法的是在他的父类中实现的,但是我们发先调用父类中的sayNB方法和子类直接调用sayNB方法打印的结果是一样的,所以基本上可以断定,方法的查找流程是现在自己本类中查找是否有该方法的实现没有的话直接去父类中找
- 将上述给到的源码中的main文件编译成.cpp文件,看底层的方法调用的实现
-
objc_msgSend 快速查找流程分析
- 在源码中搜索objc_msgSend找到源码实现
查找过程会发现_objc_msgSend是使用汇编实现的,然后查找过程中会发现好几个文件都有该方法的实现,应为我们的真机是arm64架构的所以我们只需要查看arm64.s文件中的objc_msgSend实现大致流程如下//objc_msgSend方法入口 ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame //p0是objc_msgSend第一个参数,也就是消息的接受者 //用消息的接受者和空进行判断 cmp p0, #0 // nil check and tagged pointer check #if SUPPORT_TAGGED_POINTERS //支持小对象类型 直接走LNilOrTagged流程 b.le LNilOrTagged // (MSB tagged pointer looks negative) #else //如果不支持小对象类型直接返回空 b.eq LReturnZero #endif //消息接受者不为空的时候走如下流程 //根据对象去除isa 存放在p13寄存器中 ldr p13, [x0] // p13 = isa //从isa中区出类型新放到p16寄存器中 GetClassFromIsa_p16 p13 // p16 = class LGetIsaDone: // calls imp or objc_msgSend_uncached //找到isa之后就进入到缓存查找流程,也就是快读查找 CacheLookup NORMAL, _objc_msgSend小对象类型概念补充www.jianshu.com/p/e354f9137…
- CacheLookup源码分析
上述代码中已经将每一步注释都写得很清楚了就不在继续重复步骤LLookupStart$1: // p1 = SEL, p16 = isa //#CACHE = #define CACHE (2 * __SIZEOF_POINTER__) //__SIZEOF_POINTER__表示pointer的大小 #CACHE = 16 //x16 isa首地址 类信息中前两个参数 isa占用8位 superclass同样暂用8位 // 第三个参数是cache 所以isa平移16位刚好是cache(mask高16位 + buckets低48位) ldr p11, [x16, #CACHE] // p11 = mask|buckets #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //如果是真机 //p11(cache) & 0x0000ffffffffffff ,mask高16位抹零,得到buckets 存入p10寄存器-- 即去掉mask,留下buckets and p10, p11, #0x0000ffffffffffff // p10 = buckets //计算步骤: // 1. p11, LSR #48 p11右移48位的到mask // 2. p1 & 上一步的结果也就是mask p1是objc_msgSend的第二个参数也就是sel // 方法存入到cache的下标计算公式是sel & mask 所以第二步结果可以得到方法存储的下标 // 3. 第二步得到的index存到p12寄存器中 and p12, p1, p11, LSR #48 // x12 = _cmd & mask #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //非真机情况下 and p10, p11, #~0xf // p10 = buckets and p11, p11, #0xf // p11 = maskShift mov p12, #0xffff lsr p11, p12, p11 // p11 = mask = 0xffff >> p11 and p12, p1, p11 // x12 = _cmd & mask #else #error Unsupported cache mask storage for ARM64. #endif //p10 =buckets p12下标 // #define PTRSHIFT 3 #(1+PTRSHIFT) = 4 // p12, LSL #(1+PTRSHIFT) = p12 << 4 = ((_cmd & mask) << (1+PTRSHIFT)) = butket 实际偏移量 //p10+butket 实际偏移量 = 对应的bucket 所以p12 = bucket add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) //冲bucket分别取出imp和sel存到p17和p9 ldp p17, p9, [x12] // {imp, sel} = *bucket //p9根据上述步骤找到的sel和objc_msgSend传入的sel相对比 1: cmp p9, p1 // if (bucket->sel != _cmd) //不相等跳到对应2位置的代码 b.ne 2f // scan more //相等命中缓存返回imp CacheHit $0 // call or return imp 2: // not hit: p12 = not-hit bucket // 如果从最后一个元素往前遍历都找不到缓存,那么走 `CheckMiss` // 应为该查找过程是向前查找下面会有说明 CheckMiss $0 // miss if bucket->sel == 0 //p12 当前的bucket和第一个bucket相比 cmp p12, p10 // wrap if bucket == buckets //如果相等跳转到第三步 b.eq 3f //不相等的话向前移动一个bucket也就是取上一个bucket 然后拿到对应的sel和imp继续冲第一步开始往下走 ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket //递归查找继续返回到第一步 b 1b // loop 3: // wrap: p12 = first bucket, w11 = mask #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //如果是真机的状态下 // 找到buckets中的最后一个bucket存到p12中 add p12, p12, p11, LSR #(48 - (1+PTRSHIFT)) // p12 = buckets + (mask << 1+PTRSHIFT) #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //模拟器的状态下 add p12, p12, p11, LSL #(1+PTRSHIFT) // p12 = buckets + (mask << 1+PTRSHIFT) #else #error Unsupported cache mask storage for ARM64. #endif // Clone scanning loop to miss instead of hang when cache is corrupt. // The slow path may detect any corruption and halt later. //同样的取imp和sel 和上面的步骤相同只不过是冲最后一个bucket开始 ldp p17, p9, [x12] // {imp, sel} = *bucket //继续和传入的sel作对比 1: cmp p9, p1 // if (bucket->sel != _cmd) //不相等跳转到第二步 b.ne 2f // scan more //否则命中缓存返回imp CacheHit $0 // call or return imp 2: // not hit: p12 = not-hit bucket // 如果从最后一个元素往前遍历都找不到缓存,那么走 `CheckMiss` // 应为该查找过程是向前查找下面会有说明 CheckMiss $0 // miss if bucket->sel == 0 //和第一个bucket比较 cmp p12, p10 // wrap if bucket == buckets //如果相同跳转到第三步 b.eq 3f //不相同取出上一个bucket的sel 和 imp继续递归查找 ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket b 1b // loop LLookupEnd$1: LLookupRecover$1: 3: // double wrap JumpMiss $0 .endmacro .macro CheckMiss // miss if bucket->sel == 0 .if $0 == GETIMP //--- 如果为GETIMP ,则跳转至 LGetImpMiss cbz p9, LGetImpMiss .elseif $0 == NORMAL //--- 如果为NORMAL ,则跳转至 __objc_msgSend_uncached 也就是慢速查找流程 cbz p9, __objc_msgSend_uncached .elseif $0 == LOOKUP //--- 如果为LOOKUP ,则跳转至 __objc_msgLookup_uncached cbz p9, __objc_msgLookup_uncached .else .abort oops .endif .endmacro .macro JumpMiss .if $0 == GETIMP //--- 如果为GETIMP ,则跳转至 LGetImpMiss b LGetImpMiss .elseif $0 == NORMAL //--- 如果为NORMAL ,则跳转至 __objc_msgSend_uncached 也就是慢速查找流程 b __objc_msgSend_uncached .elseif $0 == LOOKUP //--- 如果为LOOKUP ,则跳转至 __objc_msgLookup_uncached b __objc_msgLookup_uncached .else .abort oops .endif .endmacro - 流程总结
- 在源码中搜索objc_msgSend找到源码实现