iOS Runtime總結 – iPhone手機開發 iPhone軟體開發教學課程

iOS 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

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

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

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

註解4:這裡一般不會直接導入

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

@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 imageNamed:],因為方法交換瞭,這樣會死循環 UIImage *image = [UIImage ln_imageNamed:name];

if (image) {

NSLog(@"runtime添加額外功能–加載成功");

} else {

NSLog(@"runtime添加額外功能–加載失敗");

}

return image;

}

/**

不能在分類中重寫系統方法imageNamed,因為會把系統的功能給覆蓋掉,而且分類中不能調用super

所以第二步,我們要 自己實現一個帶有擴展功能的方法.

+ (UIImage *)imageNamed:(NSString *)name {

}

*/

@end

// 打印輸出

2017-02-17 17:52:14.693 runtime[12761:543574] runtime添加額外功能–加載成功

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

runtime 給分類動態添加屬性

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

應用場景:給系統的類添加屬性的時候,可以使用runtime動態添加屬性方法。

註解:系統NSObject添加一個分類,我們知道在分類中是不能夠添加成員屬性的,雖然我們用瞭@property,但是僅僅會自動生成get和set方法的聲明,並沒有帶下劃線的屬性和方法實現生成。但是我們可以通過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);

// 打印輸出

2017-02-17 19:37:10.530 runtime[12761:543574] runtime動態添加屬性–name == 123

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

runtime 字典轉模型

字典轉模型的方式:

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

字典轉模型KVC實現

KVC 字典轉模型弊端:必須保證,模型中的屬性和字典中的key一一對應。

如果不一致,就會調用[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因為可能會有個屬性是沒有setter和“getter方法的。

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;

}

發佈留言