iOS底層原理之類的結(jié)構(gòu)分析

類的結(jié)構(gòu)和定義

  • 首先跟蹤源碼忠聚,找到Class的的定義,發(fā)現(xiàn)其本質(zhì)為objc_class類型的指針,并且 objc_class繼承自objc_object缘眶,其中objc_class中有一個(gè)隱藏的isa指針,最后在objc_object中發(fā)現(xiàn)了isa的定義

這里要注意的是髓废,在new版本的源碼中磅崭,objc_class繼承自objc_object,在之前的舊版本中瓦哎,isa指針直接定義在objc_class中砸喻,其中OC中的NSObject在編譯到底層的時(shí)候都會(huì)轉(zhuǎn)變成相應(yīng)的結(jié)構(gòu)體objc_object

// 舊的版本,在OBJC2中已經(jīng)廢棄
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
// 新的版本蒋譬,也是現(xiàn)在源碼編譯調(diào)試使用的版本
typedef struct objc_class *Class;

// objc_class定義
struct objc_class : objc_object {
    // Class ISA; // 8
    Class superclass; // 8
    cache_t cache;    // 16 不是8         // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
  //  下面都是方法 
}

// objc_object定義
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

2 類的成員的存儲(chǔ)

回到objc_class割岛,其內(nèi)部定義了四個(gè)成員如下

    1. Class isa
    1. Class superclass
    1. cache_t cache
    1. class_data_bits_t bits
  • 首先我們知道isa的指針是關(guān)聯(lián)對(duì)象和類,superClass指向繼承類犯助,那么類的成員能夠存儲(chǔ)的地方就只有cache和bits
  • 先看一下cache的結(jié)構(gòu)體定義(不是一個(gè)結(jié)構(gòu)體指針癣漆,是一個(gè)結(jié)構(gòu)體),其中 mask_t為固定的4字節(jié)類型的值剂买,而bucket_t則是一個(gè)8字節(jié)的指針惠爽,都不能存放我們定義的屬性值癌蓖,所以可以排除cache,這里也看出 cache的內(nèi)存大小只有4+4+8=16字節(jié)(64位下)
struct cache_t {
    struct bucket_t *_buckets; // 8
    mask_t _mask;  // 4
    mask_t _occupied; // 4
}
#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif
}
  • 這里我們可以大膽猜測(cè)婚肆,bits中是否存放有我們定義的成員以及方法
2.1屬性的存儲(chǔ)探索
  • 首先為LGPerson新建一個(gè)成員變量hobby以及屬性nickName租副,并且添加了示例方法和類方法,下面開始代碼斷點(diǎn)
@interface LGPerson : NSObject{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
+ (void)sayHappy;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        Class pClass     = object_getClass(person);
        NSLog(@"%@ - %p",person,pClass);
    }
    return 0;
}
  • x/4gx 打印pClass的內(nèi)容较性,但是除了第一以及第二的內(nèi)存用僧,是我們熟悉的isa以及superClass指針以外,第三塊地址的內(nèi)容我們完全不知曉赞咙,第四塊地址直接就不存在
  • 按照Class結(jié)構(gòu)體的成員定義順序责循,以及內(nèi)存對(duì)齊原則,我們嘗試用指針偏移的方法攀操,來找到第四塊地址bits的所在院仿,并且看看bits存放的內(nèi)容到底為何
