runtime

目錄:

  1. runtime 概念
  2. runtime 消息機制
  3. runtime 方法調用流程「消息機制」
  4. runtime 運行時常見作用
  5. runtime 常用開發應用場景「工作掌握」
    1.runtime 交換方法
    2.runtime 給分類動態添加屬性
    3.runtime 字典轉模型(Runtime 考慮三種情況實現)
  6. runtime 運行時其它作用「面試熟悉」
    1.動態添加方法
    2.動態變量控制
    3.實現NSCoding的自動歸檔和解檔
    4.runtime 下Class的各項操作
    5.runtime 幾個參數概念
  7. 什麼是 method swizzling(俗稱黑魔法)
  8. 最後一道面試題的註解
  9. runtime模塊簡友文章推薦

runtime 概念


Objective-C 是基於 C 的,它爲 C 添加了面向對象的特性。它將很多靜態語言在編譯和鏈接時期做的事放到了 runtime 運行時來處理,可以說 runtime 是我們 Objective-C 幕後工作者。

  • runtime簡稱運行時),是一套 純C(C和彙編寫的) 的API。而 OC 就是 運行時機制,也就是在運行時候的一些機制,其中最主要的是 消息機制

  • 對於 C 語言,函數的調用在編譯的時候會決定調用哪個函數

  • OC的函數調用成爲消息發送,屬於 動態調用過程。在編譯的時候並不能決定真正調用哪個函數,只有在真正運行的時候纔會根據函數的名稱找到對應的函數來調用。

  • 事實證明:在編譯階段,OC 可以 調用任何函數,即使這個函數並未實現,只要聲明過就不會報錯,只有當運行的時候纔會報錯,這是因爲OC是運行時動態調用的。而 C 語言 調用未實現的函數 就會報錯。

runtime 消息機制


我們寫 OC 代碼,它在運行的時候也是轉換成了 runtime 方式運行的。任何方法調用本質:就是發送一個消息(用 runtime發送消息,OC 底層實現通過 runtime 實現)。

消息機制原理:對象根據方法編號SEL去映射表查找對應的方法實現。

每一個 OC 的方法,底層必然有一個與之對應的 runtime 方法。


OC-->runtime

簡單示例:
驗證:方法調用,是否真的是轉換爲消息機制?

  • 必須要導入頭文件 #import <objc/message.h>

    • 註解1:我們導入系統的頭文件,一般用尖括號。

    • 註解2:OC 解決消息機制方法提示步驟【查找build setting -> 搜索msg -> objc_msgSend(YES --> NO)】

    • 註解3:最終生成消息機制,編譯器做的事情,最終代碼,需要把當前代碼重新編譯,用xcode編譯器,【clang -rewrite-objc main.m 查看最終生成代碼】,示例:cd main.m --> 輸入前面指令,就會生成 .opp文件(C++代碼)

    • 註解4:這裏一般不會直接導入<objc/runtime.h>


      message.h
  • 示例代碼:OC 方法-->runtime 方法

說明: eat(無參) 和 run(有參) 是 Person模型類中的私有方法「可以幫我調用私有方法」;

// Person *p = [Person alloc];
// 底層的實際寫法
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));

// p = [p init];
p = objc_msgSend(p, sel_registerName("init"));

// 調用對象方法(本質:讓對象發送消息)
//[p eat];

// 本質:讓類對象發送消息
objc_msgSend(p, @selector(eat));
objc_msgSend([Person class], @selector(run:),20);

//--------------------------- <#我是分割線#> ------------------------------//
// 也許下面這種好理解一點

// id objc = [NSObject alloc];
id objc = objc_msgSend([NSObject class], @selector(alloc));

// objc = [objc init];
objc = objc_msgSend(objc, @selector(init));

runtime 方法調用流程「消息機制」


