iOS 底层探究:方法的本质

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

1 Runtime

1.1 什么是Runtime

runtime翻译过来称为运行时,与之对应的是编译时。大部分的iOS开发人员,都听过runtime这个词,也知道运行时,但只是停留在表面,只是知道而已,并没有去深入的去探索和分析过。

OC语言是一门动态语言,拥有动态语言的三大特性:动态类型、动态绑定、动态加载。而底层实现就是熟悉又陌生的Runtime。

  • 运行时是一种面向对象的编程语言(面向对象编程)的运行环境。运行时表明了在某个时间段内,哪个程序正在运行。运行时是计算机程序运行生命周期内的一个阶段,其他阶段还包括:编译时、链接时和加载时。简单理解就是,代码跑起来,被装载到内存中的过程。
  • 编译时顾名思义就是正在编译的时候。那什么叫编译呢?就是编译器帮你把源代码翻译成机器能识别的二进制代码。(只是一般意义上说,实际上可能只是翻译成某个中间状态的语言。比如Java只有JVM 识别的字节码,C#中只有CLR能识别的MSIL。另外还有链接器、汇编器,为了便于理解我们可以统称为编译器)
  • 编译时就是简单的做一些翻译工作,比如检查有没有关键字的错误
  • 词法分析,语法分析之类的过程。
  • 如果发现错误,编译器就会告诉你,平时使用Xcode时,点击build就开始编译
  • 如果由errors或者warning信息,那就是编译器检查出来的。这是的错误叫做编译时错误,这个过程中做的类型检查也就是编译时类型检查,或静态类型检查(所谓静态就是没有把代码放内存中运行起来,而只是把代码当作文本来扫描下)

1.2 runtime饿使用的三种方式

runtime的使用的三种方式,其三种实现方法与编译层和底层的关系如图所示

image.png

  • 通过OC上层的代码实现,例如 [LGPerson Hello]
  • 通过NSObject方法实现,例如isKindOfClass
  • 通过Runtime API底层方法实现,例如class_getImstanceSize

图中的compiler就是编译器,就是我们熟悉的LLVM

2 OC方法的本质

我们知道平时写的OC代码,底层实现其实都是C/C++的代码实现的,再经过编译器LLVM编译,最终转化为机器语言。
通过clang编译的源码,理解了OC对象的本质是结构体,同样的,我们也可以使用clang命令编译成main.cpp文件,看安方法的本质是什么?

2.1方法底层的实现

在main函数中,写入以下代码

LGPerson *person = [LGPerson alloc]; 
[person sayNB]; 
[person say:@"NB"];
复制代码

生成cpp文件,底层代码如下

LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")); 
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB")); 
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_jl_d06jlfkj2ws74_5g45kms07m0000gn_T_main_d8842a_mi_3);
复制代码

可以看出,方法的本质:objc_msgSend消息发送
objc_msgSend的参数

  • 消息接收者
  • 消息主体(SEL+参数)

2.2objc_msgSend

在Build Setting中,将Enable Strict Checking of obc_msgSend Calls设置为NO,将严厉的检查机制关掉,否则objc_msgSend的参数会报错
导入头文件

#import <objc/message.h>
复制代码

在main函数中,写入以下代码

@interface LGPerson : NSObject 
- (void)sayNB; 
@end 
@implementation LGPerson 
- (void)sayNB{ NSLog(@"666"); } 
@end 
int main(int argc, const char * argv[]) { 
    @autoreleasepool { 
        LGPerson *person = [LGPerson alloc]; 
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB")); 
    } 
    return 0; 
} 
------------------------- 
//输出结果: 
666
复制代码
  • 和利用OC方法的执行结果相同
  • sel_registerName为@selector()的底层实现

2.2 objc_msgSendSuper

子类调用父类方法时,可使用objc_msgSendSuper,向父类发送消息
在objc源码中,找到objc_msgSendSuper的定义

OBJC_EXPORT void 
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ ) 
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
复制代码
  • 传入objc_super类型的结构体指针

找到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. 
    __unsafe_unretained _Nonnull Class super_class; 
    /* super_class is the first class to search */ 
};
复制代码
  • receiver:消息接收者
  • super_class传入第一查找的类,如果找不到,继续向所属父类一层一层的查找

在项目中调用objc_msgSendSuper
定义LGeacher,继承于LGPerson

@interface LGTeacher : LGPerson 
    - (void)sayNB; 
@end 
@implementation LGTeacher 
@end
复制代码
  • 定义sayNB当法,但不实现

在main函数中,写入以下代码

