上一篇文章中介紹了 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
這裏用到了兩個方法:app
id objc_getAssociatedObject(id object, const void *key);
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
複製代碼
object: 被關聯的對象。ide
key: 關聯的 key 值,要求惟一。函數
value: 關聯的對象。post
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 的面試題吧。