面試:消息機制方法調用流程

  • 怎麼去調用eat方法,對象方法:(保存到類對象的方法列表) ,類方法:(保存到元類(Meta Class)中方法列表)。
    • 1.OC 在向一個對象發送消息時,runtime 庫會根據對象的 isa指針找到該對象對應的類或其父類中查找方法。。
    • 2.註冊方法編號(這裏用方法編號的好處,可以快速查找)。
    • 3.根據方法編號去查找對應方法。
    • 4.找到只是最終函數實現地址,根據地址去方法區調用對應函數。
  • 補充:一個objc 對象的 isa 的指針指向什麼?有什麼作用?
    • 每一個對象內部都有一個isa指針,這個指針是指向它的真實類型,根據這個指針就能知道將來調用哪個類的方法。

runtime 常見作用


  • 動態交換兩個方法的實現

  • 動態添加屬性

  • 實現字典轉模型的自動轉換

  • 發送消息

  • 動態添加方法

  • 攔截並替換方法

  • 實現 NSCoding 的自動歸檔和解檔

runtime 常用開發應用場景「工作掌握」


runtime 交換方法

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

需求:加載一張圖片直接用[UIImage imageNamed:@"image"];是無法知道到底有沒有加載成功。給系統的imageNamed添加額外功能(是否加載圖片成功)。

  • 方案一:繼承系統的類,重寫方法.(弊端:每次使用都需要導入)
  • 方案二:使用 runtime,交換方法.

實現步驟

  • 1.給系統的方法添加分類
  • 2.自己實現一個帶有擴展功能的方法
  • 3.交換方法,只需要交換一次,

案例代碼:方法+調用+打印輸出

- (void)viewDidLoad {
    [super viewDidLoad];
    // 方案一:先搞個分類,定義一個能加載圖片並且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 方案二:交換 imageNamed 和 ln_imageNamed 的實現,就能調用 imageNamed,間接調用 ln_imageNamed 的實現。
    UIImage *image = [UIImage imageNamed:@"123"];
}

#import <objc/message.h>
@implementation UIImage (Image)
/**
 load方法: 把類加載進內存的時候調用,只會調用一次
 方法應先交換,再去調用
 */
+ (void)load {

    // 1.獲取 imageNamed方法地址
    // class_getClassMethod(獲取某個類的方法)
    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
    // 2.獲取 ln_imageNamed方法地址
    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));

    // 3.交換方法地址,相當於交換實現方式;「method_exchangeImplementations 交換兩個方法的實現」
    method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}

/**
 看清楚下面是不會有死循環的
 調用 imageNamed => ln_imageNamed
 調用 ln_imageNamed => imageNamed
 */
// 加載圖片 且 帶判斷是否加載成功
+ (UIImage *)ln_imageNamed:(NSString *)name {

    UIImage *image = [UIImage ln_imageNamed:name];
    if (image) {
        NSLog(@"runtime添加額外功能--加載成功");
    } else {
        NSLog(@"runtime添加額外功能--加載失敗");
    }
    return image;
}

/**
 不能在分類中重寫系統方法imageNamed,因爲會把系統的功能給覆蓋掉,而且分類中不能調用super
 所以第二步,我們要 自己實現一個帶有擴展功能的方法.
 + (UIImage *)imageNamed:(NSString *)name {

 }
 */
@end


// 打印輸出
2016-02-17 17:52:14.693 runtime[12761:543574] runtime添加額外功能--加載成功

總結:我們所做的就是在方法調用流程第三步的時候,交換兩個方法地址指向。而且我們改變指向要在系統的imageNamed:方法調用前,所以將代碼寫在了分類的load方法裏。最後當運行的時候系統的方法就會去找我們的方法的實現。

runtime 給分類動態添加屬性

原理:給一個類聲明屬性,其實本質就是給這個類添加關聯,並不是直接把這個值的內存空間添加到類存空間。

應用場景:給系統的類添加屬性的時候,可以使用runtime動態添加屬性方法。
註解:系統 NSObject 添加一個分類,我們知道在分類中是不能夠添加成員屬性的,雖然我們用了@property,但是僅僅會自動生成getset方法的聲明,並沒有帶下劃線的屬性和方法實現生成。但是我們可以通過runtime就可以做到給它方法的實現。

