概述
Runtime
的特征主要是消息(方法)传递,如果消息在对象中找不到,就进行转发
它是OC
面向对象和动态机制的基石
OC
是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象,进行消息传递和转发
Runtime消息传递
一个对象的方法像这样[obj method]
,编译器会转成消息发送objc_msgSend(obj, method).
Runtime
时执行的流程是这样的:
首先,通过obj
的isa
指针找到它的class
;
在class
的method list
找method
;
如果class
中没有method
,继续往它的superclass
中找;
一旦找到method
这个函数,就去执行它的实现IMP
.
但这种实现有一个问题,效率低.但一个class
往往只有20%
的函数会被经常调用,可能占到总调用次数的80%
.每一个消息都需要遍历依次objc_method_list
并不合理.如果把经常被调用的函数缓存起来,那可以大大提高函数查询的效率.这也就是objc_class
中有另一个重要成员objc_cache
做的事情–再找到method
之后,把method
的method_name
作为key
,method_imp
作为value
给存起来,当再次收到method
消息的时候,可以直接在cache
里找到,避免去遍历objc_method_list
.obj_cache
是存在objc_class
结构体的.
objc_msgSend
方法定义:
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
对象(object
)
struct objc_object {
Class isa;
};
类(class
):
struct objc_class {
Class isa;
Class super_class;
const char *name;
long verser;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols
};
方法列表:
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
int space;
struct objc_method method_list[1];
}
方法(method
):
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
}
- 系统首先找到消息的接收对象,然后通过对象的
isa
指针找到它的类. - 在它的类中查找
method_list
,是否有selector
方法. - 没有则查找父类的
method_list
. - 找到对应的
method
,执行它的IMP
. - 转发
IMP
的return
值.
一些概念
类对象(objc_class
):
typedef struct objc_class *Class;
在objc/runtime.h
中:
struct objc_class {
Class isa;
Class super_class;
const char *name;
long verser;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols
};
结构体里保存了指向父类的指针,类的名字,版本,实例大小,实例变量列表,方法列表,缓存,遵守的协议列表等.
objc_class
这个结构体存放的数据称为元数据(metadata
).
该结构体的第一个成员变量也是isa
指针,这就说明Class
本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例.
实例(objc_object
):
struct objc_object {
Class isa;
};
typedef struct objc_object *id;
类对象中的元数据存储的都是如何创建一个实例的相关信息.
类对象和类方法就是从isa
指针指向的结构体创建,类对象的isa
指针指向的我们称之为元类(metaclass
).
元类中保存了创建类对象以及类方法所需要的所有信息.
元类(Meta Class
):
struct objc_object
结构体实例的isa
指针指向类对象
类对象的isa
指针指向元类,super_class
指针指向父类的类对象,
而元类的super_class
指针指向了父类的元类,那元类的isa
指针又指向自己.
元类是一个类对象的类.
所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法).
为了调用类方法,这个类的isa
指针必须指向一个包含这些类方法的一个objc_class
结构体.这就引出了meta-class
,元类中保存了创建类对象以及类方法所需要的所有消息
任何NSObject
集成体系下的meta-class
都是使用NSObject
的meta-class
作为自己的所属类,而基类的meta-class
的isa
指针指向它自己.
Method
(objc_method
):
typedef struct objc_method *Method;
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
};
方法名, 方法类型, 方法实现
SEL
(objc_selector
):
typedef struct objc_selector *SEL;
objc_msgSend
函数第二个参数类型为SEL
, 它是selector
在OC
中的表示类型.selector
是方法选择器,可以理解为区分方法的ID
,而这个ID
的数据结构是SEL
:
@property SEL selector;
可以看到selector
是SEL
的一个实例.
其实selector
就是映射到方法的c
字符串,你可以用OC
编译器命令@selector()
或者Runtime
系统的sel_registerName
函数来获得一个SEL
类型的方法选择器.
IMP
:
typedef id (*IMP)(id, SEL, ...);
就是指向最终实现程序的内存地址的指针.
在ios
的Runtime
中,method
通过selector
和IMP
两个属性,实现了快速查询方法及实现,相对提高了性能,有保持了灵活性.
类缓存(objc_cache
):
当OC
运行时通过追踪它的isa
指针检查对象时,它可以找到一个实现许多方法的对象.然而,你可能只调用了他们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义.所以类实现一个缓存,每当你搜索一个类分派表,并且找到相应的选择器,它就把它放入它的缓存.所以当objc_msgSend
查找一个类的选择器,它首先搜索类缓存.
这是基于这个理论:如果你在类上调用了一个消息,你可能以后再次调用该消息,为了加速消息分发,系统会对方法和对应的地址进行缓存,就放下上述objc_cache
.所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime
系统实际上非常快,接近直接执行内存地址的程序速度.
Category
(objc_category
):
Category
是表示一个指向分类的结构体的指针
struct category_t {
const char *name; //是指class_name,不是category_name
classref_t cls; //要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象
struct method_list_t *instanceMethods; //category中所有给类添加的实例方法的列表
struct method_list_t *classMethods; //类方法的列表
struct protocol_list_t *protocols; //协议的列表
struct protocol_list_t *instanceProperties; //所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的.
};
从上面的category_t
的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量.
Runtime消息转发
进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索到继承树根部(通常为NSObject
),如果找不到并且消息转发都失败了就会执行doesNotRecongnzieSelector:
方法报unrecongnzied selector
错.
-
动态方法解析
首先,OC
运行时会调用+resolveInstanceMethod:
或者+resolveClassMethod:
,让你有机会提供一个函数实现,如果你添加了函数并返回YES
,那么运行时系统会重新启动一次消息发送的过程.
如果resolve
方法返回NO
,运行时就会转移到下一步:forwardingTargetForSelector
. -
备用接受者
如果目标对象实现了-forwardingTargetSelector:
,Runtime
这是会调用这个方法,给你把这个消息转发给其他对象的机会. -
完整消息转发
如果上一步还不能处理未知消息,则唯一能做的就是启动完整的消息转发机制.首先它会发送-methodSignatureForSelector:
消息获取函数的参数和返回值类型.如果-methodSignatureForSelector:
返回nil
,Runtime
则会发出-doesNotRecongnzieSelector:
消息,程序会直接挂掉.如果返回了一个函数签名,Runtime
就会创建一个NSInvication
对象并发送-forwardInvocation:
消息给目标对象.
通过签名,Runtime
生成一个对象anInvocation
,发送给了forwardInvocation
,我们可以在forwardInvocation
方法里让新对象去执行方法,签名参数v@:
.
Runtime应用
-
关联对象(
Objective-c Associated Objects
)给分类增加属性
分类是不能自定义属性和变量的,可通过关联对象实现分类添加属性//关联对象 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) //获取关联对象 id objc_getAssociatedObject(id object, const void *key) //移除关联对象 void objc_removeAssociatedObject(id object)
参数解释:
id object
: 被关联对象
const void *key
: 关联的key
,要求唯一
id value
: 关联的对象
objc_AssociationPolicy policy
: 内存管理策略通过关联对象实现的属性的内存管理也是由
ARC
管理的,所以我们只需要给定适当的内存管理策略就行,不需要操心对象的释放实现:
objc_setAssociatedObject
调用栈:void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) void objc_setAssociatedObject_non_gc(id object, void *key, id value, objc_AssociationPolicy policy) void _object_set_associative_refefence(id object, void *key, id value, uintptr_t policy)
调用栈中的
_object_set_associative_reference
方法实际完成了设置关联对象的任务
AssociationsManager
维护了spinlock_t
和AssociationsHashMap
的单例,初始化时会调用lock.lock()
方法,在析构时会调用lock.unlock()
,而associations
方法用于取得一个全局的AssociationsHashMap
单例
也就是说AssociationsManager
通过持有一个自旋锁splnlock_t
保证对AssociationsHashMap
的操作是线程安全的,即每次只会有一个线程对AssociationsHashMap
进行操作.如何存储
ObjcAssociation
ObjcAssociation
就是真正的关联对象的类,上面的所有数据结构只是更好的存储它.
首先,AssociationsHashMap
用于存储从对象的disguised_ptr_t
到objectAssociationsHashMap
映射
而ObjectAssociationsHashMap
则保存了从key
到关联对象ObjcAssociation
的映射
最关键的ObjcAssociation
包含了policy
以及value
例子:
关联对象ObjcAssociation(OBJC_ASSOCIATION_RETAIN_NONATOMIC, @"Hello")
在内存中是这样存储的:
AssociationsManager
static spinlock_t _lock
static AssociationsHashMap *_map
|
AssociationsHashMap
DISGUISE(obj) ObjectAssociationMap
|
ObjectAssociationMap
@selector(hello) ObjcAssociation
|
ObjcAssociation
OBJ_ASSOCIATION_RETAIN_NONATOMIC
@“Hello”接下来我们可以重新回到对
objc_setAssociatedObject
方法的分析了.
在这里会有两种情况:
new_value != nil
设置/更新关联对象的值
new_value == nil
删除一个关联对象new_value != nil
是这样执行的:- 使用
old_association(0, nil)
创建一个临时的ObjcAssociation
对象(用于持有原有的关联对象,方便在方法调用的最后释放值) - 调用
acquireValue
对new_value
进行retain
或者copy
- 初始化一个
AssociationsManager
,并获取唯一保存关联对象的哈希表AssociationsHashMap
- 先使用
DISGUISE(object)
作为key
寻找对应的ObjectAssociationMap
- 如果没有找到,初始化一个
ObjectAssociationMap
,在实例化ObjectAssociation
对象添加到Map
中,调用setHasAssociatedObjects
方法,表明当前对象含有关联对象 - 如果找到对应的
ObjectAssociationMap
,就会看key
是否存在,由此决定是更新原关联对象,还是增加一个 - 最后的最后,如果原关联对象有值的话,会调用
ReleaseValue()
释放关联对象的值
new_value == nil
就说明我们要删除对应key
的关联对象
这种情况下方法的实现和前面的唯一区别就是,我们会调用erase
方法,擦除ObjectAssociationMap
中key
对应的节点.objc_getAssociatedObject
调用栈:id objc_getAssociatedObject(id object, const void *key) id objc_getAssociatedObject_non_gc(id object, const void *key) id _object_get_associative_reference(id object, void *key)
- 获取静态变量
AssociationsHashMap
- 以
DISGUISE(object)
为key
查找ObjectAssociationMap
- 以
void *key
为key
查找ObjcAssociation
- 根据
policy
调用相应的方法 - 返回关联对象
ObjcAssociation
的值
- 使用
-
方法魔法(
Method Swizzling
)方法添加和替换和KVO
实现
方法添加class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
cls
被添加的方法的类
name
添加的方法的名称的SEL
imp
方法的实现,该函数必须至少要有两个参数,self
,_cmd
v@:
类型编码方法替换
swizzling
应该只在+load
中完成.在OC
的运行时中,每个类有两个方法都会被自动调用.+load
是在一个类被初始装载时调用,+initialize
是在应用第一次调用该类的类方法或者实例方法前调用.两个方法都是可选的,并且只有在方法被实现的情况下才会被调用.
swizzling
应该只在dispatch_once
中完成,由于swizzling
改变了全局的状态,所以我们需要确保每一个预防措施在运行时都是可用的.原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次,GCD
和dispatch_once
满足了所需要的需求,并且应该被当做使用swizzling
中初始化单例方法的标准.
KVO的实现. -
消息转发(热更新)解决
Bug
(JSPatch
)
JSPatch
是ios
动态更新框架,只需要在项目中引入极小的引擎,就可以使用JavaScript
调用任何OC
原生接口,获得脚本语言的又是:为项目动态添加模块,或者替换项目原生代码动态修复bug
. -
实现
NSCoding
的自动归档和自动解档
原理描述: 用Runtime
提供的函数遍历model
自身所有属性,并对属性进行encode
和decode
操作
核心方法: 在Model
的基类中重写方法 -
实现字典和模型的自动转换(
MJExtension
)
原理描述: 用Runtime
提供的函数遍历model
自身所有属性,如果属性在json
中有对应的值,则将其赋值
核心方法: 在NSObject
的分类中添加方法