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 的面試題吧。

相關文章
相關標籤/搜索