本文引用资料:
一、Runtime中一切行为,皆消息?
阅读后,就会明白为何Runtime中好多msg_send···
开头的函数;一切行为,皆消息是smalltalk
的基本思想,oc
恰恰正是沿用了这种思想。
Smalltalk 是世界上第二个面向对象的语言,其基本思想为:
-
1、基本思想一:完全的面向对象。万事万物都是对象,比Java还要彻底的面向对象,包括数据常量也是对象。
-
2、基本思想二:一切行为(也就是java中的方法),不再理解为方法调用,而是理解为向一个对象发送消息,也就是向一个对象发送一条命令,这个消息命令也可以带参数。 而OC 的发明者 Brad Cox 和 Tom Love 在当时主流且高效的 C 语言基础上,借鉴 Smalltalk 这两个思想想要搞出一个易用且轻量的 C 语言扩展,但 C 和 Smalltalk 的思想和语法格格不入,比如在 Smalltalk 中一切皆对象,一切调用都是发消息: (原文:segmentfault.com/a/119000000…)
二、Runtime的方法调用
通过oc
与 smalltalk
的故事,我们只是初步了解了runtime的奇怪的语法形式,这也恰恰是《OC缘起》中所表述模糊的,但是在《OC缘起》中,我们其实能看到更多。
OC的动态性
OC的函数调用称为消息发送,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数
在Objective-C里面调用一个方法到底意味着什么呢,是否和C++一样,任何一个非虚方法都会被编译成一个唯一的符号,在调用的时候去查找符号表,找到这个方法然后调用呢?
答案是否定的。在Objective-C里面调用一个方法的时候,runtime层会将这个调用翻译成:
[obj makeTest];
->objc_msgSend(obj,@selector(makeTest));
objc_msgSend方法包含两个必要参数:receiver、方法名(即:selector),如:[receiver message];将被转换为objc_msgSend(receiver, selector);
此外objc_msgSend方法也能使用message的参数,如: objc_msgSend(receiver, selector, arg1, arg2, …);
在Runtime中一个objc_class定义如下:
引用 iOS Runtime 之一:Class 和 meta-class 原文
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
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;
复制代码
isa:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类)
注:类是对象,类对象也是也是其他类的实例对象,所以Runtime中设计出了meta class,通过meta class来创建类对象,所以类对象的isa指向对应的meta clas,我们成为元类。而meta class 也是一个对象,所有元类的isa都指向其根元类,根元类的isa指向自己,通过这种设计,isa的整体结构形成了一个闭环。
实例对象、类对象、元类
-
实例对象:就是我们通常的类的实例化的对象比如Obj * obj = [Obj new];那么这个obj 就是一个实例对象
-
类对象:这个时候是否有点奇怪,其实类也是一个对象,比如Obj 其实也是一个类对象
-
元类:其实就是 类对象的isa指向的类。
我们关注objc_class
中的这个结构:
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率
接上文:objc_msgSend
的消息分发分为以下几个步骤:
-
判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象
-
从缓存里寻找,找到了则分发
-
如果没有找到,利用objc-class.mm中_class_lookupMethodAndLoadCache3
- 如果支持GC,忽略掉非GC环境的方法(retain等)
- 从本class的method list寻找selector,如果找到,填充到缓存中,并返回selector,否则
- 寻找父类的method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector,否则
- 调用_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则
- 转发这个selector,否则报错,抛出异常
objc_cache
的定义如下:
struct objc_cache {
uintptr_t mask; /* total = mask + 1 */
uintptr_t occupied;
cache_entry *buckets[1];
};
复制代码
objc_cache
的定义看起来很简单,它包含了下面三个变量:
-
1)、mask:可以认为是当前能达到的最大index(从0开始的),所以缓存的size(total)是mask+1
-
2)、occupied:被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目
-
3)、buckets:用数组表示的hash表,cache_entry类型,每一个cache_entry代表一个方法缓存 (buckets定义在objc_cache的最后,说明这是一个可变长度的数组)
而cache_entry
的定义如下:
typedef struct {
SEL name; // same layout as struct old_method
void *unused;
IMP imp; // same layout as struct old_method
} cache_entry;
复制代码
cache_entry定义也包含了三个字段,分别是:
- 1)、name,被缓存的方法名字
- 2)、unused,保留字段,还没被使用。
- 3)、imp,方法实现
理解了调用形式以及调用过程,包括方法的缓存,元类、类对象等概念后,回到OC缘起中,我们再看
原作者解释了整个过程大概如下:
- objc_msgSend方法按照一定的顺序进行操作,以完成动态绑定。
- objc_msgSend函数会依据接收者与selector的类型来调用适当的方法。
- 编译器执行上述转换时,在objc_msgSend函数中首先通过obj的isa指针找到obj对应的class。
- 每个对象内部都默认有一个isa指针指向这个对象所使用的类,isa是对象中的隐藏指针,指向创建这个对象的类。
- 在Class中先去cache中通过SEL查找对应函数(cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),
- 若cache中未找到,再去methodList中查找,若methodlist中未找到,则去superClass中查找,
- 若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
- 如果最终还是找不到相符的方法,那就执行“消息转发”(message forwarding)操作。
消息转发的过程:
深入理解 Objective-C Runtime 机制 原文
在对象类的methodList中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码; 如果没有找到,
- Runtime 会发送 +resolveInstanceMethod: 或者 +resolveClassMethod: 尝试去 resolve 这个消息;
- 如果 resolve 方法返回 NO,Runtime 就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象;
- 如果没有新的目标对象返回, Runtime 就会发送 -methodSignatureForSelector: 和 -forwardInvocation: 消息。你可以发送 -invokeWithTarget: 消息来手动转发消息或者发送 -doesNotRecognizeSelector: 抛出异常
三、Method Swizzling原理
上面我们清楚的了解到 runtime的方法调用,即向一个对象发送消息的实现原理。犹豫OC的动态性,我们还可以使用这个特性偷天换日。『狸猫换太子』,首先了解下IMP 上文提到在方法缓存cache_entry
的定义如下:
typedef struct {
SEL name; // same layout as struct old_method
void *unused;
IMP imp; // same layout as struct old_method
} cache_entry;
复制代码
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP指向具体的Method实现,交换IMP即可实现。IMP类似函数指针。
-
用 method_exchangeImplementations 来交换2个方法中的IMP;
-
用 class_replaceMethod 来修改类;
-
用 method_setImplementation 来直接设置某个方法的IMP,归根结底,都是偷换了selector的IMP。 引用原文OC缘起的一个例子
- (void)viewDidLoad {
[super viewDidLoad];
Method ori_Method = class_getInstanceMethod([self class], @selector(testOne));
Method my_Method = class_getInstanceMethod([self class], @selector(testTwo));
method_exchangeImplementations(ori_Method, my_Method);
[self testOne];
}
- (void)testOne {
NSLog(@"原来的");
}
- (void)testTwo {
NSLog(@"改变了");
}
复制代码
四、利用Runtime 动态加载类和方法
在运行时创建一个新类,只需要3步:
1、为 class pair分配存储空间 ,使用 objc_allocateClassPair 函数
2、增加需要的方法使用 class_addMethod 函数,增加实例变量用class_addIvar
3、用objc_registerClassPair函数注册这个类,以便它能被别人使用。
- (void)ex_registerClassPair {
Class TestClass= objc_allocateClassPair([NSObject class], "TestClass", 0);
//为类添加变量
class_addIvar(TestClass, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));
//为类添加方法 IMP 是函数指针 typedef id (*IMP)(id, SEL, ...);
IMP i = imp_implementationWithBlock(^(id this,id other){
NSLog(@"%@",other);
return @123;
});
//注册方法名为 testMethod: 的方法
SEL s = sel_registerName("testMethod:");
class_addMethod(TestClass, s, i, "i@:");
//结束类的定义
objc_registerClassPair(TestClass);
//创建对象
id t = [[TestClass alloc] init];
//KVC 动态改变 对象t 中的实例变量
[t setValue:@"测试" forKey:@"name"];
NSLog(@"%@",[t valueForKey:@"name"]);
//调用 t 对象中的 s 方法选择器对于的方法
id result = [t performSelector:s withObject:@"测试内容"];
NSLog(@"%@",result);
}
复制代码
五、category 与 extension
这篇文章讲解了一些runtime的具体应用:
- 将某些OC代码转为运行时代码,探究底层,比如block的实现原理
- 拦截系统自带的方法调用(Swizzle 黑魔法),比如拦截imageNamed:、viewDidLoad、alloc
- 实现分类也可以增加属性
- 实现NSCoding的自动归档和自动解档
- 实现字典和模型的自动转换。(MJExtension)
- 修BUG神器,如果大型框架的BUG 通过Runtime来解决,非常好用。
关于+load方案看这个,runtime 在APP的启动速度优化时可以借鉴,也恰恰是iOS Runtime(一) Runtime的应用中所缺少的。
当然还有一个很系统的介绍runtime的总结:Runtime介绍,大家可以最后看这个总结,否则很难理解或者坚持下来。
个人能力不足,会有错误,请大家留言区纠正。
重申引用资料: