Runtime入门总结

2018-12-07  本文已影响0人  黑夜里的貓
  1. 简介
  2. Runtime的基础数据结构
  3. 消息发送
    1. 方法调用流程
    2. 动态方法解析
    3. 快速转发
    4. 标准转发
  4. API使用
    1. 动态创建类,添加方法
    2. 分类中动态绑定属性
    3. 字典转模型
    4. 方法交换(method swizzling)

简介


Runtime的基础数据结构


objc_msgSend()函数是所有消息发送的必经之路,在我们详细了解消息发送流程之前,先从objc_msgSend()函数入手了解一下Runtime中的数据结构。

objc_msgSend(<#id  _Nullable self#>, <#SEL  _Nonnull op, ...#>)

SEL
objc_msgSend第二个参数为SEL类型,表示方法选择器,在objc.h文件中可以看到其定义:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

实际上它就是一个映射方法的分段字符串。用于指明调用哪个方法,可以理解为区分方法的ID。可以使用@selector()、sel_registerName()或NSSelectorFromString()来获取选择器。

IMP

typedef void (*IMP)(void /* id, SEL, ... */ ); 

它其实就是一个函数指针,指向具体的方法实现,在同一个对象中SELIMP是一一对应的。

id
objc_msgSend的第一个参数,大家对它都不陌生,可以接收OC中任何类型的对象。

typedef struct objc_object *id;

本质上就是一个结构体指针,指向类实例。接着看objc_object

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

包含一个isa指针,指向它所属的类。

Class

typedef struct objc_class *Class;

又是一个指针,指向objc_class,定义在runtime.h

struct objc_class {
    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
}

其中包括指向父类的指针super_class、类的名字name、实例变量的大小instance_size、成员变量列表ivars、方法列表methodLists、缓存cache和协议列表protocols等。其中methodLists就是一个链表,存储所有的实例方法(Method)

typedef struct objc_method *Method;

struct objc_method {
        SEL method_name;
        char *method_types;
                IMP method_imp;
    } method_list[1];

method_types存储着方法的参数类型和返回值累型。
cache用来缓存常用的方法,以达到优化方法查找效率的目的。

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

最重要的我们发现Class里也有一个isa指针。

实例对象的isa指针指向实例对象所属的类,那么类的isa指针指向哪里呢?

我们先看个图


image.png

消息发送


方法调用流程
假如有一个Person类,有一个run的实例方法。实例化一个对象p,[p run]是怎么执行的?

  1. 根据对象p的isa指针,找到所属的类
  2. 根据selector在类的cache缓存中寻找方法实现的地址,找到执行,没找到执行下一步
  3. 在类的methodLists方法列表中寻找,找到执行,没找到执行下一步
  4. 根据类的super_class指针,到父类的缓存中寻找,没找到执行下一步
  5. 在父类的方法列表中寻找,如果没找,继续向上找,一直到NSObject
  6. 如果最终没找到,则会调用resolveInstanceMethod或者resolveClassMethod方法,让我们可以动态添加方法实现

动态方法解析
.h文件

#import <Foundation/Foundation.h>

@interface Person : NSObject

- (void)run;
+ (void)eat:(NSString *)str;

@end

.m文件

#import "Person.h"
#import <objc/runtime.h>

@implementation Person

// 当找不到实例方法实现时,调用此方法。在此方法中我们动态添加一个实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        class_addMethod([self class], sel, imp_implementationWithBlock(^(id self){
            NSLog(@"run");
        }), "v@:");
        
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 找不到类方法时调用,在此动态添加一个类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(eat:)) {
        class_addMethod(object_getClass(self), sel, (IMP)eat, "v@:@");
        // 如果返回NO,则会进入消息转发
        return YES;
    }
    return [super resolveClassMethod:sel];
}

// 函数实现,函数默认都有两个参数
void eat(id self, SEL _cmd, NSString *str) {
    NSLog(@"eat %@",str);
}

@end

需要注意类方法要添加到元类中。

如果以上两个方法返回NO则会调用forwardingTargetForSelector方法进入消息转发

