RunTime学习:实际应用

上一篇文章中介绍了 Runtime 的一些基本知识,以及方法传递的具体流程。这篇文章本想主要介绍 Runtime 的另外一个核心概念——类的动态配置。可是,发如今写动态配置时,有许多实际应用的东西,索性直接写一篇实际应用吧。html

本篇文章主要介绍几种 Runtime 的实际应用:git

  • 关联对象(Associated Objects)
  • “黑魔法”(Method Swizzling)
    • 方法添加
    • 方法替换
  • 实现 NSCoding 的自动归/解档
  • 字典转模型

关联对象(Associated Objects)

一说到关联对象就联想到一个经典的面试题:“是否能经过 Category 给已有的类添加成员变量?”。github

咱们知道在分类中是不可以添加成员属性的,虽然咱们用了 @property,可是仅仅会自动生成 get 和 set 方法的声明,并无带下划线的属性和方法实现生成。可是咱们能够经过 Runtime 就能够作到给它方法的实现。面试

下面咱们经过 Category 为 NSObject 添加一个 name 属性字符串。json

---声明---
#import <Foundation/Foundation.h>

@interface NSObject (Name)

@property (nonatomic, strong) NSString *name;

@end

---实现---

#import "NSObject+ Name.h"
#import <objc/message.h>

@implementation NSObject (Name)

- (NSString *)name {
    // 利用参数key 将对象object中存储的对应值取出来
    return objc_getAssociatedObject(self, @"name");
}

- (void)setName:(NSString *)name {
    // 将某个值跟某个对象关联起来,将某个值存储到某个对象中
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

---调用---
NSObject *objc = [[NSObject alloc]init];
objc.name = @"set name";
    
NSLog(@"runtime 动态添加属性:%@", objc.name);

---输出---
runtime 动态添加属性:set name
复制代码

咱们成功在分类上添加了一个属性,实现了它的 setter 和 getter 方法。 经过关联对象实现的属性的内存管理也是有 ARC 管理的,因此咱们只须要给定适当的内存策略就好了,不须要操心对象的释放。bash

这里用到了两个方法:markdown

id objc_getAssociatedObject(id object, const void *key);

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
复制代码

object: 被关联的对象。app

key: 关联的 key 值,要求惟一。ide

value: 关联的对象。函数

objc_AssociationPolicy: 内存管理策略。能够理解为 property 的修饰关键字。

“黑魔法”(Method Swizzling)

方法添加

动态添加方法的实如今上一篇讲述消息传递过程时,咱们在动态解析阶段动态添加了方法,避免程序在未找到方法时的崩溃。

主要就是这个函数:

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
复制代码

cls: 给哪一个类添加方法。

name: 须要添加的方法名。 Objective-C 中能够直接使用 @selector(methodName) 获得方法名, Swift 中使用 #Selector(methodName)。

imp: 方法的实现,函数入口,函数名可与方法名不一样(建议与方法名相同)。函数必须至少两个参数—— self 和 _cmd。

types: 参数以及返回值类型的字符串,须要用特定符号,参考官方文档Type encodings

方法替换

常见的方法替换的实际应用应该是无侵入埋点了——在保持原有方法功能的基础上,添加额外的功能。

下面经过一个例子来看看怎么玩这个大名鼎鼎的黑魔法。

实例: 在开发中,咱们经常使用 [UIImage imageNamed:@"image"]; 方法来加载一张图片,可是咱们不知道这个方法是否真的加载成功,在使用时须要进行一次判断。如今咱们就给这个方法添加一些额外的功能(是否加载图片成功)。

代码实现:

#import "UIImage+Image.h"
#import <objc/message.h>

@implementation UIImage (Image)

/*
做用:把类加载进内存的时候调用,只会调用一次。
*/
+ (void)load {
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken, ^{
        // 1.获取 imageNamed方法地址
        Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
        // 2.获取 ln_imageNamed方法地址
        Method ln_imageNamedMethod = class_getClassMethod(self, @selector(jt_imageNamed:));
        
        // 3.交换方法地址,至关于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」
        method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
    });
}

/
 下面的代码是不会有死循环的
    调用 imageNamed 至关于调用 jt_imageNamed
    调用 jt_imageNamed 至关于调用 imageNamed
 */
+ (UIImage *)jt_imageNamed:(NSString *)name {
    UIImage *image = [UIImage jt_imageNamed: name];
    if (image) {
        NSLog(@"runtime交互方法 -> 图片加载成功");
    } else {
        NSLog(@"runtime交互方法 -> 图片加载失败");
    }
    return image;
}

@end

---调用---

UIImage *image = [UIImage imageNamed:@"Logo"];

---输出---

runtime交互方法 -> 图片加载成功
复制代码

这里咱们就替换了系统的实现,而且加入了咱们本身的代码。这里咱们能够在图片加载失败后,返回一个默认图片防止显示的空白了。

当咱们为咱们的程序进行埋点时,逻辑也是同样的,替换须要埋点的方法。这里举个简单的无侵入埋点的例子,代码以下:

// 这个 ViewController 做为程序内全部 ViewController 的基类
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSLog(@"原有的");
    
    UIImage *image = [UIImage imageNamed:@"LoginLogo"];
    
}

- (void)jt_viewDidLoad {
    
    NSLog(@"埋点代码");
    
    [self jt_viewDidLoad];
}

+ (void)load {
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL original = @selector(viewDidLoad);
        SEL swizzled = @selector(jt_viewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(class, original);
        Method swizzledMethod = class_getInstanceMethod(class, swizzled);
        
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

@end

---输出---

埋点代码
原有的
复制代码

实现 NSCoding 的自动归/解档

当咱们须要存储咱们自定义的一些类时,须要遵循 NSCoding 协议,并实现其归/解档的两个方法,可是当一个 model 的属性过多时,写代码就变成了重复的劳动,这里咱们能够利用 Runtime 的一些方法来解决。

@implementation TestModel

- (instancetype)initWithCoder:(NSCoder *)coder {
    self = [super init];
    if (self) {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [self setValue:[coder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    unsigned int outCount;
    Ivar * ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar ivar = ivars[i];
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [coder encodeObject:[self valueForKey:key] forKey:key];
    }
}
复制代码

字典转模型

这个应该大部分人都已经在实际的项目中运用过了,咱们使用的字典转模型的三方库——MJExtension,其实就是利用 Runtime 提供的函数遍历 Model 自身全部属性,若是属性在 json 中有对应的值,则将其赋值。

这个转化的过程,是利用了 Runtime 提供的函数以及一些 KVC 的知识点合做完成的。这里是我对KVC的原理的一些学习记录,字典转模型代码能够在个人github中查看哦。

小结

原本关于 Runtime 的知识点准备写三篇文章来记录的,可是关于类的动态配置的知识点写出来,感受就像是 copy API 文档,因此就直接写了这篇实际应用。可是,本身立下的 flag,怎么也要完成吧。因此,计划下一篇文章写一写常见的 Runtime 的面试题吧。

相关文章
相关标签/搜索