iOS原理探索04--類結構的分析

類的分析

  • 準備工作,我們先創建兩個類繼承NSObjectLGPerson和繼承LGPersonLGStudent
//.h文件
@interface LGPerson : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *lg_name;
- (void)sayHello;
+ (void)sayBye;
@end

//.m文件
@implementation LGPerson
- (void)sayHello
{
}
+ (void)sayBye
{
}
@end

  • main.m文件中如下設置
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson *person = [LGPerson alloc];
        LGStudent *student = [LGStudent alloc];
        NSLog(@"Hello, World! %@ - %@",person,student);
    }
    return 0;
}

  • 類結構的分析
    mian.m文件LGStudent *student = [LGStudent alloc];處打上斷點運行一下項目工程,使用lldb進行調試

    lldb調試過程及輸出結果

    • 首先我們可以根據lldb命令得到person的內存分布,我們知道0x001d8001000022ddperson的內存指針,接著使用這個指針&0x00007ffffffffff8ULL,可以獲取類的相關信息$3 = 0x00000001000022d8。
    • 接著po 0x00000001000022d8來打印類的信息,我們發現結果為LGPerson;
    • x/4gx 0x00000001000022d8,來打印LGPerson的內存情況,我們同樣的可以拿到類的isa指針地址0x00000001000022b0;
    • 下一步,p/x 0x00000001000022b0 & 0x00007ffffffffff8ULL,我們來獲取類的信息,我們得到$5 = 0x00000001000022b0類的指針地址;
    • 下一步:通過po 0x00000001000022b0,發現結果還是LGPerson,這是為什么呢?這里先簡單說一下這個LGPerson是元類;下面小節在詳細說明元類的問題。

二、元類,主要有以下幾點說明:

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

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

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

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

總結:由上圖的打印結果我們可以得出如下結論:對象 --> 類 --> 元類 --> 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(即繼承關系)的走向也有以下幾點說明:

  • 類(subClass) 繼承自 父類(superClass)
  • 父類(superClass) 繼承自 根類(RootClass),此時的根類是指NSObject。
  • 根類 繼承自 nil,所以根類即NSObject可以理解為萬物起源,即無中生有。
  • 子類的元類(metal SubClass) 繼承自 父類的元類(metal SuperClass)。
  • 父類的元類(metal SuperClass) 繼承自根元類(Root metal Class。
  • 根元類(Root metal Class) 繼承于根類(Root class),此時的根類是指NSObject。

舉例說明

2251862-48a5603729fdf0a9.png

isa 走位鏈

  • studentisa走位鏈:student(子類對象) --> LGStudent (子類)--> LGStudent(子元類) --> NSObject(根元類) --> NSObject(跟根元類,即自己)
    -personisa走位圖:person(父類對象) --> CJLPerson (父類)--> CJLPerson(父元類) --> NSObject(根元類) --> NSObject(跟根元類,即自己)

superclass走位鏈

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

四、objc_class & objc_object

在分析objc_class & objc_object我們先引入一個問題,為什么類和對象都有isa屬性呢?
我們先將main.m文件編譯為main.cpp來分析一下這個問題。我們根據clang編譯的c++源碼可以看出NSObject的底層編譯NSObject_IMPL結構體,并且對象含有Class isa,代碼如下

struct NSObject_IMPL {
    Class isa;
};

typedef struct objc_class *Class;


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

下面我們通過在objc4源碼中搜索來探索objc_class & objc_object

  • objc_class在源碼中搜索到兩種相關源碼
  • 第一種已經不再使用了,并且和我們使用Clang編譯后的一樣
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

} OBJC2_UNAVAILABLE;

另外一種,我們選擇主要的代碼進行展示如下

struct objc_class : objc_object {
    // 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_rw_t *data() const {
        return bits.data();
    }
};

我們發現objc_class繼承自objc_object,下面我們再看看objc_object源碼

  • objc_object源碼
struct objc_object {
private:
    isa_t isa;
};

總結:

  • 我們根據源碼發現objc_object結構體定義了isa作為它的一個屬性,objc_class繼承自objc_object,所以objc_class也擁有了isa屬性。
  • mian.cpp底層編譯文件中,NSObject中的isa在底層是由Class 定義的,其中class的底層編碼來自 objc_class類型,所以NSObject也擁有了isa屬性
  • NSObject是一個類,用它初始化一個實例對象objc,objc 滿足 objc_object 的特性,所以對象都有一個 isa,isa表示指向,來自于當前的objc_object。
  • 所以所有的對象都是以 objc_object為模板繼承過來的。
  • 因為對象是 來自NSObject(OC) ,但是真正到底層的 是一個objc_object(C/C++)的結構體類型,所以 objc_object對象的關系繼承關系。

