iOS之類的結(jié)構(gòu)分析

萬物皆對象

我們知道在iOS中旅掂,id可以指向所有的實例對象,Class可以指向所有的類访娶,我們來看一下他們的聲明:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// A pointer to an instance of a class.
typedef struct objc_object *id;

從聲明中可以看出商虐,OC的所有實例對象都是由objc_object結(jié)構(gòu)體擴(kuò)展而來,我們知道objc_object結(jié)構(gòu)體中有一個isa指針崖疤,所以所有的對象都有isa指針秘车。
我們再來看一下objc_class的聲明:

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的結(jié)構(gòu)可以得出,OC中所有的其實也是對象权烧,也存在isa指針眯亦,所以這就是我們常說的萬物皆對象伤溉。

對象、類妻率、元類乱顾、根元類

在前一篇iOS之isa文章的最后,插入了一張圖舌涨,如下:

類的繼承.png

很多iOS開發(fā)者糯耍,肯定不止一次見到過這張圖,我想肯定有人會有疑問囊嘉,真的像圖中這樣嗎温技?下面我們驗證一下。

@interface PYTeacher : NSObject

@end

@implementation PYTeacher

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        PYTeacher *teacher = [PYTeacher alloc];
        NSLog(@"%@", teacher);
    }
    return 0;
}

依然是上面這段代碼扭粱,我們可以通過打印對象的內(nèi)存分布來驗證舵鳞,依然以x86_64為例,ISA_MASK0x00007ffffffffff8ULL

對象的 isa 指向類

(lldb) x/2gx teacher
0x100712980: 0x001d800100002359 0x0000000000000000
// isa & ISA_MASK
(lldb) po 0x001d800100002359 & 0x00007ffffffffff8ULL
PYTeacher

從上面的打印結(jié)果琢蛤,我們得出 teacher對象的 isa 指向 PYTeacher 類:

image.png

類的 isa 指向元類

由第一節(jié)我們得出蜓堕,類也是對象,也存在isa指針博其,那類的isa指針指向哪里呢套才?我們打印一下類的isa

(lldb) x/2gx PYTeacher.class
0x100002358: 0x0000000100002330 0x00007fff940b3118

// 打印 PYTeacher 的元類
(lldb) po 0x0000000100002330 & 0x00007ffffffffff8ULL
PYTeacher

從打印結(jié)果,我們看到慕淡,PYTeacherisa也指向PYTeacher背伴,那這兩個PYTeacher類是同一個嗎?我們打印一下他們各自的地址:

(lldb) p/x 0x0000000100002330 & 0x00007ffffffffff8ULL
// PYTeacher元類的地址
(unsigned long long) $4 = 0x0000000100002330

(lldb) p/x PYTeacher.class
// PYTeacher類的地址
(Class) $5 = 0x0000000100002358 PYTeacher

我們看到這兩個PYTeacher的地址并不相同峰髓,實際上PYTeacherisa指向了他的元類 PYTeacher

image.png

元類的 isa 指向根元類

得到上面isa的指向流程以后傻寂,我們肯定會想,元類的 isa 指向誰呢携兵?我們還是通過打印來看一下:

(lldb) x/2gx 0x0000000100002330 // PYTeacher元類的地址
0x100002330: 0x00007fff940b30f0 0x00007fff940b30f0

// 打印根元類
(lldb) po 0x00007fff940b30f0 & 0x00007ffffffffff8ULL
NSObject

從打印結(jié)果看疾掰,元類的isa指向NSObject,這時徐紧,我們會想這個NSObject是我們常用的那個NSObject嗎静檬?我們來驗證一下:

(lldb) p/x 0x00007fff940b30f0 & 0x00007ffffffffff8ULL
(unsigned long long) $7 = 0x00007fff940b30f0 // 剛才得出的NSObject的地址

(lldb) p/x NSObject.class
(Class) $8 = 0x00007fff940b3118 NSObject // 我們常用的NSObject的地址

(lldb) x/2gx NSObject.class
0x7fff940b3118: 0x00007fff940b30f0 0x0000000000000000

(lldb) p/x 0x00007fff940b30f0 & 0x00007ffffffffff8ULL
(unsigned long long) $10 = 0x00007fff940b30f0 // NSObject的元類的地址

我們看到剛才得出的NSObjectNSObject元類,也就是根元類

image.png

根元類的 isa 指向自己

驗證到這里并级,肯定有人會想拂檩,根元類的 isa 又指向哪里呢?我們繼續(xù)驗證:

