iOS底層之類結構分析

上篇文章: iOS底層之isa走位探索

前言

從上篇文章中我們了解了對象的isa指針的走位邏輯萌踱,接下來咱們分析一下類的結構耗式。

一叛甫、內存偏移

在咱們分析類結構之前沦寂,咱們先來了解一下內存偏移的知識蜒程。咱們先看一個例子

void pointOffset(){
    int arr[4] = {1, 3, 5, 6};
    int *p = arr;
            
    for (int i=0; i<4; i++) {
        NSLog(@"%p -- %d", p+i, arr[i]);
    }
}

打印結果為

0x7ffeefbff4e0 -- 1
0x7ffeefbff4e4 -- 3
0x7ffeefbff4e8 -- 5
0x7ffeefbff4ec -- 6

從打印結果可以看出這四個內存地址是連續(xù)的绅你,切每個地址相差4字節(jié)伺帘,因為int類型的內存大小為4字節(jié)。也就是說如果我們知道一個對象的首地址忌锯,且知道其后排列的每個元素的內存大小伪嫁,那么我們就可以知道后面每個元素的大小。接下來我們來驗證一下

int數(shù)組內存偏移
如上圖所示偶垮,如果我們知道了首地址张咳,并且知道后面每一個元素的大小,我們就可以推出后面的元素的內存地址似舵。如果對lldb命令不熟的同學可以看下lldb內存讀取這篇文章脚猾。
可能有的同學認為這個例子有點簡單,且數(shù)組中的元素的內存大小一致砚哗,無法說明問題龙助,如果是每個元素不一致又該怎么辦。
為了更具有說服力蛛芥,接下來咱們舉一個復雜的例子

struct StructPointTest {    //內存大小      內存所在地址    比首地址多幾個字節(jié)
    double a;               // 8            (0-7)           0字節(jié)
    short b;                // 2            (8-9)           8字節(jié)
    int c;                  // 4            (12-15)         12字節(jié)
    struct Struct2 d;       //16            (16-31)         16字節(jié)
    WJPerson *e;            // 8            (32-39)         32字節(jié)
    
}structPointTest;

根據(jù)這篇iOS底層之內存對齊文章提鸟,咱們能夠知道StructPointTest結構體中每個元素的內存大小內存所在地址以及比首地址多幾個字節(jié),咱們就用這個結構體來驗證一下

struct StructPointTest str = {1.0, 3, 2, {4,5,'a',7}, [WJPerson alloc]};
        
NSLog(@"\n%p -- %f \n%p -- %d \n%p -- %d \n%p -- \n%p -- %@", &str.a, str.a, &str.b, str.b, &str.c, str.c, &str.d, &str.e, str.e);

咱們首先打印下str所在的首地址

StructPointTest結構體首地址
根據(jù)咱們上面得出的結論我們可以推導出StructPointTest里各個元素的地址:

  • str.a 的地址為str的首地址就是0x00007ffeefbff550
  • str.b 的地址比首地址多8個字節(jié)仅淑,推出地址為0x00007ffeefbff558
  • str.c 的地址比首地址多12個字節(jié)称勋,推出地址為0x00007ffeefbff55c
  • str.d 的地址比首地址多16個字節(jié),推出地址為0x00007ffeefbff560
  • str.e 的地址比首地址多32個字節(jié)涯竟,推出地址為0x00007ffeefbff570

接下來咱們使用lldb調試打印一下這些地址

相關地址的值
我們再看一下NSLog打印的值

0x7ffeefbff550 -- 1.000000 
0x7ffeefbff558 -- 3 
0x7ffeefbff55c -- 2 
0x7ffeefbff560 -- 
0x7ffeefbff570 -- <WJPerson: 0x1006b1110>

由此我們可以得出結論:只要知道一個對象的首地址的值赡鲜,就可以根據(jù)對象中元素的內存大小推導出每個元素的內存地址

二庐船、類結構分析

我們在最新的objc4源碼中搜索objc_class會發(fā)現(xiàn)兩個版本的結構體定義银酬,一個是runtime.h文件里定義的老版的objc_class

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;        //已廢棄的

一個是objc-runtime-new.h文件里定義的新版的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

    //....方法部分已省略捡硅,目前顯示的是objc_class的
}

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

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

接下來我們分析一下新版的objc_class壮韭。

1、類結構分析之bits