// 第一步
(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b37140
0x1000023c0: 0x00000001003da280 0x0000000000000000
(lldb) 
  • 首地址為0x1000023b0,其中isa指針占用8字節(jié)速和,superClass占用8字節(jié)歹垫,cache占用了16字節(jié)(上面計(jì)算過),那么按照內(nèi)存對(duì)齊原則健芭,我們用首地址偏移32個(gè)字節(jié)县钥,就應(yīng)該能得出bits的內(nèi)容 ,偏移后得出0x1000023d0為bits的理論地址慈迈,但是打印結(jié)果很迷茫若贮,這里我們強(qiáng)轉(zhuǎn)一下,再次打印痒留,終于打印出class_data_bits_t的結(jié)構(gòu)體
// 第二步
(lldb) po  0x1000023d0
objc[1017]: mutex incorrectly locked
4294976464

(lldb) p (class_data_bits_t *)0x1000023d0
(class_data_bits_t *) $2 = 0x00000001000023d0
  • class_rw_t為類中存儲(chǔ)屬性和方法的地方谴麦,看一下class_rw_t的實(shí)現(xiàn),返回的是bits.data()伸头,我們這里調(diào)用一下data方法之后得出一個(gè)class_rw_t類型的指針匾效,直接取值,結(jié)果如下
// 第三步
(lldb) p $2->data()
(class_rw_t *) $4 = 0x0000000100fd3150
(lldb) p *$4
(class_rw_t) $5 = {
  flags = 2148139008
  version = 0
  ro = 0x0000000100002308
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100002240
        arrayAndFlag = 4294976064
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x00000001000022f0
        arrayAndFlag = 4294976240
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
  demangledName = 0x0000000000000000
}
  • 使用p命令查看結(jié)果中properties的值恤磷,此時(shí)出現(xiàn)了新的類型property_array_t(在源碼的objc-runtime-new.h中有其定義)面哼,為一個(gè)二維數(shù)組,繼續(xù)探索其內(nèi)部list扫步,進(jìn)行 p $6.list魔策,此時(shí)出現(xiàn)property_list_t類型,繼承自entsize_list_tt河胎,在其內(nèi)部發(fā)現(xiàn)first方法闯袒,嘗試打印,最后找到了屬性nickName
(lldb) p $5.properties
(property_array_t) $6 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x00000001000022f0
      arrayAndFlag = 4294976240
    }
  }
}
(lldb) p $6.list
(property_list_t *) $7 = 0x00000001000022f0
(lldb) 
(lldb) p $7.first
(property_t) $8 = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
  Fix-it applied, fixed expression was: 
    $7->first
(lldb) 
// entsize_list_tt and property_list_t
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;
}
2.2 成員變量的存儲(chǔ)
  • 為了直觀的體現(xiàn),我們先重新編譯運(yùn)行一下項(xiàng)目政敢,斷點(diǎn)打上其徙,先直接跳到輸出class_rw_t
(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b37140
0x1000023c0: 0x00000001003da280 0x0000000000000000
(lldb) p (class_data_bits_t *)0x1000023d0
(class_data_bits_t *) $1 = 0x00000001000023d0
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000102139a80
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148139008
  version = 0
  ro = 0x0000000100002308
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100002240
        arrayAndFlag = 4294976064
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x00000001000022f0
        arrayAndFlag = 4294976240
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
  demangledName = 0x0000000000000000
}
  • 我們已經(jīng)知道properties存放的是類的屬性,結(jié)合class_rw_t里的方法名稱喷户,可以先嘗試探索一下ro部分
  • 先p出ro的地址唾那,得出一個(gè)class_ro_t類型的結(jié)構(gòu)體指針,我們直接取值摩骨,拿到class_ro_t結(jié)構(gòu)體的內(nèi)容通贞,從中可以找到ivars成員朗若,根據(jù)名字可以猜測(cè)恼五,成員變量有可能在其中
(lldb) p $3.ro
(const class_ro_t *) $4 = 0x0000000100002308
(lldb) p *$4
(const class_ro_t) $5 = {
  flags = 388
  instanceStart = 8
  instanceSize = 24
  reserved = 0
  ivarLayout = 0x0000000100001f89 "\x02"
  name = 0x0000000100001f80 "LGPerson"
  baseMethodList = 0x0000000100002240
  baseProtocols = 0x0000000000000000
  ivars = 0x00000001000022a8
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000022f0
}
(lldb) 
  • 繼續(xù)尋找ivar的值,打印得到ivar_list_t類型的指針哭懈,依舊取值灾馒,輸出內(nèi)容,發(fā)現(xiàn)了我們定義的成員變量hobby
(lldb) p $5.ivars
(const ivar_list_t *const) $7 = 0x00000001000022a8
(lldb) p *$7
(const ivar_list_t) $8 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x0000000100002378
      name = 0x0000000100001e64 "hobby"
      type = 0x0000000100001fa6 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}