(lldb) x/2gx 0x00007fff940b30f0 // 根元類的地址
0x7fff940b30f0: 0x00007fff940b30f0 0x00007fff940b3118

(lldb) p/x 0x00007fff940b30f0 & 0x00007ffffffffff8ULL
(unsigned long long) $11 = 0x00007fff940b30f0 // 根元類isa指向的地址

(lldb) po 0x00007fff940b30f0 & 0x00007ffffffffff8ULL
NSObject

打印結(jié)果得出:根元類的 isa 指向了自己死遭。

image.png

類的結(jié)構(gòu)

我們在iOS的源碼中看到objc_class的結(jié)構(gòu)如下:

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的成員變量在內(nèi)存中的分布如下:

image.png

從內(nèi)存分布可以看出钉迷,我們可以通過移動pointer(類的地址指針),來讀取不同地址的信息钠署。我們知道 isa8 個字節(jié)糠聪,所以對 pointer移動8個字節(jié),就可以讀取superclass的內(nèi)容谐鼎,下面我們可以驗證一下:

@interface PYEnglishTeacher : PYTeacher
@end

@implementation PYEnglishTeacher
@end

我們創(chuàng)建了一個PYEnglishTeacher類繼承自PYTeacher類:

(lldb) p/x PYEnglishTeacher.class
(Class) $0 = 0x0000000100002438 PYEnglishTeacher

(lldb) po 0x0000000100002438 + 8
<PYTeacher: 0x100002440>

所以舰蟆,對PYEnglishTeacher類的地址移動8個字節(jié),就可以讀取到superclass狸棍。

Methods

我們發(fā)現(xiàn)在objc_class中身害,有isasuperclass草戈、cache塌鸯、bits等成員變量,但是我們自己聲明的屬性唐片、方法丙猬、協(xié)議存儲在哪呢?我們知道费韭,在以前的objc_class定義中茧球,是有objc_ivar_listobjc_method_list星持、objc_cache抢埋、objc_protocol_list等成員變量的,那現(xiàn)在蘋果是如何設(shè)計的呢钉汗?我們發(fā)現(xiàn)objc_class中有class_rw_t *data()的方法羹令,class_rw_t定義如下:

struct class_rw_t {

   省略。损痰。福侈。

    const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>()->ro;
        }
        return v.get<const class_ro_t *>();
    }

    void set_ro(const class_ro_t *ro) {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            v.get<class_rw_ext_t *>()->ro = ro;
        } else {
            set_ro_or_rwe(ro);
        }
    }

    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>()->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
        }
    }
};

class_rw_t 的結(jié)構(gòu),我們得出可以通過 class_rw_t 來讀取方法列表等卢未。所以首先我們要知道肪凛,需要移動多少字節(jié),才能讀取objc_class中的bits辽社。
我們知道isa為8字節(jié)伟墙,superclass為8字節(jié),那cache是多少字節(jié)呢滴铅?我們看一下cache_t的結(jié)構(gòu):

struct cache_t {

    explicit_atomic<struct bucket_t *> _buckets;  // 8字節(jié)
    explicit_atomic<mask_t> _mask;  // 4字節(jié)
    省略戳葵。。
#if __LP64__
    uint16_t _flags;  // 2字節(jié)
#endif
    uint16_t _occupied; // 2字節(jié)
}

所以 cache 是16字節(jié)汉匙。所以pointer移動32個字節(jié)就是bits的地址拱烁。我們打印一下PYTeacher

(lldb) p/x PYTeacher.class
(Class) $0 = 0x0000000100002208 PYTeacher

// 打印class_data_bits_t bits地址
(lldb) p (class_data_bits_t *)(0x0000000100002208 + 32)
(class_data_bits_t *) $1 = 0x0000000100002228

$1class_data_bits_t結(jié)構(gòu)體的指針生蚁,通過調(diào)用data()方法可以得到class_rw_t的指針:

(lldb) p $1->data()
(class_rw_t *) $2 = 0x00000001021100b0

我們打印$2指向的class_rw_t結(jié)構(gòu)體:

(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975600
  }
  firstSubclass = PYEnglishTeacher
  nextSiblingClass = NSUUID
}

現(xiàn)在我們PYTeacher添加一些方法和屬性:

@interface PYTeacher : NSObject {
    NSString *name;
}

@property (nonatomic, assign) int age;

- (void)teach;
+ (void)study;

@end

