Runtime : 運行時詳解

1、簡介

1.1 什麼是Runtime
Runtime是一套底層純C語言API,OC代碼最終都會被編譯器轉化爲運行時代碼,經過消息機制決定函數調用方式,這也是OC做爲動態語言使用的基礎。
複製代碼
2.1 消息機制的基本原理

在Object-C的語言中,對象方法調用都是相似[receiver selector] 的形式,其本質:就是讓對象在運行時發送消息的過程。json

而方法調用[receiver selector] 分爲兩個過程:數組

  • 編譯階段

[receiver selector] 方法被編譯器轉化,分爲兩種狀況:緩存

1.不帶參數的方法被編譯爲:objc_msgSend(receiver,selector)
2.帶參數的方法被編譯爲:objc_msgSend(recevier,selector,org1,org2,…)
複製代碼
  • 運行時階段

消息接收者recever尋找對應的selector,也分爲兩種狀況:bash

1.接收者能找到對應的selector,直接執行接收receiver對象的selector方法。
2.接收者找不到對應的selector,消息被轉發或者臨時向接收者添加這個selector對應的實現內容,不然崩潰
複製代碼

總而言之:數據結構

OC調用方法[receiver selector],編譯階段肯定了要向哪一個接收者發送message消息,可是接收者如何響應決定於運行時的判斷
複製代碼
1.3 Runtime中的概念解析

1.3.1 objc_msgSendapp

全部 Objective-C 方法調用在編譯時都會轉化爲對 C 函數 objc_msgSend 的調用。objc_msgSend(receiver,selector); 是 [receiver selector]; 對應的 C 函數
複製代碼

1.3.2 Object(對象)框架

objc/runtime.h 中Object(對象) 被定義爲指向 objc_object 結構體 的指針,objc_object結構體 的數據結構以下:函數

//runtime對objc_object結構體的定義
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

//id是一個指向objc_object結構體的指針,即在Runtime中:
typedef struct objc_object *id;

//OC中的對象雖然沒有明顯的使用指針,可是在OC代碼被編譯轉化爲C以後,每一個OC對象其實都是擁有一個isa的指針的
複製代碼

1.3.2 Class(類)測試

objc/runtime.h 中Class(類) 被定義爲指向 objc_class 結構體 的指針,objc_class結構體 的數據結構以下:ui

//runtime對objc_class結構體的定義
struct objc_class {
    Class _Nonnull isa;                                          // objc_class 結構體的實例指針

#if !__OBJC2__
    Class _Nullable super_class;                                 // 指向父類的指針
    const char * _Nonnull name;                                  // 類的名字
    long version;                                                // 類的版本信息,默認爲 0
    long info;                                                   // 類的信息,供運行期使用的一些位標識
    long instance_size;                                          // 該類的實例變量大小;
    struct objc_ivar_list * _Nullable ivars;                     // 該類的實例變量列表
    struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定義的列表
    struct objc_cache * _Nonnull cache;                          // 方法緩存
    struct objc_protocol_list * _Nullable protocols;             // 遵照的協議列表
#endif

};


//class是一個指向objc_class結構體的指針,即在Runtime中:
typedef struct objc_class *Class; 
複製代碼

1.3.3 SEL (方法選擇器)

objc/runtime.h 中SEL (方法選擇器) 被定義爲指向 objc_selector 結構體 的指針:

typedef struct objc_selector *SEL;

//Objective-C在編譯時,會依據每個方法的名字、參數序列,生成一個惟一的整型標識(Int類型的地址),這個標識就是SEL
複製代碼

注意:

1.不一樣類中相同名字的方法對應的方法選擇器是相同的。
2.即便是同一個類中,方法名相同而變量類型不一樣也會致使它們具備相同的方法選擇器。
複製代碼

一般獲取SEL有三種方法:

1.OC中,使用@selector(「方法名字符串」)
2.OC中,使用NSSelectorFromString(「方法名字符串」)
3.Runtime方法,使用sel_registerName(「方法名字符串」)
複製代碼

1.3.4 Ivar

objc/runtime.h 中Ivar 被定義爲指向 objc_ivar 結構體 的指針,objc_ivar結構體 的數據結構以下:

