iOS 问题整理04----Runtime
本篇文章主要解決以下問題
- class_rw_t 與 class_ro_t 的區別?
- runtime 中,SEL 和 IMP 的區別?
- runtime 如何通過 selector 找到對應 IMP 的地址?
- objc 中向一個 nil 對象發送消息,會發生什么?
- 消息轉發到 forwordinginvacation 方法,如何拿到返回值?
- 什么時候會報 unrecognized selector 異常?
- runtime怎么添加屬性、方法等
- 使用runtime Associate方法關聯的對象,需要在主對象dealloc的時候釋放么?
- 什么是method swizzling?
- 能否向編譯后得到的類中增加實例變量?能否向運行時創建的類中添加實例變量?為什么?
1. 說說你對 runtime 的理解。
答:
Runtime 提供了一套 C/C++ 的 API,使 OC 程序可以在運行期改變其結構,因此 OC 是一門動態性比較強的語言。
在 iOS 系統中 KVO、Category、weak 等都是基于 runtime 實現的。
自己也經常利用 runtime 的特性來實現一些東西。(詳見本篇第 6 問)
****** 低調的分割線 ******
我覺得上面第一部分的答案還是不夠口語化,也不太好,顯得空洞,感覺面試答起來比較像是背的,聽起來比較尷尬,我重新反思了一下,這是新的第一部分的答案:
像其它的語言,比如說 C 語言,在編譯的時候就已經知道了程序應該執行的代碼。但是在 OC 中,會盡可能地把決策去推遲到運行的時候再去做。比如到運行的時候再去確定對象的類型、確定方法的接收者,給對象添加方法等等。這些都是可以通過 runtime 的 api 來實現的,用書面一點的話來說就是:runtime 的 API 使 OC 程序可以在它運行到時候改變結構,所以 OC 是一門動態性比較強的語言。
對于 runtime 來說,最重要的就是消息傳遞機制,要理解清楚消息傳遞機制就需要弄明白 isa 的作用,再弄清楚類的的結構,再去理解消息傳遞機制的流程,才能理解的透徹。(扯到這一步就好說了,這些問題下邊都有講到)
分析:
這種開放的問題難以把握如何回答好,筆者認為首先要答到 objc 是一門動態性比較強的語言(為什么說比較強,因為還有更強的),然后再說說系統的一些基于 runtime 實現的東西,再說說自己項目中基于 runtime 實現的一些東西。
2. 你了解 isa 指針嗎?
答:
在 64bit 之前 isa 是一個普通的指針,指向 Class/Meta-Class。
在 64bit 之后,蘋果對其進行了優化,把它做成了一個共用體,運用了位域的技術存儲了更多的信息。(isa 還是只占用 8 個字節,用 8 個字節存儲了更多的信息,但是要找到相應了 class 或者 meta-class 需要與 ISA_MASK 進行一次位運算,要更詳細的了解可以點擊這里。)
我們可以簡單地認為,instance 的 isa 指向 class,class 的 isa 指向 meta-class,meta-class 的 isa 指向基類的 meta-class。
在使用 objc_msgSend 的時候,是通過 receiver 的 isa 找到它的類或者元類,然后去找方法的。
分析:
回顧一下之前的內容,OC 中的對象分為 instance、class與meta-class,它們的 isa 與 superclass 的指向關系如圖所示。
這里需要注意的是途中右上角有一根看起來不太和諧的箭頭,即 Root class(meta) 的 superclass 指向 Root class(class)。這個問題主要也是談三點:
- isa 是什么
- 怎么指向的
- 在消息發送中的作用
3. 類的結構是怎樣的?
答:
分析:
前面的文章在講 OC 對象的存儲結構時說到 class 對象中存儲的有 isa、superclass 指針、方法列表、屬性列表、協議列表。meta-class 與 class 都是 Class 類型的,所以他們的結構其實是一樣的,只不過存儲的東西有區別,meta-class 的方法列表中存儲的是類方法,class 的方法列表中存儲的是對象方法。
具體的代碼結構如上圖所示。(將就看一下吧,作圖工具:網頁版美圖秀秀....)
這是在源碼中找到的結構,筆者對其做了精簡,保留了現在我們需要關注的信息,源碼可以點擊這里下載。
現在分析一下這些"美圖"。過程比較長,但這是這年頭出去面試毫無疑問必須肯定百分之百要掌握的,建議準備面試的同學每天默寫一遍筆者下邊敲出來的代碼。
struct objc_class : objc_object {// Class ISA;Class superclass;cache_t cache;class_data_bits_t bits; } 復制代碼在源碼中可以找到這樣一句代碼 typedef struct objc_class *Class;,所以 Class 其實本質上就是 objc_class結構體。
我們都知道 Class 中有 isa 和 superclass,但是這里成員變量 ISA 前面有兩個斜線,它被注釋掉了,很奇怪,這是因為 objc_class 是繼承于 objc_object 的,來看一下 objc_object 的結構就明白了。
objc_object 中有 isa 這個共用體,所以objc_class中也有它。superclass 指向父類。(回憶:基類的元類對象有什么特殊的地方?)
cache 是方法緩存,用于存儲先前已經調用過的方法的信息。
使用 objc_msgSend 向接受者發送消息時,會先根據 isa 找到 class/meta-class,然后去 class/meta-class 的成員 cache 中找方法,如果沒有找到,才會去方法列表中找。這樣可以提升效率。
bits 是一串很長的數字,它存儲了多項信息,把它和 FAST_DATA_MASK 進行與運算,可以得到一個新的數字,這個數字是一個地址,指向 class_rw_t 結構體。
objc_class 的成員變量已經簡單地介紹完了,現在來看一看更具體的。
先來看一看 cache_t,cache 的類型是 cache_t。
struct cache_t {struct bucket_t *_buckets;mask_t _mask;mask_t _occupied; } 復制代碼_buckets 是一個數組,用于存放方法信息,數組中元素的類型是 struct bucket_t。
_mask 翻譯過來就是掩碼,看見 mask 就要意識到它是用來做位運算的,比如 ISA_MASK、FAST_DATA_MASK。先記住一個結論,_mask 的長度是 _buckets 的長度減一。
_oppcupied 表示的是 _buckets 中存儲的方法的數量。
bucket_t 的結構是這樣的:
struct bucket_t {SEL _key; //筆者這里與源碼不同,因為這里實際上就是以 @selector(xxx) 作為 key 的,這樣寫方便記憶一些IMP _imp; //函數的內存地址 } 復制代碼方法緩存的機制并不是簡單的調用一個就往 _buckets 中添加一個,否則這里也就不需要有 _mask 與 _occupied 這兩個成員了。它是一個散列表。
當創建一個類之后,系統自動會給 _buckets 分配一段內存空間,它里面有 N 個元素,都用 NULL 填充,_mask 的值是 N-1。
當調用 objc_msgSend(obj, foo) 的時候
- 在 objClass 中找到了 cache
- 然后得到@selector(foo) 的地址p,計算 index = p & _mask
- 使這個 index 做為 _buckets的索引,找到 _buckets[index]
- 兩個數相與 C = A & B,那么 C 必定不大于 A 和 B 中最小的數。所以 _buckets 的索引最大的就是 _mask,所以 _mask 的值是 _buckets 的長度減一。
- 判斷 if (_bucket[index]._key == @selector(foo))
- 如果相等的話就證明找到了,就可以直接使用了。
- 如果 _bucket[index]._key == 0,證明這個方法沒有存入數組中并且這個位置還沒有被別的方法占用,將 @selector(foo) 存入 _bucket[index] 做為 _key,將函數 foo 的地址存入 _imp。
- 如果 _bucket[index]._key != 0 && _bucket[index]._key != @selector(foo)
- 不同的兩個數和 _mask 相與,是有可能得到一個結果的。
- 出現這種情況說明這個位置被比 foo 先調用的一個函數占用了。現在還不能確定 foo 在不在 _buckets 中。
- 這時讓 index -= 1,然后又進入上一步:使 index 作為索引...
- 如果 index 已經減到零了,還是沒有空位,就令 index = _mask,然后繼續找
- 如果找完一圈之后,還是沒有找到
- 那么就證明 foo 確實不在散列表中
- 也說明散列表已經滿了
- 散列表會擴容變成原來的兩倍,然后修改 _mask 的值
- 將散列表清空。因為 _mask 已經變了,對于之前的元素來說,已經不能通過 _mask 準確地計算出索引了
- 計算 @selector(foo) 的地址,與新的 _mask 相與得到新的索引 index,然后將 foo 存入散列表
- 查詢過程的代碼如下bucket_t * cache_t::find(cache_key_t k, id receiver) {assert(k != 0);bucket_t *b = buckets();mask_t m = mask();mask_t begin = cache_hash(k, m);mask_t i = begin;do {if (b[i].key() == 0 || b[i].key() == k) {return &b[i];}} while ((i = cache_next(i, m)) != begin);// hackClass cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));cache_t::bad_cache(receiver, (SEL)k, cls); }static inline mask_t cache_next(mask_t i, mask_t mask) {return i ? i-1 : mask; } 復制代碼
cache 緩存以及方法查找方式是要重點掌握的,至少得知道是以散列表的方式,然后以什么為索引,通過什么進行比較。這里已經介紹完畢,我先去抽根煙,呆會說說 bits。
bits 是一串 64 位的二進制數。不同的位上寫著不同的數,控制它和不同的掩碼 A_MASK、B_MASK、C_MASK 相與,就可以得到我們想要的位上的數據。bits & FAST_DATA_MASK 就可以得到 class_rw_t 的地址。
class_rw_t 中我們關注有一個指向 struct class_ro_t 的指針 ro,以及存放類信息的二維數組們。這里的代碼就不敲了,要記住的是 class_rw_t 中有 ro 指針,還有存放方法列表、屬性列表、協議列表的二維數組。
method_array_t 是一個二維數組,它里面存放的是 method_list_t,method_list_t 中存放的是 method_t。(property_arry_t 與 protocol_array_t 也是,它們類似。)
method_t 結構如下
struct method_t {SEL name;const char * types;IMP imp; } 復制代碼name 就代表的是方法的名稱,imp 代表的是方法實現的地址。types 是方法的類型編碼,主要就是返回值以及各個參數的類型。當我們想找一個方法的時候,我們找到了 method_t,也就知道了它的名字,地址,以及返回值、參數的類型,不就是找到它了嗎?當你想報復一個人的時候,你有他的名字、地址以及鑰匙......
class_rw_t 與 class_ro_t
- 從名字上來看,一個是 readwrite 、一個是readonly。所以你懂的,前者是可讀寫的,后者是只讀的。
- class_ro_t 包含的是類的初始信息,class_rw_t 會包含分類中的信息。
- 前面介紹過了,category 編譯之后是一個結構體,里面有各種信息。運行過程中 runtime 會把這些信息合并到 class/meta-class 中去,其實就是合并到 class/meta-class 中通過 bits 找到的 class_rw_t 的那些二維數組中來。而類初始信息只有一份,所以 class_ro_t 結構體中的那些數組用一維的就可以了。
- 你可能注意到了(我知道你沒有),class_ro_t 中有一個 ivars 在 class_rw_t 中是沒有的。這個是類的成員變量。ivars 存在于一個 readonly 數組中,這也可以作為解釋以下兩個問題的一個角度
- 為啥 category 不能添加成員變量呢?
- 為啥能添加方法呢?
- 為啥不能向編譯后的類中添加實例變量呢?
- 為啥 category 不能添加成員變量呢?
類的結構說完了,你能否回答這兩道面試題?
- runtime 中,SEL 和 IMP 的區別?
- runtime 如何通過 selector 找到對應 IMP 的地址?
4. objc 的消息發送機制是怎樣的?
ObjC 的動態特性是基于 Runtime 的消息傳遞機制的,在 ObjC 中,消息的傳遞都是動態的。
ObjC - 基于 Runtime 的語言,它會盡可能地把決策從編譯時和連接時推遲到運行時(簡單來說,就是編譯后的文件不全是機器指令,還有一部分中間代碼,在運行的時候,通過 Runtime 再把需要轉換的中間代碼在翻譯成機器指令)這使得 ObjC 有著很大的靈活性。比如:
1、動態的確定類型
2、我們可以動態的確定消息傳遞的對象
3、動態的給對象增加方法的實現 等等
什么是消息傳遞?和 C語言 的調用函數有什么區別?
- 函數調用就是直接跳到地址執行。代碼在編譯、優化之后生成了匯編代碼,然后連接各種庫,完了就生成了可以執行的代碼。
- C語言 在編譯時就已經決定了程序所應執行的代碼。
- Objc 中向 receiver 發送消息,receiver 并不一定調用這個方法,而是到了運行時才會去看 receiver 是否響應這個消息,再決策是執行這個方法還是其它方法,或者轉發給其它對象。
Tips: 編譯時,編譯器只是簡單的進行語法分析。比如 NSData *obj = [[NSObject alloc] init];,在編譯時 obj 是 NSData 類型的,在運行時它是 NSObject 類型的。
接下來對消息機制進行講述。
當我們程序執行 [obj foo]的時候,你可能會和筆者一樣去想foo是個什么鬼東西?。
當我們程序執行 [obj foo] 的時候,這句代碼會被編譯成 objc_msgSend(obj, @selector(foo)),即向 obj 發送消息 foo。然后就會進入消息機制的三大階段,消息發送、動態解析、消息轉發。
- 消息發送:runtime 會先找到 obj 的 isa,然后通過 isa 找到 class/meta-class,在 class/meta-class 以及他們的父類的 cache 和方法列表中去找 foo。
- 動態解析:如果在上階段沒有找到就會進入該階段。runtime 就會給 class/meta-class 發送 resolveInstanceMethod:/resolveClassMethod: 消息,在這里可以給接受者增加執行方法。
- 消息轉發:如果沒有在動態解析中進行處理,就會進入到該階段。
下邊這張圖描述了消息發送的具體流程:
這張圖注意兩個地方:
- 可以向 nil 發送消息,不會崩潰,也不會有結果。
- 就算是從父類找到的方法,最后也會緩存到消息接收者的 class/meta-class 的 cache 中。
這一張是動態方法解析的流程圖:
我們已經提到了好幾遍動態解析這個詞語,你可能還不明白它是什么意思。我們來看一個實際的例子:
@interface Person : NSObject - (void)run; @end@implementation Person @end 復制代碼實現一個 Person 類,聲明一個 -(void)run 方法,但并不實現它。然后在某個 main 函數中讓 person 實例對象調用 run,嗯,你知道的它會崩潰。
現在,我們在 Person 的 implementation 中添加代碼:
@implementation Person+ (BOOL)resolveInstanceMethod:(SEL)sel {return [super resolveInstanceMethod:sel]; }@end 復制代碼然后在方法內打上斷點,再運行程序,通過斷點可以發現,程序會進入這個方法。
既然它來了,我們就可以在這里做點事情。我們用 Runtime API 給類把添加一個 run 方法吧,在 OC 中,編譯的時候你沒添加方法的實現,沒關系,運行的時候動態地添加一個也是 OK 的。 void aTmpMethod(id self, SEL _cmd) {NSLog(@"%s", __func__); }+ (BOOL)resolveInstanceMethod:(SEL)sel {if (sel == @selector(run)) {class_addMethod(self, sel, (IMP)aTmpMethod, "v@:");return YES;}return [super resolveInstanceMethod:sel]; } 復制代碼
我們利用 class_addMethod 給 Person 動態地添加了一個方法,四個參數分類代表要添加給誰,你的名字是啥,你家在哪,鑰匙呢。
如果 aTmpMethod 不是 C 語言的函數,而是一個 OC 方法。那么可以這樣寫:
Method method = class_getInstanceMethod(self, @selector(aTmpMethod)); class_addMethod(self, self, method_getImplementation(method), method_getTypeEncoding(method)) 復制代碼這個時候再運行程序,發現它不會崩潰了,并且調用了 aTmpMethod 函數。
上述過程我們就是動態地給 Person 添加了一個名為 run 的方法。如果給 Person meta-class 添加方法,流程類似。
現在我們再來回過頭看這張流程圖。
當消息機制在消息發送階段沒有找到方法的后,會來到動態方法解析階段。
- 系統會判斷,如果這個方法曾經來到過動態解析階段,那么就直接進入下一個階段-消息轉發。
- 因為如果它之前到過動態解析階段,然后有回到消息發送階段,但是走完消息發送階段又沒有找到方法,說明它上次來的時候就沒有給它動態添加方法。所以就直接進入下一階段。
- 如果這是第一次來動態方法解析,那么就會進入 resolveInstanceMethod:/resolveClassMethod:。
- 然后將它標記為:已經動態解析過的。
- 最后再回到消息發送,再走一遍消息發送的流程。
最后一個階段是消息轉發:
如果第一二階段都沒有處理成功,就會來到這一階段-消息轉發。
- 消息轉發會先調用 forwardingTargetForSelector: 方法
- 如果這個方法返回了一個對象,那么就讓這個對象去處理這個消息
- objc_msgSend(對象, sel)
- 返回值為nil,就會調用 methodSignatureForSelector: 方法
- methodSignatureForSelector: 要求返回一個方法的簽名
- 方法的簽名指的是方法的返回值類型以及參數的類型,常用的生成方式如下:
- [NSMethodSignature signatureWithObjCTypes:"v@:"];
- 如果返回nil,則會直接調用doesNotRecognizeSelector: 方法。
- methodSignatureForSelector: 要求返回一個方法的簽名
- 如果 methodSignatureForSelector: 返回不為 nil,則會調用 forwardInvocation: 方法。
- 來到這個方法后,就算什么都不處理,運行程序也不會崩潰了。
- 比如你這個方法里面什么都不寫
- 比如你只寫一句 NSLog(@"Hello world!");
- 還可以處理這個參數 anInvocation。
- NSInvocation封裝了方法調用者、方法名、方法的簽名。
- 比如可以修改方法的調用者
- [anInvocation invokeWithTarget:[[Cat alloc] init]]
- 比如拿到方法的返回值類型
- 上邊提到 NSInvocation 中有方法、調用者、方法簽名
- 所以只要拿到方法簽名,就能找到返回值的類型
- 來到這個方法后,就算什么都不處理,運行程序也不會崩潰了。
objc_msgSend 已經講完了,相信你已經可以回答下邊這三個問題了:
- objc 中向一個 nil 對象發送消息,會發生什么?
- 消息轉發到 forwordinginvacation 方法,如何拿到返回值?
- 什么時候會報 unrecognized selector 異常?
講一下 super 的問題,先看一個實際的例子:
現在問你打印什么?如果你對 super 和消息機制不了解的話,這道題你是不明白所以然的。 下邊是打印的結果:現在講明白為什么是這樣:
- self 是什么?
- self 是 run 的隱藏參數
- 每一個方法都有隱藏參數 self 與 _cmd
- 比如 - (void)run 編譯后會變成 - (void)run:(id)self sel: (SEL)_cmd,_cmd 就是 @selector(run)
- 所以 self 是一個對象,類型是方法調用者
- [self class] 發生了什么?
- 向 self 發送消息 class
- 通過 self 的 isa 找到了 Person,然而 Person 中找不到 class 的實現
- 就通過 Person 的 superclass 指針找到了?NSObject
- 在 NSObject 中找到 class 的實現并調用。
- super 是什么?
- super 是編譯器的一個標識、一個關鍵字
- 它并不是一個對象
- [super class] 發生了什么?
- 編譯器看見這句代碼的時候會把它編譯成struct __rw_objc_super arg = {self,class_getSuperclass(objc_getClass("Person")) } objc_msgSendSuper2(arg, @selector(class)) 復制代碼
- objc_msgSendSuper2 的第一個參數是結構體,結構體的第一個成員是 self,第二個成員是 Person 的父類
- 當執行 objc_msgSendSuper2(arg, @selector(class)) 時
- 會向 arg 的第一個參數 self 發送消息 class,即 self 是消息的接收者
- 但是會從第二個參數的緩存中開始查找方法 class
- 制在 NSObject 中找到了 class 方法,進行調用,由于消息的接收者是 self,所以返回的是 self 的類,而不是 NSObject。
5. runtime 中常用的方法。
/*動態創建一個類參數分別是要創建的類的父類、類名、額外的內存空間(一般傳0)常考問題:為什么是 Pair,它是是什么意思?答:pair 的意思是一對。創建一個類就是創建 Class 和 Meta-Class */ objc_allocateClassPair(Class _Nullable __unsafe_unretained superclass, const char * _Nonnull name, size_t extraBytes)//注冊一個類(要在類注冊之前添加成員變量) void objc_registerClassPair(Class cls)//銷毀一個類 void objc_disposeClassPair(Class cls)//獲取isa指向的Class Class object_getClass(id obj)//設置isa指向的Class Class object_setClass(id obj, Class cls)//判斷一個OC對象是否為Class BOOL object_isClass(Class cls)//判斷一個Class是否為元類 BOOL class_isMetaClass(Class cls)//獲取父類 Class class_getSuperclass(Class cls)//獲取一個實例變量 Ivar class_getInstanceVariable(Class cls, const char *name)//拷貝實例變量列表(最后需要調用free釋放) Ivar *class_copyIvarList(Class cls, unsigned int *outCount)//動態添加成員變量(已經注冊的類是不能動態添加成員變量的) class_addIvar(Class _Nullable __unsafe_unretained cls, const char * _Nonnull name, size_t size, uint8_t alignment, const char * _Nullable types)//獲取成員變量的相關信息 const char *ivar_getName(Ivar v) const char *ivar_getTypeEncoding(Ivar v)//獲取一個屬性 objc_property_t class_getProperty(Class cls, const char *name)//拷貝屬性列表(最后需要調用free釋放) objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)//動態添加屬性 class_addProperty(Class _Nullable __unsafe_unretained cls, const char * _Nonnull name, const objc_property_attribute_t * _Nullable attributes, unsigned int attributeCount)//動態替換屬性 class_replaceProperty(Class _Nullable __unsafe_unretained cls, const char * _Nonnull name, const objc_property_attribute_t * _Nullable attributes, unsigned int attributeCount)//獲取屬性的一些信息 const char *property_getName(objc_property_t property) const char *property_getAttributes(objc_property_t property)//獲得一個實例方法、類方法 Method class_getInstanceMethod(Class cls, SEL name) Method class_getClassMethod(Class cls, SEL name)//方法實現相關操作 IMP class_getMethodImplementation(Class cls, SEL name) IMP method_setImplementation(Method m, IMP imp) void method_exchangeImplementations(Method m1, Method m2)//拷貝方法列表(最后需要調用free釋放) Method *class_copyMethodList(Class cls, unsigned int *outCount)//動態添加方法 BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)//動態替換方法 IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)//獲取方法相關的信息(帶有copy的需要調用free去釋放) SEL method_getName(Method m) IMP method_getImplementation(Method m) const char *method_getTypeEncoding(Method m) unsigned int method_getNumberOfArguments(Method m) char *method_copyReturnType(Method m) char *method_coayArgumentType(Method m, unsigned int index) 復制代碼6. 你在項目中用過runtime嗎?舉個例子。
- 給分類添加屬性
- 實現一個線程安全的數組
- 使用方法交換去 hook 系統的方法
- 使用class_copyIvarList 實現 json 轉 model
- 使用class_copyPropertyList 實現自定義類的歸檔與 copy
總結
以上是生活随笔為你收集整理的iOS 问题整理04----Runtime的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 空指针异常的处理
- 下一篇: stn,spatial transfor