Runtime在工作中的運用

minjing_lin發表於2019-04-18

這篇文章是筆者結合一些參考文章和當初學習Runtime的心得而寫的一篇總結,主要講解Runtime在工作中的運用,沒有涉及到太底層的知識,極盡詳略,適合初中級學者,水平有限,有錯誤的地方,還請大佬在評論中指出,一起快樂學習。持續更新中。。。

Runtime在工作中的運用

1.Runtime簡介

  • Runtime 簡稱執行時,是一套C語言的API(引入 <objc/runtime.h><objc/message.h>)。OC 就是執行時機制,也就是在執行時候的一些機制,其中最主要的是 訊息機制

  • 對於C語言,函式的呼叫在編譯的時候會決定呼叫哪個函式。

  • 對於OC,函式的呼叫稱為訊息傳送,屬於動態呼叫過程。在編譯的時候並不能決定真正呼叫哪個函式,只有在真正執行的時候才會根據函式的名稱找到對應的函式來呼叫。

2.Runtime訊息機制

訊息機制原理:物件根據方法編號SEL去對映表查詢對應的方法實現。

驗證

1.在main.m中建立一個物件;

id object = [NSObject alloc];
object = [object init];
複製程式碼

2.終端切換到該目錄下,執行命令clang -rewrite-objc main.m,編譯後會生成一個main.cpp(C++檔案);

3.在.cpp檔案中搜尋autoreleasepool,可以找到上述物件建立的底層程式碼;

id object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
object = ((id (*)(id, SEL))(void *)objc_msgSend)((id)object, sel_registerName("init"));
複製程式碼

可以看出呼叫方法本質就是發訊息[[NSObject alloc]init]語句發了兩次訊息,第一次發了alloc 訊息,第二次傳送init 訊息。

4.我們自己來嘗試實現,首先匯入標頭檔案 #import <objc/message.h>,然後讓訊息機制方法有提示(【build setting -> 搜尋msg -> objc_msgSend(YES --> NO)】)。

id object = objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc"));
object = objc_msgSend(object, sel_registerName("init"));
/**
 objc_getClass(const char *name) 獲取當前類
 sel_registerName(const char *str) 註冊個方法編號
 objc_msgSend(id self:誰傳送訊息, SEL op:傳送什麼訊息, ...:引數)
 */
複製程式碼

換個寫法:

id objc = objc_msgSend([NSObject class], @selector(alloc));
objc = objc_msgSend(objc, @selector(init));
複製程式碼

引數處理:

objc_msgSend(p, sel_registerName("height:"), 180);
複製程式碼

注:

objc_msgSend:這是個最基本的用於傳送訊息的函式。

其實編譯器會根據情況在objc_msgSendobjc_msgSend_fpretobjc_msgSend_stretobjc_msgSendSuper, 或 objc_msgSendSuper_stret 五個方法中選擇一個來呼叫。

如果訊息是傳遞給超類,那麼會呼叫名字帶有 Super 的函式;

如果訊息返回值是浮點數,那麼會呼叫名字帶有fpret 的函式;

如果訊息返回值是資料結構而不是簡單值時,那麼會呼叫名字帶有stret的函式。

3.Runtime方法呼叫流程

  • 物件方法:(儲存到類物件的方法列表)
  • 類方法:(儲存到元類(Meta Class)中方法列表)

1.訊息傳遞:
一個物件的方法像這樣[obj foo],編譯器轉成訊息傳送objc_msgSend(obj, foo)Runtime時執行的流程是什麼樣的吶?

1.首先,通過objisa指標找到它的 class ;
2.註冊方法編號SEL,可以快速查詢;
3.根據方法編號,在 classmethod listfoo ;
3.如果 class 中沒到 foo,繼續往它的 superclass 中找 ;
4.一旦找到 foo 這個函式,就去執行它的實現IMP

2.Runtime的三次轉發流程

Runtime在工作中的運用

動態方法解析

Objective-C執行時會呼叫 +resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機會提供一個函式實現。如果你新增了函式,那執行時系統就會重新啟動一次訊息傳送的過程(動態新增方法)。如果未實現方法,執行時就會移到下一步:forwardingTargetForSelector

備用接收者

如果目標物件實現了-forwardingTargetForSelector:Runtime 這時就會呼叫這個方法,給你把這個訊息轉發給其他物件的機會。如果還不能處理未知訊息,就會進入完整訊息轉發階段。

完整訊息轉發

Runtime系統會向物件傳送methodSignatureForSelector:訊息,並取到返回的方法簽名用於生成NSInvocation物件。為接下來的完整的訊息轉發生成一個 NSMethodSignature物件。NSMethodSignature 物件會被包裝成 NSInvocation 物件,forwardInvocation: 方法裡就可以對 NSInvocation 進行處理了。如果未實現,Runtime則會發出 -doesNotRecognizeSelector: 訊息,程式這時也就掛掉了。

