iOS_DeviOS Developer程序员

Runtime (整理笔记)

2017-01-18  本文已影响215人  果哥爸

强烈推荐,关于runtime只需要看下一缕殇流化隐半边冰霜的这几篇文章就够。

作者:一缕殇流化隐半边冰霜
神经病院Objective-C Runtime住院第二天——消息发送与转发
神经病院Objective-C Runtime入院第一天——isa和Class
神经病院Objective-C Runtime出院第三天——如何正确使用Runtime

**说明:此文是自己的总结笔记,主要参考这几篇文章:
iOS开发-Runtime详解
NSHipster里面的这两篇文章 :
Associated Objects
Method Swizzling
**

树1.jpg

一.Runtime简介

可能通过以上你看不出它的价值,但是我们需要了解OC是一门动态语言,它会将一些工作放在代码运行时才处理而非在编译的时候,也就是说,有很多类和成员变量在我们编译的时候是不知道的,而在运行时,我们所编写的代码会转换成完整的代码运行。
因此,编译器是不够,我们还需要一个运行时的系统来处理编译后的代码

二. Runtime的作用:

三.Runtime的术语的数据结构

1.SEL

2.id
id 是一个参数类型,它是指向某个类的实例的指针。定义如下:

typedef struct objc_object *id;
struct objc_object { Class isa; };

通过以上定义,可以看到:objc_object结构体包含一个isa指针,根据isa指针就可以找到对象所属的类。

注意:isa指针在代码运行时并不总是指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型需要用对象的 - class方法。

3.Class

typedef struct objc_class *Class;

Class 其实是指向 objc_class 结构体的指针。objc_class的数据结构如下:

 struct objc_class {
     Class isa;//指针,顾名思义,表示是一个什么,
      //实例的isa指向类对象,类对象的isa指向元类

   #if !__OBJC2__
      Class super_class;  //指向父类
      const char *name;  //类名
      long version; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
      long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
      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;
  /* Use `Class` instead of `struct objc_class *` */

从结构体可以看出,一个运行时类中关联了它的父类指针、类名、成员变量、方法、缓存以及附属的协议。
其中objc_ivar_list和objc_method_list 分别是成员变量列表和方法列表:

// 成员变量列表
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

// 方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}



我们都知道,OC中一切都被设计成对象,一个类被初始化成一个实例,这个实例是一个对象。实际上一个类的本质也是一个对象,在runtime中用如上结构体表示。
关于isa指针:

 比如 : NSString *tmpStr = [NSString string];

这里的tmpStr的isa指针指向类对象NSString,而NSString的isa指针指向元类NSObject.

4. Method
Method 代表类中某个方法的类型

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

objc_method存储了方法名、方法类型和方法实现:

5.Ivar
Ivar 是成员变量的类型。

typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char *ivar_name                                     // 变量名称;
    char *ivar_type                                       // 变量类型;
    int ivar_offset                                         // 基地址偏移字节;
#ifdef __LP64__
    int space                                               // 大小;
#endif
}

其中 ivar_offset 是基地址偏移字节

6.IMP
IMP在objc.h中的定义是:
typedef id (*IMP)(id, SEL, ...);
它是一个函数指针,这是由编译器生成的。当你发送一个objc消息之后,最终它会执行那段代码,就是由这个函数指针指定的。而IMP这个函数指针就指向了这个方法的实现。

我们发现IMP指向的方法和objc_msgSend函数类型相同,参数都包含id和SEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组id和SEL参数就能确定唯一的实现方法地址。所以一个确定的方法也只有一组id和SEL参数:
比如:

NSString *tmpStr = [NSString string];
BOOL isContain =  [tmpStr containsString:@"1"];

这里的containsString的实现方法就是由id(NSString)和SEL参数(containsString)确定的。

7.Cache

typedef struct objc_cache *Cache

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

Cache主要用来提高查找效率,当一个方法被调用,首先在Cache列表中查找,如果找到直接返回,如果没有找到,再到类的方法列表去查找,找到了将该方法返回同时存入缓存列表。

8.Property

typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用

