iOS-Runtime-原理篇

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找到它的元类. 这在调用实例方法和类方法的时候起到重要的作用.


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. 找不到, 执行"消息转发".
方法查找

消息转发

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

  • 动态方法解析 : 先问接收者所属的类, 你看能不能动态添加个方法来处理这个"未知的选择子"? 如果能, 则消息转发结束.
  • 备胎(后备接收者) : 请接收者看看有没有其他对象能处理这条消息? 如果有, 则把消息转给那个对象, 消息转发结束.
  • 消息签名 : 这里会要求你返回一个消息签名, 如果返回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..

消息转发流程

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

Method Swizzling

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

NSString类的选择子映射表

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

经过一些操作后的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, 感兴趣的话还请大家关注关注_

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 一、Runtime简介 Runtime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消...
    林安530阅读 1,008评论 0 2
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,097评论 0 9
  • 如果想了解Runtime的实际应用请看Runtime全面剖析之简单使用 一:Runtime简介二: Runtime...
    iYeso阅读 783评论 0 2
  • 目录 Objective-C Runtime到底是什么 Objective-C的元素认知 Runtime详解 应用...
    Ryan___阅读 1,896评论 1 3