4.Runtime動態新增方法

使用場景:如果一個類方法非常多,載入類到記憶體的時候也比較耗費資源,需要給每個方法生成對映表,可以使用動態給某個類,新增方法解決。

經典面試題:有沒有使用performSelector,其實主要想問你有沒有動態新增過方法。

方法介紹

// 引數1:給哪個類新增方法
// 引數2:新增方法的方法編號SEL
// 引數3:新增方法的函式實現IMP(函式地址)
// 引數4:函式的型別,(返回值+引數型別)
class_addMethod(Class cls, SEL name, IMP imp, const char * types)
複製程式碼

1.class_addMethod會新增一個覆蓋父類的實現,但不會取代原有類的實現。

2.函式的型別官方文件

方法示例

假如Person物件呼叫eat方法,而該方法並沒有實現,則會報錯。我們可以利用RuntimePerson類中動態新增eat方法,來實現該方法的呼叫。

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
    p = objc_msgSend(p, sel_registerName("init"));

    [p performSelector:@selector(eat)];
}

@end
複製程式碼
@implementation Person

/**
 void的前面沒有+、-號,因為只是C的程式碼;
 必須有兩個指定引數(id self,SEL _cmd)
 */
void eat(id self, SEL sel)
{
    NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}

// 當一個物件呼叫未實現的方法,會呼叫這個方法處理,並且會把對應的方法列表傳過來.
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        //函式的型別,(返回值+引數型別) v:void @:物件->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)eat, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}

@end
複製程式碼

5.Runtime方法交換(Method Swizzling)

使用場景:當第三方框架或者系統原生方法功能不能滿足我們的時候,我們可以在保持系統原有方法功能的基礎上,新增額外的功能。

方法介紹

// 交換方法地址,交換兩個方法的實現
method_exchangeImplementations(Method m1, Method m2)
複製程式碼

方法封裝:為了後續呼叫方便,我們可以將Method Swizzling功能封裝為類方法,作為NSObject的類別。

@interface NSObject (Swizzling)

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
                         bySwizzledSelector:(SEL)swizzledSelector;

@end
複製程式碼
#import "NSObject+Swizzling.h"
#import <objc/message.h>

@implementation NSObject (Swizzling)

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector
{
    Class class = [self class];
    //原有方法
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    //替換原有方法的新方法
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    //先嚐試給源SEL新增IMP,這裡是為了避免源SEL沒有實現IMP的情況
    BOOL didAddMethod =
    class_addMethod(class,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {//新增成功:說明源SEL沒有實現IMP,將源SEL的IMP替換到交換SEL的IMP
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {//新增失敗:說明源SEL已經有IMP,直接將兩個SEL的IMP交換即可
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
@end
複製程式碼

**方法示例:**例如我們想要替換ViewController生命週期方法,可以這樣做。

#import "UIViewController+Swizzling.h"
#import "NSObject+Swizzling.h"

@implementation UIViewController (Swizzling)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(viewWillAppear:) bySwizzledSelector:@selector(mj_viewWillAppear:)];
    });
}

- (void)mj_viewWillAppear:(BOOL)animated{
    [self mj_viewWillAppear:animated];
    
    NSLog(@"被呼叫了");
}
@end
複製程式碼

1.swizzling建議在+load中完成。+load+initialize 是Objective-C runtime會自動呼叫兩個類方法。+load 是在一個類被初始載入時呼叫,一定會被呼叫;+initialize 是在應用第一次呼叫該類的類方法或例項方法前呼叫,相當於懶載入方式,可能不被呼叫。此外 +load 方法還有一個非常重要的特性,那就是子類、父類和分類中的 +load 方法的實現是被區別對待的。換句話說在 Objective-C runtime 自動呼叫 +load 方法時,分類中的 +load 方法並不會對主類中的 +load 方法造成覆蓋。

2.swizzling應該只在dispatch_once 中完成,由於swizzling 改變了全域性的狀態,所以我們需要確保在任何情況下(多執行緒環境,或者被其他人手動再次呼叫+load方法)只交換一次,防止再次呼叫又將方法交換回來。+load方法本身即為執行緒安全,為什麼仍需新增dispatch_once,其原因就在於+load方法本身無法保證其中程式碼只被執行一次。

6.Runtime動態新增屬性

場景:分類是不能自定義屬性和變數的,這時候可以使用runtime動態新增屬性方法;

原理:給一個類宣告屬性,其實本質就是給這個類新增關聯,並不是直接把這個值的記憶體空間新增到類存空間。

方法

