(轉)ios學習--你會遇到的runtime面試題(詳)


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

Snip20161207_7.png
  • 交換方法的幾種實現方式
    • 利用 method_exchangeImplementations 交換兩個方法的實現
    • 利用 class_replaceMethod 替換方法的實現
    • 利用 method_setImplementation 來直接設置某個方法的IMP

      Snip20161207_8.png

三、補充(重要)

1、消息機制

  • 1、方法調用底層實現


    Snip20161107_5.png
  • 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



文/柳駿(簡書作者)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM