iOS 模式詳解—「runtime&runloop 面試、工作」看我就 ? 了 ^_^.

不知名開發者發表於2017-06-13

引導


相信對於從事開發人員來說 runtime 這個名稱都不陌生,就像我起初只知道「 runtime 叫執行時 」,後來知道 runtime 同樣可以像 KVC 一樣訪問私有成員變數,還有「 給類動態新增屬性:LNTextField.placeholderColor || 交換方法:imageNamed => ln_imageNamed 」,還有深入的 「 訊息機制的呼叫流程 || 字典轉模型 || 實現NSCoding歸解檔 」以及我們常說的“黑魔法” 是什麼?

runtime 是程式設計中比較難的模組,想要深入學習,這個模組你必須掌握,同樣還有寫的另一篇 runloop 模組,下面是我對 runtime 的整理,從零開始,由淺入深,且帶了幾個 Runtime 實踐場景 --> 大廠來的工友們可選擇性路過。

微眾-不知名開發者

目錄:

  1. runtime.h 釋義
  2. 訊息機制 1.isa指標釋義 2.方法呼叫,是否真的是轉換為訊息機制? 2.objc_msgSend 引數概念釋義
  3. 訊息機制(方法呼叫流程)
  4. 常見作用
  5. 開發場景「工作掌握」 1.交換方法 2.給系統分類動態新增屬性 3.字典轉模型(Runtime 考慮三種情況實現)
  6. 其它作用「面試熟悉」 1.動態新增方法 2.動態變數控制 3.實現NSCoding的自動歸檔和解檔 4.runtime 部分函式 5.method swizzling(俗稱黑魔法)
  7. 一道面試題的註解
  8. 模組博文推薦(❤️數量較多)
  9. Runtime & Runloop 常面問題整理(附答案)
  10. Demo 重要的部分程式碼中都有相應的註解和文字列印,執行程式可以很直觀的表現
  11. iOS 模組註解—「Runloop面試、工作」看我就 ? 了 ^_^.

釋義


Objective-C 是基於 C 的,它為 C 新增了物件導向的特性。它將很多靜態語言在編譯和連結時期做的事放到了 runtime 執行時來處理,可以說 runtime 是我們 Objective-C 幕後工作者。 1.runtime簡稱執行時),是一套 純C(C和彙編)寫的API。而 OC 就是執行時機制,也就是在執行時候的一些機制,其中最主要的是 訊息機制

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

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

4.事實證明:在編譯階段,OC 可以 呼叫任何函式,即使這個函式並未實現,只要宣告過就不會報錯,只有當執行的時候才會報錯,這是因為OC是執行時動態呼叫的。而 C 語言 呼叫未實現的函式 就會報錯

訊息機制


我們寫 OC 程式碼,它在執行的時候也是轉換成了 runtime 方式執行的。任何方法呼叫本質:就是傳送一個訊息(用 runtime傳送訊息,OC 底層實現通過 runtime 實現),每一個 OC 的方法,底層必然有一個與之對應的 runtime 方法。

iOS 模式詳解—「runtime&runloop 面試、工作」看我就 ? 了 ^_^.

驗證示例:方法呼叫,是否真的是轉換為訊息機制?

訊息機制原理:物件根據方法編號SEL去對映表查詢對應的方法實現。 註解: 1.必須要匯入標頭檔案 #import <objc/message.h> 2.我們匯入系統的標頭檔案,一般用尖括號。 3.OC 解決訊息機制方法提示步驟【查詢build setting -> 搜尋msg -> objc_msgSend(YES --> NO)】 4.最終生成訊息機制,編譯器做的事情,最終程式碼,需要把當前程式碼用xcode重新編譯,【clang -rewrite-objc main.m 檢視最終生成程式碼】,示例:cd main.m --> 輸入前面指令,就會生成 .opp檔案(C++程式碼) 5.這裡一般不會直接匯入<objc/runtime.h>

iOS 模式詳解—「runtime&runloop 面試、工作」看我就 ? 了 ^_^.

示例程式碼:OC 方法 <--> runtime 方法