objc_classobjc_object、isaobject、NSObject等的整體的關系,如下圖所示

2251862-7b4c0996f92eb166.png

類的方法

我們根據下面類的底層實現源碼來探索一下,類的實例方法存儲在哪里,來具體探索一下類的結構是怎樣的?

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    

 // ......部分代碼不在展示

}
  • 在探索之前我們需要補充一下知識,關于內存偏移,我們先使用實例說明一下這個內存偏移
    【普通指針】
        //普通指針
        int a = 10; //變量
        int b = 10;
        NSLog(@"%d -- %p", a, &a);
        NSLog(@"%d -- %p", b, &b);

輸出的結果
截屏2020-09-21 09.37.28.png

我們可以從控制臺看出,a和b兩個指針指向了同一片的存儲著10的空間。

  • a、b都指向10,但是a、b的地址``不一樣,這是一種拷貝,屬于值拷貝,也稱為淺拷貝

  • a,b的地址之間相差4個字節,這取決于a、b的類型
    【對象指針】

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

輸出結果

截屏2020-09-21 09.42.12.png

  • p1、p2 是指針,p1是 指向 [LGPerson 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);

輸出結果
截屏2020-09-21 09.45.06.png
  • &c&c[0] 都是取這個數組的首地址,所以``數組名等同于首地址;
  • &c&c[1] 相差4個字節,地址之間相差的字節數,主要取決于存儲的數據類型可以通過首地址+偏移量取出數組中的其他元素,其中偏移量數組的下標內存中首地址實際移動的字節數 等于偏移量 * 數據類型字節數`;
計算類結構的內存大小

通過上面類結構的源碼我們來計算一下類的大小
-isa屬性:繼承自objc_objectisa,占 8字節;

  • superclass屬性:Class類型,Class是由objc_object定義的,是一個指針,占8字節
  • cache屬性:是cache_t結構體類型,我們應該按照計算結構體內存大小的規則來計算,而結構體指針才是8字節;下面代碼可以計算出cache占16字節
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;
    mask_t _mask_unused;
//省略部分代碼
#if __LP64__
    uint16_t _flags; //是uint16_t類型,uint16_t是 unsigned short 的別名,占 2個字節
#endif
    uint16_t _occupied; //是uint16_t類型,uint16_t是 unsigned short 的別名,占 2個字節
//省略部分代碼
}
    
  • bits屬性:只有首地址經過上面3個屬性的內存大小總和的平移,才能獲取到bits`;
類結構中方法分析流程
(lldb) p/x LGPerson.class
(Class) $0 = 0x00000001000022f0 LGPerson
(lldb) //平移32個字節
error: '//平移32個字節' is not a valid command.
(lldb) p/x 0x00000001000022f0 + 32
(long) $1 = 0x0000000100002310
(lldb) 獲取bit 
error: '獲取bit' is not a valid command.
(lldb) p (class_data_bits_t *)0x0000000100002310
(class_data_bits_t *) $2 = 0x0000000100002310
(lldb) 通過結構體的data()來獲取bits
error: '通過結構體的data()來獲取bits' is not a valid command.
(lldb) p $2 -> data()
(class_rw_t *) $3 = 0x0000000102018a20
(lldb) 獲取methods()
error: '獲取methods()' is not a valid command.
(lldb) p $3.methods()
(const method_array_t) $4 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002180
      arrayAndFlag = 4294975872
    }
  }
}
  Fix-it applied, fixed expression was: 
    $3->methods()
(lldb) 獲取list
error: '獲取list' is not a valid command.
(lldb) p $4.list
(method_list_t *const) $5 = 0x0000000100002180
(lldb) 打印list內容
error: '打印list內容' is not a valid command.
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayHello"
      types = 0x0000000100000f85 "v16@0:8"
      imp = 0x0000000100000d80 (KCObjc`-[LGPerson sayHello])
    }
  }
}
(lldb) 
截屏2020-09-18 17.49.44.png
  • 關于類的屬性
(lldb) p $3.properties()
(const property_array_t) $7 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x0000000100002230
      arrayAndFlag = 4294976048
    }
  }
}
  Fix-it applied, fixed expression was: 
    $3->properties()
(lldb) p $7.list
(property_list_t *const) $8 = 0x0000000100002230
(lldb) p *$8
(property_list_t) $9 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "lg_name", attributes = "T@\"NSString\",C,N,V_lg_name")
  }
}

總結:類的實例方法和類的屬性都存在bits中,我們發現類的類方法和類的成員變量卻沒有打印,我們可以思考一下,它們存在哪里呢?類的類方法會不會存在元類里面呢?下一節我們接著探索一下這個內容。

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