@implementation PYTeacher

- (void)teach {
    
}

+ (void)study {
    
}

@end

這時,我們再打印class_rw_t的內(nèi)容:

(lldb) p/x PYTeacher.class
(Class) $0 = 0x0000000100002300 PYTeacher

// 打印bits的地址
(lldb) p (class_data_bits_t *)(0x0000000100002300 + 32)
(class_data_bits_t *) $1 = 0x0000000100002320

// 獲取class_rw_t的指針
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000100677d80

// 打印 class_rw_t 內(nèi)容
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975632
  }
  firstSubclass = PYEnglishTeacher
  nextSiblingClass = NSUUID
}

// 獲取class_rw_t中的方法列表
(lldb) p $3.methods()
(const method_array_t) $4 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000020d8
      arrayAndFlag = 4294975704
    }
  }
}

// 獲取method_list_t的數(shù)組指針
(lldb) p $4.list
(method_list_t *const) $5 = 0x00000001000020d8

// 打印method_list_t數(shù)組戏自,其實就是數(shù)組的第0位
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "teach"
      types = 0x0000000100000f74 "v16@0:8"
      imp = 0x0000000100000de0 (KCObjc`-[PYTeacher teach] at main.m:24)
    }
  }
}

// 打印數(shù)組中的內(nèi)容
(lldb) p $6.get(0)
(method_t) $7 = {
  name = "teach"
  types = 0x0000000100000f74 "v16@0:8"
  imp = 0x0000000100000de0 (KCObjc`-[PYTeacher teach] at main.m:24)
}

(lldb) p $6.get(1)
(method_t) $8 = {
  name = ".cxx_destruct"
  types = 0x0000000100000f74 "v16@0:8"
  imp = 0x0000000100000e30 (KCObjc`-[PYTeacher .cxx_destruct] at main.m:22)
}

(lldb) p $6.get(2)
(method_t) $9 = {
  name = "age"
  types = 0x0000000100000f8a "i16@0:8"
  imp = 0x0000000100000df0 (KCObjc`-[PYTeacher age] at main.m:15)
}

(lldb) p $6.get(3)
(method_t) $10 = {
  name = "setAge:"
  types = 0x0000000100000f92 "v20@0:8i16"
  imp = 0x0000000100000e10 (KCObjc`-[PYTeacher setAge:] at main.m:15)
}

我們看到方法列表中有4個方法邦投,但是沒有
+ (void)study方法,為什么呢擅笔?
仔細(xì)觀察我們會發(fā)現(xiàn)志衣,這4個方法,除了1個C++的方法外猛们,其他3個都是對象方法念脯,也就是對象方法存在類的方法列表里,我們說萬物皆對象阅懦,元類對象和二,難道說類對象”對象方法“存在元類的方法列表里?下面我們驗證一下:

(lldb) x/2gx PYTeacher.class
0x100002300: 0x00000001000022d8 0x0000000100334140
(lldb) po 0x00000001000022d8 & 0x00007ffffffffff8ULL
PYTeacher

// 打印 PYTeacher元類 的地址
(lldb) p/x 0x00000001000022d8 & 0x00007ffffffffff8ULL
(unsigned long long) $13 = 0x00000001000022d8

// 打印元類中的 bits
(lldb) p (class_data_bits_t *)(0x00000001000022d8 + 32)
(class_data_bits_t *) $14 = 0x00000001000022f8

// 獲取 class_rw_t 的地址
(lldb) p $14->data()
(class_rw_t *) $15 = 0x0000000100677d40

// 打印 class_rw_t 的內(nèi)容
(lldb) p *$15
(class_rw_t) $16 = {
  flags = 2684878849
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975528
  }
  firstSubclass = 0x0000000100002328
  nextSiblingClass = 0x00007fff8b968cd8
}

// 獲取 元類 的方法列表
(lldb) p $16.methods()
(const method_array_t) $17 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002070
      arrayAndFlag = 4294975600
    }
  }
}

// 打印方法列表的地址
(lldb) p $17.list
(method_list_t *const) $18 = 0x0000000100002070

// 打印方法列表信息
(lldb) p *$18
(method_list_t) $19 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "study"
      types = 0x0000000100000f74 "v16@0:8"
      imp = 0x0000000100000dd0 (KCObjc`+[PYTeacher study] at main.m:28)
    }
  }
}

正如我們猜測的那樣耳胎,+ (void)study方法存在元類方法列表中惯吕。

Properties

