iOS之深入解析对象isa的底层原理
生活随笔
收集整理的這篇文章主要介紹了
iOS之深入解析对象isa的底层原理
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
對象本質
一、NSObject 本質
OC代碼的底層實現實質是 C/C++代碼 ,繼而編譯成匯編代碼,最終變成機器語言。
① clang C/C++ 編譯器
- Clang 是?個 C 語?、C++、Objective-C 語?的 輕量級編譯器 ,源代碼發布于 BSD 協議下。
- Clang 將?持其 普通lambda表達式 ,返回類型的簡化處理以及更好的 處理constexpr關鍵字 。
- Clang 是?個由 Apple 主導編寫,基于 LLVM的C/C++/Objective-C編譯器 。
- 2013年4?,Clang 已經全??持 C++11標準 ,并開始實現 C++1y特性 (也就是 C++14,這是 C++ 的下?個?更新版本)。Clang 將?持其 普通lambda表達式 ,返回類型的簡化處理以及更好的處理 constexpr關鍵字 。
- Clang 是?個 C++ 編寫,基于 LLVM 發布于 LLVM BSD 許可證下的 C/C++/Objective-C/Objective-C++ 編譯器。它與 GNU C 語?規范?乎完全兼容(當然也有部分不兼容的內容,包括編譯命令選項也會有點差異),并在此基礎上增加了額外的語法特性,?如 C 函數重載(通過 attribute((overloadable) )來修飾函數),其?標(之?)就是超越 GCC 。
- clang -rewrite-objc main.m -o main.cpp 把?標?件編譯成c++?件。
- UIKit報錯問題:Xcode安裝的時候順帶安裝了 xcrun 命令, xcrun 命令在 clang 的基礎上進?了?些封裝。
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o
main-arm64.cpp (模擬器) - xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o mainarm64.cpp (?機)
② 運用 clang 將目標文件編譯成 cpp(C++文件)
- 在main.m中添加以下代碼:
- 打開終端,進入main.m所在的文件夾,通過 clang rewirte-objc main.m -o main.cpp 或 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 命令,生成cpp文件。
- 然后回到main.m的文件中,打開cpp文件,搜索“YDWHandsomeBoy”,即可看到,對象YDWHandsomeBoy在底層被編譯成了一個結構體:
- 可以看到上面的c++代碼中有個NSObject_IMPL NSObject_IVARS這個東西,然后搜索NSObject_IMPL可以發現:
- 由此可以得出:
- NSObject的底層實現實質是一個 結構體 ,而結構體中的成員 isa是Class類型 ;
- 通過源碼 typedef struct objc_class *Class 可知它是一個指針,在64為環境下指針占8個字節,而在32位機下是占4個字節,因此該結構體占8個字節(因為該結構體只有一個成員)。
二、NSObject 對象內存
- 初始化一個NSObject并打印:
- 可知NSObject對象占16個字節。那么與上文中NSObject結構體中占8個字節是否沖突?再次打印:
- 不難發現:獲取NSObject類的實例對象的成員變量所占用的(內存對齊之后)大小,顯示確實為8個字節。
- 在objc的源碼中找到 class_getInstanceSize方法,發現它返回的是 cls->alignedInstanceSize() ,對它的描述為Class’s ivar size rounded up to a pointer-size boundary意指 返回成員變量占據的大小 。因此創建一個NSObject對象需要分配16個字節,只是真正利用的只有8個字節,即isa這個成員的大小。
- 查看allocWithZone的源碼發現它最底層的創建實例的方法實際上是調用了C語言的 calloc方法 ,在該方法中,規定若 分配的字節不滿16將把它分配為16個字節 。
三、若一個YDWHandsomeBoy類繼承自NSObject類,那么YDWHandsomeBoy類的對象占多少內存?
- 新建YDWHandsomeBoy類,添加成員變量,在main中實現以下代碼:
- 通過以上代碼可以看出:
- 若一個類繼承自另一個類,則它的底層會 將父類的成員變量放在結構體的最前面,此后依次放置本類的成員變量 。
- 而從之前的分析可知,NSObject_IMPL的本質就是一個 裝有成員變量isa的結構體 ,因此,YDWHandsomeBoy類對象所占的內存為isa的內存8加上YDWHandsomeBoy類成員變量所占的空間,若不滿16個字節,會強制分配到16個字節。
- 由于 內存對齊 的規定,結構體的最終大小必須是 最大成員的倍數 。
isa
一、isa 簡介
- alloc初始化時不僅 創建對象并且分配內存 ,同時 初始化 isa 指針屬性。
- Objective-C 對象在底層本質上是 結構體 ,所有的對象里面都會包含有一個 isa ,isa 的定義是一個 聯合體 isa_t ,isa_t 包含了 當前對象指向類的信息 。
- isa 是一個 聯合體,而這其實是從內存管理層面來設計的,因為聯合體是 所有成員共享一個內存,聯合體 內存的大小取決于內部成員內存大小最大的那個元素 。
- 對于 isa 指針來說,就不用額外聲明很多的屬性,直接在 內部的 ISA_BITFIELD 保存信息 。
- 由于聯合體 屬性間互斥 ,所以 cls 和 bits 在 isa 初始化流程時是在 兩個分支 中被賦值的。
- isa_t 是一個聯合體,聯合體的特性就是 內部所有的成員共用一塊內存地址空間 ,也就是說isa_t、cls、bits會共用同一塊內存地址空間,這塊 內存地址空間大小取決于最大長度內部成員的大小 ,即64位8字節,由此可以知道isa 的所占的內存空間大小為8字節。isa_t聯合體如下:
二、isa 結構
isa 作為一個聯合體,有一個結構體屬性為 ISA_BITFIELD ,其大小為 8 個字節,也就是 64 位。基于__arm64__ 和 x86 64 架構如下:
# if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL # define ISA_BITFIELD \uintptr_t nonpointer : 1; \uintptr_t has_assoc : 1; \uintptr_t has_cxx_dtor : 1; \uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \uintptr_t magic : 6; \uintptr_t weakly_referenced : 1; \uintptr_t deallocating : 1; \uintptr_t has_sidetable_rc : 1; \uintptr_t extra_rc : 19 # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18)# elif __x86_64__ # define ISA_MASK 0x00007ffffffffff8ULL # define ISA_MAGIC_MASK 0x001f800000000001ULL # define ISA_MAGIC_VALUE 0x001d800000000001ULL # define ISA_BITFIELD \uintptr_t nonpointer : 1; \uintptr_t has_assoc : 1; \uintptr_t has_cxx_dtor : 1; \uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \uintptr_t magic : 6; \uintptr_t weakly_referenced : 1; \uintptr_t deallocating : 1; \uintptr_t has_sidetable_rc : 1; \uintptr_t extra_rc : 8 # define RC_ONE (1ULL<<56) # define RC_HALF (1ULL<<7)# else # error unknown architecture for packed isa # endif- union-isa_t 存儲分配如下:
- nonpointer: 表示是否對 isa 指針 開啟指針優化 :
0: 純 isa 指針;
1: 不止是類對象地址, isa 中包含了類信息、對象的引用計數等。 - has_assoc: 關聯對象標志位 ,0 沒有,1 存在。
- has_cxx_dtor: 該對象 是否有 C++ 或者 Objc 的析構器 ,如果有析構函數,則需要做析構邏輯,如果沒有,則可以更快的釋放對象。
- shiftcls: 存儲類指針的值 ,開啟指針優化的情況下,在 arm64 架構中有 33 位用來存儲類指針。
- magic: 用于 調試器判斷當前對象是真的對象還是沒有初始化的空間 。
- weakly_referenced: 標志對象是否被指向或者曾經 指向一個 ARC 的弱變量 ,沒有弱引用的對象可以更快釋放。
- deallocating: 標志對象 是否正在釋放內存 。
- has_sidetable_rc: 當對象引用技術大于 10 時,則需要借用該變量 存儲進位 。
- extra_rc: 當表示該 對象的引用計數值 ,實際上是引用計數值減 1。 例如,如果對象的引用計數為 10,那么 extra_rc 為 9;如果引用計數大于 10, 則需要使用到has_sidetable_rc。
三、isa 初始化
① isa 源碼實現
- 在objc的源碼中有isa的初始化方法:
- 由于nonpointer傳入的是true,SUPPORT_INDEXED_ISA定義為0,所以可以對這段代碼簡化一下:
② isa 初始化數據
- 可以看到對bits的賦值ISA_MAGIC_VALUE = 0x001d800000000001ULL,將此轉為二進制,在結合isa_t的結構得出如下的isa_t的初始數據圖:
- 對 isa 賦值ISA_MAGIC_VALUE初始化實際上只是設置了indexed和magic兩部分的數據:
- indexed表示 isa_t 的類型 :0表示 raw isa,也就是沒有結構體的部分,訪問對象的 isa 會直接返回一個指向 cls 的指針,也就是在 iPhone 遷移到 64 位系統之前時 isa 的類型;1則表示當前 isa 不是指針,但是其中也有 cls 的信息,只是其中關于類的指針都是保存在 shiftcls 中。
- magic 用于 調試器判斷當前對象是否有初始化空間 。
- 在設置indexed和magic的值后會對has_cxx_dtor進行設值。has_cxx_dtor表示該對象是否有 C++ 或者 Objc 的析構器 ,如果有析構函數,則需要做析構邏輯,如果沒有,則可以更快的釋放對象。
- 將當前對象的類指針存放在 shiftcls 中:
- 對cls的地址右移動3位的目的是 為了減少內存的消耗 ,因為類的指針需要按照8字節對齊,也就是說類的指針的大小必定是8的倍數,其二進制后三位為0 ,右移三位抹除后面的3位0并不會產生影響。
③ isa的初始化流程示意
四、isa 關聯對象和類
isa 是對象中的第一個屬性,這是在繼承的時候發生的,要早于對象的成員變量,屬性列表,方法列表以及所遵循的協議列表。在 alloc 底層,有一個方法叫做 initIsa ,這個方法的作用就是 初始化 isa 聯合體位域 。上文中我們已經看到了這個方法:
newisa.shiftcls = (uintptr_t)cls >> 3;① cls 存儲到 isa
- isa 剛初始化時,還沒有被賦值,bits 全為空值,p newisa 如下:
- 繼續向下執行,當斷點執行到如下位置的時候,bits 會被賦上默認值(nonpointer = 1,magic = 59),繼續p newisa如下:
- 為什么 magic = 59 呢?其實,通過計算器可以轉換算出:0x001d800000000001 = 59,上面我們已經 po 0x001d800000000001ULL 了,可以看到這個默認值。
- 繼續輸出 bits 二進制、輸出 cls 指針二進制可以看到:在未設置 shiftcls 時,bits 從右到左 [3, 46] 位都是0。如下:
- 為什么要右移三位?在 Objective-C 中,類的指針是按照字節(8 bits)對齊的,也就是說類指針地址轉化成十進制后,都是8的倍數,也就是說,類指針地址轉化成二進制后,后三位都是0。既然是沒有意義的0,那么在存儲時就可以省略,用節省下來的空間存儲一些其他信息。
- 當 bits 被賦值之后,如下:
- 可以看到,現在的 bits 的 [3, 46] 位正好是之前 cls 指針右移三位的內容。
② isa 關聯對象和類
- 通過 LLDB 進行調試打印,就可以知道一個對象的 isa 會關聯到這個對象所屬的類:
- LLDB 調試的時候左移右移操作其實很好理解,先觀察 isa 的 ISA_BITFIELD 位域的結構:ISA_BITFIELD 的前 3 位是 nonpointer、has_assoc、has_cxx_dtor ,中間 44 位是 shiftcls ,后面 17 位是剩余的內容,同時因為 iOS 是 小端模式 ,那么就需要去掉右邊的 3 位和左邊的 17位,所以就會采用 >> 3 << 3 然后 << 13 >> 13 的操作。
五、isa 走位分析
① class object(類對象)/ metaclass(元類)
- Object-C的對象其本質就是結構體,前面也分析了每一個對象都會有一個isa。同時類的本質也是一個結構體,而且是繼承自objc_object的。
- 在objc_class中也有isa:
- class_isMetaClass用于判斷Class對象是否為元類,object_getClass用于獲取對象的isa指針指向的對象。
- 我們知道:對象可以創建多個,但是類是否可以創建多個呢?其實答案是否定的,類在內存中只會存在一份。
- 通過 LLDB 調試打印,其實可以發現:類的內存結構里面的第一個結構打印出來還是 Boy,那么是不是就意味著 對象 ->類->類 這樣的死循環呢?這里的第二個類其實是 元類,是由系統創建的,這個元類無法被我們實例化。
- 一個實例對象通過class方法獲取的Class就是它的isa指針指向的類對象,而類對象不是元類,類對象的isa指針指向的對象是元類。關系如下:
② isa 走位
- 官方的經典 isa 走位圖:
- 實例對象的isa指向的是類;
- 類的isa指向的元類;
- 元類指向根元類;
- 根元類指向自己;
- NSObject的父類是nil,根元類的父類是NSObject。
- LLDB 調試打印:
六、對象的本質 isa
- OC 對象的本質就是一個結構體,在 libObjc 源碼的 objc-private.h 源文件中可以看到:
- 對于對象所屬的類來說,也可以在 objc-runtime-new.h 源文件中找到(即 objc_class 內存中第一個位置是 isa,第二個位置是 superclass):
- 對象在底層其實是一個結構體 objc_object ,而Class 在底層也是一個結構體 objc_class 。
總結
以上是生活随笔為你收集整理的iOS之深入解析对象isa的底层原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS之深入解析YYImage图片处理的
- 下一篇: iOS之深入解析AFNetworking