需求:給系統 NSObject 類動態添加屬性 name 字符串。

案例代碼:方法+調用+打印

@interface NSObject (Property)

// @property分類:只會生成get,set方法聲明,不會生成實現,也不會生成下劃線成員屬性
@property NSString *name;
@property NSString *height;
@end

@implementation NSObject (Property)

- (void)setName:(NSString *)name {

    // objc_setAssociatedObject(將某個值跟某個對象關聯起來,將某個值存儲到某個對象中)
    // object:給哪個對象添加屬性
    // key:屬性名稱
    // value:屬性值
    // policy:保存策略
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, @"name");
}

// 調用
NSObject *objc = [[NSObject alloc] init];
objc.name = @"123";
NSLog(@"runtime動態添加屬性name==%@",objc.name);

// 打印輸出
2016-02-17 19:37:10.530 runtime[12761:543574] runtime動態添加屬性--name == 123

總結:其實,給屬性賦值的本質,就是讓屬性與一個對象產生關聯,所以要給NSObject的分類的name屬性賦值就是讓nameNSObject產生關聯,而runtime可以做到這一點。

runtime 字典轉模型

字典轉模型的方式:

  • 一個一個的給模型屬性賦值(初學者)。

  • 字典轉模型KVC實現

    • KVC 字典轉模型弊端:必須保證,模型中的屬性和字典中的key 一一對應。
    • 如果不一致,就會調用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:] 報key找不到的錯。
    • 分析:模型中的屬性和字典的key不一一對應,系統就會調用setValue:forUndefinedKey:報錯。
    • 解決:重寫對象的setValue:forUndefinedKey:,把系統的方法覆蓋,就能繼續使用KVC,字典轉模型了。
  • 字典轉模型 Runtime 實現

    • 思路:利用運行時,遍歷模型中所有屬性,根據模型的屬性名,去字典中查找key,取出對應的值,給模型的屬性賦值(從提醒:字典中取值,不一定要全部取出來)。

    • 考慮情況

      • 1.當字典的key和模型的屬性匹配不上。
      • 2.模型中嵌套模型(模型屬性是另外一個模型對象)。
      • 3.數組中裝着模型(模型的屬性是一個數組,數組中是一個個模型對象)。
    • 註解:根據上面的三種特殊情況,先是字典的key和模型的屬性不對應的情況。不對應有兩種,一種是字典的鍵值大於模型屬性數量,這時候我們不需要任何處理,因爲runtime是先遍歷模型所有屬性,再去字典中根據屬性名找對應值進行賦值,多餘的鍵值對也當然不會去看了;另外一種是模型屬性數量大於字典的鍵值對,這時候由於屬性沒有對應值會被賦值爲nil,就會導致crash,我們只需加一個判斷即可。考慮三種情況下面一一註解

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

  • MJExtension 字典轉模型實現

    • 底層也是對 runtime 的封裝,纔可以把一個模型中所有屬性遍歷出來。(你之所以看不懂,是 MJ 封裝了很多層而已^_^.)。

這裏針對字典轉模型 KVC 實現,就不做詳解了,如果你 對 KVC 詳解使用或是實現原理 不是很清楚的,可以參考 實用「KVC編碼 & KVO監聽

字典轉模型 Runtime 方式實現
說明:下面這個示例,是考慮三種情況包含在內的轉換示例,具體可以看圖上的註解


Runtime 字典轉模型

1、runtime 字典轉模型-->字典的 key 和模型的屬性不匹配「模型屬性數量大於字典鍵值對數」,這種情況處理如下:

