Runtime在工作中的运用

3,402 阅读13分钟

这篇文章是笔者结合一些参考文章和当初学习Runtime的心得而写的一篇总结,主要讲解Runtime在工作中的运用,没有涉及到太底层的知识,极尽详略,适合初中级学者,水平有限,有错误的地方,还请大佬在评论中指出,一起快乐学习。持续更新中。。。

1.Runtime简介

  • Runtime 简称运行时,是一套C语言的API(引入 <objc/runtime.h><objc/message.h>)。OC 就是运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制

  • 对于C语言,函数的调用在编译的时候会决定调用哪个函数。

  • 对于OC,函数的调用称为消息发送,属于动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

2.Runtime消息机制

消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。

验证

1.在main.m中创建一个对象;

id object = [NSObject alloc];
object = [object init];

2.终端切换到该目录下,执行命令clang -rewrite-objc main.m,编译后会生成一个main.cpp(C++文件);

3.在.cpp文件中搜索autoreleasepool,可以找到上述对象创建的底层代码;

id object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
object = ((id (*)(id, SEL))(void *)objc_msgSend)((id)object, sel_registerName("init"));

可以看出调用方法本质就是发消息[[NSObject alloc]init]语句发了两次消息,第一次发了alloc 消息,第二次发送init 消息。

4.我们自己来尝试实现,首先导入头文件 #import <objc/message.h>,然后让消息机制方法有提示(【build setting -> 搜索msg -> objc_msgSend(YES --> NO)】)。

id object = objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc"));
object = objc_msgSend(object, sel_registerName("init"));
/**
 objc_getClass(const char *name) 获取当前类
 sel_registerName(const char *str) 注册个方法编号
 objc_msgSend(id self:谁发送消息, SEL op:发送什么消息, ...:参数)
 */

换个写法:

id objc = objc_msgSend([NSObject class], @selector(alloc));
objc = objc_msgSend(objc, @selector(init));

参数处理:

objc_msgSend(p, sel_registerName("height:"), 180);

注:

objc_msgSend:这是个最基本的用于发送消息的函数。

其实编译器会根据情况在objc_msgSendobjc_msgSend_fpretobjc_msgSend_stretobjc_msgSendSuper, 或 objc_msgSendSuper_stret 五个方法中选择一个来调用。

如果消息是传递给超类,那么会调用名字带有 Super 的函数;

如果消息返回值是浮点数,那么会调用名字带有fpret 的函数;

如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret的函数。

3.Runtime方法调用流程

  • 对象方法:(保存到类对象的方法列表)
  • 类方法:(保存到元类(Meta Class)中方法列表)

1.消息传递:
一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo)Runtime时执行的流程是什么样的呐?

1.首先,通过objisa指针找到它的 class ;
2.注册方法编号SEL,可以快速查找;
3.根据方法编号,在 classmethod listfoo ;
3.如果 class 中没到 foo,继续往它的 superclass 中找 ;
4.一旦找到 foo 这个函数,就去执行它的实现IMP

2.Runtime的三次转发流程

动态方法解析

Objective-C运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程(动态添加方法)。如果未实现方法,运行时就会移到下一步:forwardingTargetForSelector

备用接收者

如果目标对象实现了-forwardingTargetForSelector:Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。如果还不能处理未知消息,就会进入完整消息转发阶段。

完整消息转发

Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。为接下来的完整的消息转发生成一个 NSMethodSignature对象。NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation: 方法里就可以对 NSInvocation 进行处理了。如果未实现,Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。

4.Runtime动态添加方法

使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。

经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。

方法介绍

// 参数1:给哪个类添加方法
// 参数2:添加方法的方法编号SEL
// 参数3:添加方法的函数实现IMP(函数地址)
// 参数4:函数的类型,(返回值+参数类型)
class_addMethod(Class cls, SEL name, IMP imp, const char * types)

1.class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。

2.函数的类型官方文档

方法示例

假如Person对象调用eat方法,而该方法并没有实现,则会报错。我们可以利用RuntimePerson类中动态添加eat方法,来实现该方法的调用。

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
    p = objc_msgSend(p, sel_registerName("init"));

    [p performSelector:@selector(eat)];
}

@end
@implementation Person

/**
 void的前面没有+、-号,因为只是C的代码;
 必须有两个指定参数(id self,SEL _cmd)
 */
void eat(id self, SEL sel)
{
    NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}

// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        //函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)eat, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}

@end

5.Runtime方法交换(Method Swizzling)

使用场景:当第三方框架或者系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。

方法介绍

// 交换方法地址,交换两个方法的实现
method_exchangeImplementations(Method m1, Method m2)

方法封装:为了后续调用方便,我们可以将Method Swizzling功能封装为类方法,作为NSObject的类别。

@interface NSObject (Swizzling)

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
                         bySwizzledSelector:(SEL)swizzledSelector;

@end
#import "NSObject+Swizzling.h"
#import <objc/message.h>