我們再來看一下property

(lldb) p $3.properties()
(const property_array_t) $20 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x0000000100002188
      arrayAndFlag = 4294975880
    }
  }
}

// 打印屬性列表地址
(lldb) p $20.list
(property_list_t *const) $21 = 0x0000000100002188

// 打印屬性列表信息
(lldb) p *$21
(property_list_t) $22 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "age", attributes = "Ti,N,V_age")
  }
}

這里我們看到了age屬性,在class_rw_t中怕午,我們卻沒找到成員變量的存儲位置废登,但是class_rw_t中,有獲取和設(shè)置class_ro_t的方法郁惜,我們來看一下class_ro_t結(jié)構(gòu)體堡距。

class_ro_t

class_ro_t結(jié)構(gòu)體如下:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    省略。兆蕉。羽戒。
};

我們看到class_ro_t中有const ivar_list_t * ivars,我們來打印一下:

(lldb) p $3.ro()
(const class_ro_t *) $23 = 0x0000000100002090

// 打印 class_ro_t 內(nèi)容
(lldb) p *$23
(const class_ro_t) $24 = {
  flags = 388
  instanceStart = 8
  instanceSize = 20
  reserved = 0
  ivarLayout = 0x0000000100000f28 "\x01"
  name = 0x0000000100000f1e "PYTeacher"
  baseMethodList = 0x00000001000020d8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000100002140
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000100002188
  _swiftMetadataInitializer_NEVER_USE = {}
}

// 打印 ivar_list_t 地址
(lldb) p $24->ivars
(const ivar_list_t *const) $25 = 0x0000000100002140
  Fix-it applied, fixed expression was: 
    $24.ivars

// 打印 ivar_list_t 內(nèi)容
(lldb) p *$25
(const ivar_list_t) $26 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x00000001000022c8
      name = 0x0000000100000f4a "name"
      type = 0x0000000100000f7c "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}

// 獲取成員變量
(lldb) p $26.get(0)
(ivar_t) $27 = {
  offset = 0x00000001000022c8
  name = 0x0000000100000f4a "name"
  type = 0x0000000100000f7c "@\"NSString\""
  alignment_raw = 3
  size = 8
}

(lldb) p $26.get(1)
(ivar_t) $28 = {
  offset = 0x00000001000022d0
  name = 0x0000000100000f4f "_age"
  type = 0x0000000100000f88 "i"
  alignment_raw = 2
  size = 4
}

ivar_list_t中存儲這name_age成員變量虎韵。

小結(jié)

  1. 對象的成員變量存儲在class_ro_t中易稠;
  2. 對象方法屬性存儲在class_rw_t中;
  3. 類方法存儲在元類class_rw_t中包蓝。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末驶社,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子测萎,更是在濱河造成了極大的恐慌亡电,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件硅瞧,死亡現(xiàn)場離奇詭異份乒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門或辖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拇勃,“玉大人,你說我怎么就攤上這事孝凌。” “怎么了月腋?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵蟀架,是天一觀的道長。 經(jīng)常有香客問我榆骚,道長片拍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任妓肢,我火速辦了婚禮捌省,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘碉钠。我一直安慰自己纲缓,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布喊废。 她就那樣靜靜地躺著祝高,像睡著了一般。 火紅的嫁衣襯著肌膚如雪污筷。 梳的紋絲不亂的頭發(fā)上工闺,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機(jī)與錄音瓣蛀,去河邊找鬼陆蟆。 笑死,一個胖子當(dāng)著我的面吹牛惋增,可吹牛的內(nèi)容都是我干的叠殷。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼器腋,長吁一口氣:“原來是場噩夢啊……” “哼溪猿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起纫塌,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤诊县,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后措左,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體依痊,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了胸嘁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓶摆。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖性宏,靈堂內(nèi)的尸體忽然破棺而出群井,到底是詐尸還是另有隱情,我是刑警寧澤毫胜,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布书斜,位于F島的核電站,受9級特大地震影響酵使,放射性物質(zhì)發(fā)生泄漏荐吉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一口渔、第九天 我趴在偏房一處隱蔽的房頂上張望样屠。 院中可真熱鬧,春花似錦缺脉、人聲如沸痪欲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽勤揩。三九已至,卻和暖如春秘蛔,著一層夾襖步出監(jiān)牢的瞬間陨亡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工深员, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留负蠕,地道東北人。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓倦畅,卻偏偏與公主長得像遮糖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子叠赐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348