可以通过class_copyPropertyList 和 protocol_copyPropertyList 方法获取类和协议中的属性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

注意返回的是属性列表,列表中的每个元素都是一个objc_property_t指针

树2.jpg

四.获取列表

有时候会有这样的需求,我们需要知道当前类中每个属性的名字(比如字典转模型,字典的key和模型对象的属性名字不匹配)。
我们可以通过runtime的一系列方法获取类的一些信息(包括属性列表、方法列表、成员变量列表和遵循的协议列表)

unsigned int count;
//获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
    const char *propertyName = property_getName(propertyList[i]);
    NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}

//获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
    Method method = methodList[i];
    NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}

//获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
    Ivar myIvar = ivarList[i];
    const char *ivarName = ivar_getName(myIvar);
    NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}

//获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
    Protocol *myProtocal = protocolList[i];
    const char *protocolName = protocol_getName(myProtocal);
    NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}

五.方法调用

方法调用在运行时的过程:


1. 首先,在相应操作对象中的缓存列表中查找调用的方法,如果找到,转向相应的实现并执行.(即先在tmpStr这个对象的缓存列表中查找是否有containsString这个方法,如果有,则转向相应的实现函数,并执行);

2.如果没有找到,在相应操作对象的方法中找调用的方法,如果找到,转向相应的实现并执行。(即tmpStr的缓存列表找没有找到containsString这个方法,就到tmpStr的方法列表里面查找,如果找到,则转向相应的实现函数,并执行);

3.如果没找到,去父类指针所指向的对象中执行1,2(即如果在tmpStr的方法列表里面没有找到containsString这个方法,则转向tmpStr的父类,也就是NSObject类去查找该方法)

4.以此类推,如果一直到根类都还没找到,转向拦截调用(即如果在父类NSObject里面也没有找到containsString这个方法,就往上一层父类再去查找,知道最顶层(根层)父类,因为NSObject在OC中是根层父类,所以如果在NSObjec的方法列表找没找到containsString,就转向拦截调用)

5.如果没有重写拦截调用的方法,程序报错。(即在tmpStr及其父类的方法列表中都没有containsString这个方法,就转向拦截调用,但是却没有实现拦截调用的方法,系统就报错)

所以:

这里的super标识,在运行时会先去执行父类的viewWillAppear方法,执行完毕之后,在回来接着执行原函数里面的隐藏导航栏的操作,如果没有 [super viewWillAppear:animated];就不会去调用父类的该方法,直接执行隐藏导航栏的操作。

六.拦截调用

拦截调用就是在找不到调用方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理,防止崩溃发生。

消息转发顺序图.png
  + (BOOL)resolveClassMethod:(SEL)sel;

该方法就是当你调用一个不存在的类方法的时候,会调用该方法,默认返回NO,你可以加上自己的处理然后返回YES.

  + (BOOL)resolveInstanceMethod:(SEL)sel;

这个方法和上一个方法相似,处理的是实例方法。(备注: NSString *tmpStr = [NSString string];像 [NSString string]这里的string就是类方法, [tmpStr containsString:@"1"]这里的containsString就是实例方法。)

  - (id)forwardingTargetForSelector:(SEL)aSelector;

该方法将你调用的不存在的方法重定向到一个声明了这个方法的类,只需要你返回一个有这个方法的target.

  - (void)forwardInvocation:(NSInvocation *)anInvocation;

该方法将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用inovkeWithTarget:方法让某个target触发这个方法。

树3.jpg

七.动态添加方法

重写了拦截调用的方法并且返回YES,接下来可以根据传入的SEL类型的selector,动态添加一个方法。

首先从外部隐式调用一个不存在的方法:

// 隐式调用方法
[target performSelector:@selector(resolveAdd:) withObject:@"test"];

然后,在target对象内部重写拦截调用的方法,动态添加方法

void runAddMethod(id self, SEL _cmd, NSString *string){
    NSLog(@"add C IMP ", string);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    //给本类动态添加一个方法
    if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) {
        class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
    }
    return YES;
}

其中class_addMethod的四个参数分别是:

八.关联对象