@implementation NSObject (Swizzling)

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector
{
    Class class = [self class];
    //原有方法
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    //替换原有方法的新方法
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    //先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况
    BOOL didAddMethod =
    class_addMethod(class,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {//添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {//添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
@end

**方法示例:**例如我们想要替换ViewController生命周期方法,可以这样做。

#import "UIViewController+Swizzling.h"
#import "NSObject+Swizzling.h"

@implementation UIViewController (Swizzling)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(viewWillAppear:) bySwizzledSelector:@selector(mj_viewWillAppear:)];
    });
}

- (void)mj_viewWillAppear:(BOOL)animated{
    [self mj_viewWillAppear:animated];
    
    NSLog(@"被调用了");
}
@end

1.swizzling建议在+load中完成。+load+initialize 是Objective-C runtime会自动调用两个类方法。+load 是在一个类被初始加载时调用,一定会被调用;+initialize 是在应用第一次调用该类的类方法或实例方法前调用,相当于懒加载方式,可能不被调用。此外 +load 方法还有一个非常重要的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。换句话说在 Objective-C runtime 自动调用 +load 方法时,分类中的 +load 方法并不会对主类中的 +load 方法造成覆盖。

2.swizzling应该只在dispatch_once 中完成,由于swizzling 改变了全局的状态,所以我们需要确保在任何情况下(多线程环境,或者被其他人手动再次调用+load方法)只交换一次,防止再次调用又将方法交换回来。+load方法本身即为线程安全,为什么仍需添加dispatch_once,其原因就在于+load方法本身无法保证其中代码只被执行一次。

6.Runtime动态添加属性

场景:分类是不能自定义属性和变量的,这时候可以使用runtime动态添加属性方法;

原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

方法

