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表示全部参数占多少个字节,id
和SEL
都是指针,故为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.pngSnip20180627_25.png1.往类中添加属性/协议/方法等信息,在注册类之前完成比较好
类对象注册完毕,即所有的有关的类信息都注册好了
2.不能往已经定好的(注册好的)类中添加成员变量,因为其是放在ro中,是只读的
Snip20180627_27.png Snip20180627_28.pngruntime中copy.create等需要手动释放,即调用free函数释放
Snip20180627_29.png注:method_exchangeImplementations方法交换的是类对象中class_rw_t中的方法数组中的mothod_t中的IMP
调用method_exchangeImplementations就会清空类对象中的方法cache
用途:1.用于往系统自带的方法添加一些新东西等
fundation框架有时存在表面上是一个类型,实际是另一种类型的情况(NSMutableArray),称之为类簇,比如,Nsstring,NSArray,NSDictionary等
网友评论