引導
對于從事 iOS 開發人員來說,所有的人都會答出「 Runtime 是運行時 」,什么情況下用 Runtime ?,大部分人能說出「 給分類動態添加屬性 || 交換方法 」,再問一句「 Runtime 消息機制的調用流程 || 能體現 Runtime 強大之處的應用場景 」,到這,能知道答案的寥寥無幾,很少有人會說到 “黑魔法” 這三個字。
Runtime 是 iOS 編程中比較難的模塊,想要深入學習 OC,那 Runtime 是你必須要熟練掌握的東西,下面是我對 Runtime 的整理,從零開始,由淺入深,并且帶了幾個 Runtime 實際開發應用場景 --> 大神可選擇性路過「思想」。
在「時間 & 知識 」有限內,總結的文章難免有「未全、不足 」的地方,還望各位好友指出,可留言指正或是補充,以提高文章質量@白開水ln原著;
目錄:
runtime 概念
runtime 消息機制
runtime 方法調用流程「消息機制」
runtime 運行時常見作用
runtime 常用開發應用場景「工作掌握」
1.runtime 交換方法
2.UITextField占位文字顏色(工具類)
3.runtime 給分類動態添加屬性
4.runtime 字典轉模型(Runtime 考慮三種情況實現)
runtime 運行時其它作用「面試熟悉」
1.動態添加方法
2.動態變量控制
3.實現NSCoding的自動歸檔和解檔
4.runtime 下Class的各項操作
5.runtime 幾個參數概念
什么是 method swizzling(俗稱黑魔法)
最后一道面試題的注解
runtime 模塊博文推薦(??數量較多)
runtime & runloop 面試最常問到的題整理【建議看】
Demo 重要的部分代碼中都有相應的注解和文字打印,運行程序可以很直觀的表現。
SourceCode、ToolsClass、WechatPublic-Codeidea
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([Personclass],@selector(run:),20);
//--------------------------- <#我是分割線#> ------------------------------//
// 也許下面這種好理解一點// id objc = [NSObject alloc];
idobjc = objc_msgSend([NSObjectclass],@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 {
[superviewDidLoad];
// 方案一:先搞個分類,定義一個能加載圖片并且能打印的方法
+ (instancetype)imageWithName:(NSString *)name;
// 方案二:交換 imageNamed 和 ln_imageNamed 的實現,就能調用 imageNamed,間接調用 ln_imageNamed 的實現。
UIImage*image = [UIImageimageNamed:@"123"];}#import@implementationUIImage(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 = [UIImageln_imageNamed:name];
if(image) {
NSLog(@"runtime添加額外功能--加載成功");
}else{
NSLog(@"runtime添加額外功能--加載失敗");
}
return image;
}/**
不能在分類中重寫系統方法imageNamed,因為會把系統的功能給覆蓋掉,而且分類中不能調用super
所以第二步,我們要 自己實現一個帶有擴展功能的方法.
+ (UIImage *)imageNamed:(NSString *)name {
}
*/@end
// 打印輸出2016-02-1717:52:14.693runtime[12761:543574] runtime添加額外功能--加載成功
總結:我們所做的就是在方法調用流程第三步的時候,交換兩個方法地址指向。而且我們改變指向要在系統的imageNamed:方法調用前,所以將代碼寫在了分類的load方法里。最后當運行的時候系統的方法就會去找我們的方法的實現。
runtime 給分類動態添加屬性
原理:給一個類聲明屬性,其實本質就是給這個類添加關聯,并不是直接把這個值的內存空間添加到類存空間。
應用場景:給系統的類添加屬性的時候,可以使用runtime動態添加屬性方法。
注解:系統NSObject添加一個分類,我們知道在分類中是不能夠添加成員屬性的,雖然我們用了@property,但是僅僅會自動生成get和set方法的聲明,并沒有帶下劃線的屬性和方法實現生成。但是我們可以通過runtime就可以做到給它方法的實現。
需求:給系統 NSObject 類動態添加屬性name字符串。
案例代碼:方法+調用+打印
@interfaceNSObject(Property)
// @property分類:只會生成get,set方法聲明,不會生成實現,也不會生成下劃線成員屬性
@propertyNSString*name;
@propertyNSString*height;
@end
@implementationNSObject(Property)
- (void)setName:(NSString*)name
{// objc_setAssociatedObject(將某個值跟某個對象關聯起來,將某個值存儲到某個對象中)
// object:給哪個對象添加屬性// key:屬性名稱// value:屬性值// policy:保存策略objc_setAssociatedObject(self,@"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}
- (NSString*)name {returnobjc_getAssociatedObject(self,@"name");}
// 調用NSObject*objc = [[NSObjectalloc] init];objc.name =@"123";
NSLog(@"runtime動態添加屬性name==%@",objc.name);
// 打印輸出2016-02-1719:37:10.530runtime[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 = [[selfalloc] init];
// 2.利用runtime給對象中的屬性賦值
/**
class_copyIvarList: 獲取類中的所有成員變量
Ivar:成員變量
第一個參數:表示獲取哪個類中的成員變量
第二個參數:表示這個類有多少成員變量,傳入一個Int變量地址,會自動給這個變量賦值
返回值Ivar *:指的是一個ivar數組,會把所有成員屬性放在一個數組中,通過返回的數組就能全部獲取到。
count: 成員變量個數
*/
unsignedintcount =0;
// 獲取類中的所有成員變量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍歷所有成員變量
for(inti =0; i < count; i++) {
// 根據角標,從數組取出對應的成員變量
Ivar ivar = ivarList[i];
// 獲取成員變量名字
NSString*ivarName = [NSStringstringWithUTF8String:ivar_getName(ivar)];
// 處理成員變量名->字典中的key(去掉 _ ,從第一個角標開始截取)
NSString*key = [ivarName substringFromIndex:1];
// 根據成員屬性名去字典中查找對應的
valueidvalue = 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)NSIntegerattitudes_count;// 屬性這里有成員變量,就不會漏掉屬性;如果有屬性,可能會漏掉成員變量;
使用runtime字典轉模型獲取模型屬性名的時候,最好獲取成員屬性名Ivar因為可能會有個屬性是沒有setter和getter方法的。
2、runtime 字典轉模型-->模型中嵌套模型「模型屬性是另外一個模型對象」,這種情況處理如下:
+ (instancetype)modelWithDict2:(NSDictionary*)dict{
// 1.創建對應的對象
id objc = [[selfalloc] init];
// 2.利用runtime給對象中的屬性賦值
unsignedintcount =0;
// 獲取類中的所有成員變量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍歷所有成員變量
for(inti =0; i < count; i++) {
// 根據角標,從數組取出對應的成員變量
Ivar ivar = ivarList[i];
// 獲取成員變量名字
NSString*ivarName = [NSStringstringWithUTF8String:ivar_getName(ivar)];
// 獲取成員變量類型
NSString*ivarType = [NSStringstringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 替換: @\"User\" ->
User ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\""withString:@""]; ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@"withString:@""];
// 處理成員屬性名->字典中的key(去掉 _ ,從第一個角標開始截取)
NSString*key = [ivarName substringFromIndex:1];
// 根據成員屬性名去字典中查找對應的
value idvalue = dict[key];
//--------------------------- <#我是分割線#> ------------------------------//////
二級轉換:如果字典中還有字典,也需要把對應的字典轉換成模型
// 判斷下value是否是字典,并且是自定義對象才需要轉換
if([value isKindOfClass:[NSDictionaryclass]] && ![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*)dic
t{
// 1.創建對應的對象
id objc = [[selfalloc] init];
// 2.利用runtime給對象中的屬性賦值
unsignedintcount =0;
// 獲取類中的所有成員變量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍歷所有成員變量
for(inti =0; i < count; i++)
{
// 根據角標,從數組取出對應的成員變量
Ivar ivar = ivarList[i];
// 獲取成員變量名字
NSString*ivarName = [NSStringstringWithUTF8String:ivar_getName(ivar)];
// 處理成員屬性名->字典中的key(去掉 _ ,從第一個角標開始截取)
NSString*key = [ivarName substringFromIndex:1];
// 根據成員屬性名去字典中查找對應的
valueidvalue = dict[key];
//--------------------------- <#我是分割線#> ------------------------------////
// 三級轉換:NSArray中也是字典,把數組中的字典轉換成模型.
// 判斷值是否是數組
if([value isKindOfClass:[NSArrayclass]])
{
// 判斷對應類有沒有實現字典數組轉模型數組的協議
// arrayContainModelClass 提供一個協議,只要遵守這個協議的類,都能把數組中的字典轉模型
if([selfrespondsToSelector:@selector(arrayContainModelClass)])
{
// 轉換成id類型,就能調用任何對象的方法
id idSelf =self;
// 獲取數組中字典對應的模型
NSString*type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel =NSClassFromString(type);
NSMutableArray*arrM = [NSMutableArrayarray];
// 遍歷字典數組,生成模型數組
for(NSDictionary*dictinvalue)
{
// 字典轉模型
id model = [classModel modelWithDict3:dict];
[arrM addObject:model];
}
// 把模型數組賦值給
valuevalue = arrM;
}
}
// 如果模型屬性數量大于字典鍵值對數理,模型屬性會被賦值為nil,而報錯
if(value) {
// 給模型中屬性賦值
[objc setValue:value forKey:key];
}
}return objc;}

runtime字典轉模型-->數組中裝著模型 打印輸出
總結:我們既然能獲取到屬性類型,那就可以攔截到模型的那個數組屬性,進而對數組中每個模型遍歷并字典轉模型,但是我們不知道數組中的模型都是什么類型,我們可以聲明一個方法,該方法目的不是讓其調用,而是讓其實現并返回模型的類型。
這里提到的你如果不是很清楚,建議參考我的Demo,重要的部分代碼中都有相應的注解和文字打印,運行程序可以很直觀的表現。
runtime 其它作用「面試熟悉」
動態添加方法
應用場景:如果一個類方法非常多,加載類到內存的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動態給某個類,添加方法解決。
注解:OC 中我們很習慣的會用懶加載,當用到的時候才去加載它,但是實際上只要一個類實現了某個方法,就會被加載進內存。當我們不想加載這么多方法的時候,就會使用到runtime動態的添加方法。
需求:runtime 動態添加方法處理調用一個未實現的方法 和 去除報錯。
案例代碼:方法+調用+打印輸出
- (void)viewDidLoad {
[superviewDidLoad];
Person *p = [[Person alloc] init];
// 默認person,沒有實現run:方法,可以通過performSelector調用,但是會報錯。
// 動態添加方法就不會報錯
[p performSelector:@selector(run:) withObject:@10];}
@implementationPerson
// 沒有返回值,1個參數
// void,(id,SEL)
void aaa(idself, 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@:@");
returnYES;
}return
[super resolveInstanceMethod:sel];
}@end
// 打印輸出2016-02-1719:05:03.917runtime[12761:543574] runtime動態添加方法--跑了10米
動態變量控制
現在有一個Person類,創建 xiaoming對象
動態獲取 XiaoMing 類中的所有屬性 [當然包括私有]
Ivar *ivar = class_copyIvarList([self.xiaomingclass], &count);
遍歷屬性找到對應name字段
constchar*varName = ivar_getName(var);
修改對應的字段值成20
object_setIvar(self.xiaoMing,var, @"20");
代碼參考
-(void)answer{unsignedintcount =0; Ivar *ivar = class_copyIvarList([self.xiaoMingclass], &count);for(inti =0; i
實現NSCoding的自動歸檔和解檔
如果你實現過自定義模型數據持久化的過程,那么你也肯定明白,如果一個模型有許多個屬性,那么我們需要對每個屬性都實現一遍encodeObject和decodeObjectForKey方法,如果這樣的模型又有很多個,這還真的是一個十分麻煩的事情。下面來看看簡單的實現方式。
假設現在有一個Movie類,有3個屬性。先看下.h文件
// Movie.h文件
//1\. 如果想要當前類可以實現歸檔與反歸檔,需要遵守一個協議NSCoding
@interfaceMovie:NSObject
@property(nonatomic,copy)NSString*movieId;
@property(nonatomic,copy)NSString*movieName;
@property(nonatomic,copy)NSString*pic_url;
@end
如果是正常寫法,.m文件應該是這樣的:
// Movie.m文件
@implementationMovie
- (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= [superinit])
{
self.movieId = [aDecoder decodeObjectForKey:@"id"];
self.movieName = [aDecoder decodeObjectForKey:@"name"];
self.pic_url = [aDecoder decodeObjectForKey:@"url"];
}
return self;
}
@end
runtime 下Class的各項操作
下面是 runtime 下Class的常見方法 及 帶有使用示例代碼。各項操作,原著 http://www.lxweimin.com/p/46dd81402f63
unsigned int count;
獲取屬性列表
objc_property_t *propertyList = class_copyPropertyList([selfclass], &count);
for(unsignedinti=0; i%@", [NSStringstringWithUTF8String:propertyName]);
}
獲取方法列表
Method *methodList = class_copyMethodList([selfclass], &count);
for(unsignedinti; i%@",NSStringFromSelector(method_getName(method))); }
獲取成員變量列表
Ivar *ivarList = class_copyIvarList([selfclass], &count);for(unsignedinti; i%@", [NSStringstringWithUTF8String:ivarName]); }
獲取協議列表
__unsafe_unretainedProtocol **protocolList = class_copyProtocolList([selfclass], &count);for(unsignedinti; i%@", [NSStringstringWithUTF8String:protocolName]); }
現在有一個Person類,和person創建的xiaoming對象,有test1和test2兩個方法
獲得類方法
Class PersonClass = object_getClass([Personclass]);SEL oriSEL = @selector(test1);Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
獲得實例方法
Class PersonClass = object_getClass([xiaomingclass]);SEL oriSEL = @selector(test2);Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
添加方法
BOOLaddSucc = 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([Personclass], &count);// 得到所有成員變量Ivar *allVariables = class_copyIvarList([Personclass], &count);// 得到所有屬性objc_property_t properties = class_copyPropertyList([Personclass], &count);// 根據名字得到類變量的Ivar指針,但是這個在OC中好像毫無意義Ivar oneCVIvar = class_getClassVariable([Personclass], name);// 根據名字得到實例變量的Ivar指針Ivar oneIVIvar = class_getInstanceVariable([Personclass], name);// 找到后可以直接對私有變量賦值object_setIvar(_per, oneIVIvar,@"Mike");//強制修改name屬性/ 動態添加方法:
第一個參數表示Class cls 類型;
第二個參數表示待調用的方法名稱;
第三個參數(IMP)myAddingFunction,IMP是一個函數指針,這里表示指定具體實現方法myAddingFunction;
第四個參數表方法的參數,0代表沒有參數;
*/class_addMethod([_perclass],@selector(sayHi), (IMP)myAddingFunction,0);// 交換兩個方法method_exchangeImplementations(method1, method2);// 關聯兩個對象objc_setAssociatedObject(idobject,constvoid*key,idvalue, objc_AssociationPolicy policy)/*
id object :表示關聯者,是一個對象,變量名理所當然也是object
const void *key :獲取被關聯者的索引key
id value :被關聯者,這里是一個block
objc_AssociationPolicy policy : 關聯時采用的協議,有assign,retain,copy等協議,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
/// 獲得某個類的類方法Method class_getClassMethod(Class cls , SEL name)// 獲得某個類的實例對象方法Method class_getInstanceMethod(Class cls , SEL name)// 交換兩個方法的實現voidmethod_exchangeImplementations(Method m1 , Method m2)// 將某個值跟某個對象關聯起來,將某個值存儲到某個對象中voidobjc_setAssociatedObject(idobject ,constvoidkey ,idvalue ,objc_AssociationPolicy policy)// 利用參數key 將對象object中存儲的對應值取出來idobjc_getAssociatedObject(idobject ,constvoidkey)// 獲得某個類的所有成員變量(outCount 會返回成員變量的總數)Ivar class_copyIvarList(Class cls ,unsignedintoutCount)// 獲得成員變量的名字constcharivar_getName(Ivar v)// 獲得成員變量的類型constcharivar_getTypeEndcoding(Ivar v)// 獲取類里面所有方法class_copyMethodList(__unsafe_unretainedClass cls,unsignedintoutCount)// 本質:創建誰的對象// 獲取類里面屬性class_copyPropertyList(__unsafe_unretainedClass cls,unsignedint*outCount)
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的定義
structobjc_class{Class isa OBJC_ISA_AVAILABILITY;//每個Class都有一個isa指針#if!__OBJC2__Class super_class OBJC2_UNAVAILABLE;//父類constcharname OBJC2_UNAVAILABLE;//類名longversion OBJC2_UNAVAILABLE;//類版本longinfo OBJC2_UNAVAILABLE;//!!供運行期使用的一些位標識。如:CLS_CLASS (0x1L)表示該類為普通class; CLS_META(0x2L)表示該類為metaclass等(runtime.h中有詳細列出)longinstance_size OBJC2_UNAVAILABLE;//實例大小structobjc_ivar_listivarsOBJC2_UNAVAILABLE;//存儲每個實例變量的內存地址structobjc_method_listmethodListsOBJC2_UNAVAILABLE;//!!根據info的信息確定是類還是實例,運行什么函數方法等structobjc_cachecacheOBJC2_UNAVAILABLE;//緩存structobjc_protocol_listprotocolsOBJC2_UNAVAILABLE;//協議#endif} OBJC2_UNAVAILABLE;
可以看到運行時一個類還關聯了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協議。
在objc_class結構體中:``ivars是objc_ivar_list指針;methodLists是指向objc_method_list指針的指針。也就是說可以動態修改*methodLists的值來添加成員方法,這也是Category實現的原理。
什么是 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開發實例匯總】http://www.lxweimin.com/p/f6dad8e1b848
最后一道面試題的注解
下面的代碼輸出什么?
@implementationSon:NSObject- (id)init{self= [superinit];if(self) {NSLog(@"%@",NSStringFromClass([selfclass]));NSLog(@"%@",NSStringFromClass([superclass])); }returnself;}@end
先思考一下,會打印出來什么?
關注我的更多干貨分享_.
答案:都輸出 Son
class獲取當前方法的調用者的類,superClass獲取當前方法的調用者的父類,super僅僅是一個編譯指示器,就是給編譯器看的,不是一個指針。
本質:只要編譯器看到super這個標志,就會讓當前對象去調用父類方法,本質還是當前對象在調用
這個題目主要是考察關于objc中對self和super的理解:
self是類的隱藏參數,指向當前調用方法的這個類的實例。而super本質是一個編譯器標示符,和self是指向的同一個消息接受者
當使用self調用方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;
而當使用super時,則從父類的方法列表中開始找。然后調用父類的這個方法
調用[self class]時,會轉化成objc_msgSend函數
idobjc_msgSend(idself, SEL op, ...)- 調用 [superclass]
時,會轉化成 objc_msgSendSuper
函數.idobjc_msgSendSuper(structobjc_super *super, SEL op, ...)第一個參數是 objc_super 這樣一個結構體,其定義如下structobjc_super { __unsafe_unretainedidreceiver; __unsafe_unretainedClass super_class; };第一個成員是 receiver, 類似于上面的 objc_msgSend函數第一個參數self第二個成員是記錄當前類的父類是什么,告訴程序從父類中開始找方法,找到方法后,最后內部是使用 objc_msgSend(objc_super->receiver,@selector(class))去調用, 此時已經和[selfclass]調用相同了,故上述輸出結果仍然返回 Sonobjc Runtime 開源代碼對- (Class)class方法的實現-(Class)class{returnobject_getClass(self); }
Runtime 模塊博文推薦 (??數量較多)
作者Runtime 模塊推薦閱讀博文
西木完整總結http://www.lxweimin.com/p/6b905584f536
天口三水羊objc_msgSendhttp://www.lxweimin.com/p/9e1bc8d890f9
夜千尋墨詳解http://www.lxweimin.com/p/46dd81402f63
袁崢Seemygo快速上手http://www.lxweimin.com/p/e071206103a4
鄭欽洪_實現自動化歸檔http://www.lxweimin.com/p/bd24c3f3cd0a
HenryCheng消息機制http://www.lxweimin.com/p/f6300eb3ec3d
賣報的小畫家SureMethod Swizzling開發實例匯總http://www.lxweimin.com/p/f6dad8e1b848
滕大鳥OC最實用的runtime總結http://www.lxweimin.com/p/ab966e8a82e2
黑花白花Runtime在實際開發中的應用http://www.lxweimin.com/p/851b21870d91
Runtime & Runloop 面試最常問到的題整理【建議看】
說明:此面試題針對性的摘錄整理,只為方便 在面試路上準備的你 ,會注有原文。
1、整理原文:2017年5月iOS招人心得(附面試題)
Runtime
objc在向一個對象發送消息時,發生了什么?
什么時候會報unrecognized selector錯誤?iOS有哪些機制來避免走到這一步?
能否向編譯后得到的類中增加實例變量?能否向運行時創建的類中添加實例變量?為什么?
runtime如何實現weak變量的自動置nil?
給類添加一個屬性后,在類結構體里哪些元素會發生變化?
RunLoop
runloop是來做什么的?runloop和線程有什么關系?主線程默認開啟了runloop么?子線程呢?
runloop的mode是用來做什么的?有幾種mode?
為什么把NSTimer對象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主運行循環以后,滑動scrollview的時候NSTimer卻不動了?
蘋果是如何實現Autorelease Pool的?
//-------------------- 【我是分割線】 ---------------------//
整理原文:2017年iOS面試題總結,附上答案
Runtime
01
問題:objc在向一個對象發送消息時,發生了什么?
解答:根據對象的 isa 指針找到類對象 id,在查詢類對象里面的 methodLists 方法函數列表,如果沒有在好到,在沿著 superClass ,尋找父類,再在父類 methodLists 方法列表里面查詢,最終找到 SEL ,根據 id 和 SEL 確認 IMP(指針函數),在發送消息;
03
問題:什么時候會報unrecognized selector錯誤?iOS有哪些機制來避免走到這一步?
解答:當發送消息的時候,我們會根據類里面的 methodLists 列表去查詢我們要動用的SEL,當查詢不到的時候,我們會一直沿著父類查詢,當最終查詢不到的時候我們會報unrecognized selector錯誤,當系統查詢不到方法的時候,會調用+(BOOL)resolveInstanceMethod:(SEL)sel動態解釋的方法來給我一次機會來添加,調用不到的方法。或者我們可以再次使用-(id)forwardingTargetForSelector:(SEL)aSelector重定向的方法來告訴系統,該調用什么方法,一來保證不會崩潰。
04
問題:能否向編譯后得到的類中增加實例變量?能否向運行時創建的類中添加實例變量?為什么?
解答:1、不能向編譯后得到的類增加實例變量 2、能向運行時創建的類中添加實例變量。【解釋】:1. 編譯后的類已經注冊在 runtime 中,類結構體中的 objc_ivar_list 實例變量的鏈表和 instance_size 實例變量的內存大小已經確定,runtime會調用 class_setvarlayout 或 class_setWeaklvarLayout 來處理strong weak 引用.所以不能向存在的類中添加實例變量。2. 運行時創建的類是可以添加實例變量,調用class_addIvar函數. 但是的在調用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.
05
問題:runtime如何實現weak變量的自動置nil?
解答:runtime 對注冊的類, 會進行布局,對于 weak 對象會放入一個 hash 表中。 用 weak 指向的對象內存地址作為 key,當此對象的引用計數為0的時候會 dealloc,假如 weak 指向的對象內存地址是a,那么就會以a為鍵, 在這個 weak 表中搜索,找到所有以a為鍵的 weak 對象,從而設置為 nil。
06
問題:給類添加一個屬性后,在類結構體里哪些元素會發生變化?
解答:instance_size :實例的內存大小;objc_ivar_list *ivars:屬性列表
RunLoop
01
問題:runloop是來做什么的?runloop和線程有什么關系?主線程默認開啟了runloop么?子線程呢?
解答:runloop: 從字面意思看:運行循環、跑圈,其實它內部就是do-while循環,在這個循環內部不斷地處理各種任務(比如Source、Timer、Observer)事件。runloop和線程的關系:一個線程對應一個RunLoop,主線程的RunLoop默認創建并啟動,子線程的RunLoop需手動創建且手動啟動(調用run方法)。RunLoop只能選擇一個Mode啟動,如果當前Mode中沒有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop。
02
問題:runloop的mode是用來做什么的?有幾種mode?
解答: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卻不動了?
解答:nstime對象是在 NSDefaultRunLoopMode下面調用消息的,但是當我們滑動scrollview的時候,NSDefaultRunLoopMode模式就自動切換到UITrackingRunLoopMode模式下面,卻不可以繼續響應nstime發送的消息。所以如果想在滑動scrollview的情況下面還調用nstime的消息,我們可以把nsrunloop的模式更改為NSRunLoopCommonModes.
04
問題:蘋果是如何實現Autorelease Pool的?
解答:Autorelease Pool作用:緩存池,可以避免我們經常寫relase的一種方式。其實就是延遲release,將創建的對象,添加到最近的autoreleasePool中,等到autoreleasePool作用域結束的時候,會將里面所有的對象的引用計數器 - autorelease.
后續遇到針對 runtime&runloop常面相關,會及時在作者博客補充 ~