是以
isa
中通過
初始化
後的
成員
的
值變化過程
,如下圖所示

>iOS 底層原理 文章彙總
本文的主要目的是了解類與isa是如何關聯的
在介紹正文之前,首先需要了解一個概念:
OC對象
的
本質
是什麼?
OC對象本質
在探索oc對象本質前,先了解一個編譯器:
clang
Clang
-
是一個由clang
主導編寫,基于Apple
的LLVM
C/C++/OC的編譯器
- 主要是用于
,将一些底層編譯
成檔案``輸出
,例如c++檔案
輸出成main.m
,其目的是為了更好的觀察main.cpp
的一些底層
及結構
的邏輯,友善了解底層原理。實作
探索對象本質
- 在
中自定義一個類main
,有一個屬性nameLGPerson
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LGPerson
@end
- 通過終端,利用
将clang
編譯成main.m
,有以下幾種編譯指令,這裡使用的是第一種main.cpp
//1、将 main.m 編譯成 main.cpp
clang -rewrite-objc main.m -o main.cpp
//2、将 ViewController.m 編譯成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
//以下兩種方式是通過指定架構模式的指令行,使用xcode工具 xcrun
//3、模拟器檔案編譯
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真機檔案編譯
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
- 打開編譯好的main.cpp,找到
的定義,發現LGPerson
在底層會被編譯成LGPerson
結構體struct
-
中的第一個屬性 其實就是LGPerson_IMPL
,是繼承自isa
,屬于NSObject
,僞繼承的僞繼承
是直接将方式
結構體定義為NSObject
中的LGPerson
,意味着第一個屬性
擁有LGPerson
中的NSObject
。所有成員變量
-
中的第一個屬性LGPerson
等效于NSObject_IVARS
中的NSObject
isa
-
//NSObject的定義
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
//NSObject 的底層編譯
struct NSObject_IMPL {
Class isa;
};
//LGPerson的底層編譯
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
NSString *_name;
};
如下圖所示
通過上述分析,了解了OC對象的本質,但是看到
NSObject
的定義,會産生一個疑問:為什麼
isa
的類型是
Class
?
- 在iOS-底層原理 02:alloc & init & new 源碼分析文章中,提及過
方法的核心之一的alloc
方法,通過檢視這個方法的源碼實作,我們發現,在初始化initInstanceIsa
指針時,是通過isa
類型初始化的,isa_t
- 而在
定義中isa的類型是NSObject
,其根本原因是由于Class
對外回報的是isa
,為了讓開發人員更加類資訊
,需要在清晰明确
傳回時做了一個isa
,類似于類型強制轉換
中的swift
的強轉。源碼中as
的isa
如下圖所示強轉
iOS-底層原理 07:isa與類關聯的原理
總結
是以從上述探索過程中可以得出:
-
其實就是OC對象的本質
結構體
-
中的LGPerson
是繼承自isa
中的NSObject
isa
objc_setProperty 源碼探索
除了
LGPersong
的底層定義,我們發現還有屬性
name
對應的
set
和
get
方法,如下圖所示,其中
set
方法的實作依賴于runtime中的
objc_setProperty
。
可以通過以下步驟來一步步解開
objc_setProperty
的底層實作
- 在objc4-781中全局搜尋
,找到objc_setProperty
的源碼實作objc_setProperty
iOS-底層原理 07:isa與類關聯的原理 - 進入
的源碼實作,其方法的原理就是reallySetProperty
新值retain,舊值release
iOS-底層原理 07:isa與類關聯的原理
總結
通過對
objc_setProperty
的底層源碼探索,有以下幾點說明:
-
方法的目的适用于objc_setProperty
的關聯 上層
方法 以及set
的底層
方法,其本質就是一個set
接口
- 這麼設計的
是,原因
的上層
方法有很多,如果set
方法中,會産生很多的直接調用底層set
,當你想臨時變量
一個sel時,會非常查找
麻煩
- 基于上述原因,蘋果采用了
,擴充卡設計模式(即将底層接口适配為用戶端需要的接口)
提供一個對外
,供上層的set方法使用,接口
調用底層的對内
,使其互相不受影響,即set方法
,或者無論上層怎麼變,下層都是不變的
,主要是達到上下層接口隔離的目的下層的變化也無法影響上層
下圖是上層、隔離層、底層之間的關系
cls 與 類 的關聯原理
在在iOS-底層原理 02:alloc & init & new 源碼分析與iOS-底層原理 05:記憶體對齊原理中分别分析了alloc中3核心的前兩個,今天來探索
initInstanceIsa
是如何将
cls
與
isa
關聯的
在此之前,需要先了解什麼是
聯合體
,為什麼
isa
的類型
isa_t
是使用
聯合體定義
聯合體(union)
構造資料類型的方式有以下兩種:
-
(結構體
)struct
-
(聯合體
,也稱為union
)共用體
結構體
結構體
是指把不
同的資料組合成一個整體
,其
變量
是
共存
的,變量不管是否使用,都會配置設定記憶體。
- 缺點:所有屬性都配置設定記憶體,比較
,假設有4個int成員,一共配置設定了浪費記憶體
位元組的記憶體,但是在使用時,你隻使用了16
位元組,剩餘的4
位元組就是屬于記憶體的浪費12
- 優點:存儲
,容量較大
,且成員之間包容性強
不會互相影響
聯合體
聯合體也是由
不同的資料類型組成
,但其變量是
互斥
的,所有的成員
共占一段記憶體
。而且
共用體
采用了
記憶體覆寫技術
,
同一時刻隻能儲存一個成員的值
,如果
對新的成員指派
,就會将
原來成員的值覆寫掉
- 缺點:,包容性弱
- 優點:所有成員共用一段記憶體,使記憶體的使用更為精細靈活,同時也節省了記憶體空間
兩者的差別
- 記憶體占用情況
- 結構體的
,互相之間各個成員會占用不同的記憶體
沒有影響
- 共用體的
,修改一個成員會所有成員占用同一段記憶體
其餘所有成員影響
- 結構體的
- 記憶體配置設定大小
- 結構體記憶體
所有成員占用的>=
(成員之間可能會有縫隙)記憶體總和
-
占用的共用體
等于記憶體
占用的最大的成員
記憶體
- 結構體記憶體
isa的類型 isa_t
以下是isa指針的類型
isa_t
的定義,從定義中可以看出是通過
聯合體(union)
定義的。
union isa_t { //聯合體
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//提供了cls 和 bits ,兩者是互斥關系
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
isa_t
類型使用
聯合體
的原因也是基于
記憶體優化
的考慮,這裡的記憶體優化是指在isa指針中通過
char + 位域
(即二進制中每一位均可表示不同的資訊)的原理實作。通常來說,
isa指針
占用的記憶體大小是
8
位元組,即
64
位,已經足夠存儲很多的資訊了,這樣可以極大的節省記憶體,以提高性能
從
isa_t
的定義中可以看出:
- 提供了兩個成員,
和cls
,由聯合體的定義所知,這兩個成員是bits
的,也就意味着,當初始化isa指針時,有兩種初始化方式互斥
- 通過
初始化,cls
bits無預設值
- 通過
初始化,bits
cls有預設值
- 通過
- 還提供了一個結構體定義的
,用于存儲類資訊及其他資訊,結構體的成員位域
,這是一個ISA_BITFIELD
定義,有兩個版本宏
(對應ios 移動端) 和__arm64__
(對應macOS),以下是它們的一些宏定義,如下圖所示__x86_64__
iOS-底層原理 07:isa與類關聯的原理 -
有兩個值,表示自定義的類等,占nonpointer
位1
- :
純isa指針
-
:不隻是1
,isa中包含了類對象位址
、對象的類資訊
等引用計數
- :
-
表示has_assoc
位,占關聯對象标志
位1
- :
對象沒有關聯
-
:1
對象存在關聯
- :
-
表示該對象是否有C++/OC的has_cxx_dtor
(類似于析構器
),占dealloc
位1
- 如果
析構函數,則需要有
邏輯做析構
- 如果
,則可以更快的沒有
對象釋放
- 如果
-
表示shiftclx
(類的位址), 即類資訊存儲類的指針的值
-
中占arm64
位,開啟指針優化的情況下,在arm64架構中有33
位用來存儲類指針33
-
中占x86_64
位44
-
-
用于調試器判斷目前對象是magic
還是真的對象
,占沒有初始化的空間
位6
-
是 指對象weakly_refrenced
或者是否被指向
曾經指向一個ARC的弱變量
- 沒有弱引用的對象可以更快釋放
-
标志對象是deallocating
記憶體是否正在釋放
-
表示 當對象has_sidetable_rc
時,則需要引用計數大于10
借用該變量存儲進位
-
(額外的引用計數) — 導尿管表示該extra_rc
,實際上是引用計數值減1對象的引用計數值
- 如果對象的
,那麼引用計數為10
為9extra_rc
- 如果對象的
-
針對兩種不同平台,其
isa
的存儲情況如圖所示
原理探索
- 通過
方法路徑,查找到alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone
,并進入其原理實作initInstanceIsa
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
//初始化isa
initIsa(cls, true, hasCxxDtor);
}
- 進入
方法的源碼實作,主要是初始化isa指針initIsa
該方法的邏輯主要分為兩部分iOS-底層原理 07:isa與類關聯的原理 - 通過
初始化cls
isa
- 通過
初始化bits
isa
- 通過
驗證 isa指針 位域(0-64)
根據前文提及的
0-64
位域,可以在這裡通過
initIsa
方法中證明有isa指針中有這些位域(目前是處于
macOS
,是以使用的是
x86_64
)
- 首先通過main中的
斷點 -->LGPerson
-->initInstanceIsa
--> 走到initIsa
中的else
初始化isa
iOS-底層原理 07:isa與類關聯的原理 - 執行lldb指令:
,得到p newisa
的詳細資訊newisa
iOS-底層原理 07:isa與類關聯的原理 - 繼續往下執行,走到
下一行,表示為newisa.bits = ISA_MAGIC_VALUE;
的isa
成員指派,重新執行lldb指令bits
,得到的結果如下p newisa
通過與前一個newsize的資訊對比,發現isa指針中有一些變化,如下圖所示iOS-底層原理 07:isa與類關聯的原理 iOS-底層原理 07:isa與類關聯的原理 - 其中
是magic
是由于将59
指針位址轉換為isa
,從二進制
(因為前面有4個位域,共占用47位,位址是從0開始)位開始讀取47
位,再轉換為6
,如下圖所示十進制
iOS-底層原理 07:isa與類關聯的原理
- 其中
isa
與 類
的關聯
isa
類
cls
與
isa
關聯
原理
就是
isa
指針中的
shiftcls
位域中存儲了
類資訊
,其中
initInstanceIsa
的過程是将
calloc
指針 和目前的
類cls
關聯起來,有以下幾種驗證方式:
- 【方式一】通過
方法中的initIsa
驗證newisa.shiftcls = (uintptr_t)cls >> 3;
- 【方式二】通過
與isa指針位址
的值ISA_MSAK
來驗證&
- 【方式三】通過runtime的方法
驗證object_getClass
- 【方式四】通過
驗證位運算
方式一:通過 initIsa 方法
- 運作至
前一步,其中newisa.shiftcls = (uintptr_t)cls >> 3;
存儲目前shiftcls
類的值資訊
- 此時檢視
,是cls
類LGPerson
-
指派的邏輯是将shiftcls
進行編碼後,LGPerson
位右移3
iOS-底層原理 07:isa與類關聯的原理
- 此時檢視
- 執行lldb指令
,結果為p (uintptr_t)cls
,再右移三位,有以下兩種方式(任選其一),将得到(uintptr_t) $2 = 4294975720
存儲到536871965
的newisa
中shiftcls
-
p (uintptr_t)cls >> 3
- 通過上一步的結果
,執行lldb指令$2
p $2 >> 3
iOS-底層原理 07:isa與類關聯的原理
-
- 繼續執行程式到
部分,此時執行isa = newisa;
p newisa
與iOS-底層原理 07:isa與類關聯的原理
結果的bits指派
,bits的對比
中有兩處變化位域
-
由cls
,變成了預設值
,将LGPerson
isa與cls完美關聯
-
由 變成了shiftcls
536871965
是以iOS-底層原理 07:isa與類關聯的原理
中通過isa
後的初始化
的成員
,如下圖所示值變化過程
iOS-底層原理 07:isa與類關聯的原理
-
為什麼在shiftcls指派時需要類型強轉?
因為
記憶體
的存儲
不能存儲字元串
,
機器碼
隻能識别
0 、1
這兩種數字,是以需要将其轉換為
uintptr_t
資料類型,這樣
shiftcls
中存儲的
類資訊
才能
被機器碼了解
, 其中
uintptr_t
是
long
為什麼需要右移3位?
主要是由于
shiftcls
處于
isa
指針位址的
中間
部分,前面還有
3
個位域,為了不
影響前面的3個位域
的資料,需要
右移
将其
抹零
。
方式二:通過 isa & ISA_MSAK
- 在方式一後,繼續執行,回到
方法,此時_class_createInstanceFromZone
,執行cls 與 isa已經關聯完成
po objc
- 執行
,得到x/4gx obj
指針的位址isa
0x001d8001000020e9
- 将
指針位址 &isa
(處于ISA_MASK
,使用macOS
中的x86_64
定義),即宏
,得出po 0x001d8001000020e9 & 0x00007ffffffffff8
LGPerson
-
中,ISA_MASK 宏定義的值為arm64
0x0000000ffffffff8ULL
-
中,ISA_MASK 宏定義的值為x86_64
0x00007ffffffffff8ULL
iOS-底層原理 07:isa與類關聯的原理
-
方式三:通過 object_getClass
通過檢視
object_getClass
的源碼實作,同樣可以驗證isa與類關聯的原理,有以下幾步:
- main中導入#import <objc/runtime.h>
- 通過
的api,即runtime
函數擷取類資訊object_getClass
object_getClass(<#id _Nullable obj#>)
- 檢視
函數 源碼的實作object_getClass
iOS-底層原理 07:isa與類關聯的原理 - 點選進入
底層實作object_getClass
iOS-底層原理 07:isa與類關聯的原理 - 進入
的源碼實作getIsa
iOS-底層原理 07:isa與類關聯的原理 - 點選
,進入源碼,可以看到如果是ISA()
類型,執行indexed
流程,反之 執行的是if
流程else
iOS-底層原理 07:isa與類關聯的原理 - 在
流程中,拿到else
的isa
這個位,再bits
,這與方式二中的原理是一緻的,& ISA_MASK
獲得目前的類資訊
- 從這裡也可以
得出 cls 與 isa 已經完美關聯
- 在
方式四:通過位運算
- 回到
方法。通過_class_createInstanceFromZone
得到x/4gx obj
的存儲資訊,目前類的資訊存儲在obj
指針中,且isa中的isa
此時占shiftcls
位(因為處于44
環境)macOS
iOS-底層原理 07:isa與類關聯的原理 - 想要
中間的讀取
位44
,就需要經過類資訊
,将右邊位運算
位,和左邊除去3
位以外的部分都44
,其抹零
的。其位運算過程如圖所示,其中相對位置是不變
即為需要shiftcls
的讀取
類資訊
iOS-底層原理 07:isa與類關聯的原理 - 将
位址isa
位:右移3
,得到p/x 0x001d8001000020e9 >> 3
0x0003b0002000041d
- 在将得到的
位:0x0003b0002000041d``左移20
,得到p/x 0x0003b0002000041d << 20
0x0002000041d00000
- 為什麼是
位?因為先左移20
移了右
位,相當于向右3
,而偏移了3位
需要左邊
的位數有抹零
位,是以一共需要移動17
位20
- 為什麼是
- 将得到的
再0x0002000041d00000
位:右移17
得到新的p/x 0x0002000041d00000 >> 17
0x00000001000020e8
- 将
- 擷取cls的位址 與 上面的進行驗證 :
也得出p/x cls
,是以由此可以證明 cls 與 isa 是關聯的0x00000001000020e8
iOS-底層原理 07:isa與類關聯的原理