struct objc_ivar {
    char * Nullable ivar_name                               OBJC2UNAVAILABLE;
    char * Nullable ivar_type                               OBJC2UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef LP64
    int space                                                OBJC2_UNAVAILABLE;
#endif
} 

//Ivar表明類中實例變量的類型,是一個指向ojbcet_ivar的結構體的指針
typedef struct objc_ivar *Ivar;
複製代碼

objc_class中看到的ivars成員列表,其中的元素就是Ivar,能夠經過實例查找其在類中的名字,這個過程被稱爲反射,下面的class_copyIvarList獲取的不只有實例變量還有屬性:

Ivar *ivarList = class_copyIvarList([self class], &count);
    for (int i= 0; i<count; i++) {
        Ivar ivar = ivarList[i];
        const char *ivarName = ivar_getName(ivar);
        NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
    }
    free(ivarList);
複製代碼

1.3.5 Method(方法)

objc/runtime.h 中Method(方法) 被定義爲指向 objc_method 結構體 的指針,在objct_class定義中看到methodLists,其中的元素就是Method,objc_method結構體 的數據結構以下:

struct objc_method {
    SEL _Nonnull method_name;                    // 方法名
    char * _Nullable method_types;               // 方法類型
    IMP _Nonnull method_imp;                     // 方法實現
};

//Method表示某個方法的類型
typedef struct objc_method *Method;

複製代碼

2、和Runtime交互的三種方式

2.1 OC源代碼
OC代碼會在編譯階段被編譯器轉化。OC中的類、方法和協議等在Runtime中都由一些數據結構來定義。
因此在平常的項目開發過程當中,使用OC語言進行編碼時,這已是在和Runtime進行交互了,只是這個過程對於開發者而言是無感的
複製代碼
2.2 NSObject方法
Runtime的最大特徵就是實現了OC語言的動態特性。
複製代碼

做爲大部分Objective-C類繼承體系的根類的NSObject,其自己就具備了一些很是具備運行時動態特性的方法, 好比:

1. -respondsToSelector:方法能夠檢查在代碼運行階段當前對象是否能響應指定的消息

2. -description:返回當前類的描述信息 

3. -isKindOfClass: 和 -isMemberOfClass:  檢查對象是否存在於指定的類的繼承體系中

4. -conformsToProtocol:    檢查對象是否實現了指定協議類的方法;

5. -methodForSelector:     返回指定方法實現的地址。
複製代碼
2.3 使用Runtime函數
Runtime系統是一個由一系列函數和數據結構組成,具備公共接口的動態共享庫。頭文件存放於/usr/include/objc目錄下。
複製代碼

在項目工程代碼裏引用Runtime的頭文件,一樣可以實現相似OC代碼的效果:

//至關於:Class class = [UIView class];
Class viewClass = objc_getClass("UIView");
    
//至關於:UIView *view = [UIView alloc];
UIView *view = ((id (*)(id, SEL))(void *)objc_msgSend)((id)viewClass, sel_registerName("alloc"));

//至關於:UIView *view = [view init];
((id (*)(id, SEL))(void *)objc_msgSend)((id)view, sel_registerName("init"));
複製代碼

3、Runtime消息轉發

3.1 動態方法解析與消息轉發
  • 動態方法解析:動態添加方法

Runtime足夠強大,可以在運行時動態添加一個未實現的方法,這個功能主要有兩個應用場景:

1. 動態添加未實現方法,解決代碼中由於方法未找到而報錯的問題;
2. 利用懶加載思路,若一個類有不少個方法,同時加載到內存中會耗費資源,可使用動態解析添加方法
複製代碼

方法動態解析主要用到的方法以下:

//OC方法:
//類方法未找到時調起,可於此添加類方法實現
+ (BOOL)resolveClassMethod:(SEL)sel

//實例方法未找到時調起,可於此添加實例方法實現
+ (BOOL)resolveInstanceMethod:(SEL)sel

//Runtime方法:
/**
 運行時方法:向指定類中添加特定方法實現的操做
 @param cls 被添加方法的類
 @param name selector方法名
 @param imp 指向實現方法的函數指針
 @param types imp函數實現的返回值與參數類型
 @return 添加方法是否成功
 */
