本文引用资料:
https://juejin.im/post/58f833458d6d81005875f872
https://draveness.me/load
https://www.jianshu.com/u/2de707c93dc4
https://zhuanlan.zhihu.com/p/26379488
https://juejin.im/post/58f830c3a22b9d0065827442
https://segmentfault.com/a/1190000009333497
https://tech.meituan.com/2015/08/12/diveintomethodcache.html
掘金地址
请阅读:oc
与 smalltalk
的故事
阅读后,就会明白为何Runtime中好多msg_send···
开头的函数;一切行为,皆消息是smalltalk
的基本思想,oc
恰恰正是沿用了这种思想。
1、基本思想一:完全的面向对象。万事万物都是对象,比Java还要彻底的面向对象,包括数据常量也是对象。
2、基本思想二:一切行为(也就是java中的方法),不再理解为方法调用,而是理解为向一个对象发送消息,也就是向一个对象发送一条命令,这个消息命令也可以带参数。
而OC 的发明者 Brad Cox 和 Tom Love 在当时主流且高效的 C 语言基础上,借鉴 Smalltalk 这两个思想想要搞出一个易用且轻量的 C 语言扩展,但 C 和 Smalltalk 的思想和语法格格不入,比如在 Smalltalk 中一切皆对象,一切调用都是发消息:
(原文:https://segmentfault.com/a/1190000009333497)
请阅读: OC缘起
通过oc
与 smalltalk
的故事,我们只是初步了解了runtime的奇怪的语法形式,这也恰恰是《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
的消息分发分为以下几个步骤:
引用 原文:深入理解Objective-C:方法缓存
判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象
从缓存里寻找,找到了则分发
如果没有找到,利用objc-class.mm中_class_lookupMethodAndLoadCache3
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定义也包含了三个字段,分别是:
原作者解释了整个过程大概如下:
深入理解 Objective-C Runtime 机制 原文
在对象类的methodList中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;
如果没有找到,
上面我们清楚的了解到 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(@"改变了");
}
在运行时创建一个新类,只需要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);
}
参考 iOS Runtime(一) Runtime的应用
这篇文章讲解了一些runtime的具体应用:
关于+load方案看这个,runtime 在APP的启动速度优化时可以借鉴,也恰恰是iOS Runtime(一) Runtime的应用中所缺少的。
当然还有一个很系统的介绍runtime的总结:Runtime介绍,大家可以最后看这个总结,否则很难理解或者坚持下来。
个人能力不足,会有错误,请大家留言区纠正。
重申引用资料:
https://juejin.im/post/58f833458d6d81005875f872
https://draveness.me/load
https://www.jianshu.com/u/2de707c93dc4
https://zhuanlan.zhihu.com/p/26379488
https://juejin.im/post/58f830c3a22b9d0065827442
https://segmentfault.com/a/1190000009333497
https://tech.meituan.com/2015/08/12/diveintomethodcache.html