/** 关联对象、set方法
id object:给哪个对象添加关联,给哪个对象设置属性
const void *key:关联的key,要求唯一,建议用char 可以节省字节
id value:关联的value,给属性设置的值
objc_AssociationPolicy policy:内存管理的策略
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 获取关联的对象、get方法
id objc_getAssociatedObject(id object, const void *key)
// 移除关联的对象
void objc_removeAssociatedObjects(id object)

内存策略对应的属性修饰表:

内存策略 属性修饰 描述
OBJC_ASSOCIATION_ASSIGN @property (assign) 或 @property (unsafe_unretained) 指定一个关联对象的弱引用。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) @property (nonatomic, strong) 指定一个关联对象的强引用,不能被原子化使用。
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 指定一个关联对象的copy引用,不能被原子化使用。
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 指定一个关联对象的强引用,能被原子化使用。
OBJC_ASSOCIATION_COPY @property (atomic, copy) 指定一个关联对象的copy引用,能被原子化使用。

示例:实现一个UIViewCategory添加自定义属性defaultColor

@interface UIView (Color)

@property (nonatomic, strong) UIColor *defaultColor;

@end
  
@implementation UIView (Color)

static char kDefaultColorKey;
- (void)setDefaultColor:(UIColor *)defaultColor
{
    objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)defaultColor {
    return objc_getAssociatedObject(self, &kDefaultColorKey);
}

@end

7.NSCoding自动归档解档

场景:如果一个模型有许多个属性,实现自定义模型数据持久化时,需要对每个属性都实现一遍encodeObjectdecodeObjectForKey方法,比较麻烦。我们可以使用Runtime来解决。

原理:用runtime提供的函数遍历Model自身所有属性,并对属性进行encodedecode操作。

方法实现

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

@implementation MJMusicModel

// 设置不需要归解档的属性
- (NSArray *)ignoredNames {
    return @[@"_musicUrl"];
}

// 归档调用方法
- (void)encodeWithCoder:(NSCoder *)encoder
{
    unsigned int count = 0;
    // 获得这个类的所有成员变量
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++) {
        // 取出i位置对应的成员变量
        Ivar ivar = ivars[i];
        // 获得成员变量的名字
        const char *name = ivar_getName(ivar);
        // 将每个成员变量名转换为NSString对象类型
        NSString *key = [NSString stringWithUTF8String:name];
        // 忽略不需要归档的属性
        if ([[self ignoredNames] containsObject:key]) {
            continue;
        }
        // 归档
        id value = [self valueForKey:key];
        [encoder encodeObject:value forKey:key];
    }
    // 注意释放内存!
    free(ivars);
}

// 解档方法
- (id)initWithCoder:(NSCoder *)decoder
{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i<count; i++) {
            // 取出i位置对应的成员变量
            Ivar ivar = ivars[i];
            // 获得成员变量的名字
            const char *name = ivar_getName(ivar);
            // 将每个成员变量名转换为NSString对象类型
            NSString *key = [NSString stringWithUTF8String:name];
            // 忽略不需要解档的属性
            if ([[self ignoredNames] containsObject:key]) {
                continue;
            }
            // 解档
            id value = [decoder decodeObjectForKey:key];
            // 设置到成员变量身上
            [self setValue:value forKey:key];
        }
        free(ivars);
    }
    return self;
}

@end

:我们可以将归解档两个方法封装为宏,在需要的地方一句宏搞定;也可以写到NSObject一个分类中,方便使用。

8.Runtime字典转模型

原理:利用Runtime,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。

步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。

接下来分别介绍一下三种情况所实现的代码:

1.简单的字典转模型

注意:模型属性数量大于字典的键值对时,由于属性没有对应值会被赋值为nil,就会导致crash,所以我们要加一个判断,获取到Value时,才给模型中属性赋值。

NSDictionary *dict = @{
                           @"name" : @"xiaoming",
                           @"age"  : @25,
                           @"weight" : @"60kg",
                           @"height" : @1.81
                           };
#import "NSObject+Model.h"
#import <objc/message.h>

@implementation NSObject (Model)

+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 创建对应的对象
    id objc = [[self alloc] init];
    
    // 成员变量个数
    unsigned int count = 0;
    // 获取类中的所有成员变量
    Ivar *ivars = class_copyIvarList(self, &count);
    
    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员变量(Ivar:成员变量,以下划线开头)
        Ivar ivar = ivars[i];
        
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根据成员属性名去字典中查找对应的value
        id value = dict[key];

        if (value) {  
            // 给模型中属性赋值
            [objc setValue:value forKey:key];
        }
    }
    // 释放ivars
    free(ivars);
    
    return objc;
}
@end

2.模型中嵌套模型(模型属性是另外一个模型对象)

利用runtime的ivar_getTypeEncoding 方法获取模型对象类型,对该模型对象类型再进行字典转模型,也就是进行递归,需要注意的是要排除系统的对象类型,例如NSString

NSDictionary *dict2 = @{
                           @"name" : @"xiaoming",
                           @"age"  : @25,
                           @"body" :@{
                                      @"weight" : @"65kg",
                                      @"height" : @1.82
                                     }
                           };
// runtime字典转模型二级转换:字典->字典;如果字典中还有字典,也需要把对应的字典转换成模型
if ([value isKindOfClass:[NSDictionary class]]) {

    // 获取成员变量类型
    NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

    // 替换: @\"User\" -> User
    ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
    ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];

    if (![ivarType hasPrefix:@"NS"]) {
        // 字典转换成模型,根据字符串类名生成类对象
        Class modelClass = NSClassFromString(ivarType);
        if (modelClass) { // 有对应的模型才需要转
            // 把字典转模型
            value = [modelClass modelWithDict:value];
        }
    }
}

3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)

拦截到模型的数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,需要声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。

NSDictionary *dict3 = @{
                            @"name" : @"xiaoming",
                            @"age"  : @25,
                            @"body" :@{
                                    @"weight" : @"65kg",
                                    @"height" : @1.82
                                    },
                            @"children" : @[
                                    @{
                                        @"sex" : @"男",
                                        @"love" : @"篮球",
                                        },
                                    @{
                                        @"sex" : @"nv",
                                        @"love" : @"钢琴",
                                        }
                                    ],
                            };
// runtime字典转模型三级转换:字典->数组->字典;NSArray中也是字典,把数组中的字典转换成模型.
if ([value isKindOfClass:[NSArray class]]) {
    // 判断对应类有没有实现字典数组转模型数组的协议
    // arrayContainModelClass 提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型
    if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

        // 转换成id类型,就能调用任何对象的方法
        id idSelf = self;

        // 获取数组中字典对应的模型
        NSString *type =  [idSelf arrayContainModelClass][key];

        // 生成模型
        Class classModel = NSClassFromString(type);
        NSMutableArray *arrM = [NSMutableArray array];
        // 遍历字典数组,生成模型数组
        for (NSDictionary *dict in value) {
            // 字典转模型
            id model =  [classModel modelWithDict:dict];
            [arrM addObject:model];
        }

        // 把模型数组赋值给value
        value = arrM;
    }
}
#import <Foundation/Foundation.h>

@protocol ModelDelegate <NSObject>

@optional
/**
 提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型
 */
+ (NSDictionary *)arrayContainModelClass;

@end

@interface NSObject (Model)

/**
 dict -> model
 利用runtime 遍历模型中所有属性,根据模型中属性去字典中取出对应的value给模型属性赋值
 */
+ (instancetype)modelWithDict:(NSDictionary *)dict;

@end

实现协议类:

+ (NSDictionary *)arrayContainModelClass
{
    // 数组属性 : 数组中的类名
    return @{@"children" : @"MJChild"};
}

不忙的时候,就整理知识,经过几天时间的努力,终于写好了。途中参考大量资料,并通过Demo验证其正确性,也算对自己的一次全面级的学习与复习。

下一篇,会深入了解Runtime底层语言。

I’m not perfect. But I keep trying.

参考文献:

苹果官方文档
OC最实用的runtime总结
让你快速上手Runtime
runtime详解
iOS 模式详解—
iOS Runtime详解
装逼技术RunTime的总结篇