說明:
eat(無參) 和 run(有參NSInteger) 是 LNPerson模型類中的私有方法「runtime 作用:可以呼叫私有方法」
示例分別以 OC寫法 和 最底層寫法 對照驗證.
- (void)msgSend
{
    // 方法一:
    //id objc = [NSObject alloc];
    LNPerson *person = objc_msgSend(objc_getClass("LNPerson"), sel_registerName("alloc"));
    
    //objc = [objc init];
    person = objc_msgSend(person, sel_registerName("init"));
    
    // 呼叫
    //[objc eat];
    //[objc run:10];
    objc_msgSend(person,@selector(eat)); // 無參
    objc_msgSend(person,@selector(run:),10); // 有殘
}
/
 註解:
    // 用最底層寫
    objc_getClass(const char *name) 獲取當前類
    sel_registerName(const char *str) 註冊個方法編號
    objc_msgSend(id self:誰傳送訊息, SEL op:傳送什麼訊息, ...)
    讓LNPerson這個類物件傳送了一個alloc訊息,返回一個分配好的記憶體物件給你,再傳送一個訊息初始化.
 */

// 方法二:
#pragma mark - 也許下面這種好理解一點
- (void)test
{
    // id objc = [NSObject alloc];
    id objc = objc_msgSend([NSObject class], @selector(alloc));
    
    // objc = [objc init];
    objc = objc_msgSend(objc, @selector(eat));
    
}
複製程式碼
objc_msgSend 引數概念
/
 objc_msgSend(<#id  _Nullable self#>, <#SEL  _Nonnull op, ...#>)

 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`型別的方法選擇器。
 
 3id
    `objc_msgSend`第一個引數型別為`id`,大家對它都不陌生,它是一個指向類例項的指標:
    `typedef struct objc_object *id;`
    那`objc_object`又是啥呢:
    `struct objc_object { Class isa; };`
    `objc_object`結構體包含一個`isa`指標,根據`isa`指標就可以順藤摸瓜找到物件所屬的類。
 */
複製程式碼

訊息機制「方法呼叫流程」


面試:訊息機制方法呼叫流程❓ 怎麼去呼叫eat方法, 物件方法:(儲存到類物件的方法列表) ,類方法:(儲存到元類(Meta Class)中方法列表)。

1.OC 在向一個物件傳送訊息時,runtime 庫會根據物件的 isa指標找到該物件對應的類或其父類中查詢方法。。 2.註冊方法編號(這裡用方法編號的好處,可以快速查詢)。 3.根據方法編號去查詢對應方法。 4.找到只是最終函式實現地址,根據地址去方法區呼叫對應函式。

補充:一個objc 物件的 isa 的指標指向什麼?有什麼作用? 每一個物件內部都有一個isa指標,這個指標是指向它的真實型別,根據這個指標就能知道將來呼叫哪個類的方法。

isa指標相關釋義

上面也提到OC底層都是轉化為runtime方式來實現的,類和類的例項(物件)都相對於的isa指標。 我們可以在Xcode中使用 [Shift+Cmd+O ] 快速開啟檔案objc.h 能看到類的定義:

objc.h
isa:是一個Class 型別的指標
runtime 物件,類,元類的isa指標關係圖.png
總結:runtime 物件,類,元類的isa指標關係圖 1、每一個物件本質上都是一個類的例項。其中類定義了成員變數和成員方法的列表。物件通過物件的isa指標指向所屬類。 2、每一個類本質上都是一個物件,類其實是元類(meteClass)的例項。元類定義了類方法的列表。類通過類的isa指標指向元類。 3、元類儲存了類方法的列表。當類方法被呼叫時,先會從本身查詢類方法的實現,如果沒有,元類會向他父類查詢該方法。同時注意的是:元類(meteClass)也是類,它也是物件。元類通過isa指標最終指向的是一個根元類(root meteClass)。 4、根元類的isa指標指向本身,這樣形成了一個封閉的內迴圈

常見作用


/
 1.動態交換兩個方法的實現
 2.動態新增屬性
 3.實現字典轉模型的自動轉換
 4.動態新增方法
 5.攔截並替換方法
 6.實現 NSCoding 的自動歸檔和解檔

補充常用runtime示例:Demo中有體現
    1.新增屬性和交換方法示例:UITextField佔位文字顏色placeholderColor
    2.交換方法示例:交換dealloc方法實現,新增功能那個控制器被銷燬了
 */
複製程式碼

開發場景「工作掌握」


runtime 交換方法

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

需求:載入一張圖片直接用[UIImage imageNamed:@"image"];是無法知道到底有沒有載入成功。給系統的imageNamed新增額外功能(是否載入圖片成功)。 方案一:繼承系統的類,重寫方法.(弊端:每次使用都需要匯入) 方案二:使用 runtime,交換方法.

步驟: 1.給系統的方法新增分類 2.自己實現一個帶有擴充套件功能的方法 3.交換方法,只需要交換一次,

場景程式碼:方法+呼叫+列印輸出

#import "UIImage+Image.h"
#import <objc/message.h>

@implementation UIImage (Image)

/
 看清楚下面是不會有死迴圈的
    呼叫 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 {
 
 }
 */

/
 作用:把類載入進記憶體的時候呼叫,只會呼叫一次
 呼叫:方法應先交換,再去呼叫
 */
+ (void)load {
    
    // 1.獲取 imageNamed方法地址
    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);
}

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

- - -
// 列印輸出
2016-03-17 17:52:14.693 runtime[12761:543574] runtime互動方法 -> 圖片載入成功
複製程式碼

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

給系統分類動態新增屬性

場景:給系統的類新增額外屬性的時候,可以使用runtime動態新增屬性方法。 原理:給一個類宣告屬性,其實本質就是給這個類新增關聯,並不是直接把這個值的記憶體空間新增到類存空間。 註解:給系統 NSObject 新增一個分類,我們知道在分類中是不能夠新增成員屬性的,雖然我們用了@property,但是僅僅會自動生成getset方法的宣告,並沒有帶下劃線的屬性和方法實現生成。但是我們可以通過runtime就可以做到給它方法的實現。

需求:給系統 NSObject 類動態新增屬性 name 字串。

場景程式碼:方法+呼叫+列印

#import <Foundation/Foundation.h>
@interface NSObject (Property)

@property NSString *name;
@end

- - -
#import "NSObject+Property.h"
#import <objc/message.h>
//#import <objc/runtime.h>

@implementation NSObject (Property)

- (NSString *)name
{
    // 利用引數key 將物件object中儲存的對應值取出來
    return objc_getAssociatedObject(self, @"name");
}

- (void)setName:(NSString *)name
{
    /**
     將某個值跟某個物件關聯起來,將某個值儲存到某個物件中
     objc_setAssociatedObject(<#id  _Nonnull object#>:給哪個物件新增屬性, <#const void * _Nonnull key#>:屬性名稱, <#id  _Nullable value#>:屬性值, <#objc_AssociationPolicy policy#>:儲存策略)
     */
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    NSLog(@"name---->%p",name);
}
@end

// 呼叫
NSObject *objc = [[NSObject alloc] init];
objc.name = @"CoderLN";
NSLog(@"runtime動態新增屬性name==%@",objc.name);

// 列印輸出
2016-03-17 19:37:10.530 runtime[12761:543574] runtime動態新增屬性name == CoderLN
複製程式碼

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

字典轉模型

字典轉模型的方式

  • 給模型中屬性,在 .m 依次賦值(初學者)。
  • 字典轉模型 KVC 實現
    • KVC 字典轉模型弊端:必須保證,模型中的屬性和字典中的key 一一對應。
    • 如果不一致,就會呼叫[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]key找不到的錯。
    • 分析:模型中的屬性和字典的key不一一對應,系統就會呼叫setValue:forUndefinedKey:報錯。
    • 解決:重寫物件的setValue:forUndefinedKey:,把系統的方法覆蓋,就能繼續使用KVC,字典轉模型了。
  • 字典轉模型 Runtime 實現
    • 思路:利用執行時,遍歷模型中所有屬性,根據模型的屬性名,去字典中查詢key,取出對應的值,給模型的屬性賦值(從提醒:字典中取值,不一定要全部取出來);提供一個NSObject分類,專門字典轉模型,以後所有模型都可以通過這個分類實現字典轉模型。

    • 考慮情況: 1.當字典的key和模型的屬性匹配不上。 2.模型中巢狀模型(模型屬性是另外一個模型物件)。 3.陣列中裝著模型(模型的屬性是一個陣列,陣列中是一個個模型物件)。

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

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

示例:runtime 字典轉模型考慮三種情況

Runtime 字典轉模型

1、runtime 字典轉模型-->字典的 key 和模型的屬性不匹配「模型屬性數量大於字典鍵值對數」,這種情況處理如下:
#import "NSObject+Model.h"
#import <objc/message.h>

@implementation NSObject (Model)

// 思路:利用runtime 遍歷模型中所有屬性,根據模型中屬性,去字典中取出對應的value給模型屬性賦值
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 1.建立對應的物件
    id objc = [[self alloc] init];
    
    // 2.利用runtime給物件中的屬性賦值
    /**
      獲取類中的所有成員變數
        class_copyIvarList(Class _Nullable cls:表示獲取哪個類中的成員變數, unsigned int * _Nullable outCount:表示這個類有多少成員變數,傳入一個Int變數地址,會自動給這個變數賦值)
      返回值Ivar * =
        指的是一個ivar陣列,會把所有成員屬性放在一個陣列中,通過返回的陣列就能全部獲取到
     */
    // 成員變數個數
    unsigned int count = 0;
    // 獲取類中的所有成員變數
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍歷所有成員變數
    for (int i = 0; i < count; i++) {
        // 根據角標,從陣列取出對應的成員變數(Ivar:成員變數,以下劃線開頭)
        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(去掉 _ ,從第一個角標開始擷取) 得到屬性名。

原因

{
    int _a; // 成員變數
}
@property (nonatomic, assign) NSInteger attitudes_count; // 屬性

`Ivar:成員變數,以下劃線開頭`,
`Property 屬性`
`class_copyPropertyList` 獲取類裡面屬性 
`class_copyIvarList` 獲取類中的所有成員變數 
這裡有成員變數,就不會漏掉屬性;如果有屬性,可能會漏掉成員變數;
使用`runtime`字典轉模型獲取模型屬性名的時候,最好獲取成員屬性名`Ivar`因為可能會有個屬性是沒有`setter`和`getter`方法的。
複製程式碼
2、runtime 字典轉模型-->模型中巢狀模型「模型屬性是另外一個模型物件」,這種情況處理如下:
// 思路:利用runtime 遍歷模型中所有屬性,根據模型中屬性,去字典中取出對應的value給模型屬性賦值
+ (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 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 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;
}
複製程式碼

iOS 模式詳解—「runtime&runloop 面試、工作」看我就 ? 了 ^_^.

總結: 我們既然能獲取到屬性型別,那就可以攔截到模型的那個陣列屬性,進而對陣列中每個模型遍歷並字典轉模型,但是我們不知道陣列中的模型都是什麼型別,我們可以宣告一個方法,該方法目的不是讓其呼叫,而是讓其實現並返回模型的型別。

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

其它作用「面試熟悉」


動態新增方法

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

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

需求:runtime 動態新增方法處理呼叫一個未實現的方法 和 去除報錯。

場景程式碼:方法+呼叫+列印輸出

#import "Person.h"
#import <objc/message.h>

@implementation Person

/**
 呼叫:只要一個物件呼叫了一個未實現的方法就會呼叫這個方法,進行處理
 作用:動態新增方法,處理未實現
 註解:任何方法預設都有兩個隱式引數,self,_cmd(當前方法的方法編號)
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == NSSelectorFromString(@"roll:")) {
        /**
         class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>:給哪個類新增方法, <#SEL  _Nonnull name#>:新增哪個方法,即新增方法的方法編號, <#IMP  _Nonnull imp#>:方法實現 => 函式 => 函式入口 => 函式名(新增方法的函式實現(函式地址)), <#const char * _Nullable types#>:方法型別,(返回值+引數型別) v:void @:物件->self :表示SEL->_cmd)
         */
        // 給類新增roll:滾了多遠方法
        class_addMethod(self, sel, (IMP)LNRoll, "v@:@");
        
        return YES;
    }
    
    if ([NSStringFromSelector(sel) isEqualToString:@"go:"]) {
        // 給類新增go:走了多遠方法
        class_addMethod(self, sel, (IMP)LNGO, "v@:@");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

// 呼叫
Person *p = [[Person alloc] init];
// 執行某個方法
[p performSelector:@selector(roll:) withObject:@"11"];
[p performSelector:@selector(go:) withObject:@10];

// 列印輸出
2016-03-17 19:05:03.917 runtime[12761:543574] 我滾了 11 米遠的屎蛋
2016-03-17 19:05:04.617 runtime[12761:543574] 我走了 10 公里才到的家 
複製程式碼

實現NSCoding的自動歸檔和解檔

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

假設現在有一個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
複製程式碼

優化: 上面是encodeWithCoderinitWithCoder這兩個方法抽成巨集。我們可以把這兩個巨集單獨放到一個檔案裡面,這裡以後需要進行資料持久化的模型都可以直接使用這兩個巨集。

runtime 下Class的各項操作

1.runtime 部分函式

#warning - 以下為功能模組相關的方法示例, 具體方法作用、使用、註解請移步 -> github.com/CoderLN
以下的這些方法應該算是`runtime`在實際場景中所應用的大部分的情況了,平常的編碼中差不多足夠用了。

 0、class_copyPropertyList 獲取類中所有的屬性
        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]);
        }

 0、class_copyMethodList 獲取類的所有方法
        Method *methodList = class_copyMethodList([self class], &count);
        for (unsigned int i; i<count; i++) {
            Method method = methodList[i];
            NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
        }

 0、class_copyIvarList 獲取類中所有的成員變數(outCount 會返回成員變數的總數)
        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]);
        }
 
 0、class_copyProtocolList 獲取協議列表
    __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]);
    }
 
 0、object_getClass 獲得類方法
        Class PersonClass = object_getClass([Person class]);
        SEL oriSEL = @selector(test1);
        Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
 
 0、class_getInstanceMethod 獲得例項方法
        Class PersonClass = object_getClass([xiaoming class]);
        SEL oriSEL = @selector(test2);
        Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
 
 0、class_addMethod 動態新增方法
        BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
 
 0、class_replaceMethod 替換原方法實現
        class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
 
 0、method_exchangeImplementations 交換兩個方法的實現
        method_exchangeImplementations(method1, method2);

 0、根據名字得到類變數的Ivar指標,但是這個在OC中好像毫無意義
    Ivar oneCVIvar = class_getClassVariable([Person class], name);

 0、根據名字得到例項變數的Ivar指標
    Ivar oneIVIvar = class_getInstanceVariable([Person class], name);

 0、找到後可以直接對私有成員變數賦值(強制修改name屬性)
    object_setIvar(_per, oneIVIvar, @"age");


 0、動態新增方法
    class_addMethod([person class]:Class cls 型別, @selector(eat):待呼叫的方法名稱, (IMP)myAddingFunction:(IMP)myAddingFunction,IMP是一個函式指標,這裡表示指定具體實現方法myAddingFunction, 00代表沒有引數);

 0、獲得某個類的類方法
    Method class_getClassMethod(Class cls , SEL name)

 0、獲得成員變數的名字
    const char *ivar_getName(Ivar v);

 0、將某個值跟某個物件關聯起來,將某個值儲存到某個物件中
    void objc_setAssociatedObject(id object:表示關聯者,是一個物件,變數名理所當然也是object , const void *key:獲取被關聯者的索引key ,id value :被關聯者 ,objc_AssociationPolicy policy:關聯時採用的協議,有assignretaincopy等協議,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC)

 0、利用引數key 將物件object中儲存的對應值取出來
    id objc_getAssociatedObject(id object , const void *key)
 */
複製程式碼

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開發例項彙總

一道面試題的註解


下面的程式碼輸出什麼?

@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中對 selfsuper 的理解:

  • 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在實際開發中的應用 http://www.jianshu.com/p/851b21870d91

Runtime & Runloop 常面問題整理(附答案)

同一個面試問題並非只有一個答案,而同一個答案並不是在任何面試場合都有效,關鍵在於應聘者掌握了規律後,對面試的具體情況進行把握,有意識地揣摩面試官提出問題的心理 (真實問答),要 get 的到問的點,然後答其所問,算是“ 投其所好 ”吧。 摘錄: http://www.jianshu.com/p/56e40ea56813 http://www.jianshu.com/p/f9eb6b315c08

Runtime
01 / objc在向一個物件傳送訊息時,發生了什麼?
參考1:根據物件的 isa 指標找到類物件 id,在查詢類物件裡面的 methodLists 方法函式列表,如果沒有在好到,在沿著 superClass ,尋找父類,再在父類 methodLists 方法列表裡面查詢,最終找到 SEL ,根據 id 和 SEL 確認 IMP(指標函式),在傳送訊息;
02 / 問題:什麼時候會報unrecognized selector錯誤?iOS有哪些機制來避免走到這一步?
參考1:當傳送訊息的時候,我們會根據類裡面的 methodLists 列表去查詢我們要動用的SEL,當查詢不到的時候,我們會一直沿著父類查詢,當最終查詢不到的時候我們會報 unrecognized selector 錯誤,當系統查詢不到方法的時候,會呼叫 +(BOOL)resolveInstanceMethod:(SEL)sel 動態解釋的方法來給我一次機會來新增,呼叫不到的方法。或者我們可以再次使用 -(id)forwardingTargetForSelector:(SEL)aSelector 重定向的方法來告訴系統,該呼叫什麼方法,一來保證不會崩潰。
03 / 問題:能否向編譯後得到的類中增加例項變數?能否向執行時建立的類中新增例項變數?為什麼?
參考1:1、不能向編譯後得到的類增加例項變數 2、能向執行時建立的類中新增例項變數。
分析:1. 編譯後的類已經註冊在 runtime 中,類結構體中的 objc_ivar_list 例項變數的連結串列和 instance_size 例項變數的記憶體大小已經確定,runtime會呼叫 class_setvarlayout 或 class_setWeaklvarLayout 來處理strong weak 引用.所以不能向存在的類中新增例項變數。2. 執行時建立的類是可以新增例項變數,呼叫class_addIvar函式. 但是的在呼叫 objc_allocateClassPair 之後,objc_registerClassPair 之前,原因同上.
04 / 問題:runtime如何實現weak變數的自動置nil?
參考1:runtime 對註冊的類, 會進行佈局,對於 weak 物件會放入一個 hash 表中。 用 weak 指向的物件記憶體地址作為 key,當此物件的引用計數為0的時候會 dealloc,假如 weak 指向的物件記憶體地址是a,那麼就會以a為鍵, 在這個 weak 表中搜尋,找到所有以a為鍵的 weak 物件,從而設定為 nil。
05 / 問題:給類新增一個屬性後,在類結構體裡哪些元素會發生變化?
參考1:instance_size :例項的記憶體大小;objc_ivar_list *ivars:屬性列表
RunLoop
01 / 問題:runloop是來做什麼的?runloop和執行緒有什麼關係?主執行緒預設開啟了runloop麼?子執行緒呢?
參考1:runloop: 從字面意思看:執行迴圈、跑圈,其實它內部就是do-while迴圈,在這個迴圈內部不斷地處理各種任務(比如Source、Timer、Observer)事件。runloop和執行緒的關係:一個執行緒對應一個RunLoop,主執行緒的RunLoop預設建立並啟動,子執行緒的RunLoop需手動建立且手動啟動(呼叫run方法)。RunLoop只能選擇一個Mode啟動,如果當前Mode中沒有任何Source(Sources0、Sources1)、Timer,那麼就直接退出RunLoop。
02 / 問題:runloop的mode是用來做什麼的?有幾種mode?
參考1:model:是runloop裡面的執行模式,不同的模式下的runloop處理的事件和訊息有一定的差別。系統預設註冊了5個Mode:(1)kCFRunLoopDefaultMode: App的預設 Mode,通常主執行緒是在這個 Mode 下執行的。(2)UITrackingRunLoopMode: 介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響。(3)UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用。(4)GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到。(5)kCFRunLoopCommonModes: 這是一個佔位的 Mode,沒有實際作用。注意iOS 對以上5中model進行了封裝 NSDefaultRunLoopMode、NSRunLoopCommonModes
03 / 問題:為什麼把NSTimer物件以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)新增到主執行迴圈以後,滑動scrollview的時候NSTimer卻不動了?
參考1:nstime物件是在 NSDefaultRunLoopMode下面呼叫訊息的,但是當我們滑動scrollview的時候,NSDefaultRunLoopMode模式就自動切換到UITrackingRunLoopMode模式下面,卻不可以繼續響應nstime傳送的訊息。所以如果想在滑動scrollview的情況下面還呼叫nstime的訊息,我們可以把nsrunloop的模式更改為NSRunLoopCommonModes.
04 / 問題:蘋果是如何實現Autorelease Pool的?
參考1:Autorelease Pool作用:快取池,可以避免我們經常寫relase的一種方式。其實就是延遲release,將建立的物件,新增到最近的autoreleasePool中,等到autoreleasePool作用域結束的時候,會將裡面所有的物件的引用計數器 - autorelease.

附上寫的小樣 Demo,重要的部分程式碼中都有相應的註解和文字列印,執行程式可以很直觀的表現

相關文章