上一篇文章中介紹了 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 的面試題吧。