iOS-Runtime-原理篇

iOS-Runtime-原理篇_第1张图片
Runtime

号外 : 一些关于runtime的小demo在我的下一篇文章iOS-Runtime-实践篇中

我们都知道Objective-C是一门动态语言, 动态之处体现在它将许多静态语言编译链接时要做的事通通放到运行时去做, 这大大增加了我们编程的灵活性.

毫不过分地说, Runtime就是OC的灵魂.

接下来我就要拨开OC最外层的外衣, 带大家看看OC的真面目(C/C++).

目录

  1. 类和对象
  2. 消息发送和转发
  3. KVO原理

类和对象

@interface Person : NSObject {
    NSString *_name;
    int _age;
}

- (void)study;
+ (void)study;

@end

@implementation Person

- (void)study
{
    NSLog(@"instance - study");
}

+ (void)study
{
    NSLog(@"class - study");
}

@end

为了更好地说明类在底层的表现形式是怎样, 我们将上面代码利用clang -rewrite-objc Person.m指令将其用C/C++重写, 一窥究竟.

把不必要的删除, 整理后为下面

struct _class_t { 
    struct _class_t *isa; // isa指针
    struct _class_t *superclass; // 父类
    void *cache;
    void *vtable;
    struct _class_ro_t *ro; // class的其他信息
};
// class包含的信息
struct _class_ro_t { 
    unsigned int flags;
    unsigned int instanceStart;
    unsigned int instanceSize;
    unsigned int reserved;
    const unsigned char *ivarLayout;
    const char *name; // 类名
    const struct _method_list_t *baseMethods; // 方法列表
    const struct _objc_protocol_list *baseProtocols; // 协议列表
    const struct _ivar_list_t *ivars; // ivar列表
    const unsigned char *weakIvarLayout;
    const struct _prop_list_t *properties; // 属性列表
};

// Person(class)
struct _class_t OBJC_CLASS_$_Person  = {
    .isa = &OBJC_METACLASS_$_Person, // 指向Person-metaclass
    .superclass = &OBJC_CLASS_$_NSObject, // 指向NSObject-class
    .cache = &_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_CLASS_RO_$_Person, // 包含了实例方法, ivar信息等
};

// Person(metaclass)
struct _class_t OBJC_METACLASS_$_Person = {
    .isa = &OBJC_METACLASS_$_NSObject, // 指向NSObject-metaclass
    .superclass = &OBJC_METACLASS_$_NSObject, // 指向NSObject-metaclass
    .cache = &_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_METACLASS_RO_$_Person, // 包含了类方法
};

原来(显然), 我们的类其实就是一个结构体!!! 类跟我们的对象一样, 都有一个isa指针, 所以类其实也是对象的一种.

isa指针

isa指针非常重要, 对象需要通过isa指针找到它的类, 类需要通过isa找到它的元类. 这在调用实例方法和类方法的时候起到重要的作用.


iOS-Runtime-原理篇_第2张图片
isa指针

实例对象在调用方法时, 首先通过isa指针找到它所属的类, 然后在类的缓存(cache)里找该方法的IMP, 如果没有, 则去类的方法列表中查找, 然后找到则调用该方法, 找不到则报错.

类对象调用方法则如出一辙, 通过isa指针找到元类, 然后就跟上述一致了. 这里涉及的发送消息机制下面会详细讲..

下面展示一些运行时动态获取对象和类的属性的C语言方法

类和类名 :

// 返回对象的类
Class object_getClass ( id obj );
// 设置对象的类
Class object_setClass ( id obj, Class cls );
// 获取类的父类
Class class_getSuperclass ( Class cls );
// 创建一个新类和元类
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );
// 在应用中注册由objc_allocateClassPair创建的类
void objc_registerClassPair ( Class cls );
// 销毁一个类及其相关联的类
void objc_disposeClassPair ( Class cls );
// 获取类的类名
const char * class_getName ( Class cls );
// 返回给定对象的类名
const char * object_getClassName ( id obj );

ivar和属性 :

// 添加成员变量
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );
// 添加属性
BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
// 返回类的某一ivar
Ivar class_getInstanceVariable(__unsafe_unretained Class cls, const char *name)
// 返回对象中实例变量的值
id object_getIvar ( id obj, Ivar ivar );
// 设置对象中实例变量的值
void object_setIvar ( id obj, Ivar ivar, id value );
// 获取整个成员变量列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
// 获取属性列表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );

方法 :

// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 获取实例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 获取类方法
Method class_getClassMethod ( Class cls, SEL name );
// 获取所有方法的数组
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的实现
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 交换两个方法的实现(Method Swizzling)
void method_exchangeImplementations(Method m1, Method m2);

这里说个注意点 : addIvar并不能为一个已经存在的类添加成员变量, 只能为那些运行时动态添加的类, 并且只能在objc_allocateClassPairobjc_registerClassPair这两个方法之间才能添加Ivar.

消息发送和转发机制

在OC中, 如果向某对象发送消息, 那就会使用动态绑定机制来决定需要调用的方法. OC的方法在底层都是普通的C语言函数, 所以对象收到消息后究竟要调用什么函数完全由运行时决定, 甚至可以在运行时改变执行的方法.

[person read:book];
会被编译成
objc_msgSend(person, @selector(read:), book);

objc_msgSend的具体流程如下

