文章目錄
- 前言
- 類的分析
- 類的本質
- objc_class 、objc_object和NSObject
- objc_object:所有對象的基類型
- objc_class:類的底層結構
- NSObject:面向用戶的根類
- 小結
- 指針內存偏移
- 普通指針----值拷貝
- 對象----指針拷貝或引用拷貝
- 用數組指針引出----內存偏移
- 類的結構
- Class ISA
- Class surperclass
- cache_t cache
- class_data_bits_t bits
- 總結
前言
??本篇博客主要是筆者在學習類&類的結構的底層探索和分析時所作的筆記,主要涉及實例對象的類以及類的結構。Objective-C的類結構是其動態性和面向對象特性的核心,理解類的內存布局和內部機制,對開發、調試和性能優化至關重要。
類的分析
類的本質
??在 OC 中,類(Class)本身是一個對象(objc_class 結構體),在探索類的本質之前,我們先在main文件里自定義一個繼承自NSobjetc的TCJPerson類:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>@interface TCJPerson : NSObject@end@implementation TCJPerson@endint main(int argc, const char * argv[]) {@autoreleasepool {TCJPerson *person = [TCJPerson alloc];Class cls = object_getClass(person);}return 0;
}
然后利用clang工具將oc語言的main文件輸出為cpp文件,命令行如下:
clang -rewrite-objc main.m -o main.cpp
將文件拖到我們的objc源碼中打開,方便我們調試和查找,可以發現,這段代碼在cpp文件文件中如下:
//類型定義部分
#ifndef _REWRITER_typedef_TCJPerson
#define _REWRITER_typedef_TCJPerson
typedef struct objc_object TCJPerson; //將 TCJPerson 定義為 objc_object 結構體
typedef struct {} _objc_exc_TCJPerson; //空結構體,用于異常處理占位(實際未使用)
#endif//類的實現結構
struct TCJPerson_IMPL {struct NSObject_IMPL NSObject_IVARS; //繼承自 NSObject 的實例變量
};/* @end */// @implementation TCJPerson
// @end
//main函數的底層轉換
int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; TCJPerson *person = ((TCJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TCJPerson"), sel_registerName("alloc"));//objc_getClass("TCJPerson"):獲取 TCJPerson 的類對象(Class)//sel_registerName("alloc"):注冊方法名 alloc,返回 SEL 類型的選擇子//objc_msgSend:發送 alloc 消息給 TCJPerson 類,創建實例//強制類型轉換 (TCJPerson *(*)(id, SEL))(void *) 是為了匹配 objc_msgSend 的函數指針簽名Class cls = object_getClass(person);//獲取 person 實例的類(即 TCJPerson 的類對象)}return 0;
}
- TCJPerson 被定義為 objc_object,說明在底層,Objective-C 類實例的本質就是 objc_object 結構體。
- 因為 TCJPerson 繼承自 NSObject,所以它的底層結構會包含父類的實例變量。其中,NSObject_IVARS 就是 NSObject 的實例變量,通常就是 isa 指針(指向類的元數據)。
然后我們發現類在底層是用class接收的。
typedef struct objc_class *Class;
在左側搜索欄查找objc_class,我們可以發現objc_class繼承自objc_object。
點擊進入objc_object的源碼實現中:
小結
- Objective-C 對象的本質:
類實例(如 person)本質是 objc_object 結構體,包含 isa 指針(來自 NSObject_IVARS)。類(如 TCJPerson)本質是 objc_class 結構體,繼承自 objc_object。所以滿足萬物皆對象。 - 方法調用的本質:[TCJPerson alloc] 被編譯為 objc_msgSend 的調用,動態查找并執行方法。
- 內存布局:TCJPerson_IMPL 只包含 NSObject 的 isa,因為沒有自定義實例變量。
objc_class 、objc_object和NSObject
objc_object:所有對象的基類型
底層源碼:
struct objc_object {Class _Nonnull isa OBJC_ISA_AVAILABILITY; //必須為非空的類指針(即指向 objc_class 結構體的指針)
};
這證明objc_object是所有 Objective-C 對象的最底層結構,包括實例對象、類對象、元類對象。其通過 isa 指針實現對象與類的關聯(即“對象是什么類”)。
特點:
實例對象的 isa 指向它的類(如 TCJPerson 實例的 isa 指向 TCJPerson 類)。
類對象的 isa 指向元類(Meta Class)。
元類的 isa 指向根元類(Root Meta Class)。
關于類 元類 根元類
- ??類(Class)??
??作用??:定義對象的??實例變量??和??實例方法??(如 -init、-description)。
??內存結構??:每個類是一個 objc_class 結構體實例,包含方法列表、父類指針、緩存等。
??對象關系??:實例對象(如 MyObject *obj)的 isa 指針指向其類對象。 - ??元類(Meta-class)??
??作用??:定義類的??類方法??(如 +alloc、+new)。
元類本身也是一個類,它的實例是類對象。
??內存結構??:元類也是一個 objc_class 結構體,但其方法列表存儲類方法。
??對象關系??:類對象(如 MyObject.class)的 isa 指針指向其元類。 - ??根元類(Root Meta-class)??
??作用??:所有元類的最終基類,通常是 NSObject 的元類。
根元類的類方法(如 +alloc)會被所有類的元類繼承。
根元類的 isa 指針指向自身,形成閉環。
??Instance (MyObject)??: 一個對象實例
??Class (MyObject class)??: 定義該實例的類
??Meta-class (MyObject meta)??: 定義該類的元類
isa 指針:形成繼承鏈的核心,每個實例、類和元類都有一個指向其類型的指針
實例通過 isa 指向類,類通過 isa 指向元類,元類通過 isa 指向根元類
super_class 指針:形成繼承體系
類通過 super_class 指向父類,元類通過 super_class 指向父元類
??Root Meta-class (NSObject meta)??: 所有元類的根元類
根元類的 isa 指向自己(self),形成閉環
根元類的 super_class 指向 NSObject 類
運行時??方法查找路徑??:
實例方法首先在實例所屬的類中查找
如果沒找到,則沿著 super_class 鏈向上查找
類方法則在元類及其父元類中查找
??
繼承機制??:實例繼承自類,類繼承自元類,元類繼承自父元類,最終根元類繼承自NSObject類
objc_class:類的底層結構
底層代碼:
struct objc_class : objc_object { //繼承自objc_objectobjc_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 vtable //方法緩存class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags //類的方法、屬性、協議等數據Class getSuperclass() const {
#if __has_feature(ptrauth_calls)
# if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTHif (superclass == Nil)return Nil;......
}
這說明objc_class是 objc_object 的子類,說明類本身也是對象(即“類對象”)。
存儲類的元數據:方法列表、屬性列表、協議列表、父類指針等。
因為其繼承自 objc_object,所以其類對象也有 isa 指針(指向元類)。
其中,Class 是指向 objc_class 的指針(typedef struct objc_class *Class)。
NSObject:面向用戶的根類
底層代碼:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"Class isa OBJC_ISA_AVAILABILITY; //指向類對象的指針
#pragma clang diagnostic pop
}
NSObject 是 Objective-C 中所有類的根類(除了 NSProxy),提供面向開發者的基礎方法(如 alloc、init、description)。
與 objc_object 的關系:
NSObject 的實例對象在底層就是 objc_object。
NSObject 的類對象在底層是 objc_class。
編譯器會將 NSObject 的代碼轉換為對 objc_object 和 objc_class 的操作。
小結
通過對objc_class、objc_object和NSObject底層的大致了解,我們就可以明白三者存在以下金字塔關系:
objc_object:所有對象的終極基類(C結構體)
objc_class:繼承objc_object,說明"類也是對象"
NSObject:繼承鏈最頂端的公開類,封裝了objc_class的面向對象接口
小結
所有的對象 + 類 + 元類 都有isa屬性
所有的對象都是由objc_object繼承來的
簡單概括就是萬物皆對象,萬物皆來源于objc_object,有以下兩點結論:
所有以 objc_object為模板創建的對象,都有isa屬性
所有以 objc_class為模板創建的類,都有isa屬性
在結構層面可以通俗的理解為上層OC 與 底層的對接:
下層是通過 結構體 定義的 模板,例如objc_class、objc_object
上層是通過底層的模板創建的一些類型,例如TCJPerson
objc_class、objc_object、isa、object、NSObject等的整體的關系如下圖:
指針內存偏移
??在分析類的結構之前,我們需要先來學習一下指針內存偏移作為前綴知識。
普通指針----值拷貝
//普通指針//值拷貝int a = 10;int b = 10;NSLog(@"&a:%d--%p", a, &a);NSLog(@"&b:%d--%p", b, &b);
通過代碼及其運行結果可以看出來,變量a和b雖然都是被賦值為10,但是變量a和b的內存地址是不一樣的,我們稱這種方式為值拷貝。
對象----指針拷貝或引用拷貝
//對象NSObject *obj1 = [[NSObject alloc] init];NSObject *obj2 = [[NSObject alloc] init];NSLog(@"%@--%p", obj1, &obj1);NSLog(@"%@--%p", obj2, &obj2);
通過運行結果,我們可以看到obj1和obj2對象不僅自身內存地址不一樣,其指向的對象的內存地址也不一樣,這被稱為指針拷貝或引用拷貝。
用數組指針引出----內存偏移
//數組指針int arr[4] = {1, 2, 3, 4};int *c = arr;NSLog(@"&arr:%p--%p--%p", arr, &arr[0], &arr[1]);NSLog(@"&c:%p--%p--%p", c, c + 1, c + 2);for (int i = 0; i < 4; i++) {int value = *(c + i);NSLog(@"value:%d", value);}
通過運行結果可以看到:
- &a和&a[0]的地址是相同的——即首地址就代表數組的第一個元素的地址。
- 第一個元素地址0x16fdff2f8和第二個元素地址0x16fdff2fc相差4個字節,也就是int的所占的4字節,因為他們的數據類型相同。
- d、d+1、d+2這個地方的指針相加就是偏移地址。地址加1就是偏移,偏移一個位數所在元素的大小。
- 可以通過地址,取出對應地址的值。
小結
類的結構
從objc_class的定義可以得出,類有4個屬性:isa、superclass、cache、bits。
Class ISA
不但實例對象中有isa指針,類對象中也有isa指針關聯著元類。
Class本身就是一個指針,占用8字節。
Class surperclass
顧名思義就是類的父類(一般為NSObject)superclass是Class類型,所以占用8字節。
cache_t cache
進入cache_t的實現源碼,我們能看到(筆者對部分進行了注釋,方便理解):
struct cache_t {
private:explicit_atomic<uintptr_t> _bucketsAndMaybeMask; //原子存儲緩存桶指針或掩碼(根據配置不同復用同一內存)//原子性??:保證并發訪問時的線程安全(如 objc_msgSend 高頻調用場景)union {// Note: _flags on ARM64 needs to line up with the unused bits of// _originalPreoptCache because we access some flags (specifically// FAST_CACHE_HAS_DEFAULT_CORE and FAST_CACHE_HAS_DEFAULT_AWZ) on// unrealized classes with the assumption that they will start out// as 0.struct {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED && !__LP64__// Outlined cache mask storage, 32-bit, we have mask and occupied.explicit_atomic<mask_t> _mask; //緩存掩碼uint16_t _occupied; //已占用槽位數
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED && __LP64__// Outlined cache mask storage, 64-bit, we have mask, occupied, flags.explicit_atomic<mask_t> _mask;uint16_t _occupied;uint16_t _flags; //狀態標志(FAST_CACHE_HAS_DEFAULT_CORE: 是否有默認核心實現 FAST_CACHE_HAS_CUSTOM_DEALLOC_INITIATION: 是否自定義釋放邏輯)
# define CACHE_T_HAS_FLAGS 1
#elif __LP64__// Inline cache mask storage, 64-bit, we have occupied, flags, and// empty space to line up flags with originalPreoptCache.//// Note: the assembly code for objc_release_xN knows about the// location of _flags and the// FAST_CACHE_HAS_CUSTOM_DEALLOC_INITIATION flag within. Any changes// must be applied there as well.uint32_t _unused;uint16_t _occupied;uint16_t _flags;
# define CACHE_T_HAS_FLAGS 1
#else// Inline cache mask storage, 32-bit, we have occupied, flags.uint16_t _occupied;uint16_t _flags;
# define CACHE_T_HAS_FLAGS 1
#endif
cache在英文中的意思是緩存。
cache_t是一個結構體,內存長度由所有元素決定:
_bucketsAndMaybeMask是long類型,它是一個指針,占用8字節;
mask_t是個uint32_t類型,_mask占用4字節;
_occupied和_flags都是uint16_t類型,uint16_t是 unsigned short 的別名,所以_occupied占用2字節;
_flags占用2字節;
所以,cache_t共占用16字節。
這里對cache簡單了解一下,后面還會呢詳細學習。
class_data_bits_t bits
class_data_bits_t實現源碼如下:
struct class_data_bits_t {friend objc_class;// Values are the FAST_ flags above.uintptr_t bits;
private:bool getBit(uintptr_t bit) const{return bits & bit;}// Atomically set the bits in `set` and clear the bits in `clear`.// set and clear must not overlap. If the existing bits field is zero,// this function will mark it as using the RW signing scheme.void setAndClearBits(uintptr_t set, uintptr_t clear){ASSERT((set & clear) == 0);uintptr_t newBits, oldBits = LoadExclusive(&bits);do {uintptr_t authBits= (oldBits? (uintptr_t)ptrauth_auth_data((class_rw_t *)oldBits,CLASS_DATA_BITS_RW_SIGNING_KEY,ptrauth_blend_discriminator(&bits,CLASS_DATA_BITS_RW_DISCRIMINATOR)): FAST_IS_RW_POINTER);newBits = (authBits | set) & ~clear;newBits = (uintptr_t)ptrauth_sign_unauthenticated((class_rw_t *)newBits,CLASS_DATA_BITS_RW_SIGNING_KEY,ptrauth_blend_discriminator(&bits,CLASS_DATA_BITS_RW_DISCRIMINATOR));} while (slowpath(!StoreReleaseExclusive(&bits, &oldBits, newBits)));}void setBits(uintptr_t set) {setAndClearBits(set, 0);}void clearBits(uintptr_t clear) {setAndClearBits(0, clear);}public:void copyRWFrom(const class_data_bits_t &other) {bits = (uintptr_t)ptrauth_auth_and_resign((class_rw_t *)other.bits,CLASS_DATA_BITS_RW_SIGNING_KEY,ptrauth_blend_discriminator(&other.bits,CLASS_DATA_BITS_RW_DISCRIMINATOR),CLASS_DATA_BITS_RW_SIGNING_KEY,ptrauth_blend_discriminator(&bits,CLASS_DATA_BITS_RW_DISCRIMINATOR));}void copyROFrom(const class_data_bits_t &other, bool authenticate) {ASSERT((flags() & RO_REALIZED) == 0);if (authenticate) {bits = (uintptr_t)ptrauth_auth_and_resign((class_ro_t *)other.bits,CLASS_DATA_BITS_RO_SIGNING_KEY,ptrauth_blend_discriminator(&other.bits,CLASS_DATA_BITS_RO_DISCRIMINATOR),CLASS_DATA_BITS_RO_SIGNING_KEY,ptrauth_blend_discriminator(&bits,CLASS_DATA_BITS_RO_DISCRIMINATOR));} else {bits = other.bits;}}
雖然我們不是很能看懂,但至少能看出來這里是oc運行時用來存儲數據的。
根據上面的分析,class指針各為8字節,cache_t cache為16字節,所以想要獲取bits的中的內容,只需通過類的首地址平移32字節即可。
總結
??在學習過程中,筆者關于LLDB調試出了比較多的問題,至今還沒解決,所以這篇筆記還有待完善,等筆者解決完問題后,會再次進行編撰,LLDB在源碼學習中還是很不錯的工具。