// Runtime:根據模型中屬性,去字典中取出對應的value給模型屬性賦值
// 思路:遍歷模型中所有屬性->使用運行時
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 1.創建對應的對象
    id objc = [[self alloc] init];

    // 2.利用runtime給對象中的屬性賦值
    /**
     class_copyIvarList: 獲取類中的所有成員變量
     Ivar:成員變量
     第一個參數:表示獲取哪個類中的成員變量
     第二個參數:表示這個類有多少成員變量,傳入一個Int變量地址,會自動給這個變量賦值
     返回值Ivar *:指的是一個ivar數組,會把所有成員屬性放在一個數組中,通過返回的數組就能全部獲取到。
     count: 成員變量個數
     */
    unsigned int count = 0;
    // 獲取類中的所有成員變量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍歷所有成員變量
    for (int i = 0; i < count; i++) {
        // 根據角標,從數組取出對應的成員變量
        Ivar ivar = ivarList[i];

        // 獲取成員變量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 處理成員變量名->字典中的key(去掉 _ ,從第一個角標開始截取)
        NSString *key = [ivarName substringFromIndex:1];

        // 根據成員屬性名去字典中查找對應的value
        id value = dict[key];

        // 【如果模型屬性數量大於字典鍵值對數理,模型屬性會被賦值爲nil】
        // 而報錯 (could not set nil as the value for the key age.)
        if (value) {
            // 給模型中屬性賦值
            [objc setValue:value forKey:key];
        }

    }

    return objc;
}

註解:這裏在獲取模型類中的所有屬性名,是採取 class_copyIvarList 先獲取成員變量(以下劃線開頭) ,然後再處理成員變量名->字典中的key(去掉 _ ,從第一個角標開始截取) 得到屬性名。

原因Ivar:成員變量,以下劃線開頭Property 屬性
獲取類裏面屬性 class_copyPropertyList
獲取類中的所有成員變量 class_copyIvarList

{
    int _a; // 成員變量
}

@property (nonatomic, assign) NSInteger attitudes_count; // 屬性

這裏有成員變量,就不會漏掉屬性;如果有屬性,可能會漏掉成員變量;

使用runtime字典轉模型獲取模型屬性名的時候,最好獲取成員屬性名Ivar因爲可能會有個屬性是沒有settergetter方法的。


2、runtime 字典轉模型-->模型中嵌套模型「模型屬性是另外一個模型對象」,這種情況處理如下:

+ (instancetype)modelWithDict2:(NSDictionary *)dict
{
    // 1.創建對應的對象
    id objc = [[self alloc] init];

    // 2.利用runtime給對象中的屬性賦值
    unsigned int count = 0;
    // 獲取類中的所有成員變量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍歷所有成員變量
    for (int i = 0; i < count; i++) {
        // 根據角標,從數組取出對應的成員變量
        Ivar ivar = ivarList[i];

        // 獲取成員變量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 獲取成員變量類型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

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

        // 處理成員屬性名->字典中的key(去掉 _ ,從第一個角標開始截取)
        NSString *key = [ivarName substringFromIndex:1];

        // 根據成員屬性名去字典中查找對應的value
        id value = dict[key];

        //--------------------------- <#我是分割線#> ------------------------------//
        //
        // 二級轉換:如果字典中還有字典,也需要把對應的字典轉換成模型
        // 判斷下value是否是字典,並且是自定義對象才需要轉換
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {

            // 字典轉換成模型 userDict => User模型, 轉換成哪個模型
            // 根據字符串類名生成類對象
            Class modelClass = NSClassFromString(ivarType);

            if (modelClass) { // 有對應的模型才需要轉
                // 把字典轉模型
                value = [modelClass modelWithDict2:value];
            }
        }

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

3、runtime 字典轉模型-->數組中裝着模型「模型的屬性是一個數組,數組中是字典模型對象」,這種情況處理如下:

// Runtime:根據模型中屬性,去字典中取出對應的value給模型屬性賦值
// 思路:遍歷模型中所有屬性->使用運行時
+ (instancetype)modelWithDict3:(NSDictionary *)dict
{
    // 1.創建對應的對象
    id objc = [[self alloc] init];

    // 2.利用runtime給對象中的屬性賦值
    unsigned int count = 0;
    // 獲取類中的所有成員變量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍歷所有成員變量
    for (int i = 0; i < count; i++) {
        // 根據角標,從數組取出對應的成員變量
        Ivar ivar = ivarList[i];

        // 獲取成員變量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 處理成員屬性名->字典中的key(去掉 _ ,從第一個角標開始截取)
        NSString *key = [ivarName substringFromIndex:1];

        // 根據成員屬性名去字典中查找對應的value
        id value = dict[key];


        //--------------------------- <#我是分割線#> ------------------------------//
        //

        // 三級轉換: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 modelWithDict3:dict];
                    [arrM addObject:model];
                }

                // 把模型數組賦值給value
                value = arrM;

            }
        }

        // 如果模型屬性數量大於字典鍵值對數理,模型屬性會被賦值爲nil,而報錯
        if (value) {
            // 給模型中屬性賦值
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}

runtime字典轉模型-->數組中裝着模型 打印輸出

總結:我們既然能獲取到屬性類型,那就可以攔截到模型的那個數組屬性,進而對數組中每個模型遍歷並字典轉模型,但是我們不知道數組中的模型都是什麼類型,我們可以聲明一個方法,該方法目的不是讓其調用,而是讓其實現並返回模型的類型。

這裏提到的你如果不是很清楚,建議參考我的Demo,重要的部分代碼中都有相應的註解和文字打印,運行程序可以很直觀的表現。

runtime 其它作用「面試熟悉」


動態添加方法

應用場景:如果一個類方法非常多,加載類到內存的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動態給某個類,添加方法解決。

註解:OC 中我們很習慣的會用懶加載,當用到的時候纔去加載它,但是實際上只要一個類實現了某個方法,就會被加載進內存。當我們不想加載這麼多方法的時候,就會使用到 runtime 動態的添加方法。

需求:runtime 動態添加方法處理調用一個未實現的方法 和 去除報錯。 

案例代碼:方法+調用+打印輸出

- (void)viewDidLoad {
    [super viewDidLoad];   
    Person *p = [[Person alloc] init];
    // 默認person,沒有實現run:方法,可以通過performSelector調用,但是會報錯。
    // 動態添加方法就不會報錯
    [p performSelector:@selector(run:) withObject:@10];
}

@implementation Person
// 沒有返回值,1個參數
// void,(id,SEL)
void aaa(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@米", meter);
}

// 任何方法默認都有兩個隱式參數,self,_cmd(當前方法的方法編號)
// 什麼時候調用:只要一個對象調用了一個未實現的方法就會調用這個方法,進行處理
// 作用:動態添加方法,處理未實現
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // [NSStringFromSelector(sel) isEqualToString:@"run"];
    if (sel == NSSelectorFromString(@"run:")) {
        // 動態添加run方法
        // class: 給哪個類添加方法
        // SEL: 添加哪個方法,即添加方法的方法編號
        // IMP: 方法實現 => 函數 => 函數入口 => 函數名(添加方法的函數實現(函數地址))
        // type: 方法類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)aaa, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

// 打印輸出
2016-02-17 19:05:03.917 runtime[12761:543574] runtime動態添加方法--跑了10

動態變量控制

現在有一個Person類,創建 xiaoming對象

  • 動態獲取 XiaoMing 類中的所有屬性 [當然包括私有]

    Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);
  • 遍歷屬性找到對應name字段

    const char *varName = ivar_getName(var);
  • 修改對應的字段值成20

    object_setIvar(self.xiaoMing, var, @"20");
  • 代碼參考

    -(void)answer{
       unsigned int count = 0;
       Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
       for (int i = 0; i<count; i++) {
           Ivar var = ivar[i];
           const char *varName = ivar_getName(var);
           NSString *name = [NSString stringWithUTF8String:varName];
           if ([name isEqualToString:@"_age"]) {
               object_setIvar(self.xiaoMing, var, @"20");
               break;
           }
       }
       NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
    }

實現NSCoding的自動歸檔和解檔

如果你實現過自定義模型數據持久化的過程,那麼你也肯定明白,如果一個模型有許多個屬性,那麼我們需要對每個屬性都實現一遍encodeObject 和 decodeObjectForKey方法,如果這樣的模型又有很多個,這還真的是一個十分麻煩的事情。下面來看看簡單的實現方式。

假設現在有一個Movie類,有3個屬性。先看下 .h文件

// Movie.h文件
//1. 如果想要當前類可以實現歸檔與反歸檔,需要遵守一個協議NSCoding
@interface Movie : NSObject<NSCoding>

@property (nonatomic, copy) NSString *movieId;
@property (nonatomic, copy) NSString *movieName;
@property (nonatomic, copy) NSString *pic_url;
@end

如果是正常寫法, .m 文件應該是這樣的:

// Movie.m文件
@implementation Movie

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:_movieId forKey:@"id"];
    [aCoder encodeObject:_movieName forKey:@"name"];
    [aCoder encodeObject:_pic_url forKey:@"url"];

}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super init]) {
        self.movieId = [aDecoder decodeObjectForKey:@"id"];
        self.movieName = [aDecoder decodeObjectForKey:@"name"];
        self.pic_url = [aDecoder decodeObjectForKey:@"url"];
    }
    return self;
}
@end

