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

从打印结果可以看出这四个内存地址是连续的,切每个地址相差4字节,因为int类型的内存大小为4字节。也就是说如果我们知道一个对象的首地址,且知道其后排列的每个元素的内存大小,那么我们就可以知道后面每个元素的大小。接下来我们来验证一下

int数组内存偏移
如上图所示,如果我们知道了首地址,并且知道后面每一个元素的大小,我们就可以推出后面的元素的内存地址。如果对lldb命令不熟的同学可以看下lldb内存读取这篇文章。
可能有的同学认为这个例子有点简单,且数组中的元素的内存大小一致,无法说明问题,如果是每个元素不一致又该怎么办。
为了更具有说服力,接下来咱们举一个复杂的例子

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

根据这篇iOS底层之内存对齐文章,咱们能够知道StructPointTest结构体中每个元素的内存大小内存所在地址以及比首地址多几个字节,咱们就用这个结构体来验证一下

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结构体首地址
根据咱们上面得出的结论我们可以推导出StructPointTest里各个元素的地址:

  • str.a 的地址为str的首地址就是0x00007ffeefbff550
  • str.b 的地址比首地址多8个字节,推出地址为0x00007ffeefbff558
  • str.c 的地址比首地址多12个字节,推出地址为0x00007ffeefbff55c
  • str.d 的地址比首地址多16个字节,推出地址为0x00007ffeefbff560
  • str.e 的地址比首地址多32个字节,推出地址为0x00007ffeefbff570

接下来咱们使用lldb调试打印一下这些地址

相关地址的值
我们再看一下NSLog打印的值

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

由此我们可以得出结论:只要知道一个对象的首地址的值,就可以根据对象中元素的内存大小推导出每个元素的内存地址

二、类结构分析

我们在最新的objc4源码中搜索objc_class会发现两个版本的结构体定义,一个是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个成员变量,分别为isasuperclasscachebitsisasuperclass我们已经了解了,cache我们根据名字就可以知道是缓存信息,那么class_data_bits_t bits;中又存放了一下什么信息呢。
正常情况下我们无法直接访问objc_class中的bits内容,要想了解bits的信息,我们就需要想办法访问bits所在的内存空间,这时候就需要用到我们上文提到的内存偏移的知识了。
我们已经知道isasuperclass的内存大小都是8字节,那么cache又占了多少字节呢。我们先看下cache_t中除去static修饰的静态变量和方法外还有什么。

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;    //8字节
    explicit_atomic<mask_t> _mask;                  //4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;     //8字节
    mask_t _mask_unused;                            //4字节
#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字节
    mask_t _mask_unused;                            //4字节
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
    uint16_t _flags;                                //2字节
#endif
    uint16_t _occupied;                             //2字节
}

可以看到第一个#if#endif直接不管满足什么条件都是12字节,如果第二个#if满足条件就是16字节,否则就是14字节,但是不管是14字节还是16字节,根据内存对齐原则cache_t的大小都是16字节。所以我们最后得出结论:cache_t的大小为16字节
接下来我们通过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();
    }
};

通过上面代码我们发现,除了class_rw_t* data()const class_ro_t *safe_ro()返回了对象外,剩下的返回值就是bool类型、void类型或基础数据类型。所以我们接下来主要看class_rw_t* data()const class_ro_t *safe_ro()
通过上面的分析计算得出bits比首地址多8(isa)+8(superclass)+16(cache)也就是32个字节。接下来我们实际操作获取下信息。
我们先获取下class_rw_t* data()的信息

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

struct class_rw_t {
    //这里只展示我们经常接触的内容,如需看完整代码请自行查看源码
    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中有methodspropertiesprotocols。我们在类中定义了一些属性和方法看下能不能在这里看到。

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;
        }
    }
};

发现class_ro_t还有baseMethodListbaseProtocolsivars方法协议属性等信息。我们再来打印一下这些信息

class_ro_t中的baseMethodList信息
class_ro_t中的方法列表
class_ro_t中的ivars信息
class_ro_t的属性列表
通过实验我们发现在class_ro_t中的ivars存的是成员变量属性baseMethodList存的和class_rw_t中的methods一模一样。
那么WJPerson中的类方法哪去了呢?

预知后事如何,且听下会分解。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341