/** 關聯物件、set方法
id object:給哪個物件新增關聯,給哪個物件設定屬性
const void *key:關聯的key,要求唯一,建議用char 可以節省位元組
id value:關聯的value,給屬性設定的值
objc_AssociationPolicy policy:記憶體管理的策略
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 獲取關聯的物件、get方法
id objc_getAssociatedObject(id object, const void *key)
// 移除關聯的物件
void objc_removeAssociatedObjects(id object)
複製程式碼

記憶體策略對應的屬性修飾表:

記憶體策略 屬性修飾 描述
OBJC_ASSOCIATION_ASSIGN @property (assign) 或 @property (unsafe_unretained) 指定一個關聯物件的弱引用。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) @property (nonatomic, strong) 指定一個關聯物件的強引用,不能被原子化使用。
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 指定一個關聯物件的copy引用,不能被原子化使用。
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 指定一個關聯物件的強引用,能被原子化使用。
OBJC_ASSOCIATION_COPY @property (atomic, copy) 指定一個關聯物件的copy引用,能被原子化使用。

示例:實現一個UIViewCategory新增自定義屬性defaultColor

@interface UIView (Color)

@property (nonatomic, strong) UIColor *defaultColor;

@end
  
@implementation UIView (Color)

static char kDefaultColorKey;
- (void)setDefaultColor:(UIColor *)defaultColor
{
    objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)defaultColor {
    return objc_getAssociatedObject(self, &kDefaultColorKey);
}

@end
複製程式碼

7.NSCoding自動歸檔解檔

場景:如果一個模型有許多個屬性,實現自定義模型資料持久化時,需要對每個屬性都實現一遍encodeObjectdecodeObjectForKey方法,比較麻煩。我們可以使用Runtime來解決。

原理:用runtime提供的函式遍歷Model自身所有屬性,並對屬性進行encodedecode操作。

方法實現

#import "MJMusicModel.h"
#import <objc/runtime.h>

@implementation MJMusicModel

// 設定不需要歸解檔的屬性
- (NSArray *)ignoredNames {
    return @[@"_musicUrl"];
}

// 歸檔呼叫方法
- (void)encodeWithCoder:(NSCoder *)encoder
{
    unsigned int count = 0;
    // 獲得這個類的所有成員變數
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++) {
        // 取出i位置對應的成員變數
        Ivar ivar = ivars[i];
        // 獲得成員變數的名字
        const char *name = ivar_getName(ivar);
        // 將每個成員變數名轉換為NSString物件型別
        NSString *key = [NSString stringWithUTF8String:name];
        // 忽略不需要歸檔的屬性
        if ([[self ignoredNames] containsObject:key]) {
            continue;
        }
        // 歸檔
        id value = [self valueForKey:key];
        [encoder encodeObject:value forKey:key];
    }
    // 注意釋放記憶體!
    free(ivars);
}

// 解檔方法
- (id)initWithCoder:(NSCoder *)decoder
{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i<count; i++) {
            // 取出i位置對應的成員變數
            Ivar ivar = ivars[i];
            // 獲得成員變數的名字
            const char *name = ivar_getName(ivar);
            // 將每個成員變數名轉換為NSString物件型別
            NSString *key = [NSString stringWithUTF8String:name];
            // 忽略不需要解檔的屬性
            if ([[self ignoredNames] containsObject:key]) {
                continue;
            }
            // 解檔
            id value = [decoder decodeObjectForKey:key];
            // 設定到成員變數身上
            [self setValue:value forKey:key];
        }
        free(ivars);
    }
    return self;
}

@end
複製程式碼

:我們可以將歸解檔兩個方法封裝為巨集,在需要的地方一句巨集搞定;也可以寫到NSObject一個分類中,方便使用。

8.Runtime字典轉模型

原理:利用Runtime,遍歷模型中所有屬性,根據模型的屬性名,去字典中查詢key,取出對應的值,給模型的屬性賦值。

步驟:提供一個NSObject分類,專門字典轉模型,以後所有模型都可以通過這個分類實現字典轉模型。

接下來分別介紹一下三種情況所實現的程式碼:

1.簡單的字典轉模型

注意:模型屬性數量大於字典的鍵值對時,由於屬性沒有對應值會被賦值為nil,就會導致crash,所以我們要加一個判斷,獲取到Value時,才給模型中屬性賦值。

NSDictionary *dict = @{
                           @"name" : @"xiaoming",
                           @"age"  : @25,
                           @"weight" : @"60kg",
                           @"height" : @1.81
                           };
複製程式碼
#import "NSObject+Model.h"
#import <objc/message.h>

@implementation NSObject (Model)

+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 建立對應的物件
    id objc = [[self alloc] init];
    
    // 成員變數個數
    unsigned int count = 0;
    // 獲取類中的所有成員變數
    Ivar *ivars = class_copyIvarList(self, &count);
    
    // 遍歷所有成員變數
    for (int i = 0; i < count; i++) {
        // 根據角標,從陣列取出對應的成員變數(Ivar:成員變數,以下劃線開頭)
        Ivar ivar = ivars[i];
        
        // 獲取成員變數名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 處理成員變數名->字典中的key(去掉 _ ,從第一個角標開始擷取)
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根據成員屬性名去字典中查詢對應的value
        id value = dict[key];

        if (value) {  
            // 給模型中屬性賦值
            [objc setValue:value forKey:key];
        }
    }
    // 釋放ivars
    free(ivars);
    
    return objc;
}
@end

複製程式碼

2.模型中巢狀模型(模型屬性是另外一個模型物件)

利用runtime的ivar_getTypeEncoding 方法獲取模型物件型別,對該模型物件型別再進行字典轉模型,也就是進行遞迴,需要注意的是要排除系統的物件型別,例如NSString

NSDictionary *dict2 = @{
                           @"name" : @"xiaoming",
                           @"age"  : @25,
                           @"body" :@{
                                      @"weight" : @"65kg",
                                      @"height" : @1.82
                                     }
                           };
複製程式碼
// runtime字典轉模型二級轉換:字典->字典;如果字典中還有字典,也需要把對應的字典轉換成模型
if ([value isKindOfClass:[NSDictionary class]]) {

    // 獲取成員變數型別
    NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

    // 替換: @\"User\" -> User
    ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
    ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];

    if (![ivarType hasPrefix:@"NS"]) {
        // 字典轉換成模型,根據字串類名生成類物件
        Class modelClass = NSClassFromString(ivarType);
        if (modelClass) { // 有對應的模型才需要轉
            // 把字典轉模型
            value = [modelClass modelWithDict:value];
        }
    }
}
複製程式碼

3.陣列中裝著模型(模型的屬性是一個陣列,陣列中是一個個模型物件)

攔截到模型的陣列屬性,進而對陣列中每個模型遍歷並字典轉模型,但是我們不知道陣列中的模型都是什麼型別,需要宣告一個方法,該方法目的不是讓其呼叫,而是讓其實現並返回模型的型別。

NSDictionary *dict3 = @{
                            @"name" : @"xiaoming",
                            @"age"  : @25,
                            @"body" :@{
                                    @"weight" : @"65kg",
                                    @"height" : @1.82
                                    },
                            @"children" : @[
                                    @{
                                        @"sex" : @"男",
                                        @"love" : @"籃球",
                                        },
                                    @{
                                        @"sex" : @"nv",
                                        @"love" : @"鋼琴",
                                        }
                                    ],
                            };
複製程式碼
// runtime字典轉模型三級轉換:字典->陣列->字典;NSArray中也是字典,把陣列中的字典轉換成模型.
if ([value isKindOfClass:[NSArray class]]) {
    // 判斷對應類有沒有實現字典陣列轉模型陣列的協議
    // arrayContainModelClass 提供一個協議,只要遵守這個協議的類,都能把陣列中的字典轉模型
    if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

        // 轉換成id型別,就能呼叫任何物件的方法
        id idSelf = self;

        // 獲取陣列中字典對應的模型
        NSString *type =  [idSelf arrayContainModelClass][key];

        // 生成模型
        Class classModel = NSClassFromString(type);
        NSMutableArray *arrM = [NSMutableArray array];
        // 遍歷字典陣列,生成模型陣列
        for (NSDictionary *dict in value) {
            // 字典轉模型
            id model =  [classModel modelWithDict:dict];
            [arrM addObject:model];
        }

        // 把模型陣列賦值給value
        value = arrM;
    }
}
複製程式碼
#import <Foundation/Foundation.h>

@protocol ModelDelegate <NSObject>

@optional
/**
 提供一個協議,只要遵守這個協議的類,都能把陣列中的字典轉模型
 */
+ (NSDictionary *)arrayContainModelClass;

@end

@interface NSObject (Model)

/**
 dict -> model
 利用runtime 遍歷模型中所有屬性,根據模型中屬性去字典中取出對應的value給模型屬性賦值
 */
+ (instancetype)modelWithDict:(NSDictionary *)dict;

@end
複製程式碼

實現協議類:

+ (NSDictionary *)arrayContainModelClass
{
    // 陣列屬性 : 陣列中的類名
    return @{@"children" : @"MJChild"};
}
複製程式碼

不忙的時候,就整理知識,經過幾天時間的努力,終於寫好了。途中參考大量資料,並通過Demo驗證其正確性,也算對自己的一次全面級的學習與複習。

下一篇,會深入瞭解Runtime底層語言。

I’m not perfect. But I keep trying.

參考文獻:

蘋果官方文件
OC最實用的runtime總結
讓你快速上手Runtime
runtime詳解
iOS 模式詳解—
iOS Runtime詳解
裝逼技術RunTime的總結篇

相關文章