如果這裏有100個屬性,那麼我們也只能把100個屬性都給寫一遍嗎。

不過你會使用runtime後,這裏就有更簡便的方法,如下。

#import "Movie.h"
#import <objc/runtime.h>
@implementation Movie

- (void)encodeWithCoder:(NSCoder *)encoder

{
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([Movie class], &count);

    for (int i = 0; i<count; i++) {
        // 取出i位置對應的成員變量
        Ivar ivar = ivars[i];
        // 查看成員變量
        const char *name = ivar_getName(ivar);
        // 歸檔
        NSString *key = [NSString stringWithUTF8String:name];
        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([Movie class], &count);
        for (int i = 0; i<count; i++) {
        // 取出i位置對應的成員變量
        Ivar ivar = ivars[i];
        // 查看成員變量
        const char *name = ivar_getName(ivar);
       // 歸檔
       NSString *key = [NSString stringWithUTF8String:name];
      id value = [decoder decodeObjectForKey:key];
       // 設置到成員變量身上
        [self setValue:value forKey:key];

        }
        free(ivars);
    } 
    return self;
}
@end

這樣的方式實現,不管有多少個屬性,寫這幾行代碼就搞定了。怎麼,代碼有點多,
好說下面看看更加簡便的方法:兩句代碼搞定。

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

#define encodeRuntime(A) \
\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [self valueForKey:key];\
[encoder encodeObject:value forKey:key];\
}\
free(ivars);\
\

#define initCoderRuntime(A) \
\
if (self = [super init]) {\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [decoder decodeObjectForKey:key];\
[self setValue:value forKey:key];\
}\
free(ivars);\
}\
return self;\
\

@implementation Movie

- (void)encodeWithCoder:(NSCoder *)encoder

{
    encodeRuntime(Movie)
}

- (id)initWithCoder:(NSCoder *)decoder
{
    initCoderRuntime(Movie)
}
@end

優化:上面是encodeWithCoder 和 initWithCoder這兩個方法抽成宏。我們可以把這兩個宏單獨放到一個文件裏面,這裏以後需要進行數據持久化的模型都可以直接使用這兩個宏。

runtime 下Class的各項操作

下面是 runtime 下Class的常見方法 及 帶有使用示例代碼。各項操作,原著 http://www.jianshu.com/p/46dd81402f63

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(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
    }
  • 獲取方法列表

    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i; i<count; i++) {
       Method method = methodList[i];
       NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
    }
  • 獲取成員變量列表

    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (unsigned int i; i<count; i++) {
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
    }
  • 獲取協議列表

    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (unsigned int i; i<count; i++) {
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
    }

現在有一個Person類,和person創建的xiaoming對象,有test1和test2兩個方法

  • 獲得類方法

    Class PersonClass = object_getClass([Person class]);
    SEL oriSEL = @selector(test1);
    Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
  • 獲得實例方法

    Class PersonClass = object_getClass([xiaoming class]);
    SEL oriSEL = @selector(test2);
    Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
  • 添加方法

    BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
  • 替換原方法實現

    class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
  • 交換兩個方法的實現

    method_exchangeImplementations(oriMethod, cusMethod);