從最新的objc_class的定義中我們知道objc_class中有4個成員變量纹因,分別為isa喷屋,superclasscachebits瞭恰。isasuperclass我們已經(jīng)了解了屯曹,cache我們根據(jù)名字就可以知道是緩存信息,那么class_data_bits_t bits;中又存放了一下什么信息呢。
正常情況下我們無法直接訪問objc_class中的bits內容恶耽,要想了解bits的信息密任,我們就需要想辦法訪問bits所在的內存空間,這時候就需要用到我們上文提到的內存偏移的知識了偷俭。
我們已經(jīng)知道isasuperclass的內存大小都是8字節(jié)浪讳,那么cache又占了多少字節(jié)呢。我們先看下cache_t中除去static修飾的靜態(tài)變量和方法外還有什么涌萤。

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;    //8字節(jié)
    explicit_atomic<mask_t> _mask;                  //4字節(jié)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;     //8字節(jié)
    mask_t _mask_unused;                            //4字節(jié)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    // _maskAndBuckets stores the mask shift in the low 4 bits, and
    // the buckets pointer in the remainder of the value. The mask
    // shift is the value where (0xffff >> shift) produces the correct
    // mask. This is equal to 16 - log2(cache_size).
    explicit_atomic<uintptr_t> _maskAndBuckets;     //8字節(jié)
    mask_t _mask_unused;                            //4字節(jié)
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
    uint16_t _flags;                                //2字節(jié)
#endif
    uint16_t _occupied;                             //2字節(jié)
}

可以看到第一個#if#endif直接不管滿足什么條件都是12字節(jié)淹遵,如果第二個#if滿足條件就是16字節(jié),否則就是14字節(jié)负溪,但是不管是14字節(jié)還是16字節(jié)透揣,根據(jù)內存對齊原則cache_t的大小都是16字節(jié)。所以我們最后得出結論:cache_t的大小為16字節(jié)川抡。
接下來我們通過lldb打印一下bits的信息辐真。

@interface WJPerson : NSObject
{
    NSString *habby;
}

@property (nonatomic, copy) NSString *name;

- (void)sayHello;

+ (void)sayGoodbye;

@end

@implementation WJPerson

- (void)sayHello{}

+ (void)sayGoodbye{}

@end

我們先給WJPerson添加一下信息,然后再看一下WJPerson的信息崖堤。
我們先來看下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.
    void setAndClearBits(uintptr_t set, uintptr_t clear)
    {
        ASSERT((set & clear) == 0);
        uintptr_t oldBits;
        uintptr_t newBits;
        do {
            oldBits = LoadExclusive(&bits);
            newBits = (oldBits | set) & ~clear;
        } while (!StoreReleaseExclusive(&bits, oldBits, newBits));
    }

    void setBits(uintptr_t set) {
        __c11_atomic_fetch_or((_Atomic(uintptr_t) *)&bits, set, __ATOMIC_RELAXED);
    }

    void clearBits(uintptr_t clear) {
        __c11_atomic_fetch_and((_Atomic(uintptr_t) *)&bits, ~clear, __ATOMIC_RELAXED);
    }

public:

    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    void setData(class_rw_t *newData)
    {
        ASSERT(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
        // Set during realization or construction only. No locking needed.
        // Use a store-release fence because there may be concurrent
        // readers of data and data's contents.
        uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
        atomic_thread_fence(memory_order_release);
        bits = newBits;
    }

    // Get the class's ro data, even in the presence of concurrent realization.
    // fixme this isn't really safe without a compiler barrier at least
    // and probably a memory barrier when realizeClass changes the data field
    const class_ro_t *safe_ro() {
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) {
            // maybe_rw is rw
            return maybe_rw->ro();
        } else {
            // maybe_rw is actually ro
            return (class_ro_t *)maybe_rw;
        }
    }

    void setClassArrayIndex(unsigned Idx) {
#if SUPPORT_INDEXED_ISA
        // 0 is unused as then we can rely on zero-initialisation from calloc.
        ASSERT(Idx > 0);
        data()->index = Idx;
#endif
    }

    unsigned classArrayIndex() {
#if SUPPORT_INDEXED_ISA
        return data()->index;
#else
        return 0;
#endif
    }

    bool isAnySwift() {
        return isSwiftStable() || isSwiftLegacy();
    }

    bool isSwiftStable() {
        return getBit(FAST_IS_SWIFT_STABLE);
    }
    void setIsSwiftStable() {
        setAndClearBits(FAST_IS_SWIFT_STABLE, FAST_IS_SWIFT_LEGACY);
    }

    bool isSwiftLegacy() {
        return getBit(FAST_IS_SWIFT_LEGACY);
    }
    void setIsSwiftLegacy() {
        setAndClearBits(FAST_IS_SWIFT_LEGACY, FAST_IS_SWIFT_STABLE);
    }

    // fixme remove this once the Swift runtime uses the stable bits
    bool isSwiftStable_ButAllowLegacyForNow() {
        return isAnySwift();
    }

    _objc_swiftMetadataInitializer swiftMetadataInitializer() {
        // This function is called on un-realized classes without
        // holding any locks.
        // Beware of races with other realizers.
        return safe_ro()->swiftMetadataInitializer();
    }
};