快速转发

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        return [Proxy new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

可以在此将消息转发给其他对象。所以我们需要一个可以相应此消息的对象。新建一个类Proxy,只需在.m文件中给出方法实现。

#import "Proxy.h"

@implementation Proxy

- (void)run{
    NSLog(@"proxy run");
}

@end

这样便完成了快速转发,当Person实例调用run方法时,就会转发到Proxy中,如果Proxy类里有对应的实现,则会执行。
forwardingTargetForSelector中,如果返回nil或者self则会进入标准转发forwardInvocation

标准转发
在调用forwardInvocation之前,会先调用methodSignatureForSelector获取方法签名,方法签名中包含了参数,返回值,以及消息接受者的相关信息。然后包装成一个NSInvocation对象调用forwardInvocation进行最后的消息转发。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        // 构造一个方法签名
        NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        return sig;
    }
    return [super methodSignatureForSelector:aSelector];
}

// 可以将消息转发,可以更改参数值,还可以更改所要调用的方法。总之可以肆无忌惮的做任何事情
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 将方法选择器更改为eat方法
    [anInvocation setSelector:sel_registerName("eat:")];
    NSString *str = @"apple";
    // 添加参数,函数默认有两个参数,所以我们添加参数下标要从2开始
    [anInvocation setArgument:&str atIndex:2];
    // 转发给Proxy 对象
    [anInvocation invokeWithTarget:[Proxy new]];
}

如果在消息标准转阶段不做处理,最后就会抛出unrecognized selector异常,导致程序crash 。
总体来说消息发送的过程可以归纳成下图:

image.png
如果想更加深入了解请看 消息发送与转发机制原理

Runtime API的使用


动态创建类、添加方法、变量

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    // 动态创建一个类
    Class DynaClass = objc_allocateClassPair([NSObject class], "DynaClass", 0);
    
    // 从类中取出一个方法
    Method des = class_getClassMethod([NSObject class], @selector(description));
    // 获取方法的特征,包含参数与返回值的信息
    const char* types = method_getTypeEncoding(des);
    
    // 添加实例方法
    class_addMethod(DynaClass, @selector(objcMethod), (IMP)objcMethod, types);
    
    //添加一个成员变量,只能在objc_registerClassPair之前添加
    class_addIvar(DynaClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
    
    // 注册类
    objc_registerClassPair(DynaClass);
    
    // 获取元类
    Class metaCls = objc_getMetaClass("DynaClass");
    // 添加一个类方法
    class_addMethod(metaCls, NSSelectorFromString(@"classMethod"), imp_implementationWithBlock(^(id self, NSString *str){
        NSLog(@"class method %@", str);
    }), "v@:");
    
    
    
    // 根据动态创建的类,实例化一个对象
    id dynaObjc = [[DynaClass alloc] init];
    
    // 访问成员变量
    [dynaObjc setValue:@"动态添加属性" forKey:@"name"];
    NSLog(@"%@", [dynaObjc valueForKey:@"name"]);

    // 调用方法
    NSString *res = [dynaObjc performSelector:@selector(objcMethod)];
    NSLog(@"%@", res);
    
    [DynaClass performSelector:@selector(classMethod) withObject:@"我是参数"];    
}

NSString *objcMethod(id self, SEL _cmd)
{
    return [NSString stringWithFormat:@"hello"];
}

分类中动态绑定属性
分类(Category)本来是不支持添加属性的,即使我们使用@property也只会声明settergetter ,并没有生成对应的实例变量和方法实现。我们可以使用runtime进行动态绑定来达到添加属性的效果,但是实质上只是添加一个关联,并不是真正的添加一个变量到类的地址空间中。

- (void)setTitle:(NSString *)title {
    objc_setAssociatedObject(self, "title", title, OBJC_ASSOCIATION_COPY);
}

- (NSString *)title {
    return objc_getAssociatedObject(self, "title");
}

详细请看关联对象实现原理

字典转模型

  1. 获取model对象的所有属性
  2. 根据属性名字查找字典中的key,取出对应的value
  3. 赋值给model
