上一篇文章中介绍了 Runtime 的一些基本知识,以及方法传递的具体流程。这篇文章本想主要介绍 Runtime 的另外一个核心概念——类的动态配置。可是,发如今写动态配置时,有许多实际应用的东西,索性直接写一篇实际应用吧。html
本篇文章主要介绍几种 Runtime 的实际应用:git
一说到关联对象就联想到一个经典的面试题:“是否能经过 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 的修饰关键字。
动态添加方法的实如今上一篇讲述消息传递过程时,咱们在动态解析阶段动态添加了方法,避免程序在未找到方法时的崩溃。
主要就是这个函数:
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 协议,并实现其归/解档的两个方法,可是当一个 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 的面试题吧。