BOOL class_addMethod(Class _Nullable cls,
                     SEL _Nonnull name,
                     IMP _Nonnull imp,
                     const char * _Nullable types)
複製代碼
  • 解決方法無響應崩潰問題

執行OC方法其實就是一個發送消息的過程,若方法未實現,能夠利用方法動態解析與消息轉發來避免程序崩潰,這主要涉及下面一個處理未實現消息的過程:

在這個過程當中,可能還會使用到的方法有:

例子:
#import "ViewController.h"
#import <objc/runtime.h>

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 執行 fun 函數
    [self performSelector:@selector(fun)];
}

// 重寫 resolveInstanceMethod: 添加對象方法實現
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(fun)) { // 若是是執行 fun 函數,就動態解析,指定新的 IMP
        class_addMethod([self class], sel, (IMP)funMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void funMethod(id obj, SEL _cmd) {
    NSLog(@"funMethod"); //新的 fun 函數
}
@end

//日誌輸出:

2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] funMethod
複製代碼

從執行任務的輸出日誌中,能夠看到:

雖然沒有實現 fun 方法,可是經過重寫 resolveInstanceMethod: ,利用 class_addMethod 方法添加對象方法實現 funMethod 方法,並執行。從打印結果來看,成功調起了funMethod 方法。
複製代碼
3.2 消息接收者重定向:

若是上一步中 +resolveInstanceMethod:或者 +resolveClassMethod: 沒有添加其餘函數實現,運行時就會進行下一步:消息接受者重定向。

若是當前對象實現了 -forwardingTargetForSelector:Runtime 就會調用這個方法,容許將消息的接受者轉發給其餘對象,其主要方法以下:

//重定向類方法的消息接收者,返回一個類
- (id)forwardingTargetForSelector:(SEL)aSelector

//重定向實例方法的消息接受者,返回一個實例對象
- (id)forwardingTargetForSelector:(SEL)aSelector
複製代碼
例子:
#import "ViewController.h"
#import <objc/runtime.h>

@interface Person : NSObject
- (void)fun;
@end

@implementation Person

- (void)fun {
    NSLog(@"fun");
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 執行 fun 方法
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 爲了進行下一步 消息接受者重定向
}

// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(fun)) {
        return [[Person alloc] init];
        // 返回 Person 對象,讓 Person 對象接收這個消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

//日誌輸出:

2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] fun
複製代碼

從執行任務的輸出日誌中,能夠看到:

雖然當前 ViewController 沒有實現 fun 方法,+resolveInstanceMethod: 也沒有添加其餘函數實現。
可是咱們經過 forwardingTargetForSelector 把當前 ViewController 的方法轉發給了 Person 對象去執行了
複製代碼

經過forwardingTargetForSelector 能夠修改消息的接收者,該方法返回參數是一個對象,若是這個對象是否是 nil,也不是 self,系統會將運行的消息轉發給這個對象執行。不然,繼續進行下一步:消息重定向流程

3.3 消息重定向:

若是通過消息動態解析、消息接受者重定向,Runtime 系統仍是找不到相應的方法實現而沒法響應消息,Runtime 系統會利用 -methodSignatureForSelector: 方法獲取函數的參數和返回值類型。

其過程:

1. 若是 -methodSignatureForSelector: 返回了一個 NSMethodSignature 對象(函數簽名),Runtime 系統就會建立一個 NSInvocation 對象,
   並經過 -forwardInvocation: 消息通知當前對象,給予這次消息發送最後一次尋找 IMP 的機會。
2. 若是 -methodSignatureForSelector: 返回 nil。則 Runtime 系統會發出 -doesNotRecognizeSelector: 消息,程序也就崩潰了
複製代碼

因此能夠在-forwardInvocation:方法中對消息進行轉發。

其主要方法:

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;

// 獲取函數的參數和返回值類型,返回簽名
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
複製代碼
例子:
#import "ViewController.h"
#import <objc/runtime.h>

@interface Person : NSObject
- (void)fun;
@end

@implementation Person
- (void)fun {
    NSLog(@"fun");
}
@end


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 執行 fun 函數
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 爲了進行下一步 消息接受者重定向
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil; // 爲了進行下一步 消息重定向
}