比如现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。
这种情况的一般解决办法就是继承。
但是,只增加一个属性,就去继承一个类,总结太麻烦,这时候runtime的关联属性就发挥它的作用了。

1.首先定义一个全局变量,用它的地址作为关联对象的key

static char kAssociatedObjectKey; 

2.在NSObject+AssociatedObject.h里面添加新的属性

 NSObject+AssociatedObject.h

@interface NSObject (AssociatedObject) 
@property (nonatomic, strong) id associatedObject; 
@end 

3.在NSObject+AssociatedObject.m里面添加设置和获取方法

 //设置关联对象
@implementation NSObject (AssociatedObject) 
@dynamic associatedObject; 

- (void)setAssociatedObject:(id)object { 
     objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
} 

//获取关联对象
- (id)associatedObject { 
    return objc_getAssociatedObject(self, @selector(associatedObject)); 
} 

objc_setAssociatedObject的四个参数:

objc_getAssociatedObject 的两个参数:

其实,你还可以把添加和获取关联对象的方法写在你需要用到这个功能类的类别里面,方便调用。

//添加关联对象
- (void)addAssociatedObject:(id)object{
    objc_setAssociatedObject(self, @selector(getAssociatedObject), object,   OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//获取关联对象
- (id)getAssociatedObject{
    return objc_getAssociatedObject(self, _cmd);
}

** 注意 :**
4.移除关联对象:
objc_removeAssociatedObjects()这个函数很容易让对象恢复成它"原始状态",你不应该使用它来移除关联的对象,因为它也会移除包括其他地方加入的全部关联对象。所以你一般只需要通过调用objc_setAssociatedObject并传入nil值类清除关联值。

优秀样例

错误模式

在不必要的时候使用关联对象。使用视图时一个常见的情况是通过数据模型或一些复合的值来创建一个便利的方法设置填充字段或属性。如果这些值在后面不会再被使用到,最好就不要使用关联对象了。(比如你将自定义的UITableViewCell跟模型关联起来,但这个cell值用在一个ViewController里面,也就是说这个关联对象只用到一处,之后就不再使用,这种情况下就没必要使用关联对象)。

使用关联对象来保存一个可以被推算出来的值。例如,有人可能想通过关联对象存储UITableViewCell上一个自定义accessoryView的引用,使用tableView:accessoryButtonTappedForRowWithIndexPath: 和 cellForRowAtIndexPath:即可以达到要求。

使用关联对象来代替X。其中X代表下面的一些项:
子类化,当使用继承比使用组合更合适的时候。

Target-Action给响应者添加交互事件。

手势识别,当target-action模式不够用的时候。

代理,当事件可以委托给其他对象。

消息 & 消息中心使用低耦合的方式来广播消息。

树4.jpg

九.方法交换

顾名思义:就是将两个方法的实现交换,比如,将A方法和B方法交换,调用A方法的时候,就回去执行B方法中的代码,反之亦然。
参考Mattt Thompson的[Method Swizzling]文章:

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

在自己定义的viewController中重写viewWillAppear

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear");
}

就会调用xxx_viewWillAppear,输出log, method swizzling在视图控制器的生命周期、响应事件、绘制视图或者Foundation框架的网络栈等方法中需要插入代码的时候,都是很好的解决方法。

+load vs +initialize:

swizzling应该只在+load中完成。 在 Objective-C 的运行时中,每个类有两个方法都会自动调用。

dispatch_once:

swizzling 应该只在 dispatch_once 中完成。

由于 swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once 满足了所需要的需求,并且应该被当做使用 swizzling 的初始化单例方法的标准。

Selectors, Methods, & Implementations
苹果定义:

Selector(typedef struct objc_selector *SEL):在运行时 Selectors 用来代表一个方法的名字。Selector 是一个在运行时被注册(或映射)的C类型字符串。Selector由编译器产生并且在当类被加载进内存时由运行时自动进行名字和实现的映射。

Method(typedef struct objc_method *Method):方法是一个不透明的用来代表一个方法的定义的类型。

Implementation(typedef id (*IMP)(id, SEL,...)):这个数据类型指向一个方法的实现的最开始的地方。该方法为当前CPU架构使用标准的C方法调用来实现。该方法的第一个参数指向调用方法的自身(即内存中类的实例对象,若是调用类方法,该指针则是指向元类对象metaclass)。第二个参数是这个方法的名字selector,该方法的真正参数紧随其后。

三者之间的关系:

在运行时,类(Class)维护了一个消息分发列表来解决消息的正确发送。每一个消息列表的入口是一个方法(Method),这个方法映射了一对键值对,其中键值是这个方法的名字 selector(SEL),值是指向这个方法实现的函数指针 implementation(IMP)。 Method swizzling 修改了类的消息分发列表使得已经存在的 selector 映射了另一个实现 implementation,同时重命名了原生方法的实现为一个新的 selector。

也就是说swizzling只是交换两个方法在函数表中的指向地址而已。

调用 _cmd

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}

