目錄:
- runtime 概念
- runtime 消息機制
- runtime 方法調用流程「消息機制」
- runtime 運行時常見作用
- runtime 常用開發應用場景「工作掌握」
1.runtime 交換方法
2.runtime 給分類動態添加屬性
3.runtime 字典轉模型(Runtime 考慮三種情況實現)- runtime 運行時其它作用「面試熟悉」
1.動態添加方法
2.動態變量控制
3.實現NSCoding的自動歸檔和解檔
4.runtime 下Class的各項操作
5.runtime 幾個參數概念- 什麼是 method swizzling(俗稱黑魔法)
- 最後一道面試題的註解
- 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
方法。
簡單示例:
驗證:方法調用,是否真的是轉換爲消息機制?
-
必須要導入頭文件
#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>
-
-
示例代碼: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.找到只是最終函數實現地址,根據地址去方法區調用對應函數。
- 1.OC 在向一個對象發送消息時,
- 補充:一個
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
,但是僅僅會自動生成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);
// 打印輸出
2016-02-17 19:37:10.530 runtime[12761:543574] runtime動態添加屬性--name == 123
總結:其實,給屬性賦值的本質,就是讓屬性與一個對象產生關聯,所以要給NSObject
的分類的name
屬性賦值就是讓name
和NSObject
產生關聯,而runtime
可以做到這一點。
runtime 字典轉模型
字典轉模型的方式:
-
一個一個的給模型屬性賦值(初學者)。
-
字典轉模型KVC實現
- KVC 字典轉模型弊端:必須保證,模型中的屬性和字典中的
key
一一對應。 - 如果不一致,就會調用
[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]
報key
找不到的錯。 - 分析:模型中的屬性和字典的
key
不一一對應,系統就會調用setValue:forUndefinedKey:
報錯。 - 解決:重寫對象的
setValue:forUndefinedKey:
,把系統的方法覆蓋,就能繼續使用KVC,字典轉模型了。
- KVC 字典轉模型弊端:必須保證,模型中的屬性和字典中的
-
字典轉模型 Runtime 實現
-
思路:利用運行時,遍歷模型中所有屬性,根據模型的屬性名,去字典中查找
key
,取出對應的值,給模型的屬性賦值(從提醒:字典中取值,不一定要全部取出來)。 -
考慮情況:
- 1.當字典的
key
和模型的屬性匹配不上。 - 2.模型中嵌套模型(模型屬性是另外一個模型對象)。
- 3.數組中裝着模型(模型的屬性是一個數組,數組中是一個個模型對象)。
- 1.當字典的
-
註解:根據上面的三種特殊情況,先是字典的
key
和模型的屬性不對應的情況。不對應有兩種,一種是字典的鍵值大於模型屬性數量,這時候我們不需要任何處理,因爲runtime
是先遍歷模型所有屬性,再去字典中根據屬性名找對應值進行賦值,多餘的鍵值對也當然不會去看了;另外一種是模型屬性數量大於字典的鍵值對,這時候由於屬性沒有對應值會被賦值爲nil
,就會導致crash
,我們只需加一個判斷即可。考慮三種情況下面一一註解; -
步驟:提供一個
NSObject
分類,專門字典轉模型,以後所有模型都可以通過這個分類實現字典轉模型。
-
-
MJExtension 字典轉模型實現
- 底層也是對
runtime
的封裝,纔可以把一個模型中所有屬性遍歷出來。(你之所以看不懂,是 MJ 封裝了很多層而已^_^.)。
- 底層也是對
這裏針對字典轉模型 KVC 實現,就不做詳解了,如果你 對 KVC 詳解使用或是實現原理 不是很清楚的,可以參考 實用「KVC編碼 & KVO監聽
字典轉模型 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;
}
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;
}
總結:我們既然能獲取到屬性類型,那就可以攔截到模型的那個數組屬性,進而對數組中每個模型遍歷並字典轉模型,但是我們不知道數組中的模型都是什麼類型,我們可以聲明一個方法,該方法目的不是讓其調用,而是讓其實現並返回模型的類型。
這裏提到的你如果不是很清楚,建議參考我的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
。
- 交換方法的幾種實現方式
- 利用
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 |