(lldb) 
  • 到這里遣总,成員變量存放的位置我們也已經(jīng)找到睬罗,但是我們只是定義了一個(gè)hobby屬性,但是count顯示個(gè)數(shù)為2旭斥,我們用get方法拿到剩余的一個(gè)值_nickName容达,這里也證明了屬性的定義會(huì)自動(dòng)生成對(duì)應(yīng)的成員變量
(lldb) p $8.get(1)
(ivar_t) $9 = {
  offset = 0x0000000100002380
  name = 0x0000000100001e6a "_nickName"
  type = 0x0000000100001fa6 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) 
2.3 方法的存儲(chǔ)
  • 依然按照上面尋找成員變量存儲(chǔ)位置之時(shí)運(yùn)行的代碼,從打印出$3垂券,也就是class_rw_t的結(jié)構(gòu)開始進(jìn)行方法的查找花盐,可以發(fā)現(xiàn),class_rw_t中的properties代表著屬性的存儲(chǔ)菇爪,ro代表著成員變量的存儲(chǔ)算芯,那么可以推斷,methods則應(yīng)該存放類的方法

  • 先執(zhí)行 p $3.methods方法凳宙,獲得一個(gè)method_array_t類型的結(jié)構(gòu)體熙揍,打印出其中的list地址,并且取值得到一個(gè)entsize_list_tt氏涩,內(nèi)部的第一個(gè)元素存放著我們的sayHello方法

(lldb) p $3.methods
(method_array_t) $12 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002240
      arrayAndFlag = 4294976064
    }
  }
}
(lldb) p $12.list
(method_list_t *) $13 = 0x0000000100002240
(lldb) p *$13
(method_list_t) $14 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayHello"
      types = 0x0000000100001f8b "v16@0:8"
      imp = 0x0000000100001b90 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
    }
  }
}
(lldb) 
  • 但是可以發(fā)現(xiàn)届囚,其數(shù)組個(gè)數(shù)count=4,也就是除了我們定義的sayHello方法之外還有另外的三個(gè)方法是尖,輸出打印其他三個(gè)方法名稱依次為C++的析構(gòu)函數(shù)destruct方法意系,屬性nickName的gettersetter方法
(lldb) p $14.get(1)
(method_t) $15 = {
  name = ".cxx_destruct"
  types = 0x0000000100001f8b "v16@0:8"
  imp = 0x0000000100001c60 (LGTest`-[LGPerson .cxx_destruct] at LGPerson.m:11)
}
(lldb) p $14.get(2)
(method_t) $16 = {
  name = "nickName"
  types = 0x0000000100001f93 "@16@0:8"
  imp = 0x0000000100001bf0 (LGTest`-[LGPerson nickName] at LGPerson.h:17)
}
(lldb) p $14.get(3)
(method_t) $17 = {
  name = "setNickName:"
  types = 0x0000000100001f9b "v24@0:8@16"
  imp = 0x0000000100001c20 (LGTest`-[LGPerson setNickName:] at LGPerson.h:17)
}
  • 至此,我們已經(jīng)探索出了類的示例方法存放在類的class_rw_t結(jié)構(gòu)體中的methods里面
2.4 類方法的存儲(chǔ)
  • 通過上面的步驟析砸,我們已經(jīng)可以了解到類的實(shí)例方法的存儲(chǔ)昔字,但是并沒有發(fā)現(xiàn)類方法sayHello的存儲(chǔ),通過class_rw_t結(jié)構(gòu)體內(nèi)部的名稱分析,基本可以判斷并沒有適合存放類方法的位置

  • 那么再回到Class的基本結(jié)構(gòu)成員作郭,isa陨囊,superClass,cache夹攒,bits四個(gè)成員蜘醋,其中bits.data就是我們一直在尋找的class_rw_t結(jié)構(gòu)體,已經(jīng)證明其內(nèi)部不可能存放類方法咏尝,cache內(nèi)部的8字節(jié)的bucket_t指針存放的是key和imp的鍵值對(duì)压语,和我們了解的方法的存儲(chǔ)結(jié)構(gòu)并不一樣,所以我們暫時(shí)先跳過编检。而superClass則是其父類胎食,LGPerson的父類為NSObject,但是NSObject內(nèi)部并沒有sayHello方法允懂,所以也可以排除在外

  • 最后剩下isa指針厕怜,在之前的文章中isa指針走向
    ,我們探索過了isa指針的走向蕾总,了解到了類的isa指針粥航,指向的是一個(gè)同名類,我們把它叫做元類生百,那么類方法會(huì)不會(huì)保存在元類中递雀,我們測(cè)試一下

  • lldb控制臺(tái)輸入命令 x/4gx pClass之后,先通過isa指針查找到LGPerson的元類

(lldb)  x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b37140
0x1000023c0: 0x00000001003da280 0x0000000000000000
(lldb) p/x 0x001d800100002389&0x00007ffffffffff8ULL
(unsigned long long) $19 = 0x0000000100002388
(lldb) po 0x0000000100002388
LGPerson

(lldb) 
  • 因?yàn)樵愐彩穷惖囊环N蚀浆,也是繼承自NSObject的一種特殊結(jié)構(gòu)缀程,所以我們也可以依舊按照對(duì)類的查找方法來進(jìn)行元類的結(jié)構(gòu)探索,其中元類的地址為0x0000000100002388蜡坊,依次找出class_data_bits_t杠输,通過->data()方法找到class_rw_t結(jié)構(gòu)體,打印出里面的methods秕衙,獲取其中的list數(shù)組蠢甲,最后找到了我們定義的類方法sayHappy流程和查找類的實(shí)例方法一樣,所以直接看結(jié)果
(lldb) p $26.list
(method_list_t *) $27 = 0x00000001000021d8
(lldb) p *$27
(method_list_t) $28 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "sayHappy"
      types = 0x0000000100001f8b "v16@0:8"
      imp = 0x0000000100001bc0 (LGTest`+[LGPerson sayHappy] at LGPerson.m:17)
    }
  }
}
(lldb) 

