最近準備找工作,在網上和麵試中收集了很多面試題。利用一兩個月時間做出整理和解答,目的是鞏固自己的基礎知識以及掌握這些問題該如何去回答。
本篇文章要回答的問題
- OC 物件的實現機制
- 一個 NSObject 物件佔用多少記憶體?
- 建立一個物件的時候,比如呼叫 alloc 方法,系統怎麼知道應該分配多大空間的?
- 一個 OC 物件的的儲存空間是怎樣的?或者說物件資料分別是存在哪裡的?
- 物件的 isa 指標指向哪裡?
- 你使用過 runtime 中的哪些方法?
1. OC 物件的實現機制
答:Objective-C的物件都是基於C/C++的結構體來實現的。
Objective-C 程式碼底層實現其實都是C/C++程式碼,
C/C++程式碼又會被編譯器轉成彙編程式碼,
最終彙編程式碼會被轉成機器語言執行在我們的裝置上。
思考:我們有沒有什麼辦法來驗證我們這個答案呢?
既然 Objective-C 程式碼是被編譯器轉成 C/C++ 程式碼的,
那麼我們就可以使用編譯器工具將它轉換成功,
看看它轉成C/C++之後究竟是什麼樣子!
我們使用Xcode自帶的clang編譯器來進行轉換:
在終端進入將被轉換成C/C++檔案的.m檔案所在的目錄,執行命令(圖一):
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC原始檔 -o 輸出的CPP檔案
現在就可以在當前目錄檢視到.cpp檔案了(圖二),在Xcode中開啟它即可檢視C/C++程式碼。
我們在這個檔案中搜尋 NSObject,可以找到這樣一段程式碼:(圖三)
struct NSObject_IMPL {
Class isa;
}
這就是 NSObject Implementation,就是一個C/C++的結構體!
複製程式碼
圖一
圖二 圖三2. 一個 NSObject 物件佔用多少記憶體?
結論:
系統分配給了 NSObject 物件 16 個位元組的空間,但實際上它真正利用的空間只有 8 個位元組。(64bit)
分析:
上面我們已經知道了一個 NSObject 物件實際上就是一個 C/C++ 的結構體,並且這個結構體裡面只有一個 isa,我們可以看到它的型別是 Class。那麼現在的問題就是這個 Class 型別是什麼,它佔多少記憶體?
我們在 Xcode 中檢視 NSObject.h 檔案。
按住 command + control點選 Class。 在這裡我們可以看到 Class 其實是一個指標。指標在64位機器中佔用的是8個位元組,在32位機器中佔用的是4個位元組。這裡我們僅考慮64位機器。
到這裡答案就已經浮出水面了:因為 isa 要佔用8個位元組,所以 NSObject_IMPL 結構體需要佔用8個位元組,所以一個 NSObject 物件在記憶體中需要佔用8個位元組的空間,注意我這裡的措辭,是需要佔用8個位元組,而不是說它就佔用了8個位元組,接下來會繼續說明。
現在我們在.m檔案中編寫這麼一段程式碼:
NSObject *obj = [[NSObject alloc] init];
複製程式碼
我們現在要解決的問題就是: obj 這個指標指向的那一塊的記憶體佔用的位元組數是多少。
我們將兩個函式來探究這個問題,這兩個函式看起來像是獲取物件的記憶體大小:
class_getInstanceSize()
malloc_size()
繼續編寫程式碼:
#import <objc/runtime.h>
#import <malloc/malloc.h>
NSObject *obj = [[NSObject alloc] init];
NSLog(@"class_getInstanceSize - %zd", class_getInstanceSize([NSObject class]));
NSLog(@"malloc_size - %zd", malloc_size((__bridge const void *)(obj)));
複製程式碼
執行之後可以看到輸出的結果:
很奇怪,一個是8,一個是16。這兩個函式看起來都是用來獲取 NSObject 物件的記憶體大小的,為什麼會不一樣呢?我們要探究一下class_getInstanceSize()
的內部究竟是如何實現的才能解答這個問題。
先在蘋果的開源網站 https://opensource.apple.com/tarballs/objc4/ 下載最新的 OC 底層原始碼(數字最大就是最新的),解壓後用 Xcode 開啟專案,全域性搜尋class_getInstanceSize
,找到它的實現方法:
alignedInstanceSize
可以看到alignedInstanceSize
的實現,它上面寫了一句註釋說明這個方法返回的是類的ivar(成員變數)的大小。我們上面已經確定了 NSObject 轉成的 C/C++ 結構體中的成員變數 isa 佔的位元組數是8,這也與控制檯 log 的資料 8 是吻合的。 再根據函式的名稱word_align
,我們可以得出這樣的結論:
[6. 你使用過 runtime 中的哪些方法?] class_getInstanceSize() 方法的作用是獲取類的例項物件的成員變數記憶體對齊後所佔用的大小。(接下來會用到很多runtime的方法,每個方法前面我都會用中括號打上標記,解答這道面試題的時候就可以搜尋這個標記。) 關於記憶體對齊稍後會有簡要說明。
malloc_size
是一個C語言函式,它的作用是獲得指標所指向的記憶體的大小。
所以 obj 所指向的記憶體佔用的位元組數是16,但是實際上它真正用到的位元組只有8個。
16 個位元組是系統分配的,為什麼系統會分配16個位元組給它呢,下個問題中的分析中會有詳細的解釋。
我們對這個問題做一下延伸,建立一個 Person 類,Person 例項物件佔用多少記憶體空間,以下程式碼輸出什麼?
我們可以先將程式碼轉成C/C++程式碼,檢視 Person 的底層實現: 我們可以知道NSObject_IVARS
佔8個位元組,_age
佔4個位元組。
根據結構體記憶體對齊規則,我們可以得出Person_IMPL
結構體在對齊後佔用16個位元組,所以class_getInstanceSize([Person class])
為16。執行後發現列印的兩個值都是16。
再給 Person 新增兩個成員變數
執行後發現列印的值分別是24,32。前面我們的指示完全可以解釋第一個值為什麼是24,現在我們還無法理解為什麼系統會給 Person 物件分配32個位元組。這裡還需要再補充一點知識:作業系統對記憶體分配都有一定的規則,在iOS中,系統給物件分配的記憶體的大小必須是16的整數倍。
根據這個例子來說明,我們計算出來Person物件實際需要8(nsobject_ivars)+4+4+1一共17個位元組,進行記憶體對齊之後它需要24個位元組,再經過iOS作業系統管理之後,分配給它了32個位元組。
看完上邊的內容之後,我們可以對簡單的記憶體計算問題進行處理了。還有更為複雜的問題,比如繼承關係再複雜一些、結構體巢狀、成員變數的型別更多一些、成員變數的順序打亂,遇到這些複雜的情況,需要根據結構體記憶體對齊規則去一點點地計算對齊後的記憶體大小,再根據iOS的規則去計算系統實際分配的大小。
3. 建立一個物件的時候,比如呼叫 alloc 方法,系統怎麼知道應該分配多大空間的?
解答:
系統通過底層的instanceSize()
函式計算應該分配的空間。
分析:
解答這個問題必須閱讀原始碼,看看 alloc 究竟是如何實現的。
在剛剛下載的原始碼中搜尋 alloc,找到 alloc 的實現:
_objc_rootAlloc
:
按照同樣的方法繼續點,最終會來到_class_createInstanceFromZone
這裡:
這段程式碼中有一個size
,這個 size 就是系統要分配的空間的大小。
我們可以看到:
size_t size = cls->instanceSize(extraBytes);
複製程式碼
在點的過程中我們也可以看到 extraBytes 這個引數的值是0。
我們再往裡面繼續看 instacneSize()
的實現
instanceSize()
函式來計算記憶體,這裡也解釋了上個問題的疑問。
4. 一個 OC 物件的的儲存空間是怎樣的?或者說物件資料分別是存在哪裡的?
答案:
- instance 物件儲存的是 isa 指標、其它成員變數;
- class 物件儲存的是 isa 指標、superclass 指標、類的屬性資訊、類的物件方法資訊、類的協議資訊、類的成員變數資訊;
- meta-class 物件儲存的是 isa 指標、superclass指標、類的類方法資訊
分析:
Objective-C 中的物件分為三種:
- instance 物件 (例項物件)
- class 物件 (類物件)
- meta-class (元類物件)
instance 物件
instance 物件就是通過類 alloc 出來的物件,每次呼叫 alloc 都會產生新的 instance 物件。
instance 物件在記憶體中儲存的資訊包括
- isa 指標
- 其它成員變數
class 物件
每個類在記憶體都有且只有一個 class 物件。
class 物件在記憶體中儲存的資訊包括
- isa 指標
- superclass 指標
- 類的屬性資訊(@property)、類的物件方法資訊(instace method)
- 類的協議資訊(protocol)、類的成員變數資訊(ivar)(這裡的成員變數資訊指的不是成員變數的值,是成員變數的描述資訊,比如型別,名稱等)
獲取 class 物件的方式有三種:
Class objectClass = [NSObject class];
Class objectClass = [obj class];
Class objectClass = object_getClass(obj); //Runtime API
如果在 Xcode 中列印這三個物件的地址,你會發現他們都是同一個地址,這也就說明了 class 物件在記憶體中有且只有一份。
meta-class 物件
用於描述類相關的一些東西。每個類在記憶體中有且只有一個 meta-class 物件。
獲取 meta-class 物件:
Class objectMetaClass = object_getClass([NSObject class]);
meta-class 物件和 class 物件的記憶體結構是一樣的(它們的型別都是 Class 型別),但是用途不一樣。meta-class 物件在記憶體中儲存的資訊包括:
- isa 指標
- superclass 指標
- 類的類方法資訊(class method)
這裡介紹一下object_getClass()
的作用:
[6. 你使用過 runtime 中的哪些方法?] object_getClass() 返回引數的 isa 指標指向的物件。
通過檢視原始碼,我們就可以不難發現這點。關於 isa 指標,在下部分會有詳細介紹。
5. 物件的 isa 指標指向哪裡?
答案:
- instance 物件的 isa 指向 class 物件
- class 物件的 isa 指向 meta-class 物件
- meta-class 物件的 isa 指向基類的 meta-class 物件
分析:
- intance 的 isa 指向 class
- class 的 isa 指向 meta-class
- meta-class 的 isa 指向基類的 meta-class
- class 的 superclass 指向父類的 class。如果沒有父類,superclass 為 nil
- meta-class 的 superclass指向父類的 meta-class。基類的 meta-class 的 superclass 指向基類的 class
- instance 呼叫物件方法的軌跡:通過 instance 的 isa 找到 class,如果 class 的方法列表中沒有這個方法,就通過 class 的 superclass 找到父類的 class
- class 呼叫類方法的軌跡:通過 class 的 isa 找到 meta-class,如果 meta-class 的方法列表中沒有這個方法,就通過 meta-class 的 superclass 找到父類的 meta-class