常用方法

// 得到類的所有方法
    Method *allMethods = class_copyMethodList([Person class], &count);
// 得到所有成員變量
    Ivar *allVariables = class_copyIvarList([Person class], &count);
// 得到所有屬性
    objc_property_t *properties = class_copyPropertyList([Person class], &count);
// 根據名字得到類變量的Ivar指針,但是這個在OC中好像毫無意義
Ivar oneCVIvar = class_getClassVariable([Person class], name);
// 根據名字得到實例變量的Ivar指針
    Ivar oneIVIvar = class_getInstanceVariable([Person class], name);
// 找到後可以直接對私有變量賦值
    object_setIvar(_per, oneIVIvar, @"Mike");//強制修改name屬性
/* 動態添加方法:
     第一個參數表示Class cls 類型;
     第二個參數表示待調用的方法名稱;
     第三個參數(IMP)myAddingFunction,IMP是一個函數指針,這裏表示指定具體實現方法myAddingFunction;
     第四個參數表方法的參數,0代表沒有參數;
     */
    class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0);
// 交換兩個方法
    method_exchangeImplementations(method1, method2);

// 關聯兩個對象
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
/*
 id object                     :表示關聯者,是一個對象,變量名理所當然也是object
 const void *key               :獲取被關聯者的索引key
 id value                      :被關聯者,這裏是一個block
 objc_AssociationPolicy policy : 關聯時採用的協議,有assign,retain,copy等協議,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
*/
runtime 幾個參數概念

以上的幾種方法應該算是runtime在實際場景中所應用的大部分的情況了,平常的編碼中差不多足夠用了。
這裏在對 runtime 幾個參數概念,做一簡單說明

1、objc_msgSend
這是個最基本的用於發送消息的函數。
其實編譯器會根據情況在objc_msgSend, objc_msgSend_stret,,objc_msgSendSuper, 或 objc_msgSendSuper_stret 四個方法中選擇一個來調用。如果消息是傳遞給超類,那麼會調用名字帶有 Super 的函數;如果消息返回值是數據結構而不是簡單值時,那麼會調用名字帶有stret的函數。

2、SEL
objc_msgSend函數第二個參數類型爲SEL,它是selector在Objc中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解爲區分方法的 ID,而這個 ID 的數據結構是SEL:
typedef struct objc_selector *SEL;
其實它就是個映射到方法的C字符串,你可以用 Objc 編譯器命令@selector()``或者 Runtime 系統的sel_registerName函數來獲得一個SEL類型的方法選擇器。

3、id
objc_msgSend第一個參數類型爲id,大家對它都不陌生,它是一個指向類實例的指針:
typedef struct objc_object *id;
objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object結構體包含一個isa指針,根據isa指針就可以順藤摸瓜找到對象所屬的類。

4、runtime.h裏Class的定義

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;//每個Class都有一個isa指針

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;//父類
    const char *name                                         OBJC2_UNAVAILABLE;//類名
    long version                                             OBJC2_UNAVAILABLE;//類版本
    long info                                                OBJC2_UNAVAILABLE;//!*!供運行期使用的一些位標識。如:CLS_CLASS (0x1L)表示該類爲普通class; CLS_META(0x2L)表示該類爲metaclass等(runtime.h中有詳細列出)
    long instance_size                                       OBJC2_UNAVAILABLE;//實例大小
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;//存儲每個實例變量的內存地址
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;//!*!根據info的信息確定是類還是實例,運行什麼函數方法等
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;//緩存
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;//協議
#endif

} OBJC2_UNAVAILABLE;

可以看到運行時一個類還關聯了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協議。
objc_class結構體中:`ivars是objc_ivar_list指針;methodLists是指向objc_method_list指針的指針。也就是說可以動態修改*methodLists的值來添加成員方法,這也是Category`實現的原理。

