iOS runtime 理解消息传递、转发机制和使用案例

关于runtime,网上的资料都很全了,这里是根据自己的理解写一个学习总结报告。主要借鉴文章如下:
https://www.jianshu.com/p/db6dc23834e3 神经病院Objective-C Runtime出院第三天——如何正确使用Runtime
https://www.cnblogs.com/ioshe/p/5489086.html [iOS开发-Runtime详解]

1.runtime是用来做什么的?

1.1 与runtime相关最大的,就是OC语言的动态绑定机制。

动态绑定是指一个对象发送消息后,该消息的实现(实际执行的函数)根据运行环境的不同而不同(此处只针对OC,Swift中已经不是运行时加载方法,而是和C语言类似,在编译阶段就确定了)。实现该机制,常用的就是分类(categor)、类扩展(extension)、子类(subclass)继承等我们每个人都会使用的设计模式。
正常情况下,我们使用OC的这些特性就能够解决大部分问题。但是有些情况下,为了优雅、高效的解决问题,我们有时候希望从更底层的层面进行操纵。


1.2. 一个经典案例

有一个业务需求,我们希望统计某个页面viewController被点击的次数,或者在进入某些页面的时候添加引导图。常规的做法是在这些对应的页面的viewDidLoad中进行对应的需求作业。但是,如果项目比较大,页面非常多,或者层级很复杂,这样操作就效率很低,需要到不同的界面去进行分散的操作,日后新增、修改、维护或者调整也很麻烦。一个比较高效、优雅的做法是在基类UIViewContoller的viewDidLoad中实现该方法,因为所有的页面都会继承基类的viewDidLoad方法,在该基类中实现之后我们只需要在此处维护和新增就够了。
所以我们需要给UIViewContoller基类添加一个category分类,在分类中重写viewDidLoad方法。但是如果直接在分类中重写,会导致基类代码中的viewDidLoad不执行。此时,我们就需要使用runtime相关的方法来解决该问题(具体方式见第4节,第2条方法交换,如果需要深入了解,可以看看动态埋点统计的实例)。


1.3 常用runtime实现的强大功能

OC本质上是C的扩展和封装。我们的OC代码运行时,底层调用的实际上是c语言的代码。runtime(翻译过来即运行时)就是苹果暴露给用户的一个偏底层的可以操作底层代码的API接口,是对常用的设计模式的一个必要补充。通过该接口的一些函数,我们可以直接干预消息发送过程,从而实现很多强大的功能。比如

  • (1) 实现多继承Multiple Inheritance (利用消息转发机制)
  • (2) 在分类中重写原类方法而又不失去原类方法中的功能 (利用class的Method Swizzling)
  • (3) Aspect Oriented Programming (切片编程)
  • (4) 重写class方法(Isa Swizzling)
  • (5) 给分类添加属性变量( 利用Associated Object给分类添加关联对象)
  • (6) 动态的增加方法 (利用消息转发机制,在运行时实现方法)
  • (7) NSCoding的自动归档和自动解档(利用类底层的结构查询函数,批量给所有属性自动添加相同的解档、归档方法)

2. runtime API中主要内容

2.1 对象、类的定义

从下表可以看到,本质上类是一个指向类结构体的指针,而对象是一个指向对象结构体的指针,对象结构体中存储有一个isa类,它动态的指向该对象的类。类结构体中存储有类的名字,父类名字,类的成员变量(无论是通过@property还是直接定义的成员变量都存储在这里),类的实例变量大小(我们定义实例变量的时候变量空间大小就已经确定了),类的方法链表(普通类里面存储着该类的实例方法,元类中存储中该类的类方法),协议链表,方法缓存表(我们发送消息时第一个查询的结构体)等。

//对象结构体中存储有一个isa类
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;//Isa标识对象的类;它指向一个结构的类定义编译。
};
//所有的对象本质上都是一个id,而id是一个指向对象结构体的指针
typedef struct objc_object *id;
//类是一个指向类结构体的指针
typedef struct objc_class *Class;` 
`//类结构体中存储有该类定义的所有相关数据;` 
`struct objc_class {`
`Class isa  OBJC_ISA_AVAILABILITY;//Isa标识对象的类;它指向一个结构的类定义编译。`
`#if !__OBJC2__`
`Class super_class;``//父类`
`const char *name;``//类名`
`long version;``//类的版本信息,默认为0`
`long info;``//类信息,供运行期使用的一些位标识`
`long instance_size;``//类的实例变量大小`
`struct objc_ivar_list *ivars;``// 类的成员变量链表`
`struct objc_method_list **methodLists;``// 方法链表`
`struct objc_cache *cache;``//方法缓存`
`struct objc_protocol_list *protocols;``//协议链表#`
`endif} `
`OBJC2_UNAVAILABLE;`