// 獲取函數的參數和返回值類型,返回簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;   // 從 anInvocation 中獲取消息
    
    Person *p = [[Person alloc] init];

    if([p respondsToSelector:sel]) {   // 判斷 Person 對象方法是否能夠響應 sel
        [anInvocation invokeWithTarget:p];  // 若能夠響應,則將消息轉發給其餘對象處理
    } else {
        [self doesNotRecognizeSelector:sel];  // 若仍然沒法響應,則報錯:找不到響應方法
    }
}
@end

//日誌輸出:
2019-09-01 23:24:34.911774+0800 XKRuntimeKit[30032:8724248] fun
複製代碼

從執行任務的輸出日誌中,能夠看到:

在 -forwardInvocation: 方法裏面讓 Person 對象去執行了 fun 函數
複製代碼

既然 -forwardingTargetForSelector:-forwardInvocation: 均可以將消息轉發給其餘對象處理,那麼二者的區別在哪?

區別就在於 -forwardingTargetForSelector: 只能將消息轉發給一個對象。而 -forwardInvocation: 能夠將消息轉發給多個對象。
複製代碼

4、Runtime的應用

4.1 動態方法交換

實現動態方法交換(Method Swizzling )是Runtime中最具盛名的應用場景,其原理是:

經過Runtime獲取到方法實現的地址,進而動態交換兩個方法的功能
複製代碼
關鍵方法:
//獲取類方法的Mthod
Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)

//獲取實例對象方法的Mthod
Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)

//交換兩個方法的實現
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
複製代碼
  • 動態方法交換
#import "RuntimeKit.h"
#import <objc/runtime.h>

@implementation RuntimeKit

- (instancetype)init
{
    self = [super init];
    if (self) {
        
        //交換方法的實現,並測試打印
        Method methodA = class_getInstanceMethod([self class], @selector(testA));
        Method methodB = class_getInstanceMethod([self class], @selector(testB));
        method_exchangeImplementations(methodA, methodB);
        
        [self testA];
        [self testB];
    }
    return self;
}

- (void)testA{
    NSLog(@"我是A方法");
}

- (void)testB{
    NSLog(@"我是B方法");
}
@end

日誌輸出:
2019-09-01 21:25:32.858860+0800 XKRuntimeKit[1662:280727] 我是B方法
2019-09-01 21:25:32.859059+0800 XKRuntimeKit[1662:280727] 我是A方法
複製代碼
  • 攔截並替換系統方法
#import "UIViewController+xk.h"
#import <objc/runtime.h>

@implementation UIViewController (xk)

+ (void)load{
    
    //獲取系統方法地址
    Method sytemMethod = class_getInstanceMethod([self class], @selector(viewWillAppear:));
    
    //獲取自定義方法地址
    Method customMethod = class_getInstanceMethod([self class], @selector(run_viewWillAppear:));
    
    //判斷存在與否
    if (!class_addMethod([self class], @selector(viewWillAppear:), method_getImplementation(customMethod), method_getTypeEncoding(customMethod))) {
        method_exchangeImplementations(sytemMethod, customMethod);
    }
    else{
        class_replaceMethod([self class], @selector(run_viewWillAppear:), method_getImplementation(sytemMethod), method_getTypeEncoding(sytemMethod));
    }
}


- (void)run_viewWillAppear:(BOOL)animated{
    [self run_viewWillAppear:animated];
    NSLog(@"我是運行時替換的方法-viewWillAppear");
}

- (void)run_viewWillDisappear:(BOOL)animated{
    [self run_viewWillDisappear:animated];
    NSLog(@"我是運行時替換的方法-viewWillDisappear");
}
@end

日誌輸出:

2019-09-01 21:36:55.610385+0800 XKRuntimeKit[1921:310118] 我是運行時替換的方法-viewWillAppear
複製代碼

將該分類引入,從執行結果能夠看到,但系統的控制器執行viewWillAppear時,則會進入已經替換的方法run_viewWillAppear之中。

4.2 類目添加新的屬性

在平常開發過程當中,經常會使用類目Category爲一些已有的類擴展功能。雖然繼承也可以爲已有類增長新的方法,並且相比類目更是具備增長屬性的優點,可是繼承畢竟是一個重量級的操做,添加沒必要要的繼承關係無疑增長了代碼的複雜度。

