1、了解runtime嗎?是什么?
2、你怎么知道的?
3、對象如何找到對應方法去調用的
于是我總結了很多網上被問到的一些關于runtime的題目,并做了詳細的回答,并在后面補充了我在學習runtime時敲的一些代碼,如果想吃透runtime的朋友,可以把后面補充的內容好好看完
一、你會被問到的關于runtime筆試題:
1. runtime怎么添加屬性、方法等
2. runtime 如何實現 weak 屬性
3. runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和實例方法)
4. 使用runtime Associate方法關聯的對象,需要在主對象dealloc的時候釋放么?
5. _objc_msgForward函數是做什么的?直接調用它將會發生什么?
6. 能否向編譯后得到的類中增加實例變量?能否向運行時創建的類中添加實例變量?為什么?
7. 簡述下Objective-C中調用方法的過程(runtime)
8. 什么是method swizzling(俗稱黑魔法)
如果上面的題目你全部答得出來,那就不要浪費時間,直接return吧,程序猿的時間很寶貴的
二、解答
1. runtime怎么添加屬性、方法等
ivar表示成員變量
class_addIvar
class_addMethod
class_addProperty
class_addProtocol
class_replaceProperty
2. runtime 如何實現 weak 屬性
首先要搞清楚weak屬性的特點
weak策略表明該屬性定義了一種“非擁有關系” (nonowning relationship)。為這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign類似;然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)
那么runtime如何實現weak變量的自動置nil?
runtime對注冊的類,會進行布局,會將 weak 對象放入一個 hash 表中。用 weak 指向的對象內存地址作為 key,當此對象的引用計數為0的時候會調用對象的 dealloc 方法,假設 weak 指向的對象內存地址是a,那么就會以a為key,在這個 weak hash表中搜索,找到所有以a為key的 weak 對象,從而設置為 nil。
weak屬性需要在dealloc中置nil么
在ARC環境無論是強指針還是弱指針都無需在 dealloc 設置為 nil , ARC 會自動幫我們處理
即便是編譯器不幫我們做這些,weak也不需要在dealloc中置nil
在屬性所指的對象遭到摧毀時,屬性值也會清空
objc// 模擬下weak的setter方法,大致如下- (void)setObject:(NSObject *)object{ objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }];}
3. runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和實例方法)
- 每一個類對象中都一個對象方法列表(對象方法緩存)
類方法列表是存放在類對象中isa指針指向的元類對象中(類方法緩存)
方法列表中每個方法結構體中記錄著方法的名稱,方法實現,以及參數類型,其實selector本質就是方法名稱,通過這個方法名稱就可以在方法列表中找到對應的方法實現.
當我們發送一個消息給一個NSObject對象時,這條消息會在對象的類對象方法列表里查找
當我們發送一個消息給一個類時,這條消息會在類的Meta Class對象的方法列表里查找
4. 使用runtime Associate方法關聯的對象,需要在主對象dealloc的時候釋放么?
無論在MRC下還是ARC下均不需要被關聯的對象在生命周期內要比對象本身釋放的晚很多,它們會在被 NSObject -dealloc 調用的object_dispose()方法中釋放
補充:對象的內存銷毀時間表,分四個步驟
>1、調用 -release :引用計數變為零
* 對象正在被銷毀,生命周期即將結束.
* 不能再有新的 __weak 弱引用,否則將指向 nil.
* 調用 [self dealloc]
>
2、 父類調用 -dealloc
* 繼承關系中最直接繼承的父類再調用 -dealloc
* 如果是 MRC 代碼 則會手動釋放實例變量們(iVars)
* 繼承關系中每一層的父類 都再調用 -dealloc
>3、NSObject 調 -dealloc
* 只做一件事:調用 Objective-C runtime 中object_dispose() 方法
>4. 調用 object_dispose()
* 為 C++ 的實例變量們(iVars)調用 destructors
* 為 ARC 狀態下的 實例變量們(iVars) 調用 -release
* 解除所有使用 runtime Associate方法關聯的對象
* 解除所有 __weak 引用
* 調用 free()
5. _objc_msgForward函數是做什么的?直接調用它將會發生什么?
_objc_msgForward是IMP類型,用于消息轉發的:當向一個對象發送一條消息,但它并沒有實現的時候,_objc_msgForward會嘗試做消息轉發
直接調用_objc_msgForward是非常危險
的事,這是把雙刃刀,如果用不好會直接導致程序Crash,但是如果用得好,能做很多非常酷的事
JSPatch就是直接調用_objc_msgForward來實現其核心功能的
詳細解說參見這里的第一個問題解答
6. 能否向編譯后得到的類中增加實例變量?能否向運行時創建的類中添加實例變量?為什么?
- 不能向編譯后得到的類中增加實例變量;
能向運行時創建的類中添加實例變量;
-
分析如下:
因為編譯后的類已經注冊在runtime中,類結構體中的objc_ivar_list 實例變量的鏈表和instance_size實例變量的內存大小已經確定,同時runtime 會調用class_setIvarLayout 或 class_setWeakIvarLayout來處理strong weak引用,所以不能向存在的類中添加實例變量
運行時創建的類是可以添加實例變量,調用 class_addIvar函數,但是得在調用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。
7. 簡述下Objective-C中調用方法的過程(runtime)
- Objective-C是動態語言,每個方法在運行時會被動態轉為消息發送,即:objc_msgSend(receiver, selector),整個過程介紹如下:
* objc在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類
* 然后在該類中的方法列表以及其父類方法列表中尋找方法運行
* 如果,在最頂層的父類(一般也就NSObject)中依然找不到相應的方法時,程序在運行時會掛掉并拋出異常unrecognized selector sent to XXX
* 但是在這之前,objc的運行時會給出三次拯救程序崩潰的機會,這三次拯救程序奔潰的說明見問題《什么時候會報unrecognized selector的異常》中的說明
- 補充說明:Runtime 鑄就了Objective-C 是動態語言的特性,使得C語言具備了面向對象的特性,在程序運行期創建,檢查,修改類、對象及其對應的方法,這些操作都可以使用runtime中的對應方法實現。
8. 什么是method swizzling(俗稱黑魔法)
- 簡單說就是進行方法交換
在Objective-C中調用一個方法,其實是向一個對象發送消息,查找消息的唯一依據是selector的名字。利用Objective-C的動態特性,可以實現在運行時偷換selector對應的方法實現,達到給方法掛鉤的目的
每個類都有一個方法列表,存放著方法的名字和方法實現的映射關系,selector的本質其實就是方法名,IMP有點類似函數指針,指向具體的Method實現,通過selector就可以找到對應的IMP
[圖片上傳失敗...(image-1c1c5f-1515728553053)]
-
交換方法的幾種實現方式
利用 method_exchangeImplementations 交換兩個方法的實現
利用 class_replaceMethod 替換方法的實現
-
利用 method_setImplementation 來直接設置某個方法的IMP
[圖片上傳失敗...(image-d0dcc9-1515728553053)]
三、補充(重要)
1、消息機制
-
1、方法調用底層實現
[圖片上傳失敗...(image-1a2231-1515728553053)]
2、runtime:千萬不要隨便使用,不得已才使用
//消息機制:
//作用:調用已知私有方法,如調用沒有在.h文件申明但是在.m文件實現了的方法
// 用runtime調用私有方法:方法編號后面開始,依次就是傳入給方法的參數
objc_msgSend(p, @selector(run: str:),20,@"haha");
objc_msgSend(p, @selector(eat));
// [p eat] => objc_msgSend(p, @selector(eat));
-
3、對象如何找到對應的方法去調用
// 方法保存到什么地方?對象方法保存到類中,類方法保存到元類(meta class),每一個類都有方法列表methodList
//明確去哪個類中調用,通過isa指針
1.根據對象的isa去對應的類查找方法,isa:判斷去哪個類查找對應的方法 指向方法調用的類
2.根據傳入的方法編號SEL,里面有個哈希列表,在列表中找到對應方法Method(方法名)
3.根據方法名(函數入口)找到函數實現,函數實現在方法區
2、交換方法
- 1、需求:比如我有個項目,已經開發2年,之前都是使用UIImage去加載圖片,組長想要在調用imageNamed,就給我提示,是否加載成功,如果用方法2,每個調用imageNamed方法的,都要改成xmg_imageNamed:才能擁有這個功能,很麻煩。解決:用runtime交換方法,即下面方法3
①解決方式 自定義UIImage類,缺點:每次用要導入自己的類
②解決方法:UIImage分類擴充一個這樣方法,缺點:需要導入,無法寫super和self,會干掉系統方法,解決:給系統方法加個前綴,與系統方法區分,如:xmg_imageNamed:
③交互方法實現,步驟: 1.提供分類 2.寫一個有這樣功能方法 3.用系統方法與這個功能方法交互實現,在+load方法中實現
注意:在分類一定不要重寫系統方法,就直接把系統方法干掉,如果真的想重寫,在系統方法前面加前綴,方法里面去調用系統方法
思想:什么時候需要自定義,系統功能不完善,就自定義一個這樣類,去擴展這個類
/#import "UIImage+Image.h"
/#import <objc/message.h>
@implementation UIImage (Image)
// 加載類的時候調用,肯定只會調用一次
+(void)load
{
// 交互方法實現xmg_imageNamed,imageNamed
/**
獲取類方法名
@param Class cls,#> 獲取哪個類方法 description#>
@param SEL name#> 方法編號 description#>
@return 返回Method(方法名)
class_getClassMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
*/
/**
獲取對象方法名
@param Class cls,#> 獲取哪個對象方法 description#>
@param SEL name#> 方法編號 description#>
@return 返回Method(方法名)
class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
*/
Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
Method xmg_imageNameMethod = class_getClassMethod(self, @selector(xmg_imageNamed:));
//用runtime對imageNameMethod和xmg_imageNameMethod方法進行交換
method_exchangeImplementations(imageNameMethod, xmg_imageNameMethod);
}
//外界調用imageNamed:方法,其實是調用下面方法,調用xmg_imageNamed就是調用imageNamed:
+ (UIImage *)xmg_imageNamed:(NSString *)name
{
//已經把xmg_imageNamed換成imageNamed,所以下面其實是調用的imageNamed:
UIImage *image = [UIImage xmg_imageNamed:name];
if (image == nil) {
NSLog(@"加載失敗");
}
return image;
}
@end
3、動態添加方法
動態添加方法:
為什么動態添加方法? OC都是懶加載,有些方法可能很久不會調用
應用場景:電商,視頻,社交,收費項目:會員機制中,只要會員才擁有這些功能
- 1、美團面試題:有沒有使用過performSelector,使用,什么時候使用?動態添加方法的時候使用? 為什么動態添加方法
// 默認OC方法都有兩個默認存在的隱式參數,self(哪個類的方法),_cmd(方法編號)
void run(id self, SEL _cmd, NSNumber *metre) {
NSLog(@"跑了%@",metre);
}
- 2、什么時候調用:只要調用沒有實現的方法 就會調用方法去解決,這里可以拿到那個未實現的方法名
// 作用:去解決沒有實現方法,動態添加方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
class:給誰添加方法
SEL:添加哪個方法
IMP:方法實現,函數入口,函數名,如:(IMP)run,方法名run強轉成IMP
type:方法類型,通過查蘋果官方文檔,V:void,
class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)
// [NSStringFromSelector(sel) isEqualToString:@"eat"];
if (sel == @selector(run:)) {
// 添加方法
class_addMethod(self, sel, (IMP)run,"v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
4、動態添加屬性
- 1、需求:給NSObject添加一個name屬性,動態添加屬性 -> runtime
思考:
①給NSObject添加分類,在分類中添加屬性。問題:@property在分類中作用:僅僅是生成get,set方法聲明,不會生成get,set方法實現和下劃線成員屬性,所以要在.m文件實現setter/getter方法,用static保存下滑線屬性,這樣一來,當對象銷毀時,屬性無法銷毀
②用runtime動態添加屬性:本質是讓屬性與某個對象產生一段關聯
使用場景:給系統的類添加屬性時
#import <objc/message.h>
@implementation NSObject (Property)
//static NSString *_name; //通過這樣去保存屬性沒法做到對象銷毀,屬性也銷毀,static依然會讓屬性存在緩存池中,所以需要動態的添加成員屬性
// 只要想調用runtime方法,思考:誰的事情
-(void)setName:(NSString *)name
{
// 保存name
// 動態添加屬性 = 本質:讓對象的某個屬性與值產生關聯
/*
object:保存到哪個對象中
key:用什么屬性保存 屬性名
value:保存值
policy:策略,strong,weak
objc_setAssociatedObject(<#id object#>, <#const void *key#>, <#id value#>, <#objc_AssociationPolicy policy#>)
*/
objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// _name = name;
}
- (NSString *)name
{
return objc_getAssociatedObject(self, "name");
// return _name;
}
@end
5、自動生成屬性代碼
開發中,從網絡數據中解析出字典數組,將數組轉為模型時,如果有幾百個key需要用,要寫很多@property成員屬性,下面提供一個萬能的方法,可直接將字典數組轉為全部@property成員屬性,打印出來,這樣直接復制在模型中就好了
#import "NSDictionary+PropertyCode.h"
@implementation NSDictionary (PropertyCode)
//1??通過這個方法,自動將字典轉成模型中需要用的屬性代碼
// 私有API:真實存在,但是蘋果沒有暴露出來,不給你。如BOOL值,不知道類型,打印得知是__NSCFBoolean,但是無法敲出來,只能用NSClassFromString(@"__NSCFBoolean")
// isKindOfClass:判斷下是否是當前類或者子類,BOOL是NSNumber的子類,要先判斷BOOL
- (void)createPropetyCode
{
// 模型中屬性根據字典的key
// 有多少個key,生成多少個屬性
NSMutableString *codes = [NSMutableString string];
// 遍歷字典
[self enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) {
NSString *code = nil;
// NSLog(@"%@",[value class]);
if ([value isKindOfClass:[NSString class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSString *%@;",key];
} else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]){
code = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",key];
} else if ([value isKindOfClass:[NSNumber class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, assign) NSInteger %@;",key];
} else if ([value isKindOfClass:[NSArray class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",key];
} else if ([value isKindOfClass:[NSDictionary class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",key];
}
// 拼接字符串
[codes appendFormat:@"%@\n",code];
}];
NSLog(@"%@",codes);
}
@end
6、KVC字典轉模型
// 需求:就是在開發中,通常后臺會給你很多數據,但是并不是每個數據都有用,這些沒有用的數據,需不需要保存到模型中
@implementation Status
+ (instancetype)statusWithDict:(NSDictionary *)dict{
// 創建模型
Status *s = [[self alloc] init];
// 字典value轉模型屬性保存
[s setValuesForKeysWithDictionary:dict];
// s.reposts_count = dict[@"reposts_count"];
// 4??MJExtension:可以字典轉模型,而且可以不用與字典中屬性一一對應,runtime,遍歷模型中有多少個屬性,直接去字典中取出對應value,給模型賦值
// 1??setValuesForKeysWithDictionary:方法底層實現:遍歷字典中所有key,去模型中查找對應的屬性,把值給模型屬性賦值,即調用下面方法:
/*
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// source
// 這行代碼才是真正給模型的屬性賦值
[s setValue:dict[@"source"] forKey:@"source"];
//底層實現是:
2?? [s setValue:dict[@"source"] forKey:@"source"];
1.首先會去模型中查找有沒有setSource方法,直接調用set方法 [s setSource:dict[@"source"]];
2.去模型中查找有沒有source屬性,source = dict[@"source"]
3.去米線中查找有沒有_source屬性,_source = dict[@"source"]
4.調用對象的 setValue:forUndefinedKey:直接報錯
[s setValue:obj forKey:key];
}];
*/
return s;
}
// 3??用KVC,不想讓系統報錯,重寫系統方法思想:
// 1.想給系統方法添加功能
// 2.不想要系統實現
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
}
@end
7、MJExtention的底層實現
#import "NSObject+Model.h"
#import <objc/message.h>
// class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>) 獲取屬性列表
@implementation NSObject (Model)
/**
字典轉模型
@param dict 傳入需要轉模型的字典
@return 賦值好的模型
*/
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
id objc = [[self alloc] init];
//思路: runtime遍歷模型中屬性,去字典中取出對應value,在給模型中屬性賦值
// 1.獲取模型中所有屬性 -> 保存到類
// ivar:下劃線成員變量 和 Property:屬性
// 獲取成員變量列表
// class:獲取哪個類成員變量列表
// count:成員變量總數
//這個方法得到一個裝有成員變量的數組
//class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
int count = 0;
// 成員變量數組 指向數組第0個元素
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍歷所有成員變量
for (int i = 0; i < count; i++) {
// 獲取成員變量 user
Ivar ivar = ivarList[i];
// 獲取成員變量名稱,即將C語言的字符轉為OC字符串
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 獲取成員變量類型,用于獲取二級字典的模型名字
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 將type這樣的字符串@"@\"User\"" 轉成 @"User"
type = [type stringByReplacingOccurrencesOfString:@"@\"" withString:@""];
type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 成員變量名稱轉換key,即去掉成員變量前面的下劃線
NSString *key = [ivarName substringFromIndex:1];
// 從字典中取出對應value dict[@"user"] -> 字典
id value = dict[key];
// 二級轉換
// 并且是自定義類型,才需要轉換
if ([value isKindOfClass:[NSDictionary class]] && ![type containsString:@"NS"]) { // 只有是字典才需要轉換
Class className = NSClassFromString(type);
// 字典轉模型
value = [className modelWithDict:value];
}
// 給模型中屬性賦值 key:user value:字典 -> 模型
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}
@end