Runtime介绍
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。 Runtime就是这个运行时系统。
Runtime 基本是用C和汇编写的,OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。
你可以在 这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。
平时的业务中主要是使用官方Api,解决我们框架性的需求。
Runtime消息传递
调用一个对象的方法: [obj foo]
编译器转成消息发送:objc_msgSend(obj, foo)
一个简单的demo,在main.m文件中
Person * person = [[Person alloc]init];
[person run];
复制代码
clang命令编译 clang -rewrite-objc main.m
打开编译后的main.cpp文件,一直拉到最后可以看见我们刚刚写的两行代码的编译结果
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person * person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("run"));
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
复制代码
可以清晰的看到[person run] 被编译成了objc_msgSend(person,run),我们常说在OC中调用一个对象,就是像一个对象发送一个方法指令。
要了解objc_msgSend消息传递的原理,先来了解几个概念:
1、实例(objc_object)
在objc.h中
typedef struct objc_object *id; // 指向 objc_object 结构体的指针
复制代码
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; // isa指针 - 指向类对象:类对象中存储了创建一个实例的信息
};
复制代码
2、类对象(objc_class)
objc.h 中 calss 的定义
typedef struct objc_class *Class; // 类对象是一个指向 objc_class 结构体的指针
复制代码
runtime.h 中 objc_class 结构体的定义
struct objc_class {
// isa指针 - 指向元类:元类存储了创建类对象以及类方法的所有信息
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
// 父类指针
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
// 变量列表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
// 方法列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
// 缓存
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
// 协议列表
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
复制代码
3、元类(Meta Class)
所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。 为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念。
实例中的isa指针指向类对象,类中保存了创建一个实例对象及实例方法所需的所有信息,类对象的isa指针指向元类,元类中保存了创建类对象以及类方法所需的所有信息。
基类的meta-class的isa指针是指向它自己
通过上图我们可以看出整个体系构成了一个自闭环
4、Method方法(objc_method)
runtime.h 文件中
typedef struct objc_method *Method; // 指向 objc_method 结构体的指针
复制代码
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE; // 方法名
char * _Nullable method_types OBJC2_UNAVAILABLE; // 方法类型
IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // 方法实习
}
复制代码
5、SEL方法(objc_selector)
objc.h 中
typedef struct objc_selector *SEL;
复制代码
@property SEL selector;
SEL 是 selector 的表示类型,
selector是方法选择器,是区分方法的ID,这个ID的数据结构是 objc_selector 结构体
复制代码
- 源码中没有objc_selector结构体的具体定义
- 其实就是个映射到方法的C字符串,命名规则是 className+methodName
- 导致不能像C语言一样写重载函数,就是函数名相同,参数不同。因为select只记了方法名没有参数,所以没有办法区分不同参数的方法。
6、 IMP 指针 - 指向最终实现程序的内存地址
objc.h 中
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
复制代码
Method通过 selector 和 IMP 两个属性,实现了快速查询方法及实现
7、 类缓存(objc_cache)
基于理论:如果你在类上调用一个消息,你可能以后会再次调用该消息。
为了加快消息分发,系统会对方法和对应的地址进行缓存,放在objc_cache中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method _Nullable buckets[1] OBJC2_UNAVAILABLE;
};
复制代码
mask 可以理解为当前能达到的最大的index
occupied 被占用的槽位
buckets 用数组表示的Hash表
runtime会根据这三项找到缓存的位置、经过一些计算在 bukets数组中找到buket、每一个bucket包含一个selector和一个IMP,通过对比selector来判断是否有缓存
在回过头来看objc_msgSend 的执行流程:
首先,判断接收对象 person 是否为nil;
根据person对象的isa指针找到它的class类;
从类的缓存中找run,找到则分发
如果缓存中没有,在 class 类的 method list 找 run ;
如果 class 中没到 run,继续往它的 superClass 中找 ;直到基类;
都没有找到,报错,抛出异常;
逐行剖析objc_msgSend汇编源码文章对objc_msgSend的汇编指令进行分析,缓存详细的分析了是怎么在方法中找到缓存。
消息转发
如果在一个对象的类和父类基类中都没有找到要执行的方法,程序会crash;控制台会显示类似错误信息:unrecognized selector,消息被发送给了不能处理它的对象。
OC是一门动态语言,我们可以在运行期做一些事来让crash不发生,消息转发机制就是用来解决这个问题的,在运行期通过3分 【接盘侠】方法,给对象和消息更多的机会来完成成功的调用,而不是直接 crash。
在一个函数找不到时,OC提供了三种方式去补救:
一号接盘侠:
动态解析阶段:运行期添加方法
+(BOOL)resolveInstanceMethod:(SEL)sel (实例方法调用)
+(BOOL)resolveClassMethod:(SEL)sel (类方法调用)
复制代码
通过class_addMethod动态添加一个方法
二号接盘侠:
备援接受者:转发给另1个对象、改变方法时
-(id)forwardingTargetForSelector:(SEL)aSelector
复制代码
询问是否把消息转发给其他接受者处理
三号接盘侠:
完整消息转发:需要转发给多个对象时
-(void)forwardInvocation:(NSInvocation *)anInvocation
复制代码
如果都不中,调用doesNotRecognizeSelector抛出异常。
Runtime的应用
1. 方法交换
使用 method_exchangeImplementations
Method m1 = class_getClassMethod([M1 class], @selector(method1name));
Method m2 = class_getClassMethod([M2 class], @selector(method2name));
method_exchangeImplementations(m1, m2);
复制代码
runtime的源码,在runtime.h中方法的声明
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
复制代码
objc-runtime-new.mm 文件中方法的实现,可以看到核心的代码实现,是交换了方法 m1 和 m2 的imp指针,所以当我们调用方法 m1 时,实际调用的是 m2 的imp,也就实现了方法的交换。这也能体现 OC 运行时语言的特点。
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
rwlock_writer_t lock(runtimeLock);
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp;
// RR/AWZ updates are slow because class is unknown
// Cache updates are slow because class is unknown
// fixme build list of classes whose Methods are known externally?
flushCaches(nil);
updateCustomRR_AWZ(nil, m1);
updateCustomRR_AWZ(nil, m2);
}
复制代码
2. 为category新增属性
我们都知道category中不能新增属性,准确的说是只能声明属性,而不能为我们增加属性的实现。category的实现原理,以及为什么不能新增属性,请移步这里有详细的介绍。
3. 其他
1、动态的添加一个类
KVO的实现就是利用runtime动态的添加类,系统是在程序运行的时候根据你要监听的类,动态添加一个新类继承自该类,然后重写原类的setter方法并在里面通知observer的;
2、通过 Runtime 获取一个类的所有属性
YYModel 等数据解析的框架都有用到,获取类的多有属性,属性名称,属性类型,利用递归的方式和 json 数据一一赋值;
3、动态变量控制,动态增加方法
4、自动归档和解档
5、插件开发
XCode官方不支持插件开发,通过头文件方法名猜测方法的作用,swizzle 这些方法,插入自己的代码实现插件逻辑。
6、JSPatch 热更新,其根本原理都是利用OC的动态语言特性去动态修改类的方法实现。
小结
runtime的应用还有很多,没一个点深入研究都是一个 topc,大家有兴趣和时间的时候可以逐一去研究其中的原理和实现。总之,runtime 的应用,就是利用 OC 动态语言的特性,在运行时做一些 ‘手脚’,去完成一些功能。