ios 底层原理之Runimte 运行时

194 阅读5分钟

前言

在上一篇cache_t初探中我们探索了类的成员变量cache,发现cache中缓存着方法SEL、IMP,也知道了是insert方法把bucket插入了cache中。有插入就会有读取,那么又是什么时候读取cache中缓存的方法呢?我们知道方法的本质是消息发送,那么就从Runtime出发分析一下objc源码

1.0 Runtime定义

  • 运行时就是代码跑起来了,被装载到内存中去了。你的代码保存在磁盘上没装入内存之前是个死家伙.只有跑到内 存中才变成活的,而运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样,不是简单的扫描代码,而是在内存中做些操作,做些判断。
  • 编译时:编译器把源代码翻译成机器能够识别的代码。编译时会进行词法分析,语法分析主要是检查代码是否符合苹果的规范,这个检查的过程通常叫做静态类型检查。

1.1 对象方法调用的本质

我们一般会用三种方式调用Runtime

  • objective-C code,[p sayHello]
  • Framework & Serivce方式,isKindOfClass
  • Runtime API方式,class_getInstanceSize

用纯OC的方式调用一下方法,看看底层编译发生了什么变化?main方法如下

@interface LGPerson : LGTeacher
- (void)sayHello;
@end
@implementation LGPerson
- (void)sayHello{
    NSLog(@"666");
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayHello];
       }
       return 0;
   }

clang编译成main.cpp看看底层给我们编译成了什么?

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        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("sayHello"));
    }
    return 0;
}

分析:LGPerson对象的sayHello方法,在底层给我们编译成了objc_msgSend方法,第一个参数是对象person,第二个参数是方法名sayHello。既然底层是objc_msgSend,那么能不能直接用objc_msgSend调用方法呢?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayHello];
        objc_msgSend((id)person, sel_registerName("sayHello"));
       }
       return 0;
   }

输出如下:

666
666

分析:[person sayHello]和objc_msgSend()方法最后输出的都是666,说明OC方法调用的本质就是实现objc_msgSend方法。

注意:xcode需要关闭objc_msgSend检查机制:target --> Build Setting -->搜索objc_msgSend -- Enable strict checking of obc_msgSend calls设置为NO

1.2 调用父类方法的本质

像上面一样,写个LGPerson的父类LGTeacher,实现方法sayNB(),子类person调用父类的方法[person sayNB],发现会调用objc_msgSendSuper方法,即objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...),第一个参数是objc_super结构体指针,第二个参数是SEL。看一下objc_super结构体,有成员变量receiver和super_class

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
#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_msgSendSuper代码还原一下子类调用父类的方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayNB];

        struct objc_super gy_objc_suber;
        gy_objc_suber.receiver=person;
        gy_objc_suber.super_class=LGTeacher.class;
        objc_msgSendSuper(&gy_objc_suber,@selector(sayNB));
        }
        return 0;
        }

输出:

很🐂比**
很🐂比**

分析:[perosn sayNB]和直接通过objc_msgSendSuper给父类发消息的结过是一样的,子类的对象可以调用父类的方法。注意这里的receiver是person,即谁调用方法,谁就是接受者

2.0 objc_msgSend底层探究

源码搜索objc_msgSend发现此函数是汇编写的,简单分析一下汇编的思路,这里仅分析arm64下的汇编。汇编比较生涩,所以用注释的方式一行行过。注意:p0-p17是对x0-x17寄存器的重新定义,这里就把他们看成是等价的。

ENTRY _objc_msgSend //ENTRY入口,objmsgSend(id,SEL)两个参数,x0=receiver,x1=sel        
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // x0和0进行比较,即receiver和nil比较
#if SUPPORT_TAGGED_POINTERS //如果是64位系统就支持
b.le LNilOrTagged //  (MSB tagged pointer looks negative)//小于等于0,进入LNilOrTagged流程
#else
b.eq LReturnZero  //等于0,进入LReturnZero流程,此处返回空
#endif
ldr p13, [x0] // p13 = isa,[x0]就是取x0地址的值,reservier地址即为isa地址
GetClassFromIsa_p16 p13, 1, x0 // p16 = class,把p13, 1, x0作为参数传入GetClassFromIsa_p16中
LGetIsaDone://拿到isa操作完成以后继续下面
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
\
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend //结束
ENTRY _objc_msgLookup

分析: 判断receiver是否为空,为空返回nil->不为空调用GetClassFromIsa_p16传入isa获取class->CacheLookup

2.1 GetClassFromIsa_p16

//根据上面分析,src=p13=isa ,needs_auth=1,auth_address=x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA   //armv7k 此处只分析arm64跳过
// 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     //isa放入p16
.else //因为needs_auth==1所以走下面的
// 64-bit packed isa
ExtractISA p16, \src, \auth_address// 把p16、src、auth_address传入ExtractISA 得到的结果复制给p16
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro

