iOS底层原理-Runtime

Runtime:运行时,提供了一套C语言的api来支撑OC的动态性

isa内部结构

  • 在arm64架构之前,isa就是一个普通指针,存储着类对象或原类对象的内存地址
  • 在arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息,即内部结构如下
union isa_t 
{
    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; 
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
};
  • isa中各位存储的信息


    Snip20180613_2.png
  • 由于1个字节有8位,故可以通过位为最基本单位存储许多信息,但掩码的设计必须为特定的取值方式
    &掩码 :可以用来取出特定的位
    !!可以令一个值转换成bool类型
    |掩码 :可以用来输入特定的位为YES
    &~掩码 :可以用来输入特定的位为NO

  • 位域

struct {
    char tall :1;
    char rich :1;
    char handsome :1;
}_tallRichHandsome;
//结构体内的1即表示位域,与左边的char及时什么类型都无关
  • 当使用位域来进行取值时,若结果为1,则转换为bool类型会输出-1的结果,是因为bool包含8位,而一位的0b1转换为bool值会被填充为0b1111 1111,故结果为-1
    解决方案有:
    1.输出结果前为!!即可
    2.将位域改为2位,即
struct {
    char tall : 2;
    char rich : 2;
    char hansome : 2;
}

tip.
结构体是无法直接位运算的
由于isa指针其中33位放地址值的,且后面3位一定为0
真机即为arm64,模拟器和mac即为x86_64

  • 共用体(union):大家共用一个内存,往共用体中添加一个结构体是不影响的

Class内部结构

  • 原类对象是一种特殊的类对象,只是里面存储的只有类方法


    Snip20180622_11.png
  • 类对象调用data()方法,结果相当就是class_rw_t结构体

  • class_rw_t里面的methods,properties,protocols是二维数组,是可读可写的,包含了类的初始内容,分类的内容,即类和分类的声明的属性,方法,协议都在里面

  • class_ro_t里面的baseMethodList,baseProtocols,baseProperties是一维数组,是只读的,包含了类的初始内容,相当于只装着类声明的属性和方法,协议等初始信息,不包含分类

  • 原先的bits原先是指向class_ro_t,后来重新创建了一个class_rw_t,再讲bits指向class_rw_t,class_rw_t里面的class_ro_t又指向原先的class_ro_t

举例:methods是一个二维数组,里面每个元素是method_list_t,而method_list_t又是一个数组,数组里存放着每个元素是method_t类型元素,另外两个依次类推
ro中的数组为一维数组,里面就是method_t类型,另外两个依次类推
这么设计的好处是:便于动态的添加方法

两者的结合:类一开始声明的属性和方法,协议等初始信息,存储在class_ro_t中对应的baseMethodList,baseProtocols,baseProperties中,在程序运行时,再将分类中的方法,协议等信息重新组合,成class_rw_t对应的二维数组,即class_rw_t中部分信息是从class_ro_t中来的

方法method_t

  • 每个方法最终都是一个method_t,method_t是对方法/函数的封装
  • 定义:
struct method_t{
    SEL name; //函数名
    const char *types; //编码(返回值类型、参数类型)
    IMP imp; //指向函数的指针(函数地址)
};
  • 各参数具体含义:
    IMP:代表函数的具体实现(也就是函数的地址)
    SEL:代表方法/函数名,一般叫选择器,底层结构和char*类似,也就是C语言的字符串,说白了就是一个名字

1.获取SEL的方式:

SEL sel1 = sel_registerName("test");
SEL sel2 = @selector(test);

2.可以通过sel_getName()和NSStringFromSelector()转换成字符串
3.不同类中相同名字的方法,所对应的方法选择器是相同的,即无论SEL创建多少次,只要SEL的名字相同,该SEL都是相同的

Types:编码
  每个方法例如-(void)test,都会默认传递2个参数,即,默认方法就为:- (void)test:id(self) _cmd:(SEL)_cmd
即通过断点可知,对于- (void)test方法,types的值为v16@0:8,其中,v代表void返回值类型,@代表id类型,:代表SEL参数,第一个数字16表示全部参数占多少个字节,idSEL都是指针,故为16字节,@0中的0表示从第几个字节开始,id类型的参数是从第0个字节开始的,故为0,后面的数字同理

Type Encoding:
iOS提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码

对应的类型的encode值如下图,即@encode(id)结果就为@


Snip20180622_14.png

Class方法缓存

  • Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度(空间换时间)
    首次查找方法还是按正常流程查找,当找到后,会将方法缓存到cache中,下次再次调用方法,会先从cache中查找,若有则直接使用

  • 方法缓存(cache_t)内部结构

struct cache_t{
    struct bucket_t *_bucket; //散列表
    mask_t _mask; //散列表长度 - 1
    mask_t _occupied; //已经缓存的方法数量
}

