一、ObjC的动态特性(Runtime)

Reference:
Objective-C Runtime Programming Guide
iOS Runtime详解

简述

Runtime抽象的说OC的运行时系统,实质点讲它是一个由C和汇编编写的库(后逐步用C++重写)。这个库完成了OC对C的扩展,在编译期,通过它实现了OC向C/C++的装换;同时在运行时,它是OC面向对象和动态特性的基石:它让OC可以动态的创建类和对象、并通过方法调用来完成函数的动态绑定,进行消息的传递和转发。

概览

RuntimePreview/1.png)

要点

  • 类和对象都有个指向类的isa指针,所以类又称类对象,对象的isa指向所属类,类的isa指向元类;
  • 所有元类的isa都指向根类(NSObject)的元类,包括它自己;
  • 类别可以为类添加属性,但不能添加成员变量,可以用关联对象来代替成员变量,保存属性的值;
  • 消息决策时先在当前类的方法缓存中查找,再去继承链上的方法列表中查找,继承连上找到后,会缓存到当前类的方法缓存中;
  • 消息转发的系列方法中要注意调用父类的方法,以便处理当前类未处理的消息;

详解

Runtime的代码是开源的,从objc4-493版本开始,Runtime的实现代码逐步用C++进行重写,本文参照的源码是最后一个没有C++的版本objc4-437.3。

面向对象

1.对象

源码中对象的定义:

1
2
3
typedef struct objc_object {
Class isa;
} *id;

对象的定义是一个结构体,内部只有一个指向所属类的isa指针。

2.类

源码中类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct objc_class *Class;

struct objc_class {
Class 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;

Meta Class

类的结构体中也有一个指向所属类的isa指针,还有一个指向父类的super_class指针,类的isa指向的是类的类,被称之为元类;元类的super_class指针指向元类的父类,元类的isa指针则都指向根元类(即NSObject的元类);值得注意的是根元类的super_class指向的是根类(即NSObject),isa指针依然指向根元类(即它自己)

对象、类和元类之间的关系如图
Object-Class/2.png)

类对象:对象和类都有isa指针指向所属的类,甚至在C++实现的Runtime版本中,类的结构体直接继承自对象的结构体,所以说类也是对象,称之为类对象,类对象和元类是一一对应的,都是运行时由系统创建的单例。

objc_ivar_list

类中struct objc_ivar_list *ivars是成员变量列表,源码中定义的成员变量其结构如下:

1
2
3
4
5
6
7
8
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
}

其中ivar_type存储的是成员变量的数据类型、内存管理、访问权限等信息编码后的结果;提供Ivar *class_copyIvarList(Class cls, unsigned int *outCount)函数来获取一个类的成员变量列表。

在类中使用@property为类同时添加了属性和成员变量,属性的本质是set和get方法,提供objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)函数获取一个类的属性列表。

objc_method_list

类中struct objc_method_list **methodLists是对象方法列表,类方法就是类对象的方法,保存在元类的方法列表中,源码中方法的结构如下:

1
2
3
4
5
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

方法中method_name即Selector,method_types是方法的签名,method_imp则是方法的实现,提供Method *class_copyMethodList(Class cls, unsigned int *outCount)来获取一个类的对象方法列表。

objc_cache

类中struct objc_cache *cache是Selector的方法缓存,其中保存的数据结构如下:

1
2
3
4
5
typedef struct {
SEL name; // same layout as struct old_method
void *unused;
IMP imp; // same layout as struct old_method
} cache_entry;

方法缓存的设计源于一个理念:类的某个方法调用后,接下来会有很大的概率被再次调用。当向一个对象发送消息后,需要查找Selecotr对应的实现(IMP),查找顺序是先查找当前类的方法缓存,如果没有找到,则从当前类开始,沿着继承链在类的方法列表中查找,找到后会将Selector和IMP的映射缓存在当前类的方法缓存中。

objc_protocol_list

类中struct objc_protocol_list *protocols是协议列表,源码中协议的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface Object
{
Class isa; /* A pointer to the instance's class structure */
}

@interface Protocol : Object
{
@private
char *protocol_name OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocol_list OBJC2_UNAVAILABLE;
struct objc_method_description_list *instance_methods OBJC2_UNAVAILABLE;
struct objc_method_description_list *class_methods OBJC2_UNAVAILABLE;
}

协议是一个继承自Object的类,其内有该协议遵循的协议列表、对象方法列表、和类方法列表;提供Protocol **class_copyProtocolList(Class cls, unsigned int *outCount)函数来获取一个类的协议列表。