上面講到的所有東西都在Demo裏,如果你感覺這樣難以理解,那強烈建議你下載Demo ,運行代碼加上文字註解,效果會更好,如果你覺得不錯,還請爲我的Demo star一個。

什麼是 method swizzling(俗稱黑魔法)

  • 簡單說就是進行方法交換

  • Objective-C中調用一個方法,其實是向一個對象發送消息,查找消息的唯一依據是selector的名字。利用Objective-C的動態特性,可以實現在運行時偷換selector對應的方法實現,達到給方法掛鉤的目的

  • 每個類都有一個方法列表,存放着方法的名字和方法實現的映射關係,selector的本質其實就是方法名,IMP有點類似函數指針,指向具體的Method實現,通過selector就可以找到對應的IMP


selector --> 對應的IMP
  • 交換方法的幾種實現方式
    • 利用 method_exchangeImplementations 交換兩個方法的實現
    • 利用 class_replaceMethod 替換方法的實現
    • 利用 method_setImplementation 來直接設置某個方法的IMP

      交換方法

這裏可以參考簡友這篇:【Runtime Method Swizzling開發實例彙總】

這裏可以參考權威這篇:OC運行時黑魔法 Method Swizzling


 -- 小黑不要走,我一定咬吃了你!

最後一道面試題的註解


下面的代碼輸出什麼?

@implementation Son : NSObject
- (id)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

先思考一下,會打印出來什麼❓
關注我的更多幹貨分享 ^_^.


答案:都輸出 Son

  • class 獲取當前方法的調用者的類,superClass 獲取當前方法的調用者的父類,super 僅僅是一個編譯指示器,就是給編譯器看的,不是一個指針。
  • 本質:只要編譯器看到super這個標誌,就會讓當前對象去調用父類方法,本質還是當前對象在調用

這個題目主要是考察關於objc中對 self 和 super 的理解:

  • self 是類的隱藏參數,指向當前調用方法的這個類的實例。而 super 本質是一個編譯器標示符,和 self 是指向的同一個消息接受者

  • 當使用 self 調用方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;

  • 而當使用 super時,則從父類的方法列表中開始找。然後調用父類的這個方法

  • 調用 [self class] 時,會轉化成 objc_msgSend 函數

id objc_msgSend(id self, SEL op, ...)
- 調用 `[super class]`時,會轉化成 `objc_msgSendSuper` 函數.

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一個參數是 objc_super 這樣一個結構體,其定義如下
 struct objc_super {
 __unsafe_unretained id receiver;
 __unsafe_unretained Class super_class;
 };

第一個成員是 receiver, 類似於上面的 objc_msgSend函數第一個參數self
第二個成員是記錄當前類的父類是什麼,告訴程序從父類中開始找方法,找到方法後,最後內部是使用 objc_msgSend(objc_super->receiver, @selector(class))去調用, 此時已經和[self class]調用相同了,故上述輸出結果仍然返回 Son

objc Runtime 開源代碼對- (Class)class方法的實現
-(Class)class { return object_getClass(self); 
}

runtime模塊簡友文章推薦(❤️數量較多)


簡友 runtime模塊推薦閱讀文章
西木 完整總結 http://www.jianshu.com/p/6b905584f536
天口三水羊 objc_msgSend http://www.jianshu.com/p/9e1bc8d890f9
夜千尋墨 詳解 http://www.jianshu.com/p/46dd81402f63
袁崢Seemygo 快速上手 http://www.jianshu.com/p/e071206103a4
鄭欽洪_ 實現自動化歸檔 http://www.jianshu.com/p/bd24c3f3cd0a
HenryCheng 消息機制 http://www.jianshu.com/p/f6300eb3ec3d
賣報的小畫家Sure Method Swizzling開發實例彙總 http://www.jianshu.com/p/f6dad8e1b848
滕大鳥 OC最實用的runtime總結 http://www.jianshu.com/p/ab966e8a82e2

runtime效果圖



坐下來 品一杯白開水,寫的小樣在下面 ~
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章