總結(jié)

  • 類之間除了基本的繼承關(guān)系之外据忘,還依靠isa指針進(jìn)行對(duì)象和類的關(guān)聯(lián)鹦牛,也就是*對(duì)象-類-元類-根源類-根源類這一組isa關(guān)系圖
  • 其中類的屬性和成員變量都存放在類的class_rw_t結(jié)構(gòu)體中
  • 屬性的定義,還伴隨著成員變量以及其getter和setter的自動(dòng)生成
  • 類的類方法勇吊,則以實(shí)例方法的形式曼追,存放在元類中,而元類又是繼承自NSObject汉规,形成一個(gè)閉環(huán)
  • 至此礼殊,類的基本結(jié)構(gòu)以及其成員變量驹吮,屬性和方法的存儲(chǔ)也基本探索清楚了,如果有失誤或者補(bǔ)足的地方晶伦,還望留言一起討論~
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末碟狞,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子婚陪,更是在濱河造成了極大的恐慌族沃,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泌参,死亡現(xiàn)場(chǎng)離奇詭異脆淹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)沽一,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門盖溺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锯玛,你說我怎么就攤上這事咐柜〖骝冢” “怎么了攘残?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長为狸。 經(jīng)常有香客問我歼郭,道長,這世上最難降的妖魔是什么辐棒? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任病曾,我火速辦了婚禮,結(jié)果婚禮上漾根,老公的妹妹穿的比我還像新娘泰涂。我一直安慰自己,他們只是感情好辐怕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布逼蒙。 她就那樣靜靜地躺著,像睡著了一般寄疏。 火紅的嫁衣襯著肌膚如雪是牢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天陕截,我揣著相機(jī)與錄音驳棱,去河邊找鬼。 笑死农曲,一個(gè)胖子當(dāng)著我的面吹牛社搅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼形葬,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼却汉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荷并,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤合砂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后源织,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翩伪,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年谈息,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缘屹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡侠仇,死狀恐怖轻姿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情逻炊,我是刑警寧澤互亮,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站余素,受9級(jí)特大地震影響豹休,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜桨吊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一威根、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧视乐,春花似錦洛搀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至渣聚,卻和暖如春独榴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背奕枝。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國打工棺榔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人隘道。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓症歇,卻偏偏與公主長得像郎笆,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子忘晤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355