Runtime知识点整理

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 动态语言的特性,在运行时做一些 ‘手脚’,去完成一些功能。

转载于:https://juejin.im/post/5b5fd65d5188251b1b44a594

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值