通過上面代碼我們發(fā)現(xiàn)拆祈,除了class_rw_t* data()const class_ro_t *safe_ro()返回了對象外,剩下的返回值就是bool類型倘感、void類型或基礎數(shù)據(jù)類型。所以我們接下來主要看class_rw_t* data()const class_ro_t *safe_ro()
通過上面的分析計算得出bits比首地址多8(isa)+8(superclass)+16(cache)也就是32個字節(jié)咙咽。接下來我們實際操作獲取下信息老玛。
我們先獲取下class_rw_t* data()的信息

class_rw_t* data()返回值
接下來我們看一下class_ro_t里有什么

struct class_rw_t {
    //這里只展示我們經(jīng)常接觸的內容,如需看完整代碼請自行查看源碼
    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中有methods钧敞、propertiesprotocols蜡豹。我們在類中定義了一些屬性和方法看下能不能在這里看到。

methods的信息
methods方法列表
屬性列表

從上面結果可以看出class_rw_t確實包含了一下屬性和方法谭期,不過只包含了我們添加的屬性撵割、實例方法和屬性的settergetter方法斗幼,那么我們定義的成員變量類方法呢,是不是放在了class_ro_t里面了呢娇唯,接下來我們看一下class_ro_t里的信息

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;

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];

    _objc_swiftMetadataInitializer swiftMetadataInitializer() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            return _swiftMetadataInitializer_NEVER_USE[0];
        } else {
            return nil;
        }
    }

    method_list_t *baseMethods() const {
        return baseMethodList;
    }

    class_ro_t *duplicate() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            size_t size = sizeof(*this) + sizeof(_swiftMetadataInitializer_NEVER_USE[0]);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];
            return ro;
        } else {
            size_t size = sizeof(*this);
            class_ro_t *ro = (class_ro_t *)memdup(this, size);
            return ro;
        }
    }
};

發(fā)現(xiàn)class_ro_t還有baseMethodListbaseProtocols寂玲、ivars方法塔插、協(xié)議屬性等信息拓哟。我們再來打印一下這些信息

class_ro_t中的baseMethodList信息
class_ro_t中的方法列表
class_ro_t中的ivars信息
class_ro_t的屬性列表
通過實驗我們發(fā)現(xiàn)在class_ro_t中的ivars存的是成員變量屬性想许,baseMethodList存的和class_rw_t中的methods一模一樣。
那么WJPerson中的類方法哪去了呢?

預知后事如何流纹,且聽下會分解糜烹。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市漱凝,隨后出現(xiàn)的幾起案子疮蹦,更是在濱河造成了極大的恐慌,老刑警劉巖碉哑,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挚币,死亡現(xiàn)場離奇詭異,居然都是意外死亡扣典,警方通過查閱死者的電腦和手機妆毕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贮尖,“玉大人笛粘,你說我怎么就攤上這事∈酰” “怎么了薪前?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長关斜。 經(jīng)常有香客問我示括,道長,這世上最難降的妖魔是什么痢畜? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任垛膝,我火速辦了婚禮,結果婚禮上丁稀,老公的妹妹穿的比我還像新娘吼拥。我一直安慰自己,他們只是感情好线衫,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布凿可。 她就那樣靜靜地躺著,像睡著了一般授账。 火紅的嫁衣襯著肌膚如雪枯跑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天白热,我揣著相機與錄音全肮,去河邊找鬼。 笑死棘捣,一個胖子當著我的面吹牛辜腺,可吹牛的內容都是我干的休建。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼评疗,長吁一口氣:“原來是場噩夢啊……” “哼测砂!你這毒婦竟也來了?” 一聲冷哼從身側響起百匆,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤砌些,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后加匈,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體存璃,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年雕拼,在試婚紗的時候發(fā)現(xiàn)自己被綠了纵东。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡啥寇,死狀恐怖偎球,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情辑甜,我是刑警寧澤衰絮,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站磷醋,受9級特大地震影響猫牡,放射性物質發(fā)生泄漏。R本人自食惡果不足惜邓线,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一淌友、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧褂痰,春花似錦、人聲如沸症虑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谍憔。三九已至匪蝙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間习贫,已是汗流浹背逛球。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留苫昌,地道東北人颤绕。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親奥务。 傳聞我的和親對象是個殘疾皇子物独,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355