struct lg_objc_super { 
    __unsafe_unretained _Nonnull id receiver; 
    __unsafe_unretained _Nonnull Class super_class;
}; 
int main(int argc, const char * argv[]) { 
    @autoreleasepool { 
        LGTeacher *teacher = [LGTeacher alloc]; 
        struct lg_objc_super lgSuper; 
        lgSuper.receiver=teacher; 
        lgSuper.super_class=[LGPerson class];
        objc_msgSendSuper(&lgSuper, @selector(sayNB)); 
    } 
    return 0; 
} 
------------------------- 
//输出结果: 
666
复制代码
  • 按照objc_super相同结构,定义lg_objc_super结构体
  • 我们已经知道LGTeacher中没有sayNB方法的实现,所以super_class直接传入LGPerson的类对象
  • 如果super_class传入LGTeacher的类方法,打印结果相同。区别在于需要向父类多查找一层。

3 objc_msgSend汇编代码

在objc4-818.2源码中,不同系统架构的汇编指令都有差异,我们只针对最常用的arn64架构下的汇编代码进行探索

image.png

3.1 objc_msgSend

    ENTRY _objc_msgSend 
    UNWIND _objc_msgSend, NoFrame 

    cmp p0, #0     // nil check and tagged pointer check 
#if SUPPORT_TAGGED_POINTERS 
    b.le LNilOrTagged     // (MSB tagged pointer looks negative) 
#else 
    b.eq LReturnZero 
#endif 
    ldr p13, [x0]     // p13 = isa 
    GetClassFromIsa_p16 p13, 1, x0    // p16 = class
复制代码
  • ENTRY _objc_msgSend:入口
  • p0寄存器,存储消息接收者
  • cmp p0, #0:消息接收者和#0比较
    • 汇编代码中,没有nil,只有0和1
    • 这里的#0比较,可以理解消息接收者为nil
  • SUPPORT_TAGGED_POINTERS:真机arm64架构,SUPPORT_TAGGED_POINTERS 定义为1
  • b.le LNilOrTagger:小于等于0,进入LNilOrTagged流程
  • b.eq LReturnZero:等于0,进入LReturnZero流程
  • 否则,消息接收者存在,继续执行代码
    • ldr p13, [x0]:将X0寄存器取地址,赋值p13寄存器
    • p13:存储消息接收者的isa
  • 进入GetClassFromIsa_p16流程,传入isa、1、消息接收者

3.2 LNilOrTagged

LNilOrTagged: 
    b.eq LReturnZero // nil check 
    GetTaggedClass 
    b LGetIsaDone
复制代码
  • b.eq:等于0,进入LReturnZero流程
  • 否则,小于0,继续执行代码
  • 进入GetTaggedClass流程
  • 进入LGetIsaDone流程

3.3 LReturnZero

LReturnZero: 
    // x0 is already zero 
    mov x1, #0 
    movi d0, #0 
    movi d1, #0 
    movi d2, #0 
    movi d3, #0 
    ret
复制代码
  • 返回nil

3.4 GetClassFromIsa_p16

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */ 
#if SUPPORT_INDEXED_ISA 
    // Indexed isa 
    mov p16, \src // optimistically set dst = src 
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa 
    // isa in p16 is indexed 
    adrp x10, _objc_indexed_classes@PAGE 
    add x10, x10, _objc_indexed_classes@PAGEOFF 
    ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index 
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array 
1: 

#elif __LP64__ 
    .if \needs_auth == 0 // _cache_getImp takes an authed class already 
        mov p16, \src 
    .else 
        // 64-bit packed isa 
        ExtractISA p16, \src, \auth_address 
    .endif 
#else 
    // 32-bit raw isa 
    mov p16, \src 
#endif 

.endmacro
复制代码
  • 入参:
    • srcisa
    • needs_auth:1
    • auth_address:消息接收者
  • 当前不符合SUPPORT_INDEXED_ISA条件,跳过
  • 符合__LP64__条件,不满足needs_auth等于0 的条件,进入else分支
  • ExtractISA p16, \src, \auth_address:进入ExtractISA流程
    • 传入p16寄存器,isa,消息接收者
    • p16寄存器的值,存储的是什么不重要,因为传入到ExtractISA中会被赋值

3.5 ExtractISA

.macro ExtractISA 
    and   $0, $1, #ISA_MASK 
.endmacro
复制代码
  • $0:p16寄存器
  • $1:isa
  • and 0,0, 1, #ISA_MASK:将isa&ISA_MASK的结果,存储到p16寄存器
    • p16:存储类对象地址

3.6 LGetIsaDone

LGetIsaDone: 
    // calls imp or objc_msgSend_uncached 
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
复制代码
  • objc_msgSend的后续流程
    • 消息接收者存在,并且得到类对象,继续LGetIsaDone流程
  • 进入CacheLookup缓存查找流程,也就是所谓的sel-imp快速查找流程
  • 内部会调用imp或objc_msgSend_uncached

4 流程图

image.png
— 为什么要获取类对象?

  • 在快速查找流程中,查找的cache存储在类对象中,所以必须拿到类对象才能进行后面的流程

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