遺憾的是,OC的類目並不支持直接添加屬性
複製代碼

爲了實現給分類添加屬性,還需藉助 Runtime的關聯對象(Associated Objects)特性,它可以幫助咱們在運行階段將任意的屬性關聯到一個對象上:

/**
 1.給對象設置關聯屬性
 @param object 須要設置關聯屬性的對象,即給哪一個對象關聯屬性
 @param key 關聯屬性對應的key,可經過key獲取這個屬性,
 @param value 給關聯屬性設置的值
 @param policy 關聯屬性的存儲策略(對應Property屬性中的assign,copy,retain等)
 OBJC_ASSOCIATION_ASSIGN             @property(assign)。
 OBJC_ASSOCIATION_RETAIN_NONATOMIC   @property(strong, nonatomic)。
 OBJC_ASSOCIATION_COPY_NONATOMIC     @property(copy, nonatomic)。
 OBJC_ASSOCIATION_RETAIN             @property(strong,atomic)。
 OBJC_ASSOCIATION_COPY               @property(copy, atomic)。
 */
void objc_setAssociatedObject(id _Nonnull object,
                              const void * _Nonnull key,
                              id _Nullable value,
                              objc_AssociationPolicy policy)
                              
/**
 2.經過key獲取關聯的屬性
 @param object 從哪一個對象中獲取關聯屬性
 @param key 關聯屬性對應的key
 @return 返回關聯屬性的值
 */
id _Nullable objc_getAssociatedObject(id _Nonnull object,
                                      const void * _Nonnull key)
                                      
/**
 3.移除對象所關聯的屬性
 @param object 移除某個對象的全部關聯屬性
 */
void objc_removeAssociatedObjects(id _Nonnull object)
複製代碼

注意:

key與關聯屬性一一對應,咱們必須確保其全局惟一性,經常使用咱們使用@selector(methodName)做爲key
複製代碼
例子:

UIViewController+xk.h中新增一個name屬性:

@interface UIViewController (xk)

//新增屬性:名稱
@property(nonatomic,copy)NSString * name;

- (void)clearAssociatedObjcet;
@end
複製代碼

UIViewController+xk.m中補充對應的實現:

#import "UIViewController+xk.h"
#import <objc/runtime.h>

@implementation UIViewController (xk)

//set方法
- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self,
                             @selector(name),
                             name,
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

//get方法
- (NSString *)name{
    return objc_getAssociatedObject(self,
                                    @selector(name));
}

//添加一個自定義方法,用於清除全部關聯屬性
- (void)clearAssociatedObjcet{
    objc_removeAssociatedObjects(self);
}
@end

複製代碼

執行任務:

ViewController * vc = [ViewController new];
vc.name = @"我是根控制器";
NSLog(@"獲取關聯屬性name:%@",vc.name);
    
[vc clearAssociatedObjcet];
NSLog(@"獲取關聯屬性name:%@",vc.name);
   
日誌輸出:
2019-09-01 21:50:05.162915+0800 XKRuntimeKit[2066:335327] 獲取關聯屬性name:我是根控制器
2019-09-01 21:50:05.163080+0800 XKRuntimeKit[2066:335327] 獲取關聯屬性name:(null)
複製代碼

一樣的,使用運行時還能夠爲類目新增一些自身沒有的方法,好比給UIView新增點擊事件:

#import <objc/runtime.h>

static char onTapGestureKey;
static char onTapGestureBlockKey;

@implementation UIView (Gesture)

//添加輕拍手勢
- (void)addTapGestureActionWithBlock:(onGestureActionBlock)block{
    UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &onTapGestureKey);
    self.userInteractionEnabled = YES;
    if (!gesture){
        gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(xk_handleActionForTapGesture:)];
        [self addGestureRecognizer:gesture];
        objc_setAssociatedObject(self, &onTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN);
    }
    
    //添加點擊手勢響應代碼塊屬性
    objc_setAssociatedObject(self, &onTapGestureBlockKey, block, OBJC_ASSOCIATION_COPY);
}

//點擊回調
- (void)xk_handleActionForTapGesture:(UITapGestureRecognizer*)sender{
    onGestureActionBlock block = objc_getAssociatedObject(self, &onTapGestureBlockKey);
    if (block) block(sender);
}
@end
複製代碼