//散列表内部结构
struct bucket_t{
    cache_key_t _key; //SEL作为key
    IMP _imp;
}
  • 散列表查找:第一次将方法放入缓存中时,会将@selector(key)&上上面的_mask,得出在数组中的索引,将其放入.若一开始数组为空,假设直接将其放入中间位置,则之前位置的内容置空,该方法是牺牲了内存空间换效率
  • 哈希表的核心,通过一个函数,将key生成一个索引,即f(key) == index
  • _mask的值为散列表长度-1,是因为&上的值,永远比_mask来的小.(&逻辑即某个值&_mask,其值也是小于_mask的)
  • 若key&mask的地址已经存在,则会直接将结果-1,即若原先的值为4,则会判断索引为3的位置是否有对应方法,有则存入,没有则继续-1操作.若索引为0,则直接将其值变为mask
  • 散列表数组当容量不足时会进行扩容,一旦散列表数组扩容,则会将缓存清空,扩容策略是,原先长度乘以2

若长度为4,当第4个方法即将进入缓存时,由于容量已满,则系统会进行扩容,扩容至8个,然后将刚刚的第4个方法放入,清空其他所有的,故此时当前的占用方法数(occupied)为1

  • 方法调用的本质:是通过传入对象和SEL去寻找对应方法并执行,即:[person test]该方法转为c/c++代码,实际是转为`objc_msgSend(person sel_registerName(“test”));
    消息接收者(receiver):person
    消息名称:test,故通过SEL作为key去缓存方法是有效率的
  • 方法缓存会先从cache中查找方法,若没有,则从方法列表中查找,若找到,则会添加到缓存cache中,若类方法没有,会从父类方法中的cache中查找,若没有,则从父类方法列表查找,依次类推...若存在,则会在自己的类对象cache中缓存一份

OC的方法调用

  • 消息机制:给方法调用者发送消息
  • OC方法调用,其实都是转换为objc_msgSend函数的调用

objc_msgSend的执行流程可以分为3大阶段
1)消息发送:即将消息发送给消息接收者,调用对应方法
2)动态方法解析
3)消息转发
objc_msgSend如果找不到合适的方法进行调用,会报错unrecogized selector sent to instance的错误

p.s 1.C语言的函数,在汇编中在方法名前会多出一个下划线”_ ”
2.在调试时输入指令 p(IMP)地址值 能查看该地址是否为对应的方法

  • 消息发送流程:


    Snip20180625_1.png
  • 动态方法解析:
    1)判断SEL,调用class_addMethod()方法
struct method_t {
    SEL sel;
    char *types;
    IMP imp;
};

//该方法会直接将方法添加到类对象的class_rw_t中,即methods中
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        //通过class_getInstanceMethod方法获取其他方法,该方法的类型为Method,内部实际为struct objc_method *,其等价于struct method_t
        struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));
        
        //Method method =  class_getInstanceMethod(self, @selector(other));
        //动态添加方法,但在开发中并不常用
        class_addMethod(self, sel, method->imp, method->types);
        return YES;//建议都返回YES,虽然返回NO也能成功
    }
    return [super resolveInstanceMethod:sel];
}
Snip20180626_4.png
  • 消息转发: 将消息转发给别人


    Snip20180626_12.png
//会先调用下面的方法查找有无实现,若实现,则直接将消息转发给return返回的对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test:)) {
        return [[MXCat alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
//若没有实现forwardingTargetForSelector:(SEL)aSelector方法,则会进入方法签名阶段
//方法签名:返回值类型、参数类型
//若方法签名返回空,则不会来到forwardInvocation方法了
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test:)) {
    
        //返回的方法签名决定了invocation的包装的参数和返回值等信息
        //参数的顺序:receiver、selector、other arguments
        return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
        
        //方法签名也可以由已实现方法的对象/类进行生成,即若MJCat对象也同样实现了对应的方法,可以使用MJCat对象生成对应方法签名
        //return [[[MJCat alloc]init] signatureWithObjCTypes:"i@:i"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//NSIvocation封装了一个方法调用,包括:方法调用者(invocation.target)、方法(invocation.selector)、方法参数(invocation getArgument方法);
//调用方法最终实现实际上是在forwardInvocation方法中
- (void)forwardInvocation:(NSInvocation *)anInvocation {

    //invoke方法可以令target对象调用对应的方法
    [anInvocation invokeWithTarget:[[MXCat alloc]init]];
    
    int age;
    [anInvocation getReturnValue:&age];
    
    NSLog(@"%d",age);
}
  • 若是类对象,对应的forwardingTargetForSelector,重签名方法,forwardInvocation方法应该改为+方法,因为从源码可知,这几个方法的调用者为消息接收者
  • forwardingTargetForSelector方法的本质就是objc_msgSend方法,故方法调用只关注消息接收者和SEL,与是否为对象方法还是类方法没有关系

@dynamic相关内容

  • 声明属性会帮忙生成get方法和set方法,以及带下划线的成员变量,同时还有set和get方法的实现,至于会自动生成set和get的实现,同时会出现@sycthesize关键字
@sycthesize age= _age, height = _height;
//意义是:为age属性自动生成一个_age的成员变量,及get和set方法的实现
//后面的版本xcode已经自动帮忙完成

@sycthesize age 
//此时,age的成员变量名为age

@dynamic age
//即不会生成_age成员变量,不会实现age的setter和getter的实现
  • @dynamic会提醒编译器不自动生成get和set方法的实现, 不要自动生成成员变量
  • @sycthesize和@dynamic均不影响set和get的声明

几道面试题

1.下面的代码输出的结果

//假设self为MXStudent类,其是MXPerson的子类
NSLog(@"[self class] = %@",[self class]);
NSLog(@"[self superclass] = %@",[self superclass]);
NSLog(@"[super class] = %@",[super class]);
NSLog(@"[super superclass] = %@",[super superclass]);
    
-----------------------------------------------------------
结果为:[self class] = MXStudent
      [self superclass] = MXPerson
      [super class] = MXStudent
      [super superclass] = MXPerson

解析:

struct objc_super {
  __unsafe_unretained _Nonnull id receiver; //消息接收者;
  __unsafe_unretained _Nonnull Class super_class; //消息接收者的父类
}

从底层看出,super方法内部为objc_msgSendSuper方法,第一个参数是上述的结构体,且该结构体为临时结构体,即为局部变量

  • super调用的receiver仍然是原先的对象本身
  • super_class表示对应的方法是从哪里开始找,及调用super后,直接从其父类中开始查找对应方法

但实际上,上述为C++代码实现,但真正的super方法底层是调用objc_msgSendSuper2方法,里面的结构体中第二个参数传入的是当前类,但在方法内会调用当前类的superclass方法从当前类的父类开始查找方法,所以本质是一样的

//class及superclass方法实现(伪代码)
- (Class)class{
    return object_getClass(self);
 }
 
- (Class)superclass{
    return class_getSuperclass(object_getClass(self));
 }

总结结论:[super message]的底层实现

  • 消息接收者仍然是子类对象
  • 从父类开始查找方法的实现

2.下面的代码输出的结果

NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); 
NSLog(@"%d", [NSObject isMemberOfClass:[NSObject class]]);
NSLog(@"%d", [MXPerson isKindOfClass:[MXPerson class]]); 
NSLog(@"%d", [MXPerson isMemberOfClass:[MXPerson class]]);

--------------------------------------
结果为:1
      0
      0
      0

解析:

  • -isMemberOfClass方法:判断调用对象的类对象是否就是后面的对象
  • -isKindOfClass方法:判断调用对象的类对象是否是后面的对象或其子类
  • +isMemberOfClass方法:判断调用对象的元类对象是否就是后面的对象
  • +isKindOfClass方法:判断调用对象的元类对象是否是后面的对象或其子类

注:[XXX isKindOfClass [NSObject class]];其中XXX不管是哪个类,只要是NSObject体系下的,都返回YES;

3.什么是runtime?平时项目中是否使用过?

  • OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
  • OC的动态性就是由runtime来支撑和实现的,runtime是一套C语言API,封装了很多动态性相关的函数
  • 平时编写OC代码,底层就是转换成了runtime API进行调用

具体应用:

  • 利用关联对象(associatedObject)给分类添加属性
  • 遍历类的所有成员变量(修改textfield的占位文字颜色,字典转模型,自动归档解档)
  • 交换方法实现(交换系统方法)
  • 利用消息转发机制解决方法找不到的问题

方法调用[person print],本质上就是通过person->isa,在类中找到对应的对象方法,即找到person对象最前面的8个字节(isa)找到对应的类对象

局部变量分配在栈空间
栈空间分配是从高地址到低地址的

oc对象的方法本质就是函数调用

LLVM的中间代码:
OC在变为机器代码之前,会被LLVM编译器转换为中间代码(.ll)

//可以通过以下指令生成中间代码:
clang -emit-llvm -S

runtime常用API:

Snip20180627_24.png

1.往类中添加属性/协议/方法等信息,在注册类之前完成比较好
类对象注册完毕,即所有的有关的类信息都注册好了
2.不能往已经定好的(注册好的)类中添加成员变量,因为其是放在ro中,是只读的

Snip20180627_25.png

runtime中copy.create等需要手动释放,即调用free函数释放

Snip20180627_27.png
Snip20180627_28.png

注:method_exchangeImplementations方法交换的是类对象中class_rw_t中的方法数组中的mothod_t中的IMP
调用method_exchangeImplementations就会清空类对象中的方法cache
用途:1.用于往系统自带的方法添加一些新东西等

Snip20180627_29.png

fundation框架有时存在表面上是一个类型,实际是另一种类型的情况(NSMutableArray),称之为类簇,比如,Nsstring,NSArray,NSDictionary等

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容