IOS底层原理 -5.运行时(1)
OC是一种动态性比较强的语言,所有的函数调用都是基于消息机制;简介参照:
文章目录
1. isa指针
1.1 简述
***注意:以下的分析都是基于arm64***
isa
在前面介绍过,实例对象可以通过 isa
找到类对象,类对象通过isa可以找到原类对象;在arm64后isa并不直接是Class类型,而是union
,同时用位域位域(w3c)来存储更多信息(struct test{uintptr_r nonpointer :1; }
),
1.2 在看isa之前先熟悉两个知识点位域
和共用体union
char _bool;//将所有BOOL值存储到一个字节中
-(void)setTall:(BOOL)tall{
if(tall){
_bool |= (1<<1);
}else{
_bool &= ~(1<<1);
}
}
-(void)setRich:(BOOL)rich{
if(rich){
_bool |= (1<<0);
}else{
_bool &= ~(1<<0);
}
}
- (BOOL)isRich{
return !!(_bool&(1<<0));//最高位存储rich
}
- (BOOL)isTall{
return !!(_bool&(1<<1));//第二位存储Tall
}
接下来看下位域中怎么实现:
@interface LYMPerson()
{
// 位域
struct {
char tall : 1;
char rich : 1;
} _bool;
}
@end
@implementation LYMPerson
- (void)setTall:(BOOL)tall
{
_bool.tall = tall;
}
- (BOOL)isTall
{
return !!_bool.tall;
}
- (void)setRich:(BOOL)rich
{
_bool.rich = rich;
}
- (BOOL)isRich
{
return !!_bool.rich;
}
@end
//简化的源码
union isa_t //共用体
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;//存放所有的数据
struct {//位域
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
1.3 isa
结构体的成员的含义:
- 结构体基本成员介绍
nonpointer | has_assoc |
---|---|
0 代表普通指针,存储着Class、Meta-Class对象的内存地址;1 代表优化过,使用位域存储更多的信息 | 是否设置过关联对象,如果没有,释放时更快 |
has_cxx_dtor | shiftcls |
是否有c++的析构函数(cxx_destruct),如果没有,释放时会更快 | 存储着Class,Meta-Class对象的内存地址信息 |
magic | weakly_referenced |
用于在调试时分辨对象是否未完成初始化 | 是否有被弱引用指向如果没有,释放速度会更快 |
deallocating | has_sidetable_rc |
对象是否正在释放 | 里面存储的值的引用计数器减1 |
extra_rc | |
引用计数器是否过大无法存储在isa中,如果为1那么引用计数会存储在一个叫SideTable的类属性中 |
- 下面来验证一下
加上weak和关联属性后的值
#import "ViewController.h"
#import <objc/message.h>
#import <objc/runtime.h>
@interface LYMAnimal:NSObject
@property(nonatomic,assign,readwrite) NSInteger age;
@end
@implementation LYMAnimal
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
LYMAnimal *animal = [[LYMAnimal alloc]init];
__weak LYMAnimal *weakSelf = animal;
objc_setAssociatedObject(animal, @"name", @"haha", OBJC_ASSOCIATION_COPY);
id va = objc_getAssociatedObject(animal, @"name");
NSLog(@"%@ %@",animal,va);
}
@end
通过对比两张图可以看出,加了weak和关联对象后相应的位的值都有改变,weakly_refrenced
的变成了1,has_assoc
对应的位变成了1,这与表格的描述刚好符合;
1.4 isa扩展
- 在之前
实例对象/类对象
通过isa找到类对象/原类对象
前都必须的&ISA_MASK
;而在源码中的定义为ISA_MASK 0x0000000ffffffff8ULL
,在联合体中shiftcls
存储真实地址只占33位,因此&ISA_MASK
刚好可以取出类对象或者原类对象的真实地址;同时也可以知道类对象或者原类对象的最后三位为零,通过一段代码验证一下:
- 从上图的输出可知最后一位全是0和8,而16进制的0和8 对应的二进制是 0000和1000,那么可以验证
实例对象和类对象、原类对象的内存地址最后三位都是0
。
Class
- 照例看下图:
- 下面详细介绍下这个结构体的成员:
class_rw_t
:可读可写,里面的methods和propeties及protocols都是二位数组,而ro指向的结构体class_ro_t
(只读)里面的baseMethodList
…是一维的,其中baseMethodList
中是method_t
类型;
method_t
:结构体包含 IMP imp
,指向函数地址的指针;SEL name
函数名;const chat *type;
返回值类型,参数类型;
-
IMP代表函数的具体实现,
typedef id _Nullable (*IMP)(id __Nonnull,SEL _nonnull ,...)
; -
SEL代表方法/函数名,也叫做选择器,底层结构和char类似,可以通过
@selector()、sel_registerName()
获得,不同类中相同名字的方法,所对应的方法选择器是相同的,它的底层是:
typedef struct objc_selector *SEL
-
types 包含了函数的返回值,参数的编码信息;
v@:
:v
代表返回值是void,@
代表参数id类型,:
代表参数sel,@encode()
指令,可以将具体的类型表示成字符串编码,苹果在其官方文档中也有说明;这里列举部分如下图:
3. 方法缓存(cache_t
)
- 在之前研究
objc_class
结构体的时候,我们知道在Class
结构体内部有一个方法缓存结构体(cache_t cache
),使用散列表
来缓存使用过的方法来提高方法的查找速度;
- 散列表:是一个长度不固定的列表,当长度不够存储的时候会重新创建后将原来的释放掉,主要是通过一定的算法将数据存入内存列表的指定索引位置,查找的时候根据同一算法算出的索引直接去列表中取出,当只有一个数据需要存储的时候,算出的索引在什么位置就存储在什么索引位置,该索引以外的其他索引存储为
NULL
;因此可以说散列表牺牲了部分内存换取查找速度;oc中如果算出的索引相同继续比较key,如果key不相同则将值减1,依次查找直到找到; - 顺序:实例对象isa–> 类对象–>类对象的cache中查找,没有–>methodList中找,找到返回,同时加入cache中
如果methodList找没有找到–>superclass找到父类–>cache中查找–>找到返回,同时缓存到类对象的cache中
如果在父类的cache找没有找到–>methodList中查找–>找到返回,同时缓存到类对象的cache中; 即:如下图
2. objc_msgSend(id,SEL);
OC中的方法调用
2.1 简述
OC中的方法调用都是基于消息发送即:objc_msgSend(id,SEL);
,看一个简单的示例:
LYMAnimal *animal = [[LYMAnimal alloc]init];
[animal callEat];
在执行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 main.m
的c++文件里最后里(约32000多行)如下:
#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_v7_xv8f6lp50hbcxftgjf7yb_sw0000gn_T_main_773e9c_mi_0);
LYMAnimal *animal = ((LYMAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)((LYMAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LYMAnimal"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("callEat"));
}
return 0;
}
名词: receiver
从上述代码可以看到在main函数里[animal callEat];
最终转换成了objc_msgSend
;objc_msgSend在苹果开源的运行时代码里是以汇编实现的,因为这部分代码肯定是调用频率非常的高;
2.2 执行阶段:消息发送
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
//x0寄存器:消息接收者,判断消息接受者是不是(<0)空,是则跳转到LNilOrTagged 然后LReturnZero最后返回(ret)
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative) //进行判断如果上述成立就跳转
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached //这里开始查找缓存
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
LExtTag:
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret //相当于return
END_ENTRY _objc_msgSend
首先会判断消息接受者(receiver)是不是空的,空的直接返回;如果不是空则在缓存中查找CacheLookup
:
.macro CacheLookup
// x1 = SEL, x16 = isa
ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp 查找到返回IMP
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0//缓存中没有找到跳转
.endmacro
在上述汇编中进行查找,没有找到跳转到JumpMiss
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached//这个是一个C语言的函数__class_lookupMethodAndLoadCache3
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
接着便是在这个函数中执行:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)//汇编代码比c代码多一个"_"
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
在lookUpImpOrForward
中会重新查找一次缓存,因为这里有可能动态添加方法;这个方法也是最核心的方法:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.read();
if (!cls->isRealized()) {
// Drop the read-lock and acquire the write-lock.
// realizeClass() checks isRealized() again to prevent
// a race while the lock is down.
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
retry:
runtimeLock.assertReading();
// Try this class's cache.
imp = cache_getImp(cls, sel);//会再次去缓存中查找
if (imp) goto done;
// Try this class's method lists. 去方法列表中查找
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists. 去父类的缓存和方法列表中查找
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);//父类中找到后缓存到当前类中
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
通过上述代码可知:方法调用时候,底层转换成消息发送(obj_msgSend),首先如果有缓存则在缓存中查找,缓存中没有则在方法列表中查找,在方法列表中进行查找的时候会执行一次缓存查找避免该过程中动态加入的方法导致的问题;如果方法列表找那个也没有则去父类的缓存和方法列表中查找;
2.3 执行阶段:动态方法解析 (dynamic method resolution)
在其父类中还找不到方法且父类没有父类,那么就会进入动态方法解析,这里会调用+(BOOL)resolveInstanceMethod:(SEL)sel
,这里可以动态添一次方法,然后会重新开始一次方法查找:
2.3.1 实例方法动态添加
在动态添加之前会判断是不是已经动态添加过,如果添加过就不会添加;就是直接到消息转发阶段
void callEat(id self,SEL _cmd){
NSLog(@"callEat 动态添加");
}
@implementation LYMAnimal
//-(void)callEat{
// NSLog(@"eat======");
//}
struct method_t {
SEL sel;
char *types;
IMP imp;
};
//-(void)otherCall{
// NSLog(@"%s",__func__);
//}
//+(BOOL)resolveInstanceMethod:(SEL)sel{
// if (sel == @selector(callEat)) {
// struct method_t *meth = (struct method_t *)class_getInstanceMethod(self, @selector(otherCall));
// //动态添加方法
// class_addMethod(self, sel, meth->imp, meth->types);
// return YES;
// }
// return [super resolveInstanceMethod:sel];
//}
+(BOOL)resolveInstanceMethod:(SEL)sel{//对象方法
if (sel == @selector(callEat)) {
//动态添加方法
class_addMethod(self, sel, (IMP)callEat, "v16@0:8");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
2.3.2 类方法动态添加:
+(BOOL)resolveClassMethod:(SEL)sel{
if (sel == @selector(callEat1)) {
//动态添加方法
class_addMethod(object_getClass(self), sel, (IMP)callEat, "v16@0:8");
return YES;
}
return [super resolveInstanceMethod:sel];
}
如果这一步没有处理,那么就会到消息转发;
2.4 执行阶段:消息转发
消息转发有三个阶段,各个阶段关系如图(图来自:<<Effective-Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法>>):
-(id)forwardingTargetForSelector:(SEL)aSelector
先看一个经典错误:
消息转发部分源码是没有开源,但是从上图可以知道:
- 先调用
__forwarding__
然后在该方法里会调用forwardingTargetForSelector
,该方法里可以返回能处理该消息的对象 - 如果上面的方法返回nil,则会调用methodSignatureForSelector
补充一个知识点:NSInvocation
封装了一个方法调用,包括方法调用者,方法名,方法参数,更改其target属性可以修改其方法调用者;
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(callEat)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];//这里不是空的就会调用forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
}
注意:类对象消息转发对应的方法:
//+(id)forwardingTargetForSelector:(SEL)aSelector
+(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(callEat1)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
//NSInvocation 封装了一个方法调用,包括方法调用者,方法名,参数,返回值
+(void)forwardInvocation:(NSInvocation *)anInvocation{
}
对应输出:
2.4 最后
应用来自《Effective-Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》的一段总结:
接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了。如果这个类的实例稍后还收到同名选择子,那么根本无须启动消息转发流程。若想在第三步里把消息转给备援的接收者,那还不如把转发操作提前到第二步。因为第三步只是修改了调用目标,这项改动放在第二步执行会更为简单,不然的话,还得创建并处理完整的NSInvocation。
3. super关键字
3.1 一个网上经典的面试题:
NSLog(@"self class = %@",[self class]);
NSLog(@"self superclass = %@",[self superclass]);
NSLog(@"super class = %@",[super class]);
NSLog(@"super superclass =%@",[super superclass]);
输出结果是:
super是一个编译器标示,告诉方法调用时候,从父类的方法中去找方法的实现,但是消息的接收者还是自己(子类对象);
其详细的底层实现解释请参考:iOS-底层原理17
3.2 底层分析
先看一下转换成c++文件中对应的结构:
struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};
// @implementation LYMWorker
static void _I_LYMWorker_eat(LYMWorker * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LYMWorker"))}, sel_registerName("eat"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_v7_xv8f6lp50hbcxftgjf7yb_sw0000gn_T_LYMWorker_b54bca_mi_0,__func__);
}
// @end
简化一下函数里的方法调用就是:objc_msgSendSuper(rw_objc_super,@selector(eat))
;结构体objc_super在objc源码里定义为:
struct objc_super{//这里是简化的写法
id receiver;
Class super_class;
}
objc_msgSendSuper参数的注释里解释,super_class
只是告诉查找方法实现
的时候是直接从父类的类对象开始查找;方法的接收者还是LYMWorker
的实例;这里有总结两点:
- 上述方法中调用的class方法在NSObject中实现,不论从哪个开始查找最终的实现都是在NSObject中的
- class只是告诉类型是什么,类型至于方法的调用者有关(也就是
消息接收者
);因此[super class]
只是告诉编译器从父类开始查找,所以最终返回的类型还是LYMWorker
;
相关文章
- iOS中xib与storyboard原理,与Android界面布局的异同
- iOS中NSURL常用属性
- iOS KVO的原理与使用
- 《iOS 8开发指南(第2版)》——第1章,第1.4节使用Xcode开发环境
- 《iOS 6高级开发手册(第4版)》——2.9节秘诀:创建基于URL的服务
- SwiftUI iOS 精品进度条组件之支持百分比和成功动画 (教程含源码)
- iOS内存泄漏检查&原理
- [译]理解 iOS 异常类型
- iOS 10 的适配问题
- iOS中__block 关键字的底层实现原理
- iOS开发技巧系列---使用链式编程和Block来实现UIAlertView
- 解析 iOS 动画原理与实现
- iOS 推送服务的简易原理与配置
- Unity iOS 之 [iOS]App上架流程[利用Archive进行上传]
- iOS - 视频开发
- iOS - 获取状态栏和导航栏尺寸(宽度和高度)
- IOS基础之UILineBreakModeWordWrap
- iOS App的加固保护原理