iOS-底層原理 08:類 & 類結構分析

iOS 底層原理 文章匯總

本文的主要目的是分析 類 & 類的結構,整篇都是圍繞一個展開的一些探索

類 的分析

類的分析 主要是分析 isa的走向 以及 繼承關系

準備工作

定義兩個類

  • 繼承自NSObject的類CJLPerson
@interface CJLPerson : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end

@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end
  • 繼承自CJLPerson的類CJLTeacher
@interface CJLTeacher : CJLPerson
@end

@implementation CJLTeacher
@end
  • 在main中分別用兩個定義兩個對象:person & teacher
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //ISA_MASK  0x00007ffffffffff8ULL
        CJLPerson *person = [CJLPerson alloc];
        CJLTeacher *teacher = [CJLTeacher alloc];
        NSLog(@"Hello, World! %@ - %@",person,teacher);  
    }
    return 0;
}

元類

首先,我們先通過一個案例的lldb調試先引入元類

  • 在main中CJLTeacher部分加一個斷點,運行程序
  • 開啟lldb調試,調試的過程如下圖所示


    lldb調試過程

根據調試過程,我們產生了一個疑問:為什么圖中的p/x 0x001d8001000022dd & 0x00007ffffffffff8ULLp/x 0x00000001000022b0 & 0x00007ffffffffff8ULL 中的類信息打印出來都是CJLPerson

  • 0x001d8001000022ddperson對象的isa指針地址,其&后得到的結果創建person的類CJLPerson
  • 0x00000001000022b0是isa中獲取的類信息所指的類的isa的指針地址,即 CJLPerson類的類isa指針地址,在Apple中,我們簡稱CJLPerson類的類元類
  • 所以,兩個打印都是CJLPerson的根本原因就是因為元類導致的

元類的說明

下面來解釋什么是元類,主要有以下幾點說明:

  • 我們都知道 對象isa 是指向的其實也是一個對象,可以稱為類對象,其isa的位域指向蘋果定義的元類

  • 元類系統給的,其定義創建都是由編譯器完成,在這個過程中,歸屬來自于元類

  • 元類類對象,每個都有一個獨一無二的元類用來存儲 類方法的相關信息

  • 元類本身是沒有名稱的,由于與關聯,所以使用了同類名一樣的名稱

下面通過lldb命令來探索元類的走向,也就是isa走位,如下圖所示,可以得出一個關系鏈:對象 --> 類 --> 元類 --> NSobject, NSObject 指向自身

isa走位

總結

從圖中可以看出

  • 對象isa 指向 (也可稱為類對象
  • isa 指向 元類
  • 元類isa 指向 根元類,即NSObject
  • 根元類isa 指向 它自己

NSObject到底有幾個?

從圖中可以看出,最后的根元類NSObject,這個NSObject 與我們日開開發中所知道的NSObject是同一個嗎?

有以下兩種驗證方式

  • 【方式一】lldb命令驗證
  • 【方式二】代碼驗證

【方式一】lldb命令驗證

我們也通過lldb調試,來驗證這兩個NSObject是否是同一個,如下圖所示


驗證NSObject的唯一性

從圖中可以看出,最后NSObject類的元類 也是NSObject,與上面的CJLPerson中的根元類(NSObject)的元類,是同一個,所以可以得出一個結論:內存中只存在存在一份根元類NSObject,根元類的元類是指向它自己

【方式二】代碼驗證

通過三種不同的方式獲取類,看他們打印的地址是否相同

//MARK:--- 分析類對象內存 存在個數
void testClassNum(){
    Class class1 = [CJLPerson class];
    Class class2 = [CJLPerson alloc].class;
    Class class3 = object_getClass([CJLPerson alloc]);
    NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3);
}

以下是代碼運行的結果


代碼驗證結果

