iOS中Runtime的设计

本文引用资料:

juejin.im/post/58f833…

draveness.me/load

www.jianshu.com/u/2de707c93…

zhuanlan.zhihu.com/p/26379488

juejin.im/post/58f830…

segmentfault.com/a/119000000…

tech.meituan.com/2015/08/12/…

一、Runtime中一切行为,皆消息?

请阅读:ocsmalltalk 的故事

阅读后,就会明白为何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缘起

通过ocsmalltalk 的故事,我们只是初步了解了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 的消息分发分为以下几个步骤:

引用 原文:深入理解Objective-C:方法缓存

  1. 判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象

  2. 从缓存里寻找,找到了则分发

  3. 如果没有找到,利用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缘起中,我们再看

原作者解释了整个过程大概如下:

  1. objc_msgSend方法按照一定的顺序进行操作,以完成动态绑定。
  2. objc_msgSend函数会依据接收者与selector的类型来调用适当的方法。
  3. 编译器执行上述转换时,在objc_msgSend函数中首先通过obj的isa指针找到obj对应的class。
  4. 每个对象内部都默认有一个isa指针指向这个对象所使用的类,isa是对象中的隐藏指针,指向创建这个对象的类。
  5. 在Class中先去cache中通过SEL查找对应函数(cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),
  6. 若cache中未找到,再去methodList中查找,若methodlist中未找到,则去superClass中查找,
  7. 若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
  8. 如果最终还是找不到相符的方法,那就执行“消息转发”(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

参考 iOS Runtime(一) Runtime的应用

这篇文章讲解了一些runtime的具体应用:

  • 将某些OC代码转为运行时代码,探究底层,比如block的实现原理
  • 拦截系统自带的方法调用(Swizzle 黑魔法),比如拦截imageNamed:、viewDidLoad、alloc
  • 实现分类也可以增加属性
  • 实现NSCoding的自动归档和自动解档
  • 实现字典和模型的自动转换。(MJExtension)
  • 修BUG神器,如果大型框架的BUG 通过Runtime来解决,非常好用。

关于+load方案看这个,runtime 在APP的启动速度优化时可以借鉴,也恰恰是iOS Runtime(一) Runtime的应用中所缺少的。

当然还有一个很系统的介绍runtime的总结:Runtime介绍,大家可以最后看这个总结,否则很难理解或者坚持下来。

个人能力不足,会有错误,请大家留言区纠正。

重申引用资料:

juejin.im/post/58f833…

draveness.me/load

www.jianshu.com/u/2de707c93…

zhuanlan.zhihu.com/p/26379488

juejin.im/post/58f830…

segmentfault.com/a/119000000…

tech.meituan.com/2015/08/12/…

猜你喜欢

转载自blog.csdn.net/weixin_34326558/article/details/91391831