1. 通过isa指针找到所属类
2. 查找类的cache列表, 如果没有则下一步
3. 查找类的"方法列表"
4. 如果能找到与选择子名称相符的方法, 就跳至其实现代码
5. 找不到, 就沿着继承体系继续向上查找
6. 如果能找到与选择子名称相符的方法, 就跳至其实现代码
7. 找不到, 执行"消息转发".
iOS-Runtime-原理篇_第3张图片
方法查找

消息转发

上面我们提到, 如果到最后都找不到, 就会来到消息转发

  • 动态方法解析 : 先问接收者所属的类, 你看能不能动态添加个方法来处理这个"未知的选择子"? 如果能, 则消息转发结束.
  • 备胎(后备接收者) : 请接收者看看有没有其他对象能处理这条消息? 如果有, 则把消息转给那个对象, 消息转发结束.
  • 消息签名 : 这里会要求你返回一个消息签名, 如果返回nil, 则消息转发结束.
  • 完整的消息转发 : 备胎都搞不定了, 那就只能把该消息相关的所有细节都封装到一个NSInvocation对象, 再问接收者一次, 快想办法把这个搞定了. 到了这个地步如果还无法处理, 消息转发机制也无能为力了.
动态方法解析 :

对象在收到无法解读的消息后, 首先调用其所属类的这个类方法 :

+ (BOOL)resolveInstanceMethod:(SEL)selector 
// selector : 那个未知的选择子
// 返回YES则结束消息转发
// 返回NO则进入备胎

假如尚未实现的方法不是实例方法而是类方法, 则会调用另一个方法resolveClassMethod:

备胎 :

动态方法解析失败, 则调用这个方法

- (id)forwardingTargetForSelector:(SEL)selector
// selector : 那个未知的选择子
// 返回一个能响应该未知选择子的备胎对象

通过备胎这个方法, 可以用"组合"来模拟出"多重继承".

消息签名 :

备胎搞不定, 这个方法就准备要被包装成一个NSInvocation对象, 在这里要先返回一个方法签名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
// NSMethodSignature : 该selector对应的方法签名
完整的消息转发 :

给接收者最后一次机会把这个方法处理了, 搞不定就直接程序崩溃!

- (void)forwardInvocation:(NSInvocation *)invocation
// invocation : 封装了与那条尚未处理的消息相关的所有细节的对象

在这里能做的比较现实的事就是 : 在触发消息前, 先以某种方式改变消息内容, 比如追加另外一个参数, 或是改变选择子等等. 实现此方法时, 如果发现某调用操作不应该由本类处理, 可以调用超类的同名方法. 则继承体系中的每个类都有机会处理该请求, 直到NSObject. 如果NSObject搞不定, 则还会调用doesNotRecognizeSelector:来抛出异常, 此时你就会在控制台看到那熟悉的unrecognized selector sent to instance..

iOS-Runtime-原理篇_第4张图片
消息转发流程

上面这4个方法均是模板方法,开发者可以override,由runtime来调用。最常见的实现消息转发,就是重写方法3和4,忽略这个消息或者代理给其他对象.

Method Swizzling

被称为黑魔法的一个方法, 可以把两个方法的实现互换.
如上文所述, 类的方法列表会把选择子的名称映射到相关的方法实现上, 使得"动态消息派发系统"能够据此找到应该调用的方法. 这些方法均以函数指针的形式来表示, 这种指针叫做IMP,

 id (*IMP)(id, SEL, ...)

iOS-Runtime-原理篇_第5张图片
NSString类的选择子映射表

OC运行时系统提供了几个方法能够用来操作这张表, 动态增加, 删除, 改变选择子对应的方法实现, 甚至交换两个选择子所映射到的指针. 如,

iOS-Runtime-原理篇_第6张图片
经过一些操作后的NSString选择子映射表

如何交换两个已经写好的方法实现?

// 取得方法
Method class_getInstanceMethod(Class aClass, SEL aSelector)
// 交换实现
void method_exchangeImplementations(Method m1, Method m2)

通过Method Swizzling可以为一些完全不知道其具体实现的黑盒方法增加日志记录功能, 利于我们调试程序. 并且我们可以将某些系统类的具体实现换成我们自己写的方法, 以达到某些目的. (例如, 修改主题, 修改字体等等)

KVO原理

KVO的实现也依赖Runtime. Apple文档曾简单提到过KVO的实现原理 :

Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...

Apple的文档提得不多, 但是大神Mike Ash在很早很早以前就已经做过研究, 摘下了KVO神秘的面纱了, 有兴趣的可以去查下, 这里不多深究, 只是简单阐述下原理.

原来当你对一个对象进行观察时, 系统会自动新建一个类继承自原类, 然后重写被观察属性的setter方法. 然后重写的setter方法会负责在调用原setter方法前后通知观察者. 然后把原对象的isa指针指向这个新类, 我们知道, 对象是通过isa指针去查找自己是属于哪个类, 并去所在类的方法列表中查找方法的, 所以这个时候这个对象就自然地变成了新类的实例对象.

不仅如此, Apple还重写了原类的- class方法, 视图欺骗我们, 这个类没有变, 还是原来的那个类. 只要我们懂得Runtime的原理, 这一切都只是掩耳盗铃罢了.


后记

这只是我的Runtime文章的第一篇, 之后还会有Runtime实践篇以及利用Runtime解决实际问题的几个demo, 感兴趣的话还请大家关注关注_

你可能感兴趣的:(iOS-Runtime-原理篇)