因为类也有一个isa 指针,所以类本质上也是一个对象,称为类对象。类对象Isa指针标识的类为该类的元类(meta class),每一个类都是这个元类的唯一实例对象。元类对象Isa指针标识的类为根元类,根元类(root meta Class)在整个系统中只有一个,所有的元类的isa指针都指向根元类,根元类的Isa指针标识的类为自己。具体如下所示,图中虚线代表类的isa指针指向,实线代表类的父类。根元类的父类是根类,同时根元类的实例对象也是根类(root class),这里形成了一个闭环。


isa、superclass指针.png

isa指针指向:实例对象->类->元类->(不经过父元类)直接到根元类(NSObject的元类),根元类的isa指向自己;

2.2 Method、IMP、SEL的定义

把他们拿出来说,是因为容易他们之间存在相关性和差异,非常容易产生误解,而且他们对我们理解消息机制很有帮助,我们可以看一下方法Method的定义如下:

typedef struct objc_method *Method;
struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}       

Method 是一个指向结构体的指针,它包含了IMP和SEL,还包含了方法类型定义、方法的参数等。
SEL是方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号;
IMP是方法的具体实现函数指针,在runtime里,我们可以使用函数改变或者设置IMP来更改一个函数的具体实现,例如:

method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) //交换两个方法的实现
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) // 给一个方法设置实现 
2.3 runtime中常用的的一些函数

runtime中的函数,一般按照结构体的层级结构来操纵。对类中成员进行操作的,以class开头,对方法中成员进行操作的以method开头,其他的以此类推。常见的函数如下:

class_getProperty(Class _Nullable cls, const char * _Nonnull name)   //获取类的所有属性列表
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types)      //给类添加方法
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types)  //替换类方法
class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size, 
              uint8_t alignment, const char * _Nullable types)    //增加类变量
method_getImplementation(Method _Nonnull m)   //获取方法的实现
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)  //设置方法的实现
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)  //交换方法的实现
imp_implementationWithBlock(id _Nonnull block) //使用一个block创建一个实现
sel_getName(SEL _Nonnull sel) //获取方法的名称
sel_registerName(const char * _Nonnull str) //注册一个方法

3.消息传递、转发机制

想要合理的利用runtime中相关API接口,必须理解runtime中的消息传递、转发机制。
(1)当一个对象发送消息时,首先,底层会执行一个消息发送函数,函数长这样

 objc_msgSend(void /* id self, SEL op, ... */ 

如果是使用super发送消息,函数长这样:

 objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ 

(2)底层会从该对象所属的类中(isa指针所指的类)的方法缓存列表查找对应的实现
(3)如果2找不到,会从该类的方法链表中继续查找
(4)如果3找不到,会跳转到该类的父类查找,父类步骤和子类一样,具体如下图所示。


isa方法传递.gif

(5)一直向上到根类,如果根类仍然找不到,就开始准备进行消息转发。转发第一步:动态消息解析。查看当前类是否实现了resolveInstanceMethod方法(如果是类方法,会看是否实现了resolveClassMethod方法)。如果该方法返回了YES,消息转发终止。我们可以在这个方法中动态添加方法实现,不实现也不要紧,只要返回YES消息发送就不会报错。

+(BOOL)resolveClassMethod:(SEL)sel
{
    NSString * selStr = NSStringFromSelector(sel);
    if ([selStr isEqualToString:@"runTest"]) {
//注意,想要给类添加方法,必须添加到它的metaClass上,所以在class_addMethod中添加的类都要是原类!!!
//  确定metaClass的方法是objc_getMetaClass(object_getClassName(self));
        if (class_addMethod(objc_getMetaClass(object_getClassName(self)), sel,class_getMethodImplementation(objc_getMetaClass(object_getClassName(self)), @selector(runTestFunction)), "s@:")) {
            return YES;
        }
        return [super resolveClassMethod:sel];
    }
    return [super resolveClassMethod:sel];
}

(6)如果第5步返回NO,就开始消息重定向。查看是否指定了其他对象来执行该方法。具体是查看当前类是否实现了forwardingTargetForSelector方法;如果该方法返回了一个对象,就在该对象上执行该selctor方法(该对象上执行该方法时步骤与本对象一致);

-(id)forwardingTargetForSelector:(SEL)aSelector

(7)如果第6步返回nil,就需要进行真正的消息转发机制。具体是查看当前类是否实现了methodSignatureForSelector方法,如果该方法返回不为nil,就执行forwardInvocation方法。如果forwardInvocation实现了,消息转发终止(但不见得消息转发完成,forwardInvocation只是一个消息的分发中心,将这些不能识别的消息转发给不同的接收对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。)。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

(8)上述步骤,如果到第7部都没有实现,系统就会报错,提示unrecognized selector sent to instance
注意:上述任何一步,都要在前一步骤没有完成的基础上 。

消息转发机制.png

4.如何使用runtime

基于runtime提供的数据结构,以及上述消息传递、转发机制,runtime提供了丰富的函数让我们来实现我们第1节中提到的强大的功能,我们这里简单梳理下实现方式:

  • (1) 实现多继承Multiple Inheritance (利用消息转发机制),如下图所示。


    1330553-c7ef6392ecc9ee9d.gif

我们在Warrior中头文件中定义一个方法negotiate,但是不实现它,而在forwardingTargetForSelector方法中,针对该selecotr,指定一个Diplomat对象,就可以将该方法实现交给diplomat类来实现。看起来就像是Warrior也继承了了Diplomat的方法一样(注意,像respondsToSelector:isKindOfClass: 这类方法只会考虑继承体系,不会考虑转发链。也就是说如果[Warrior respondsToSelector:negotiate]会返回NO)。

  • (2) 在分类中重写原类方法而又不失去原类方法中的功能 (利用class的Method Swizzling)

    实现方式是通过runtime中的实现交换函数method_exchangeImplementations。首先,在本类中定义另一个待交换的方法exchage_ViewDidLoad;待交换的方法中需要调用原方法,然后添加需要额外实现的功能(例如第1节中提到的数据统计方法)。在恰当的时机(一般是在load方法中),交换该两个方法的实现。实际执行代码的使用,调用原类方法会执行待交换的方法的实现,待交换的方法实现中又会调用原来的方法实现,从而保留了原来的方法的实现。

+(void)exchangeOriginMethodWithMethodExchangeMethod
{
//    防止方法被多次调用后交换失效;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originSEL =  @selector(viewDidLoad);
        SEL swizzSEL = @selector(exchage_ViewDidLoad);
        Method viewDidLoad = class_getInstanceMethod([self class], originSEL);
        Method exchang_viewDidLoad = class_getInstanceMethod([self class], swizzSEL);
//        测试原来的选择子是否已经添加了方法(是否已经交换了方法);
        Boolean didAddMethod = class_addMethod([self class], originSEL,method_getImplementation(exchang_viewDidLoad),method_getTypeEncoding(exchang_viewDidLoad));
        if (!didAddMethod) {
//
            //        如果没有添加方法,就直接交换
            method_exchangeImplementations(viewDidLoad, exchang_viewDidLoad);
        }else{
//            如果已经添加了,就同时更换交换后的方法实现;
            class_replaceMethod([self class], swizzSEL, method_getImplementation(viewDidLoad), method_getTypeEncoding(viewDidLoad));
        }
    });
}
-(void)exchage_ViewDidLoad
{
    NSLog(@"%@ did load",self);
    [self exchage_ViewDidLoad];//注意,exchage_ViewDidLoad的实现现在是viewDidLoad了,所以没有循环调用
}
  • (3) Aspect Oriented Programming (切片编程,内容太多,暂不展开,可以看这里)

  • (4) 重写class方法(Isa Swizzling)

    苹果著名的KVO技术和NSNotificationCenter就使用的该方法,在我们给一个对象添加了KVO键值观察方法后

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)contex。

