学习要点
根据上面图,我们就可以很清楚每当发送一个消息,这个消息在对象、类对象、元类中的传递与走向。
-
- runtime(√√√√√√√√√√√√√√√√√√)
- runloop(√√√√√√√√√√√√√√√√√√)
- category(√√√√√√√√√√√√√√√√√√)
- protocol(√√√√√√√√√√√√√√√√√√√)
- extension(√√√√√√√√√√√√√√√√√√√)
- property(√√√√√√√√√√√√√√√√√√√√)
- 深复制与浅复制(√√√√√√√√√√√√√√√√)
- NSNotification(√√√√√√√√√√√√√√√√√√)
- Block(√√√√√√√√√√√√√√√√√√√√√√√√√)
- KVO&KVC(√√√√√√√√√√√√√√√√√√√√√)
- 多态性(√√√√√√√√√√√√√√√√√)
- 继承(√√√√√√√√√√√√√√√√√√√√)
- 垃圾回收机制(√√√√√√√√√√√√√√√√)
- 异常处理(√√√√√√√√√√√√√√√√√√√)
- 多线程()
Runtime:
- 作用
- 原理
- 实战应用
- 参考文章
(1)作用
- OC最开始是参考SmartTalk,而且smartTalk是一门动态语言,所以OC可以理解为在C的基础上扩展了runtime库,从而实现具有面向对象性。
- runtime是如何实现让OC具备面向对象性?
- 首先面向对象的一大特点就是把方法与属性捆绑在一起形成对象,所以OC就通过runtime,把C语言的结构体与函数捆绑到一起,到OC代码层我们是直观地编码一门面向对象语言,但实际上就是通过runtime在动态地调用C中结构体中的变量与相应的函数指针实现效果。
- runtime的实际主要做了什么事情?
- 封装:在runtime的封装下,对象的属性可以用结构体表示,而方法可以用C函数实现,并让程序运行时能实现创建、检查、修改类、对象方法。
- 消息传递:通过objc_sendmsg(),找到相应的函数指针与数据进行运算并返回结果。
(2)原理
- 类与对象
- 成员变量与属性
- 方法与消息
- 协议与分类
- …...
- 类与对象:
由于在Runtime中,类与对象的表示实际上就是结构体,因此我们得从结构体的组成的角度去探索类与对象在OC的实现机制。
类在runtime中的表示:
struct
objc_class {
Class isa OBJC_ISA_AVAILABILITY; //
isa指针用于指向自身属于的类,在OC中对象-》类对象-》元类,所以isa指针在这里指向原类
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; //指向父类的指针
const
char
*name OBJC2_UNAVAILABLE; //类名
long version OBJC2_UNAVAILABLE; //类版本
long
info OBJC2_UNAVAILABLE; //类信息
long instance_size OBJC2_UNAVAILABLE; //实例的控件大小
struct
objc_ivar_list *ivars OBJC2_UNAVAILABLE; //变量列表指针
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; //方法列表指针
struct objc_cache *cache OBJC2_UNAVAILABLE; //缓存指针
struct
objc_protocol_list *protocols OBJC2_UNAVAILABLE; //协议指针
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
对象在runtime的表示:
/// Represents an instance of a class.
struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; /// A pointer to an instance of a class.
typedef
struct
objc_object *
id
;
|
关于isa、super_class指针与类对象、元类的概念:
isa指针:存储着对象或者类所属的类型的指针,对象-》类对象,类对象-》元类。
super_class指针:指向父类的指针,若为顶层的跟类则为NULL
类对象与元类:对象-》类对象-》元类,这是在OC中的一种独特的组织方法,但类对象与元类在runtime中,本质上都是
objc_class结构体,区别是类对象管理动态方法,元类管理静态方法。
因此若我们用一个类调用类方法,如[NSDictionary dictionary];,在runtime的表现实际上就是objc_sendmsg(NSDictionary, @selector(dictionary));就是通过NSDictionary类对象的isa指针找到其对应的元类,并找元类的方法列表中找到相应的静态方法指针,并执行相应的代码。
那么一个类的元类的isa指针又会指向哪里?元类的isa指针会直接指向跟类(这里是NSObject的meta_class),而跟类的isa指针会自指形成闭环。
那么一个跟类的super_class的指向又会指向哪里?从下图我们可以发现root class的super指向了nil,而root meta的super指向了root class,那么最终还是指向了nil。
根据上面图,我们就可以很清楚每当发送一个消息,这个消息在对象、类对象、元类中的传递与走向。
关于super指针的调用实现:
关于oc中super指针容易造成一个误解,就是super指针是直接指向父类的,但并不是,如下
struct objc_super { id receiver; Class superClass; };
根据runtime中super指针的结构体,我们可以发现消息的接收者还是对象本身,它是通过自己的superClass指针来访问父类的。
关于instance_size的对象控件创建的实现:
instance_size用于记录创建该类对象的实例需要多大的内存空间,但根据OC的继承规则,这里会有个疑问,到底instance_size是指这个类所需的全部空间,还是需要把对象的继承体系中的所有instance_size相累加才是所需的控件大小?
答案是,每个类对象的instance_size已经是创建对象所需的全部存储空间,那时因为在创建一个子类时,即使我们不用调用父类的构造方法,也能访问从父类中继承回来的属性。
关于cache指针的实现:
OC是门动态语言,那么每调用一次方法则需要一次发送消息的行为,这样相比静态语言的C、C++直接调用函数对应的指针回慢不少,毕竟中间还穿插着一个runtime寻找对象的实现。那么如何能优化这个过程。
OC采用了缓存的策略,chahe指针的定义如下:
typedef
struct
objc_cache *Cache OBJC2_UNAVAILABLE;
#define CACHE_BUCKET_NAME(B) ((B)->method_name) #define CACHE_BUCKET_IMP(B) ((B)->method_imp) #define CACHE_BUCKET_VALID(B) (B) #ifndef __LP64__ #define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>> 2 ) & (mask)) #else #define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>> 3 )) & (mask)) #endif
struct
objc_cache {
unsigned
int
mask
/* total = mask + 1 */
OBJC2_UNAVAILABLE; //
unsigned
int
occupied OBJC2_UNAVAILABLE; //
Method buckets[ 1 ] OBJC2_UNAVAILABLE; //
};
关于Method的定义:
typedef
struct
objc_method *Method;
struct
objc_method {
SEL
method_name OBJC2_UNAVAILABLE; //SEL方法子,方法名经过hash后得到的一个字符串,与IMP是一一对应关系
char
*method_types OBJC2_UNAVAILABLE; //方法的类型,动态方法/静态方法
IMP
method_imp OBJC2_UNAVAILABLE; //IMP指针,指向具体的C实现函数
}
|
每当对一个对象或者类对象发送消息时,就会先访问其cache的方法列表里是否有相同的Method,有则直接调用其IMP实现调用。
PS:iOS中的方法名字是以字符串为准的,在同一个类中,即使字符串同名参数不同也会编译不过,那时因为sel的原理就会把方法名进行hash生成唯一的字符串,所以在比较的时候,实际上只比对指针地址。
关于ivar_list指针的实现:
ivar_list实际上就是指向实例变量的指针。下面是runtime中的定义代码:
struct
objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE; //变量的数量
#ifdef __LP64__
int space OBJC2_UNAVAILABLE; #endif /* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE; //数组,数组单元为
objc_ivar结构体
}
OBJC2_UNAVAILABLE;
/// An opaque type that represents an instance variable.
typedef
struct
objc_ivar *Ivar;
struct
objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE; //变量名
char *ivar_type OBJC2_UNAVAILABLE; //变量类型
int ivar_offset OBJC2_UNAVAILABLE; //变量的内存位置偏移量
#ifdef __LP64__
int space OBJC2_UNAVAILABLE; #endif
}
|
首先访问一个对象本质上就是对其内存地址进行读写操作,而对象runtime的表示就是一个isa指针,而在内存的表示就是isa指针指向的地址+各种实例变量的存储。
所以当我们访问一个对象,实际上就是通过
objc_ivar_list获取到相应的
objc_ivar,然后通过其
ivar_offset计算出实际的内存地址,计算方法为isa指针地址+ivar_offset。
关于method_list指针的实现:
struct
objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE; //废弃方法列表
int method_count OBJC2_UNAVAILABLE; //方法总数 #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE; //方法列表数组
} OBJC2_UNAVAILABLE;
/// An opaque type that represents a method in a class definition.
typedef
struct
objc_method *Method;
struct
objc_method {
SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE;
}
|
当在类对象或者元类中查询方法,实际上就是对
objc_method_list进行操作,通过遍历
objc_method_list方法列表通过SEL找到对应IMP执行调用,以IMP的返回作为返回结果,同事把对应的
objc_method缓存在cache中。
其中
struct objc_method_list *obsolete负责管理与维护准备废弃的方法。
关于protocol指针的实现:
struct
objc_protocol_list {
struct objc_protocol_list *next; long count; Protocol *list[ 1 ];
};
#ifdef __OBJC__
@class Protocol; #else typedef struct objc_object Protocol;
#endif
|
protocol的本质也是一个方法列表,实现与动态方法与静态方法是类似的,不同的是在类中它专门由
objc_protocol_list指针管理,这样就可以加快方法查询的效率。
关于property属性的实现:
typedef struct objc_property *objc_property_t;
typedef struct {
const char *name; // 特性名
const char *value; // 特性值
} objc_property_attribute_t;
|
property的本质就是实例变量+getter&setter,通过设置修饰词
objc_property_attribute_t,赋予getter&setter不同的实现效果。
类型编码(Type Encoding):
每一个函数的参数与返回的类型,都可以通过
@encode返回其编码,这个编码为类型编码,系统就是通过这些类型编码来识别变量的类型的。
如OC不支持long double,但我们依然可以使用long double进行变量的定义,但在底层实现中其
@encode返回结构与double是一致的,所以分配的存储空间大小也与double是一致的。
关联对象:
实现思路:通过
objc_getAssociatedObject
objc_setAssociatedObject
方法,把静态字符串(keyName)与相应的实例变量联系到一起。
经典应用:为category动态添加属性。
关于category的实现:
struct
objc_category {
char *category_name OBJC2_UNAVAILABLE; //category名
char *class_name OBJC2_UNAVAILABLE; //类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; //实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; //类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; //协议方法列表
} OBJC2_UNAVAILABLE;
|
从category的runtime表示中可以大概知道,在编译期间会把category中的实例方法、类方法、协议方法写入相应类对象或元类的实例方法、类方法、协议方法,根据OC的继承体系与消息发送规则,可知,这样若有与父类同名的方法,就会被子类拦截掉,从而实现重写。
消息转发的机制:
当一条消息在其对象的继承体系中找不到相应的方法调用,这会启动消息转发机制,若消息转发机制都无法处理,则会触发sigforbid的错误,程序奔溃。
消息转发机制的三个步骤:
- 动态方法的解析:(允许动态添加实例方法/类方法,处理未知消息)
- 备用接收者:(把消息转发到具备处理该消息的别的对象里)
- 完整转发:()
动态方法的解析:
类通过实现
+resolveInstanceMethod:(实例方法)或 者+resolveClassMethod:(类方法)。方法可以为未知消息添加相应的处理方法。
备用接收者:
若动态方法解析失败,则会调用以下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
同时通过该方法可以实现类似多继承的效果。
完整转发:
如果上两个步骤都无法处理未知消息,则启动完成的消息转发机制。有以下两个关键方法需要重写:
- (
void
)forwardInvocation:(
NSInvocation
*)anInvocation;
- (
NSMethodSignature
*)methodSignatureForSelector:(
SEL
)aSelector;
|
对象会创建
NSInvocation对象,里面包含消息处理的所有参数,并且我们能修改相应的参数,通过其实现相应的消息转发,并且消息转发的处理结果会作为原始发送者的处理结果。
其中
methodSignatureForSelector一定要重写,因为
aninvocaaion中有个关键参数signature,需要重写该方法来定义,才能正常地生成
aninvocaaion。
(3)实践应用
因为根据runtime给OC带来的动态性,以及其的调用规则,我们能灵活地添加、删除、改变方法的实现,能实现很多特殊的功能与效果。
首先可以看看runtime提供的函数类别:
即可得知它大概能实现什么功能,灵活的runtime function能让我们实现功能:
Method swizzling:
通过改变SEL所对应的IMP,从而实现方法的统一修改。
|
JSPatch:
通过消息转发机制,实现通过JS往OC中动态添加、修改方法的实现。
|
多继承:
通过消息转发机制,实现类似多继承的效果
|
为Category添加属性:
在编译期,category不允许自己创建属性,但通过runtime可以实现。
|
插件:
不少的xcode插件都是通过利用runtime在运行时注入代码实现的。
|
万能控制器跳转:
|
自动归档与解档:
本来写归档与接档程序,常规写法就是一个个属性手动写入,但这样会写入大量无聊的重复代码,通过runtime的函数可以大大简化这个过程。
通过class_copyIvarList函数取出相应对象的属性,从而获得ivar_name&ivar_value从而实现归档,通过这种方式能实现像遍历NSDictionary与NSArray一样地去遍历一个对象。
|
对象转model
实现方法同上。
|
未完待续。。。
(4)参考文章
- runtime 运行时之一 类与对象
- runtime 运行时之二 成员变量与属性
- runtime 运行时之三 方法与消息
- runtime 运行时之四 method swizzling