本篇文章主要解決以下問題
- 說說你對 runtime 的理解。
- 你瞭解 isa 指標嗎?
- 類的結構是怎樣的?
- class_rw_t 與 class_ro_t 的區別?
- runtime 中,SEL 和 IMP 的區別?
- runtime 如何通過 selector 找到對應 IMP 的地址?
- objc 的訊息傳送機制是怎樣的?
- objc 中向一個 nil 物件傳送訊息,會發生什麼?
- 訊息轉發到 forwordinginvacation 方法,如何拿到返回值?
- 什麼時候會報 unrecognized selector 異常?
- runtime 中常用的方法。
- runtime怎麼新增屬性、方法等
- 使用runtime Associate方法關聯的物件,需要在主物件dealloc的時候釋放麼?
- 什麼是method swizzling?
- 能否向編譯後得到的類中增加例項變數?能否向執行時建立的類中新增例項變數?為什麼?
- 你在專案中用過runtime嗎?舉個例子。
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); // hack Class 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
不能新增成員變數呢?- 為啥能新增方法呢?
- 為啥不能向編譯後的類中新增例項變數呢?
- 為啥
- 前面介紹過了,
類的結構說完了,你能否回答這兩道面試題?
- 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:
返回不為 nil,則會呼叫forwardInvocation:
方法。- 來到這個方法後,就算什麼都不處理,執行程式也不會崩潰了。
- 比如你這個方法裡面什麼都不寫
- 比如你只寫一句
NSLog(@"Hello world!");
- 還可以處理這個引數
anInvocation
。 NSInvocation
封裝了方法呼叫者、方法名、方法的簽名。- 比如可以修改方法的呼叫者
[anInvocation invokeWithTarget:[[Cat alloc] init]]
- 比如拿到方法的返回值型別
- 上邊提到
NSInvocation
中有方法、呼叫者、方法簽名 - 所以只要拿到方法簽名,就能找到返回值的型別
NSMethodSignature *sig = [anInvocation valueForKey:@"_signature"]; //拿到方法簽名 const char *returnType = sig.methodReturnType;//這個字串中的第一個字元就是返回值的型別 複製程式碼
- 上邊提到
- 來到這個方法後,就算什麼都不處理,執行程式也不會崩潰了。
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)
複製程式碼