可是使用運行時給類目新增代理屬性時,須要注意循環應用問題,因爲運行時執行添加的屬性都是retain操做,因此每每在執行過程會致使對應的 delegate 得不到釋放,於是會致使崩潰,對此,能夠進行如下修改操做:

場景: 給UIView新增emptyDataDelegate空頁面代理,以處理一些異常狀況的顯示

UIView+EmptyDataSet.h中新增一個emptyDataDelegate屬性,:

//頁面無數據代理
@protocol XKEmptyDataSetDelegate <NSObject>
@optional
//佔位文字
- (NSString*)placeholderForEmptyDataSet:(UIScrollView*)scrollView;
@end


//空頁面設置
@interface UIView (EmptyDataSet)
@property (nonatomic,weak) id<XKEmptyDataSetDelegate>emptyDataDelegate;
@end

複製代碼

UIView+EmptyDataSet.m中藉助XKEmptyDataWeakObjectContainer實現其方法:

//弱引用代理
@interface XKEmptyDataWeakObjectContainer : NSObject
@property (nonatomic,weak,readonly)id weakObject;
- (instancetype)initWithWeakObject:(id)object;
@end

@implementation XKEmptyDataWeakObjectContainer
- (instancetype)initWithWeakObject:(id)object{
    self = [super init];
    if (self) {
        _weakObject = object;
    }
    return self;
}
@end


static char xk_EmptyDataSetDelegateKey;

//空視圖設置
@implementation UIView (EmptyDataSet)
- (void)setEmptyDataDelegate:(id<XKEmptyDataSetDelegate>)emptyDataDelegate{
     objc_setAssociatedObject(self, &xk_EmptyDataSetDelegateKey, [[XKEmptyDataWeakObjectContainer alloc] initWithWeakObject:emptyDataDelegate], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id<XKEmptyDataSetDelegate>)emptyDataDelegate{
    XKEmptyDataWeakObjectContainer * container = objc_getAssociatedObject(self, &xk_EmptyDataSetDelegateKey);
    return container.weakObject;
}
@end
複製代碼
4.3 獲取類詳細屬性
  • 獲取屬性列表
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i<count; i++) {
    const char *propertyName = property_getName(propertyList[i]);
    NSLog(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]);
}
free(propertyList);
複製代碼
  • 獲取全部成員變量
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i= 0; i<count; i++) {
    Ivar ivar = ivarList[i];
    const char *ivarName = ivar_getName(ivar);
    NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
複製代碼
  • 獲取全部方法
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i<count; i++) {
    Method method = methodList[i];
    SEL mthodName = method_getName(method);
    NSLog(@"MethodName(%d): %@",i,NSStringFromSelector(mthodName));
}
free(methodList);
複製代碼
  • 獲取當前遵循的全部協議
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (int i=0; i<count; i++) {
    Protocol *protocal = protocolList[i];
    const char *protocolName = protocol_getName(protocal);
    NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
}
free(propertyList); //C語言中使用Copy操做的方法,要注意釋放指針,防止內存泄漏
複製代碼
4.4 解決同一方法高頻率調用的效率問題

Runtime源碼中的IMP做爲函數指針,指向方法的實現。經過它,能夠繞開發送消息的過程來提升函數調用的效率。當須要持續大量重複調用某個方法的時候,會十分有用,以下:

void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);
複製代碼
4.5 動態操做屬性
  • 修改私有屬性
場景:
咱們使用第三方框架裏的Person類,在特殊需求下想要更改其私有屬性nickName,這樣的操做咱們就可使用Runtime能夠動態修改對象屬性。
複製代碼
Person *ps = [[Person alloc] init];
NSLog(@"nickName: %@",[ps valueForKey:@"nickName"]); //null

//第一步:遍歷對象的全部屬性
unsigned int count;
Ivar *ivarList = class_copyIvarList([ps class], &count);
for (int i= 0; i<count; i++) {
    //第二步:獲取每一個屬性名
    Ivar ivar = ivarList[i];
    const char *ivarName = ivar_getName(ivar);
    NSString *propertyName = [NSString stringWithUTF8String:ivarName];
    if ([propertyName isEqualToString:@"_nickName"]) {
        //第三步:匹配到對應的屬性,而後修改;注意屬性帶有下劃線
        object_setIvar(ps, ivar, @"allenlas");
    }
}
NSLog(@"nickName: %@",[ps valueForKey:@"nickName"]); //allenlas
複製代碼
  • 改進iOS歸檔和解檔

歸檔是一種經常使用的輕量型文件存儲方式,可是它有個弊端:

在歸檔過程當中,若一個Model有多個屬性,咱們不得不對每一個屬性進行處理,很是繁瑣
複製代碼

歸檔操做主要涉及兩個方法: encodeObjectdecodeObjectForKey ,對於這兩個方法,能夠利用Runtime 來進行改進:

//原理:使用Runtime動態獲取全部屬性
//解檔操做
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
    self = [super init];
    if (self) {
        unsigned int count = 0;
        
        Ivar *ivarList = class_copyIvarList([self class], &count);
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivarList[i];
            const char *ivarName = ivar_getName(ivar);
            NSString *key = [NSString stringWithUTF8String:ivarName];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivarList); //釋放指針
    }
    return self;
}

//歸檔操做
- (void)encodeWithCoder:(NSCoder *)aCoder{
    unsigned int count = 0;
    
    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (NSInteger i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(ivarList); //釋放指針
}
複製代碼
測試:
//--測試歸檔
Person *ps = [[Person alloc] init];
ps.name = @"allenlas";
ps.age  = 20;
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.archive"];
[NSKeyedArchiver archiveRootObject:ps toFile:fileTemp];

//--測試解檔
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.henry"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:fileTemp];
NSLog(@"person-name:%@,person-age:%ld",person.name,person.age); 
//person-name:allenlas,person-age:20
複製代碼
  • 實現字典與模型的轉換

在平常項目開發中,常常會使用YYModelMJExtension等對接口返回的數據對象實現轉模型操做。對於此,能夠利用KVCRuntime 來進行相似的功能實現,在這個過程當中須要解決的問題有:

利用Runtime實現的思路大致以下:

藉助Runtime能夠動態獲取成員列表的特性,遍歷模型中全部屬性,而後以獲取到的屬性名爲key,在JSON字典中尋找對應的值value;再將每個對應Value賦值給模型,就完成了字典轉模型的目的。
複製代碼
json數據:
{
    "id":"10089",
    "name": "Allen",
    "age":"20",
    "position":"iOS開發工程師",
    "address":{
            "country":"中國",
            "province": "廣州"
            },
    "tasks":[{
               "name":"Home",
               "desc":"app首頁開發"
    },{
               "name":"Train",
               "desc":"app培訓模塊開發"
    },{
               "name":"Me",
               "desc":"完成我的頁面"
    }
    ]
}
複製代碼
  1. 建立NSObject的類目 NSObject+model,用於實現字典轉模型
//在NSObject+model.h中

NS_ASSUME_NONNULL_BEGIN

//AAModel協議,協議方法能夠返回一個字典,代表特殊字段的處理規則
@protocol AAModel<NSObject>
@optional
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
@end;

@interface NSObject (model)
+ (instancetype)xk_modelWithDictionary:(NSDictionary *)dictionary;
@end

NS_ASSUME_NONNULL_END

複製代碼
#import "NSObject+model.h"
#import <objc/runtime.h>

@implementation NSObject (model)
+ (instancetype)xk_modelWithDictionary:(NSDictionary *)dictionary{
    
    //建立當前模型對象
    id object = [[self alloc] init];
    //1.獲取當前對象的成員變量列表
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    
    //2.遍歷ivarList中全部成員變量,以其屬性名爲key,在字典中查找Value
    for (int i= 0; i<count; i++) {
        //2.1獲取成員屬性
        Ivar ivar = ivarList[i];
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)] ;
        
        //2.2截取成員變量名:去掉成員變量前面的"_"號
        NSString *propertyName = [ivarName substringFromIndex:1];
        
        //2.3以屬性名爲key,在字典中查找value
        id value = dictionary[propertyName];
        
        //3.獲取成員變量類型, 由於ivar_getTypeEncoding獲取的類型是"@\"NSString\""的形式
        //因此咱們要作如下的替換
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];// 替換:
        //3.1去除轉義字符:@\"name\" -> @"name" ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        //3.2去除@符號
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        
        //4.對特殊成員變量進行處理:
        //判斷當前類是否實現了協議方法,獲取協議方法中規定的特殊變量的處理方式
        NSDictionary *perpertyTypeDic;
        if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){
            perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil];
        }
        
        //4.1處理:字典的key與模型屬性不匹配的問題,如id->uid
        id anotherName = perpertyTypeDic[propertyName];
        if(anotherName && [anotherName isKindOfClass:[NSString class]]){
            value =  dictionary[anotherName];
        }
        
        //4.2.處理:模型嵌套模型
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            Class modelClass = NSClassFromString(ivarType);
            if (modelClass != nil) {
                //將被嵌套字典數據也轉化成Model
                value = [modelClass xk_modelWithDictionary:value];
            }
        }
        
        //4.3處理:模型嵌套模型數組
        //判斷當前Vaue是一個數組,並且存在協議方法返回了perpertyTypeDic
        if ([value isKindOfClass:[NSArray class]] && perpertyTypeDic) {
            Class itemModelClass = perpertyTypeDic[propertyName];
            //封裝數組:將每個子數據轉化爲Model
            NSMutableArray *itemArray = @[].mutableCopy;
            for (NSDictionary *itemDic  in value) {
                id model = [itemModelClass xk_modelWithDictionary:itemDic];
                [itemArray addObject:model];
            }
            value = itemArray;
        }
        
        //5.使用KVC方法將Vlue更新到object中
        if (value != nil) {
            [object setValue:value forKey:propertyName];
        }
    }
    free(ivarList); //釋放C指針
    return object;
}
@end
複製代碼
  1. 分別新建 UserModelAddressModelTasksModel對json處理進行處理:
