上一篇文章中介绍了 Runtime 的一些基本知识,以及方法传递的具体流程。这篇文章本想主要介绍 Runtime 的另一个核心概念——类的动态配置。但是,发现在写动态配置时,有许多实际应用的东西,索性直接写一篇实际应用吧。
本篇文章主要介绍几种 Runtime 的实际应用:
- 关联对象(Associated Objects)
- “黑魔法”(Method Swizzling)
- 实现 NSCoding 的自动归/解档
- 字典转模型
关联对象(Associated Objects)
一说到关联对象就联想到一个经典的面试题:“是否能通过 Category 给已有的类添加成员变量?”。
我们知道在分类中是不能够添加成员属性的,虽然我们用了 @property,但是仅仅会自动生成 get 和 set 方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过 Runtime 就可以做到给它方法的实现。
下面我们通过 Category 为 NSObject 添加一个 name 属性字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| ---声明--- #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 管理的,所以我们只需要给定适当的内存策略就行了,不需要操心对象的释放。
这里用到了两个方法:
1 2 3
| id objc_getAssociatedObject(id object, const void *key);
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
|
object: 被关联的对象。
key: 关联的 key 值,要求唯一。
value: 关联的对象。
objc_AssociationPolicy: 内存管理策略。可以理解为 property 的修饰关键字。
“黑魔法”(Method Swizzling)
方法添加
动态添加方法的实现在上一篇讲述消息传递过程时,我们在动态解析阶段动态添加了方法,避免程序在未找到方法时的崩溃。
主要就是这个函数:
1
| 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”]; 方法来加载一张图片,但是我们不知道这个方法是否真的加载成功,在使用时需要进行一次判断。现在我们就给这个方法添加一些额外的功能(是否加载图片成功)。
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #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交互方法 -> 图片加载成功
|
这里我们就替换了系统的实现,并且加入了我们自己的代码。这里我们可以在图片加载失败后,返回一个默认图片防止显示的空白了。
当我们为我们的程序进行埋点时,逻辑也是一样的,替换需要埋点的方法。这里举个简单的无侵入埋点的例子,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| // 这个 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 的一些方法来解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @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 的面试题吧。