分析:GetClassFromIsa_p16,根据上面分析,src=p13=isa ,needs_auth=1,auth_address=x0,在arm64下以及needs_auth=1,所以会进入ExtractISA,传入参数p16、src、auth_address获取关系类class

2.2 ExtractISA

//根据上面分析 $0=p16,$1=src=isa
.macro ExtractISA 
and $0, $1, #ISA_MASK //and代表&操作,&0=$1&#ISA_MASK 

分析:p16=isa&#ISA_MASK即为对象receiver的class类,这是一个获取对象isa关联关系的过程。获取到p16后会进入CacheLookup流程

2.3 CacheLookup

//根据上面的分析 mode=normal,Function=objc_msgSend,MissLabelDynamic=objc_msgSend_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
mov x15, x16 //上面分析出x16=class,把class复制给x15

LLookupStart\Function:

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS//arm64的模拟器
//....省略模拟器下的代码,为了方便月度
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //arm64真机
ldr p11, [x16, #CACHE] //CACHE=2*指针大小=16,[x16, #CACHE]代表x16加上#CACHE,即x16平移#CACHE得到cache,之前文章分析过class首地址平移16个字节即为cach地址,p11=cache首地址=bucketsAndMaybeMask的地址
#if CONFIG_USE_PREOPT_CACHES //arm64真机

 #if __has_feature(ptrauth_calls)//A12以上,也省略,看正常流程
   //....省略
  #else
    and p10, p11, #0x0000fffffffffffe //p10=bucketsAndMaybeMask&0x0000fffffffffffe=buckets
    tbnz p11, #0, LLookupPreopt\Function  //如果bucketsAndMaybeMask不为0,则跳转到LLookupPreopt
   #endif

eor p12, p1, p1, LSR #7 //p1=x1=sel,ero是异或,p12=p1^(p1>>7)=sel^(sel>>7)
//p11>>48=bucketsAndMaybeMask>>48=mask
//p12=p12&(p11>>48)=sel^(sel>>7)&mask 获取index
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
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

分析:通过class内存平移16个字节获取cache,通过cache获取bucketsmask,然后计算hash下标index

2.4 mask向前遍历缓存

// p13当前的bucket
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
1: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--
cmp p9, p1 // 比较sel==要查询的是否是要查询的sel
b.ne 3f //  如果不相等就跳转到3流程 
2: CacheHit \Mode //  相等命中缓存,代表找到这个方法了 返回imp

3: cbz p9, \MissLabelDynamic // 判断sel=0 跳转MissLabelDynamic 进入objc_msgSend_uncached
cmp p13, p10 // 比较p13和p10的地址大小
b.hs 1b

分析:

  • 根据下标index找到对应的bucket->取出bucket中sel判断是否是需要查找的sel->如果是就返回IMP,进入TailCallCachedImp->如果不是继续循环查找,一直找不到进入进入objc_msgSend_uncached
  • 向前查找先找到最后一个bucket位置,然后向前查找

总结: 通过上面的分析大致总结下objc_msgSend方法查找流程

  1. objc_msgSend->receiver是否为空,为空返回nil
  2. 不为空调用GetClassFromIsa_p16传入isa获取class
  3. CacheLookup通过class内存平移16个字节获取cache,通过cache获取buckets和mask,然后计算hash下标index
  4. 根据下标index找到对应的bucket,取出bucket中sel判断是否是需要查找的sel,如果是就返回IMP,进入TailCallCachedImp,如果不是继续循环查找,一直找不到进入进入objc_msgSend_uncached

2.5 伪代码实现objc_msgSend

上面的流程看下来还是应该大概知道了objc_msgSend方法查找的流程,下面通过伪代码实现一下以便理解:

// 获取当前的对象
id person = 0x10000
// 获取isa
isa_t isa = 0x000000
// isa -> class -> cache
cache_t cache = isa + 16字节
// 获取buckets mask|buckets 在一起的
buckets = cache & 0x0000ffffffffffff
// 获取mask
mask = cache LSR #48
// 下标 = mask & sel
index = mask & p1
// bucket 从 buckets 遍历的开始 (起始查询的bucket)
bucket = buckets + index * 16 (sel imp = 16)
int count = 0
// CheckMiss $0
do{
    if ((bucket == buckets) && (count == 0)){ // 进入第二层判断
        // bucket == 第一个元素
        // bucket人为设置到最后一个元素
        bucket = buckets + mask * 16
        count++;
    }else if (count == 1) goto CheckMiss
    // {imp, sel} = *--bucket
    // 缓存的查找的顺序是: 向前查找
    bucket--;
    imp = bucket.imp;
    sel = bucket.sel;
}while (bucket.sel != _cmd)  //  // bucket里面的sel 是否匹配_cmd
// CacheHit $0
return imp

objc_msgSend方法查找流程图如下

objc_msgSend流程分析.png