iOS 類與對象底層原理
文章目錄
- iOS 類與對象底層原理
- 探索對象本質
- objc_setProperty 源碼
- cls與類的關聯原理
- 聯合體
- isa的類型isa_t
- 原理探索
- initIsa方法
- 通過setClass方法中的shiftcls來驗證綁定的一個流程
- 通過 isa & ISA_MSAK
- 通過object_getClass
- 通過位運算
- 類&類的結構
- 從終端調試開始認識調用邏輯
- 什么是元類
- 小結
- NSObject到底有幾個?
- 類存在幾份
- isa走位繼承圖
- object_class & objc_object
- 這里的objc_class 與objc_object有什么關系?
- objc_object與對象的關系
- 總結
- 類結構分析
- 類信息中有哪些內容
- 計算cache類的內存大小
- bits
- 探索property_list(屬性列表)
- 探索methods_list
- 從編譯期開始介紹class_ro_t,class_rw_t,class_rw_ext_t
- class_ro_t
- class_rw_t
- 區別
- class_rw_ext_t
- 總結
探索對象本質
- 在main中自定義一個類GCObject,有一個屬性name
@interface GGObject : NSObject
@property (nonatomic, copy) NSString* name;
@end@implementation GGObject@end
通過終端將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
在編譯之后,我們可以開始看這里的類的一個定義:
//NSObject的定義
@interface NSObject <NSObject> {Class isa OBJC_ISA_AVAILABILITY;
}//NSObject 的底層編譯
struct NSObject_IMPL {Class isa;
};struct GGObject_IMPL {struct NSObject_IMPL NSObject_IVARS; // 這里NSString *_name;
};static NSString * _I_GGObject_name(GGObject * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_GGObject$_name)); } //get方法
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);static void _I_GGObject_setName_(GGObject * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct GGObject, _name), (id)name, 0, 1); } //set方法
這里我們可以看到NSObject的定義會產生一個問題,isa的類型居然是Class,這里我們定義alloc放的核心之一的initInstance方法,通過拆看這個方法的源碼實現,我們發現在初始化isa指針的時候,是通過isa_t類型初始化的。
這里為了讓開發人員更加清晰明確,需要在isa返回的時候做了一個類型強制轉換.
這里其實我們可以看出:
- OC對象的本質就是結構體
- CGObject中的isa就是繼承自NSObject中的isa
objc_setProperty 源碼
在這之前我們可以看到這里的上面的一個set方法:
static void _I_GGObject_setName_(GGObject * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct GGObject, _name), (id)name, 0, 1); } //set方法
這里面可以看到他里面調用了一個objc_setProperty
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);bool mutableCopy = (shouldCopy == MUTABLE_COPY);reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy); //跳轉到真正的一個調用方法
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{if (offset == 0) {object_setClass(self, newValue);return;}id oldValue;id *slot = (id*) ((char*)self + offset);if (copy) {newValue = [newValue copyWithZone:nil];} else if (mutableCopy) {newValue = [newValue mutableCopyWithZone:nil];} else {if (*slot == newValue) return;newValue = objc_retain(newValue); // 新增retain}if (!atomic) {oldValue = *slot;*slot = newValue;} else {spinlock_t& slotlock = PropertyLocks[slot];slotlock.lock();oldValue = *slot;*slot = newValue; slotlock.unlock();}objc_release(oldValue); //release 舊值
}
小結
- objc_setProperty 方法的目的適用于關聯上層的set方法以及底層的set方法,其本質就是一接口
- 這么設計的原因是,上層的set方法很多,如果直接調用底層set方法中,會有很多臨時變量。當你吵著一個set的時候非常麻煩
- 這里其實采用了一種設計模式:適配器設計模式(即將底層接口適配位客戶段需要的接口),對外提供一個接口共上層的set方法使用,對內調用底層的set方法。讓他們兩者互不影響)
cls與類的關聯原理
聯合體
聯合體是指把不同的數據合成一個整體,但是所有變量是互斥的,所有的成員共占一段內存
- 缺點:包容性弱
- 優點:成員共用一段內存,是內存的使用更為精細靈活,同時節省了一個內存空間
共用體的所有成員占用同一段內存,修改一個成員影響其他所有成員變量。
結構體各個成員會占用不同的內存,互相之間沒有影響。
結構體內存>=所有成員占用的內存總和。
共用體占用的內存等于最大成員占用的內存
isa的類型isa_t
以下isa指針的類型isa_t
的定義,從定義中可以看到是通過聯合體來定義的。
union isa_t {isa_t() { } // 默認的一個構造函數isa_t(uintptr_t value) : bits(value) { }uintptr_t bits; // 這個和cls是一個互斥類型private:// Accessing the class requires custom ptrauth operations, so// force clients to go through setClass/getClass by making this// private.Class cls; // 這里把cls指針私有化,禁止直接訪問cls,必須通過getClass()和setClass()方法操作public:
#if defined(ISA_BITFIELD)struct {ISA_BITFIELD; // defined in isa.h 這里是一個位域設置一個結構體};bool isDeallocating() {return extra_rc == 0 && has_sidetable_rc == 0; //判斷位域對應的一個標志位}void setDeallocating() {extra_rc = 0; has_sidetable_rc = 0;}
#endifvoid setClass(Class cls, objc_object *obj); //提供set方法獲取cls指針更加安全Class getClass(bool authenticated); //提供get方法獲取cls指針Class getDecodedClass(bool authenticated); 。。
}; //這里是因為C++支持里一個聯合體方法,c++11以上支持聯合體里面放方法,這里的方法并會被存儲到我們的一個代碼段,不會影響聯合體的一個內存
我們現在來簡單分析一個這個isa_t
的一個定義可以看出:
- 提供了兩個成員變量
cls
和bits
由聯合體的一個定義所知,這里兩個成員是互斥的,所以這里有倆種初始化方式:- 通過cls初始化
- 通過bits初始化
- 這里提供了一個位域的內容,用于存儲類信息以及其他信息,結構體的成員
ISA_BITFIEld
這里我們就看一下他是怎么定義位域的:
有兩個版本 __arm64__
(對應ios 移動端) 和 __x86_64__
(對應macOS)
# define ISA_BITFIELD // __x86_64__ \uintptr_t nonpointer : 1; \ //是否開啟isa指針開啟指針優化 uintptr_t has_assoc : 1; \//是否有關聯對象 uintptr_t has_cxx_dtor : 1; \ // 是否有C++相關實現 uintptr_t shiftcls : 44; \/*MACH_VM_MAX_ADDRESS 0x7fffffe00000存儲類信息*/ uintptr_t magic : 6; \//判斷對象是真對象還是未初始化對象uintptr_t weakly_referenced : 1; \//對象是否被指向一個或者曾經指向ARC的一個弱變量uintptr_t unused : 1; \ //未被使用uintptr_t has_sidetable_rc : 1; \ //是否外掛一個sidetbaleuintptr_t extra_rc : 8 //額外的引用計數define ISA_BITFIELD //__arm64__ \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 unused : 1; \uintptr_t has_sidetable_rc : 1; \uintptr_t extra_rc : 19
nonpointer
有兩個值,表示自定義的類等占1位- 0:純isa指針
- 1:不只是類對象地址,isa中包含了類對象,對象的引用計數等
has_assoc
表示關聯對象標志位- 0:沒有關聯對象
- 1: 存在關聯對象
has_cxx_dtor
表示是否有C++/OC析構器:類似于dealloc,占一位- 如果有則需要做析構邏輯
- 沒有就直接可以釋放對象
shifcls
白哦是存儲類的指針的值arm64
占33位x86_64
中占44位
magic
用于調試器判斷當前對象是真的對象還是沒有初始化的空間,6位weakly_refrenced
是指對象是否被指向或者曾經指向一個ARC的弱變量has_sidetable_rc
表示 當對象引用計數大于10
時,則需要借用該變量存儲進位
extra_rc
(額外的引用計數) ,表示該對象的引用計數值
,實際上是引用計數值減1
原理探索
我們在之前講過alloc的一個調用流程,在里面有一個initInstanceIsa
,現在我們來看這部分內容:
inline voi
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{ASSERT(!cls->instancesRequireRawIsa());ASSERT(hasCxxDtor == cls->hasCxxDtor());initIsa(cls, true, hasCxxDtor);
}
initIsa方法
這里面也和之前,有一個中間層調用這部分內容,也就是真正開始綁定的一部分內容
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ //這里ASSERT(!isTaggedPointer()); isa_t newisa(0);if (!nonpointer) {newisa.setClass(cls, this); // 初始化cls指針,用之前的那個set方法} else {ASSERT(!DisableNonpointerIsa);ASSERT(!cls->instancesRequireRawIsa());#if SUPPORT_INDEXED_ISA //即isa由cls定義ASSERT(cls->classArrayIndex() > 0);newisa.bits = ISA_INDEX_MAGIC_VALUE;// isa.magic is part of ISA_MAGIC_VALUE// isa.nonpointer is part of ISA_MAGIC_VALUEnewisa.has_cxx_dtor = hasCxxDtor;newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else //bits指向的流程newisa.bits = ISA_MAGIC_VALUE; // 對bits進行一個賦值// isa.magic is part of ISA_MAGIC_VALUE// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BITnewisa.has_cxx_dtor = hasCxxDtor;
# endifnewisa.setClass(cls, this);
#endifnewisa.extra_rc = 1;}// This write must be performed in a single store in some cases// (for example when realizing a class because other threads// may simultaneously try to use the class).// fixme use atomics here to guarantee single-store and to// guarantee memory order w.r.t. the class index table// ...but not too atomic because we don't want to hurt instantiationisa = newisa;
}
我們這里是通過InitIsa來綁定isa指針的,但objc838在這里把綁定具體存儲類信息的內容放在我們的setClass
這個函數中.下面我們來看這部分代嗎內容.
cls
與 isa
關聯原理
就是isa
指針中的shiftcls
位域中存儲了類信息
,其中initInstanceIsa
的過程是將 calloc
指針 和當前的 類cls
關聯起來
通過setClass方法中的shiftcls來驗證綁定的一個流程
這里我們接著看setClass這個方法:
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{// Match the conditional in isa.h.
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE// No signing, just use the raw pointer.uintptr_t signedCls = (uintptr_t)newCls;# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT// We're only signing Swift classes. Non-Swift classes just use// the raw pointeruintptr_t signedCls = (uintptr_t)newCls;if (newCls->isSwiftStable())signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL// We're signing everythinguintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));# else
# error Unknown isa signing mode.
# endifshiftcls_and_sig = signedCls >> 3;#elif SUPPORT_INDEXED_ISA// Indexed isa only uses this method to set a raw pointer class.// Setting an indexed class is handled separately.cls = newCls;#else // Nonpointer isa, no ptrauthshiftcls = (uintptr_t)newCls >> 3; // 這里會有shiftcls賦值的邏輯是將LGPerson進行編碼后,右移3位
#endif
}
從上面這張圖可以看出我們這里是在shiftcls
中存儲一個類信息的
運行至newisa.shiftcls = (uintptr_t)cls >> 3
其中shiftcls存儲當前類信息部分
- shiftcls賦值的邏輯是將LGPerson進行編碼后,右移3位,這里為什么可以保證后三位沒有信息的原因是內存對齊后從二進制層面來說他一定是后三位為000的,(因為結構體內存對齊的一個機制,導致了我們的結構體的二進制后三位一定為0);
這個時候我們再運行會我們的一個initIsa
這里的一個創建isa的內容:isa() = newisa;
然后觀察這里的一個內存的情況:
這里我們可以清楚的看到一個內容這里一個cls完美的指向了我們的一個類.
這里為什么shiftcls
為什么會出現一個強制類型轉化的原因是,內存不可以存儲字符串,機械碼只可以識別01這種數字所以才強制類型轉化成一個uintptr_t數據類型
在64位的機器上,intptr_t和uintptr_t分別是long int、unsigned long int的別名;在32位的機器上,intptr_t和uintptr_t分別是int、unsigned int的別名。
這里為什么需要向右移動三位,是因為他的前面還有三個位域,需要右移動將其抹零.
主要是由于shiftcls
處于isa
指針地址的中間
部分,前面還有3
個位域,為了不影響前面的3個位域
的數據,需要右移
將其抹零
。
通過 isa & ISA_MSAK
- 在方式一后,繼續執行,回到
_class_createInstanceFromZone
方法,此時cls 與 isa已經關聯完成
,執行po objc
將isa
指針地址 & ISA_MASK
(處于macOS
,使用x86_64
中的宏
定義),即 po 0x0000000100008358 & 0x00007ffffffffff8 & 0x00007ffffffffff8
,得出CGObject
通過object_getClass
查看object_getClass
的源碼實現,同樣可以驗證isa與類關聯的原理,這里的原理和上面的大致一樣,所以筆者就不多贅述了.
通過位運算
我們之前提到過shiftcls
只有44位的大小.這44位其實存儲了一個類信息,需要經過位運算,將右邊三位,和去除44位的位置都抹零
所以我們只有這樣處理之后也可以得到shiftcls里面保存的是一個類,說明isa已經關聯成功了.
類&類的結構
這里主要分析的isa走向和繼承關系
在分析之前,我們先定義兩個類:
@interface CJLPerson : NSObject
{NSString *hobby;
}
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end
@interface CJLTeacher : CJLPerson
@end@implementation CJLTeacher
@end
從終端調試開始認識調用邏輯
根圖中的p/x 0x0000000100008320 & 0x00007ffffffffff8ULL
與 p/x 0x00000001000082f8 & 0x00007ffffffffff8ULL
中的類信息打印出來都是CJLPerson
?
前者打印的是person對象的一個isa指針地址,他&后的結果是創建person的類CJLPerson
后者打印的是isa中類信息所指的類的isa的指針地址,即通過CJLPerson類的類的isa地址,在蘋果中,我們把一個類的類叫做元類
什么是元類
- 對象的isa是指向類,類其實也是一個對象,可以叫做類對象.類對象的位域3指向平果定義的元類
- 元類是系統給的,他的定義和創建都是由編譯器玩測的,這個過程中,類的歸屬來自于元類
- 元類是類對象的類,每一個類都有獨一無二的元類來存儲類方法的相國信息
- 元類本身是沒有名字的,只是因為類與之關聯,所以直接采用了類的一個名字
這里我們繼續用調試來觀察
我們可以發現NSObject的元類指向了自己,CJLPerson元類的元類指向的是NSObject,從上面的調試信息我們其實可以得到下面的圖片:
小結
- 對象的isa指向類
- 類的isa指向元類
- 元類的isa指向根元類
- 根元類的isa指向自己
NSObject到底有幾個?
其實NSObject只有一個,我們可以打印出NSObject的元類的地址和前面的CJLPerson元類的地址,發現他其實都是同一個,所以可以得出一個結論,內存中只存在一份根元類NSObject,根元類是指向他自己
類存在幾份
由于類的信息在內存中只有一份,所以類對象只有一份
isa走位繼承圖
isa走位
isa的走向有以下幾點說明:
- 實例對象(Instance of Subclass 的
isa
指向類(class)
- 類對象(class)
isa
指向元類(Meta class)
- 元類(Meta class)的
isa
指向根元類(Root metal class)
- 根元類(Root metal class) 的
isa
指向它自己
本身,形成閉環
,這里的根元類
就是NSObject
superclass走位
類之間的繼承關系:
- 類繼承自父類superClass
- 父類繼承自根類rootClass
- 根力類繼承自nil,即根類可以理解為萬物起源,無中生有
元類也存在繼承,元類之間的繼承關系如下:
- 自子類的元類元類繼承自父類的元類
- 父類的元類繼承自根元類
- 根元類繼承自根類,這里的根類是指NSObject
實例對象之間沒有繼承關系,類之間由繼承關系
object_class & objc_object
isa走位我們理清楚了,又來了一個新的問題:為什么 對象
和 類
都有isa屬性
呢?這里就不得不提到兩個結構體類型:objc_class
& objc_object
這里我們重新回顧我們的一個NSObject定義的內容:
struct NSObject_IMPL {Class isa;
};
typedef struct objc_class *Class;
其中Class是用isa指針類型,是由objc_class定義的一個類型
objc_class是一個結構體,iOS中兵所有的Class都是以objc_class為模板創建的.
這里我們先認識一下這兩個結構體:
這里我們可以簡單看一下這兩個結構體:
struct objc_object {Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {objc_class(const objc_class&) = delete;objc_class(objc_class&&) = delete;void operator=(const objc_class&) = delete;void operator=(objc_class&&) = delete;// Class ISA;Class superclass;cache_t cache; // formerly cache pointer and vtableclass_data_bits_t bits;//這里代碼太長了就不展示了
這里的objc_class 與objc_object有什么關系?
- 結構體類型objc_class繼承自objec_class類型.其中objc_obejct是一個結構體,他又一個isa屬性,所以objc_class也有了isa屬性
- NSObject中的isa在底層是由Class 定義的,其中class的底層編碼來自 objc_class類型,所以NSObject也擁有了isa屬性
- NSObject是一個類,用它初始化一個實例對象objc,objc滿足objc_object的一個特性,主要還是因為isa是由objc_class繼承過來的,而objce_class繼承自objc_object,因為objc_object由isa屬性,所以每一個對象都有一個isa,isa表示一個指向,來自當前的objc_object.
- objc_object是當前的根對象,縮由對象都有這樣一個特性,即擁有isa屬性
objc_object與對象的關系
- 所有的對象都是以objc_object繼承過來的
- 所有的對象都是來源于NSObject,但是到真正的底層就是一個objc_object的一個結構體類型了
所以可以說對象是繼承于objc_object的
總結
所有的對象都是由objc_object繼承過來的
所有的對象,類,元類都有isa屬性.
簡單概括就是萬物都是對象,所有都來源于objc_object,有一下兩點結論:
- 以objc_object為模板創建的對象,都有isa
- 以objc_class為模板創建的類,都有isa
在結構層面可以通俗的理解為上層OC與底層對接:
- 下層就是通過結構體定義的模板,例如objc_class,objc_object
- 上層就是通過底層模板創建的一些類型
類結構分析
類信息中有哪些內容
探索類信息中有什么的時.事先我們并不清楚類的結構是什么樣,但是我們可以通過類得到一個首地址,然后通過地址平移去獲取里面所有的值.
開始認識我們的一個結構體:
struct objc_class : objc_object {objc_class(const objc_class&) = delete;objc_class(objc_class&&) = delete;void operator=(const objc_class&) = delete;void operator=(objc_class&&) = delete;// Class ISA;Class superclass;cache_t cache; // formerly cache pointer and vtableclass_data_bits_t bits;
- isa屬性:繼承自objec_object的isa,占8個字節
- superclass屬性:Class類型,Class實有objc_object定義的,是一個指針
- cache方法緩存,提高方法的性能
- bits屬性:只有首地址經過上面三爾個屬性的內存大小總和的平移,才能獲得到bits
計算cache類的內存大小
進入cache類cache_t
的定義(只貼出了結構體中非static修飾的屬性,主要是因為static類型的屬性 不存在結構體的內存中)
struct cache_t {
private:explicit_atomic<uintptr_t> _bucketsAndMaybeMask; //是一個結構體指針類型,占8個字節 explicit_atomic 是將普通指針轉換為原子指針的操作,為了線程安全. _bucketsAndMaybeMask 字段通過預處理宏和 buckets() 方法間接實現了 _buckets 的功能 這種設計是為了 優化內存(共享 _bucketsAndMaybeMask 字段)并支持不同架構的掩碼存儲模式。union {struct {explicit_atomic<mask_t> _maybeMask;
#if __LP64__uint16_t _flags;
#endifuint16_t _occupied;};explicit_atomic<preopt_cache_t *> _originalPreoptCache;};
這個類在LP64位的一個情況應該是只有16個字節,上面包含一個一個結構體指針類型,和一個結構體類型.這個類的結構可以大致的簡單理解為下面這個樣式:
這里展示一下,這里和objc838有所出入,在objc838版本中把bucket和mask合并了,然后通過掩碼的一個方式去訪問這部分內存.
bits
bits
:封裝了類的其他信息,例如成員變量,方法列表,協議,屬性
這里我們可以看到這里的一個中的bits的內容,這里我們先來認識bits中非常重要的一個類別class_rw_t
這是一個結構體類型,這里我們呢還沒有看到屬性列表以及方法列表:
所以我們還需要進一步探索,所以我們找一下這里最重要的內容bits中最重要的兩個方法:safe_ro
data
,后者返回的是class_rw_t
前者返回的是class_ro_t
在64位架構CPU下,bits
的第3到第46字節存儲 class_rw_t
。class_rw_t
中存儲 flags 、witness、firstSubclass、nextSiblingClass 以及 class_rw_ext_t
。
在本文的最后幾章內容中會講幾個的一個區別
探索property_list(屬性列表)
通過查看class_rw_t
這個類.我們可以發現這里的一個屬性列表和方法列表等多個列表
這里注意一個內容,這里的存儲在class_rw_t這個類是存儲一個屬性的,并不存儲一個成員變量成員變量是存儲在另一個類中:class_ro_t
的這里我們對比看一下這兩個類的區別:
這個類也有方法,屬性,協議和成員變量.但方法,屬性,協議的開頭是用base
開頭的
這里有一個成員變量列表也就是我們這里的ivars
,這個列表包含的內容不僅僅包含一個成員變量列表,除了包括在{}
中定義的一個成員變量,還包括通過屬性定義的成員變量.bits --> data() -->ro() --> ivars
通過這個流程來獲取成員變量表.
通過@property
定義的屬性,也會存儲在bits屬性中,通過bits --> data() --> properties() --> list
獲取屬性列表
,其中只包含屬性
探索methods_list
這里的methods_list是不存儲類方法的,他只存儲一個實例方法.那我們的類方法存儲在哪里呢?
類方法其實存儲在我們的一個元類的方法列表里面:
-
類的實例方法存儲在類的bits屬性中,通過
bits --> methods() --> list
獲取實例方法列表
,例如CJLPersong
類的實例方法sayHello
就存儲在CJLPerson類的bits
屬性中,類中的方法列表
除了包括實例方法
,還包括屬性的set方法
和 `get方法 -
類的類方法存儲在元類的bits屬性中,通過
元類bits --> methods() --> list
獲取類方法列表
,例如CJLPerson
中的類方法sayBye
就存儲在CJLPerson
類的元類
(名稱也是CJLPerson)的bits
屬性中
從編譯期開始介紹class_ro_t,class_rw_t,class_rw_ext_t
首先我們從編譯期程序做了什么事情開始介紹:
當類被編譯的時候,二進制類在磁盤中的表示如下:
首先是類對象本身,包含最常訪問的信息:指向元類(isa),超類(superclass)和方法緩存(cache)的指針,它還具有指向包含更多數據的結構體
class_ro_t
的指針,包含了類的名稱,方法,協議,實例變量等等編譯期確定
的信息。其中ro
表示read only
的意思。當類第一次從磁盤加載到內存時,它們總是以這樣的形式開始布局的,但是一旦使用它們,就會發生改變:
當類被 Runtime 加載之后,類的結構會發生一些變化,在了解這些變化之前,我們需要知道2個概念:
Clean Memory:加載后不會發生更改的內存塊,
class_ro_t
屬于 Clean Memory,因為它是只讀的。
Dirty Memory:運行時會發生更改的內存塊,類結構一旦被加載,就會變成 Dirty Memory,因為運行時會向它寫入新的數據。例如,我們可以通過 Runtime 給類動態的添加方法。這里要明確,Dirty Memory 比 Clean Memory 要昂貴得多。因為它需要更多的內存信息,并且只要進程正在運行,就必須保留它。另一方面, Clean Memory 可以進行移除,從而節省更多的內存空間,因為如果你需要 Clean Memory ,系統可以從磁盤中重新加載。
Dirty Memory 是這個類數據 被分成兩部分的原因。
對于我們來說,越多的 Clean Memory 顯然是更好的,因為它可以
節約更多的內存
。我們可以通過分離出永不更改的數據部分,將大多數類數據保留為 Clean Memory,應該怎么做呢?在介紹優化方法之前,我們先來看一下,在類加載之后,類的結構會變成如何呢?
類分析
在類首次被使用的時候,runtime會為它分配額外的存儲容量,用于 讀取/寫入 數據的一個結構體 class_rw_t
。
這個結構體用來存儲只有在運行時才會生成的新信息比方說所有類都會連接成一個樹狀結構,這里是通過firstSubclass和newxtSon;歐美化Class指針實現的,這樣runtime可以遍歷當前使用的所有的類.
這里之所以還會有該類的方法列表和屬性列表的信息是因為它們在運行時是可以更改的.當category被加載的時候,它可以想類中添加新的方法.而且程序員也可以通過runtimeAPI動態的添加.
class_ro_t
是只讀的,存放的是 編譯期間就確定 的字段信息;而class_rw_t
是在 runtime 時才創建的,它會先將class_ro_t
的內容拷貝一份,再將類的分類的屬性、方法、協議等信息添加進去,之所以要這么設計是因為 Objective-C 是動態語言,你可以在運行時更改它們方法,屬性等,并且分類可以在不改變類設計的前提下,將新方法添加到類中。
在實際生活中,我們會發現class_rw_t
會占用比class_ro_t
更多的內存所以誕生了一個新的結構體class_rw_ext_t
大約只有10%左右的類實際會存在動態的一個更改行為,這樣減少了一個class_rw_t
的大小減小了一半,所以會變成下面這個結構
class_ro_t
這里先展示這個類的樣式
struct class_ro_t {uint32_t flags;uint32_t instanceStart;uint32_t instanceSize;
#ifdef __LP64__uint32_t reserved;
#endifunion {const uint8_t * ivarLayout;Class nonMetaclass;};explicit_atomic<const char *> name;WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;protocol_list_t * baseProtocols;const ivar_list_t * ivars;const uint8_t * weakIvarLayout;property_list_t *baseProperties;
這個類我們可以很清楚的看到他有屬性列表,方法列表.
這個類其實是在編譯期的時候就產生了,他會將本來編譯好的內容,放到我們的ro中.
class_rw_t
先展示一下這個結構體的一個樣式:
struct class_rw_t {// Be warned that Symbolication knows the layout of this structure.uint32_t flags;uint16_t witness;
#if SUPPORT_INDEXED_ISAuint16_t index;
#endifexplicit_atomic<uintptr_t> ro_or_rw_ext; //按需存儲靜態或動態元數據,兼顧性能和內存。//作用原子化指針,指向類的 只讀元數據(class_ro_t)或 動態擴展數據(class_rw_ext_t)。Class firstSubclass; //指向當前類的 第一個直接子類Class nextSiblingClass; //指向當前類的 下一個兄弟類,與 firstSubclass 共同維護類的 鏈表結構。
從他兩者的一個成員變量其實就可以看出兩者的一個區別,這里里面之所以沒有屬性的一個內容,是因為在iOS14之后屬性的部分被放到了我們之前提到的class_rw_ext_t
中.
區別
從生成時機的角度來說, ro
編譯階段生成,rw
運行的時候生成。從存儲的內容角度來講,ro
中有方法、屬性、協議和成員變量,而rw
中并沒有成員變量。rw
中的方法屬性協議的取值方法中,也是通過取ro
或者rwe
中的值來獲得。ro
中的方法、屬性、協議都是base,也就是只有本類中的方法屬性和協議。
class_rw_ext_t
這里我們來看一下這個結構體的一個樣式:
struct class_rw_ext_t {DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)class_ro_t_authed_ptr<const class_ro_t> ro;method_array_t methods;property_array_t properties;protocol_array_t protocols;char *demangledName;uint32_t version;
};
這個結構體存儲了class_ro_t
和 methods
(方法列表)、properties
(屬性列表)、protocols
(協議列表)等信息。
而 class_ro_t
中也存儲了 baseMethodList
(方法列表)、baseProperties
(屬性列表)、baseProtocols
(協議列表) 以及 實例變量、類的名稱、大小 等等信息。
但是我們這里要注意一下他的一個創建的前提條件:
- 使用分類的類
- 使用runtime API動態修改類的結構的時候
在遇到以上2種情況的時候,類的結構(屬性、協議、方法)發生改變,原有的ro
(Claer Memory,便宜)已經不能繼續記錄類的屬性、協議、方法信息了,于是系統重新生成可讀可寫的內存結構rw_ext
(Dirty Memory, 比較貴),來存放新的類結構。
這時候我們就很好理解下面這個方法列表了:
const method_array_t methods() const {auto v = get_ro_or_rwe();if (v.is<class_rw_ext_t *>()) { // 判斷有沒有創建一個re_extreturn v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods; //如果有就在這里創建} else {return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods}; //如果沒有就在ro中讀取}}
總結
這是筆者對于iOS類與對象底層原理的一個學習,這里可能會有什么紕漏或者錯誤,還請不吝指出.
參考博客:
iOS-底層原理 08:類 & 類結構分析
iOS 類的結構分析
類結構中的class_rw_t與class_ro_t
【iOS】類與對象底層探索