3.类别

源码中类别的结构如下:

1
2
3
4
5
6
7
struct objc_category {
char *category_name OBJC2_UNAVAILABLE;
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;
}

通过类别可以为类添加属性,但不能添加成员变量,类别添加属性时,可以通过objc_setAssociatedObject添加关联对象,代替成员变量来保存属性的值。

主类和类别中的方法、协议等都是在Runtime初始化函数_objc_initmap_images时,添加到类的列表中,类别虽然后添加,但却是插在列表前面,所以类别的方法可以覆盖主类中的同名方法。

_objc_init中的回调map_images完成后,类、协议、类别等已经初始化完成,然后在load_images触发时,再调用各个类和类别的+load方法,所以在iOS中+load调用时,App相关的类都已完成初始化,因此,在iOS中+load方法的不安全指的是不能保证其他类的+load已经执行,而不是指其它类未完成初始化,关于+load的执行顺序总结如下:

  • 父类的+load执行后子类的+load才执行;
  • 所有类的+load执行后类别的+load才执行;
  • 类别之间、没有依赖关系的类之间,+load执行的顺序与编译顺序相同(即buildPhases -> Compile Sources中的文件顺序);

initialize:除了+load方法还可以选择在initialize方法中做类的初始化相关工作,关于initialize的调用规则需要分清以下几点:

  1. initialize是在类的加载完成之后,第一次调用类的方法之前被调用;
  2. 子类的initialize中不需要显式的调用父类的方法,父类的initialize会被自动的调用;
  3. Category中的initialize会覆盖类中的initialize方法;

消息传递

Runtime通过方法调用来实现函数的动态绑定,即所调用的函数地址不是编译期硬编码在指令中,而是需要在运行期读取。方法调用又叫消息传递,消息由selector及其参数所构成,消息传递的核心是objc_msgSend函数:

1
id objc_msgSend(id self, SEL op, ...);

objc_msgSend函数中,第一个参数是消息的接收类/对象,第二个参数是selector,后面才是消息的参数,所以在一个方法实现的内部,有两个默认的参数self和_cmd,它们分别代表方法调用者自身和方法的selector。

消息传递,即对象进行方法调用,到执行对应实现,所执行的步骤大致如下:

  1. 根据对象/类的isa指针找到对象/类所属的类/元类;
  2. 先在当前类/元类的方法缓存中查找selector对应的IMP,找到IMP则去执行对应的函数实现;
  3. 方法缓存中没找到,则沿着继承链在类/元类的方法列表找查找selector对应的方法;
  4. 找到对应方法后,将方法中的IMP和selector的映射添加到当前类/元类的方法缓存中,并执行IMP对应的函数实现;
  5. 如果最终沿着继承链没有找到对应的方法,则启动消息转发机制;

消息转发

当一个消息在类及其继承链上最终没有找到对应的方法实现,将启动消息转发机制,消息转发的流程如图:

MessageForward/3.png)

启动消息转发后,消息的接收者还有三次机会来对消息做出处理:

  1. 动态方法解析:在+resolveClassMethod:/+resolveInstanceMethod:中,如果为消息动态的添加了一个类/对象方法,则重新再发送一次消息,如果没有为消息动态的添加方法,则继续消息转发的下一步(与动态解析方法返回YES/NO没有关系);
  2. 备援接受者:在-forwardingTargetForSelector:中如果返回了一个非nil的备援接受者,则将消息发送给备援接受者,如果返回nil,则继续消息转发的下一步;
  3. 转发消息:需要先在-methodSignatureForSelector:返回消息对应的方法签名,再在-forwardInvocation:中处理NSInvocation,如果没有返回方法签名,则会调用-doesNotRecognizeSelector:方法,并抛出异常;

在为当前类实现以上消息转发方法时,对于没有处理的消息,都应调用父类的方法,交由父类处理。在第三步转发消息时,方法签名可以参看官方文档Type Encodings,处理NSInvocation时,可以简单的转发给另外一个target,但这就跟返回备援接受者效果一样,一般如果消息转发需要走到这一步,则是需要编辑NSInvocation来更改消息内容,比如追加参数、更换selector等。

应用

Runtime是OC动态特性的基石,使用它可以实现OC语言层面的拓展,Runtime的应用非常的广泛,Runtime实现的功能也非常的实用,Runtime编程可以参见Objective-C Runtime Programming Guide,这里只对一些常见的应用做一个简单的介绍

关联对象