后台会重新创建一个NSKVONotifying_Object类,然后偷偷将原来的类的isa指针指向该类。该类中会在属性变量修改时候,调用

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

方法,并发出相应的通知

  • (5) 给分类添加属性变量( 利用Associated Object给分类添加关联对象)

    我们可以给分类添加属性,但是分类不会自动给我们生成成员变量。因为类的成员变量在编译器已经决定了(写入了类的结构体中,具体见前面结构体的定义),但是category是在运行期才决议的。所以如果要给分类添加成员变量,需要用runtime里面函数在运行期实现。一般使用objc_setAssociatedObject和objc_getAssociatedObject函数来实现。这两个函数都是成对的出现,一个给对象添加关联对象,一个获取关联对象。具体代码如下。

  @property(nonatomic,strong)id associatedObjcet;
-(void)setAssociatedObjcet:(id)associatedObjcet{
    objc_setAssociatedObject(self, @selector(associatedObjcet), associatedObjcet, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(id)associatedObjcet{
    return objc_getAssociatedObject(self, @selector(associatedObjcet));
}
  • (6) 动态的增加方法 (利用消息转发机制,在运行时实现方法)

    动态的增加方法和多重继承有些类似,都是调用的方法在类中并没有实现代码,而是在消息转发机制的某一步才动态的添加实现代码。消息转发机制本身有多步骤,所以根据需要,可以在不同的步骤实现动态添加,常见的一般在方法动态解析resolveInstanceMethod或者在消息转发forwardInvocation的时候进行。

  • (7) NSCoding的自动归档和自动解档(利用类底层的结构查询函数,批量给所有属性自动添加相同的解档、归档方法)

NSCoding其实就是对所有的属性调用encode和decode方法。使用手动操作有一个缺陷,如果属性多起来,要写好多行相似的代码,虽然功能是可以完美实现,但是看上去不是很优雅。用runtime实现的思路就比较简单,我们循环依次找到每个成员变量的名称,然后利用KVC读取和赋值就可以完成encodeWithCoder和initWithCoder了,部分代码如下:

 Ivar *vars = class_copyIvarList([self class], &outCount); 
for (int i = 0; i < outCount; i ++) {
 Ivar var = vars[i]; 
const char *name = ivar_getName(var); 
NSString *key = [NSString stringWithUTF8String:name];
 id value = [aDecoder decodeObjectForKey:key];
 [self setValue:value forKey:key]; 
 }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容