初看这段代码,我们都会觉得会出现递归死循环。但事实不是这样的:

method swizzling 在交换方法的实现后,xxx_viewWillAppear:方法的实现已经被替换为UIViewController 的-viewWillAppear:这个原生方法。

所以当我们在UIViewController调用这个- (void)viewWillAppear:(BOOL)animated 方法的时候,实际上调用的是xxx_viewWillAppear这个方法,而 [self xxx_viewWillAppear:animated];这个方法实际上调用的是系统的viewWillAppear。

这就证实了swizzling只是交换两个方法在函数表中的指向地址而已。

常见坑

  1. Method swizzling 是非原子性的,在多线程环境下可能被多次修改,但同样 Method swizzling 又是全局性的,就会造成不可预知的错误。

  2. 可能出现命名冲突的问题,这样就不会调用到系统原方法,可能导致未知问题。

  3. Method swizzling 看起来像递归,对新人来说不容易理解。

  4. 出现问题 Method swizzling 不容易进行debug,来发现问题

  5. 随着项目迭代和人员更换,使用Method swizzling 的项目不容易维护,因为开发人员有时根本不知道在Method swizzling 里面修改了东西。

预防措施

  • 在交换方法实现后记得要调用原生方法的实现(除非你非常确定可以不用调用原生方法的实现):APIs 提供了输入输出的规则,而在输入输出中间的方法实现就是一个看不见的黑盒。交换了方法实现并且一些回调方法不会调用原生方法的实现这可能会造成底层实现的崩溃。
  • 避免冲突:为分类的方法加前缀,一定要确保调用了原生方法的所有地方不会因为你交换了方法的实现而出现意想不到的结果。
  • 理解实现原理: 只是简单的拷贝粘贴交换方法实现的代码而不去理解实现原理不仅会让 App 很脆弱,并且浪费了学习 Objective-C 运行时的机会。阅读 Objective-C Runtime Reference 并且浏览 能够让你更好理解实现原理。
  • 持续的预防: 不管你对你理解 swlzzling 框架,UIKit 或者其他内嵌框架有多自信,一定要记住所有东西在下一个发行版本都可能变得不再好使。做好准备,在使用这个黑魔法中走得更远,不要让程序反而出现不可思议的行为。

十.感想

runtime是把双刃剑,因为所有的代码都运行在它之上,改变它,可能会改变代码的正常运行逻辑和所有与之交互的东西,因此会产生可怕的副作用。但同时它强大的功能也可以给应用的框架或者代码的编写带来非常大的便利。

因此,对于runtime唯一的建议就是,需谨慎使用,一旦使用,必须先了解runtime的相关原理,做好预防措施,在添加完自己的代码之后,一定要调用系统原来的方法。

十一.最后:

送上一张喜欢的图片:

树5.jpg

提醒:不应该把runtime的使用看成是高大上的东西,并以使用这个为荣,实际开发中runtime能少用应该少用,正常的系统方法才是正道!

这是一篇总结笔记,大家有兴趣可以蛮看一下,如果觉得不错,麻烦给个喜欢或star,若发现有错误的地方请及时反馈,谢谢!

上一篇下一篇

猜你喜欢

热点阅读