Runtime提供了三个函数来管理关联对象:

1
2
3
4
5
6
//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)

关联对象一个非常常用的地方是在类别中添加属性时,代替成员变量来保存属性的值。添加关联对象时,需要指明和属性修饰符类似的关联对象内存策略,常用的有以下几个:

  • OBJC_ASSOCIATION_ASSIGN,同@property (assign)或 @property (unsafe_unretained);
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC,同@property (nonatomic, strong);
  • OBJC_ASSOCIATION_COPY_NONATOMIC,同@property (nonatomic, copy);

ISA Swizzling

对象的isa指针指向了它所属的类,ISA Swizzling是指修改对象isa指向的类的技术。Runtime提供Class object_setClass(id obj, Class cls)函数来修改一个对象所指向的类,ISA Swizzling只会影响做了替换操作的对象,不会影响到整个类,替换示例:

1
2
3
4
5
6
7
8
BOOL swizzlingObjClass(id obj, Class newCls)
{
if (class_getInstanceSize(newCls) <= class_getInstanceSize([obj class])) {
object_setClass(obj, newCls);
return YES;
}
return NO;
}

上面的示例中使用了size_t class_getInstanceSize(Class cls)来比较ISA Swizzling前后类的实例的大小,这是因为将要ISA Swizzling的对象的内存是已经分配好的,如果替换后类的实例大于替换之前分配的内存,会导致EXC_BAD_ACCESS错误,所以ISA Swizzling时,替换后的类一般为替换前类的子类,而且子类中没有新增任何成员变量或合成属性。KVO就是通过ISA Swizzling技术来实现的。

当对某个类A的对象添加KVO之后,KVO机制会动态的创建一个A的子类(通常命名为NSKVONotifying_A),然后让被观察对象的isa指向这个子类,在子类中重写了-class方法,让-class依然返回原来的类,并重写了被观察keyPathsetter方法,在setter方法中调用了-willChangeValueForKey:-didChangeValueForKey:,当通过setter方法更改被观察的值时,观察者就能收到通知。

Method Swizzling

Runtime允许动态的新增、替换方法的实现,用新的实现替换原有方法的实现,就叫做Method Swizzling。一种特殊的Method Swizzling - 交换两个已有方法的实现,配合类别可以为原方法添加hook,完成对没有源码的原方法的修改。简单的hook使用method_exchangeImplementations即可实现,但实际上hook时有许多需要注意的问题,安全而健壮的hook可以参看文章Objective-C Method Swizzling

Zombie Objects

Zombie Objects是Xcode中用来调试野指针(EXC_BAD_ACCESS)这类内存问题的工具,它充分的应用了Runtime技术:它首先通过Method Swizzling替换原来的-dealloc方法,当对象引用计数为0的时候,会调用新的__dealloc_zombie方法;新的方法中又使用ISA Swizzling技术让对象的isa指向僵尸类,然后释放对象的引用,但并不释放当前对象;最后当程序再向本该已释放的对象发送消息时,在僵尸类的消息转发中就会输出调试信息。

Zombie Objects中__dealloc_zombie打大致实现(非源码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)__dealloc_zombie
{
Class cls = object_getClass(self);
const char *clsName = class_getName(cls);

char *zombieClsPrefix = "_NSZombie_";
char *zombieClsName = malloc(strlen(zombieClsPrefix)+strlen(clsName)+1);
strcpy(zombieClsName, zombieClsPrefix);
strcat(zombieClsName, clsName);

// 查看是否存在相同的僵尸对象类名,不存在则创建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
Class baseZombieCls = objc_lookUpClass("_NSZombie_");
zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
// 释放引用。
objc_destructInstance(self);
// ISA Swizzling
object_setClass(self, zombieCls);
}

YYModel

YYModel运用Runtime面向对象的特性,为根类NSObject添加扩展,实现JSON数据和OC模型之间自动转换。YYModel非常的轻量级,代码也非常的直观,其他利用Runtime特性实现JSON与OC模型互相转换的库还有很多(如:MJExtension),它们对Runtime的利用更加复杂,功能也相对丰富。

JSPatch

JSPatch是一个热修复库,它的基本原理是:JS传递字符串给OC,OC通过Runtime动态调用接口和替换OC方法的实现。JSPatch对Runtime的应用还包括动态的注册类,为类添加方法等,示例代码如下:

1
2
3
Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
class_addMethod(cls, selector, implement, typedesc);
objc_registerClassPair(cls);
-------------This article is over, thank you for reading -------------