@implementation NSObject (Model)
+ (id)modelWithDic:(NSDictionary *)dic {
    id pModel = [[self alloc] init];
    
    unsigned int count;
    
    // 获取对象的成员变量数组
    Ivar *iList = class_copyIvarList(self, &count);
    
    for (int i=0; i<count; i++) {
        Ivar var = iList[i];
        // 获取变量的名字
        NSString *varName = [NSString stringWithUTF8String:ivar_getName(var)];
        // 获取变量的类型
        NSString *varType = [NSString stringWithUTF8String:ivar_getTypeEncoding(var)];
        
        // 成员变量都是以下划线开头,所以需要截取一下
        varName = [varName substringFromIndex:1];
        varType = [varType substringWithRange:NSMakeRange(2, varType.length - 3)];
        
        // 根据属性名获取字典的value
        id value = dic[varName];
        
        // 模型嵌套模型。字典的值是字典,需要将其也转换成对应模型
        if ([value isKindOfClass:[NSDictionary class]] && ![varType hasPrefix:@"NS"]) {
            Class class = NSClassFromString(varType);
            value = [class modelWithDic:value];
        }
        
        // 字典的值是数组,数组包含字典,将数组中的字典也转成模型
        if ([value isKindOfClass:[NSArray class]]) {
            // 判断是否实现modelClassInArray协议,协议方法返回数组中字典对应的model类
            if ([self respondsToSelector:@selector(modelClassInArray)]) {
                id idSelf = self;
                NSString *type = [idSelf modelClassInArray][varName];
                Class class = NSClassFromString(type);
                NSMutableArray *modelArray = [[NSMutableArray alloc] init];
                
                for (NSDictionary *dic in value) {
                    id model = [class modelWithDic:dic];
                    [modelArray addObject:model];
                }
                value = modelArray;
            }
        }
        
        if (value) {
            [pModel setValue:value forKey:varName];
        }
    }
    
    free(iList);
    
    return pModel;
}
@end

MJExtensionJSONModel等大部分框架应该也是这种方式实现的。

方法交换
直白点就是调用A的时候,执行的是B的实现,调用B的时候,其实执行的是A。其实就是将两个方法的实现进行交换,如下图:

image.png
举个例子,在执行[NSURL URLWithString:urlStr]这句代码的时候,如果urlStr包含中文,需要先对其进行编码才能正确返回NSURL对象。那么可不可以只修改某一个地方,不用每次调用URLWithString :前都对urlStr进行编码呢?

这时就可以利用方法交换来达到目的。

  1. 新建一个NSURL的分类
  2. 在分类添加一个方法my_ URLWithString :,进行处理中文问题
  3. load方法中将系统的URLWithString :和我们新添的my_ URLWithString :进行交换。
#import "NSURL+category.h"

@implementation NSURL (category)

+ (instancetype)my_URLWithString:(NSString *)URLString
{
    // 此处不会造成死循环,因为my_URLWithString和URLWithString已经交换,
    // 所以调用my_URLWithString实际上就是调用的URLWithString
    return [self my_URLWithString:[URLString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
}

+ (void)load {
    // 为了确保只执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        Method originalMethod = class_getClassMethod(self, @selector(URLWithString:));
        Method swizzledMethod = class_getClassMethod(self, @selector(my_URLWithString:));
        
        /*
         class_addMethod:如果类中存在方法,则添加失败。如果不存在则添加成功
         
         判断是否添加成功的目的:
            如果本类中没有实现originalMethod,但是父类中实现了。
            直接使用method_exchangeImplementations进行交换,
            交换的两个方法就是父类中的originalMethod和swizzledMethod。
            那么父类的其他子类调用originalMethod也会执行swizzledMethod。
            进行判断就是为了避免这种情况以及带来的其他麻烦
         
         在本分类中,因为确定URLWithString一定实现了,可以直接使用method_exchangeImplementations进行交换
         */
        BOOL didAddMethod = class_addMethod(object_getClass(self), @selector(URLWithString:), method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            // class_replaceMethod:替换方法的实现
            class_replaceMethod(self, @selector(my_URLWithString:), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }else {
            // 将两个方法交换
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        
    });
}

@end

这样直接使用[NSURL URLWithString:urlStr]就可以了,不必再去理会urlStr中是否存在中文。

参考:
iOS 模块分解—「Runtime面试、工作」看我就 🐒 了 _.
Objective-C Method Swizzling
Objective-C Runtime
iOS runtime和runloop

上一篇下一篇

猜你喜欢

热点阅读