從結果中可以看出,打印的地址都是同一個,所以NSObject只有一份,即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(即繼承關系)的走向也有以下幾點說明:

  • 之間 的繼承關系:
    • 類(subClass) 繼承自 父類(superClass)

    • 父類(superClass) 繼承自 根類(RootClass),此時的根類是指NSObject

    • 根類 繼承自 nil,所以根類NSObject可以理解為萬物起源,即無中生有

  • 元類也存在繼承,元類之間的繼承關系如下:
    • 子類的元類(metal SubClass) 繼承自 父類的元類(metal SuperClass)

    • 父類的元類(metal SuperClass) 繼承自 根元類(Root metal Class

    • 根元類(Root metal Class) 繼承于 根類(Root class),此時的根類是指NSObject

  • 【注意】實例對象之間沒有繼承關系之間有繼承關系

舉例說明

以前文提及的的CJLTeacher及對象teacherCJLPerson及對象person舉例說明,如下圖所示

isa走位 & 繼承 舉例說明

  • isa 走位鏈(兩條)

    • teacher的isa走位鏈:teacher(子類對象) --> CJLTeacher (子類)--> CJLTeacher(子元類) --> NSObject(根元類) --> NSObject(跟根元類,即自己)

    • person的isa走位圖:person(父類對象) --> CJLPerson (父類)--> CJLPerson(父元類) --> NSObject(根元類) --> NSObject(跟根元類,即自己)

  • superclass走位鏈(兩條)

    • 類的繼承關系鏈:CJLTeacher(子類) --> CJLPerson(父類) --> NSObject(根類)--> nil

    • 元類的繼承關系鏈:CJLTeacher(子元類) --> CJLPerson(父元類) --> NSObject(根元類)--> NSObject(根類)--> nil

objc_class & objc_object

isa走位我們理清楚了,又來了一個新的問題:為什么 對象都有isa屬性呢?這里就不得不提到兩個結構體類型:objc_class & objc_object

下面在這兩個結構體的基礎上,對上述問題進行探索。

在上一篇文章iOS-底層原理 07:isa與類關聯的原理中,使用clang編譯過main.m文件,從編譯后的c++文件中可以看到如下c++源碼

  • NSObject的底層編譯是NSObject_IMPL結構體,
    • 其中 Classisa指針的類型,是由objc_class定義的類型,
    • objc_class是一個結構體。在iOS中,所有的Class都是以 objc_class 為模板創建的`
struct NSObject_IMPL {
    Class isa;
};


typedef struct objc_class *Class;
  • 在objc4源碼中搜索objc_class的定義,源碼中對其的定義有兩個版本

    • 舊版 位于 runtime.h中,已經被廢除

      舊版objc_class定義

    • 新版 位于objc-runtime-new.h,這個是objc4-781最新優化的,我們后面的類的結構分析也是基于新版來分析的。

      新版objc_class定義

      從新版的定義中,可以看到 objc_class 結構體類型是繼承自 objc_object的,

  • 在objc4源碼中搜索objc_object (或者 objc_object {,這個類型也有兩個版本

    • 一個位于 objc.h,沒有被廢除,從編譯的main.cpp中可以看到,使用的這個版本的objc_object
      objc.h中的objc_object定義
    • 位于 objc-privat.h
      objc-privat.h中的objc_object定義

以下是編譯后的main.cpp中的objc_object的定義

struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

【問題】objc_class 與 objc_object 有什么關系?

通過上述的源碼查找以及main.cpp中底層編譯源碼,有以下幾點說明:

  • 結構體類型objc_class 繼承自objc_object類型,其中objc_object也是一個結構體,且有一個isa屬性,所以objc_class也擁有了isa屬性

  • mian.cpp底層編譯文件中,NSObject中的isa在底層是由Class 定義的,其中class的底層編碼來自 objc_class類型,所以NSObject也擁有了isa屬性

  • NSObject 是一個類,用它初始化一個實例對象objc,objc 滿足 objc_object 的特性(即有isa屬性),主要是因為isa 是由 NSObjectobjc_class繼承過來的,而objc_class繼承自objc_objectobjc_objectisa屬性。所以對象都有一個 isa,isa表示指向,來自于當前的objc_object

  • objc_object(結構體) 是 當前的 根對象所有的對象都有這樣一個特性 objc_object,即擁有isa屬性

【百度面試題】objc_object 與 對象的關系

  • 所有的對象 都是以 objc_object為模板繼承過來的

  • 所有的對象 是 來自 NSObject(OC) ,但是真正到底層的 是一個objc_object(C/C++)的結構體類型

【總結】 objc_object對象關系繼承關系

總結

  • 所有的對象 + + 元類 都有isa屬性

  • 所有的對象都是由objc_object繼承來的

  • 簡單概括就是萬物皆對象,萬物皆來源于objc_object,有以下兩點結論:

    • 所有以 objc_object為模板 創建的對象,都有isa屬性

    • 所有以objc_class為模板,創建的,都有isa屬性

  • 在結構層面可以通俗的理解為上層OC底層對接

    • 下層是通過 結構體 定義的 模板,例如objc_class、objc_object
    • 上層 是通過底層的模板創建的 一些類型,例如CJLPerson

objc_class、objc_object、isa、object、NSObject等的整體的關系,如下圖所示

整體關系圖示

類結構分析

主要是分析類信息中存儲了哪些內容

補充知識-內存偏移

在分析類結構之前,需要先了解內存偏移,因為類信息中訪問時,需要使用內存偏移

【普通指針】

//普通指針
    int a = 10; //變量
    int b = 10;
    NSLog(@"%d -- %p", a, &a);
    NSLog(@"%d -- %p", b, &b);

打印結果如下圖所示


普通指針打印結果
  • a、b都指向10,但是a、b的地址不一樣,這是一種拷貝,屬于值拷貝,也稱為深拷貝

  • a,b的地址之間相差 4 個字節,這取決于a、b的類型

其地址指向如圖所示


地址指向圖示

【對象指針】

//對象
    CJLPerson *p1 = [CJLPerson alloc]; // p1 是指針
    CJLPerson *p2 = [CJLPerson alloc];
    NSLog(@"%d -- %p", p1, &p1);
    NSLog(@"%d -- %p", p2, &p2);

打印結果如圖所示


對象指針打印結果
  • p1、p2 是指針,p1 是 指向 [CJLPerson alloc]創建的空間地址,即內存地址,p2 同理

  • &p1、&p2是 指向 p1、p2對象指針的地址,這個指針 就是 二級指針

其指針的指向如下圖所示


對象指針指向

【數組指針】

//數組指針
    int c[4] = {1, 2, 3, 4};
    int *d = c;
    NSLog(@"%p -- %p - %p", &c, &c[0], &c[1]);
    NSLog(@"%p -- %p - %p", d, d+1, d+2);

打印結果如下


數組打印結果
  • &c&c[0] 都是取 首地址,即數組名等于首地址
  • &c&c[1] 相差4個字節,地址之間相差的字節數,主要取決于存儲的數據類型
  • 可以通過 首地址+偏移量取出數組中的其他元素,其中偏移量是數組的下標,內存中首地址實際移動的字節數 等于 偏移量 * 數據類型字節數

其指針指向如下所示


數組指針指向

探索類信息中都有哪些內容

探索類信息中有什么時,事先我們并不清楚類結構是什么樣的,但是我們可以通過得到一個首地址,然后通過地址平移去獲取里面所有的值

根據前文提及的objc_class 的新版定義(objc4-781版本)如下,有以下幾個屬性

struct objc_class : objc_object {
    // Class ISA; //8字節
    Class superclass; //Class 類型 8字節
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
    //....方法部分省略,未貼出
}
  • isa屬性:繼承自objc_objectisa,占 8字節

  • superclass 屬性:Class類型,Class是由objc_object定義的,是一個指針,占8字節

  • cache屬性:簡單從類型class_data_bits_t目前無法得知,而class_data_bits_t是一個結構體類型,結構體內存大小需要根據內部的屬性來確定,而結構體指針才是8字節

  • bits屬性:只有首地址經過上面3個屬性的內存大小總和的平移,才能獲取到bits

計算 cache 類的內存大小

進入cache類cache_t的定義(只貼出了結構體中非static修飾的屬性,主要是因為static類型的屬性 不存在結構體的內存中),有如下幾個屬性

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets; // 是一個結構體指針類型,占8字節
    explicit_atomic<mask_t> _mask; //是mask_t 類型,而 mask_t 是 unsigned int 的別名,占4字節
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets; //是指針,占8字節
    mask_t _mask_unused; //是mask_t 類型,而 mask_t 是 uint32_t 類型定義的別名,占4字節
    
#if __LP64__
    uint16_t _flags;  //是uint16_t類型,uint16_t是 unsigned short 的別名,占 2個字節
#endif
    uint16_t _occupied; //是uint16_t類型,uint16_t是 unsigned short 的別名,占 2個字節
  • 計算前兩個屬性內存大小,有以下兩種情況,最后的內存大小總和都是12字節

    • 【情況一】if流程

      • buckets 類型是struct bucket_t *,是結構體指針類型,占8字節

      • maskmask_t 類型,而 mask_tunsigned int 的別名,占4字節

    • 【情況二】elseif流程

      • _maskAndBucketsuintptr_t類型,它是一個指針,占8字節

      • _mask_unusedmask_t 類型,而 mask_tuint32_t 類型定義的別名,占4字節

  • _flagsuint16_t類型,uint16_t是 unsigned short 的別名,占 2個字節

  • _occupieduint16_t類型,uint16_t是 unsigned short 的別名,占 2個字節

總結:所以最后計算出cache類的內存大小 = 12 + 2 + 2 = 16字節

獲取bits

所以有上述計算可知,想要獲取bits的中的內容,只需通過首地址平移32字節即可

以下是通過lldb命令調試的過程

獲取bits的lldb調試流程

  • 獲取類的首地址有兩種方式
    • 通過p/x CJLPerson.class直接獲取首地址

    • 通過x/4gx CJLPerson.class,打印內存信息獲取

獲取首地址圖示
  • 其中的data()獲取數據,是由objc_class提供的方法

    data方法

  • $2指針的打印結果中可以看出bits中存儲的信息,其類型是class_rw_t,也是一個結構體類型。但我們還是沒有看到屬性列表、方法列表等,需要繼續往下探索

探索 屬性列表,即 property_list

通過查看class_rw_t定義的源碼發現,結構體中有提供相應的方法去獲取 屬性列表、方法列表等,如下所示

class_rw_t中的相關方法

獲取bits并打印bits信息的基礎上,通過class_rw_t提供的方法,繼續探索 bits中的屬性列表,以下是lldb 探索的過程圖示

獲取屬性列表的lldb調試流程

  • p $8.properties()命令中的propertoes方法是由class_rw_t提供的,方法中返回的實際類型property_array_t

  • 由于list的類型是property_list_t,是一個指針,所以通過 p *$10獲取內存中的信息,同時也證明bits中存儲了 property_list,即屬性列表

  • p $11.get(1),想要獲取CJLPerson中的成員變量``bobby, 發現會報錯,提示數組越界了,說明 property_list 中只有 一個屬性cjl_name

【問題】探索成員變量的存儲

由此可得出property_list 中只有屬性,沒有成員變量,屬性與成員變量的區別就是有沒有set、get方法,如果有,則是屬性,如果沒有,則是成員變量。

那么問題來了,成員變量存儲在哪里?為什么會有這種情況?請移至文末的分析與探索

探索 方法列表,即methods_list

準備工作:在前文提及的CJLPerson中增加兩個方法(實例方法 & 類方法)

//CJLPerson.h
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end

//CJLPerson.m
@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end

也是通過lldb調試來獲取方法列表,步驟如圖所示


獲取方法列表的lldb調試流程
  • 通過 p $4.methods() 獲得具體的方法列表list結構,其中methods也是class_rw_t提供的方法

  • 通過打印的count = 4可知,存儲了4個方法,可以通過p $7.get(i)內存偏移的方式獲取單個方法,i 的范圍是0-3

  • 如果在打印 p $7.get(4),獲取第五個方法,也會報錯,提示數組越界

新問題的探索

【問題】探索成員變量的存儲

由上面的屬性列表分析可得出property_list 中只有屬性,沒有成員變量,那么問題來了,成員變量存儲在哪里?為什么會有這種情況?

通過查看objc_classbits屬性中存儲數據的類class_rw_t的定義發現,除了methods、properties、protocols方法,還有一個ro方法,其返回類型是class_ro_t,通過查看其定義,發現其中有一個ivars屬性,我們可以做如下猜測:是否成員變量就存儲在這個ivar_list_t類型的ivars屬性中呢?

下面是lldb的調試過程


成員變量存儲探索的調試
  • class_ro_t結構體中的屬性如下所示,想要獲取ivars,需要ro的首地址平移48字節
struct class_ro_t {
    uint32_t flags;     //4
    uint32_t instanceStart;//4
    uint32_t instanceSize;//4
#ifdef __LP64__
    uint32_t reserved;  //4
#endif

    const uint8_t * ivarLayout; //8
    
    const char * name; //1 ? 8
    method_list_t * baseMethodList; // 8
    protocol_list_t * baseProtocols; // 8
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    
    //方法省略
}

通過圖中可以看出,獲取的ivars屬性,其中的count2,通過打印發現 成員列表中除了有hobby,還有name,所以可以得出以下一些結論:

  • 通過{}定義的成員變量,會存儲在類的bits屬性中,通過bits --> data() -->ro() --> ivars獲取成員變量列表,除了包括成員變量,還包括屬性定義的成員變量

  • 通過@property定義的屬性,也會存儲在bits屬性中,通過bits --> data() --> properties() --> list獲取屬性列表,其中只包含屬性

【問題】探索類方法的存儲

由此可得出methods list 中只有 實例方法,沒有類方法,那么問題來了,類方法存儲在哪里?為什么會有這種情況?下面我們來仔細分析下

在文章前半部分,我們曾提及了元類類對象isa指向就是元類元類是用來存儲類的相關信息的,所以我們猜測:是否類方法存儲在元類的bits中呢?可以通過lldb命令來驗證我們的猜測。下圖是lldb命令的調試流程

類方法存儲的探索流程

通過圖中元類方法列表的打印結果,我們可以知道,我們的猜測是正確的,所以可以得出以下結論:

  • 實例方法存儲在類的bits屬性中,通過bits --> methods() --> list獲取實例方法列表,例如CJLPersong類的實例方法sayHello 就存儲在 CJLPerson類的bits屬性中,類中的方法列表除了包括實例方法,還包括屬性的set方法get方法

  • 類方法存儲在元類的bits屬性中,通過元類bits --> methods() --> list獲取類方法列表,例如CJLPerson中的類方法sayBye 就存儲在CJLPerson類的元類(名稱也是CJLPerson)的bits屬性中

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370