UserModel類
#import "NSObject+model.h"
#import "AddressModel.h"
#import "TasksModel.h"

NS_ASSUME_NONNULL_BEGIN

@interface UserModel : NSObject<AAModel>
//普通屬性
@property (nonatomic, copy) NSString * uid;
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * position;
@property (nonatomic, assign) NSInteger age;

//嵌套模型
@property (nonatomic, strong) AddressModel *address;


//嵌套模型數組
@property (nonatomic, strong) NSArray *tasks;
@end

NS_ASSUME_NONNULL_END


@implementation UserModel
+ (NSDictionary<NSString *,id> *)modelContainerPropertyGenericClass{
    //須要特別處理的屬性
    return @{@"tasks" : [TasksModel class],@"uid":@"id"};
}
@end
複製代碼
AddressModel類
#import "NSObject+model.h"

NS_ASSUME_NONNULL_BEGIN

@interface AddressModel : NSObject
@property (nonatomic, copy) NSString * country;
@property (nonatomic, copy) NSString * province;
@end

NS_ASSUME_NONNULL_END

@implementation AddressModel
@end
複製代碼
TasksModel類
#import "NSObject+model.h"

NS_ASSUME_NONNULL_BEGIN

@interface TasksModel : NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * desc;
@end
NS_ASSUME_NONNULL_END

@implementation TasksModel
@end
複製代碼
  1. 代碼測試
- (void)viewDidLoad {
    [super viewDidLoad];
    //讀取JSON數據
    NSDictionary * jsonData = @{
                                @"id":@"10089",
                                @"name": @"Allen",
                                @"age":@"20",
                                @"position":@"iOS開發工程師",
                                @"address":@{
                                        @"country":@"中國",
                                        @"province":@"廣州"
                                        },
                                @"tasks":@[@{
                                               @"name":@"Home",
                                               @"desc":@"app首頁開發"
                                               },@{
                                               @"name":@"Train",
                                               @"desc":@"app培訓模塊開發"
                                               },@{
                                               @"name":@"Me",
                                               @"desc":@"完成我的頁面"
                                               }
                                           ]
                                };
    
    //字典轉模型
    UserModel * user = [UserModel xk_modelWithDictionary:jsonData];
    TasksModel * task = user.tasks[0];
    
    NSLog(@"%@",task.name);
}
複製代碼

其執行結果,數據結構以下:

相關文章
相關標籤/搜索