iOS底层原理探究 - 类结构分析

前言

等风来不如追风去,总有那么一个人在这风景正好的季节来到你的身边。在上一篇探究了对象的本质和isa指针的底层,那么我们继续来看的底层结构。

补充知识

  • 在OC环境下使用的类,在底层都有替换的类去实现
底层实现.png

isa 和类的关联

类的isa

探索对象的时候我们已经知道,对象的结构体中的isa指向的是,这个时候就会想,也是一个对象,中也有isa,那么isa又指向哪里呢?如下图:

类和isa的关联.png
从图中可以看出,JCPerson类对应了两个内存地址0x00000001000085f00x00000001000085c8,哪一个才是JCPerson的地址呢?一个在内存中存在几个内存地址呢?接下来看下图:

类的地址.png

看到这里应该非常清晰了,一个只有一个内存地址,0x00000001000085f0JCPerson的类地址,而0x00000001000085c8这个类地址苹果把它叫做元类

总结:
  • 元类由系统编译器自动生成和编译,与创建者无关
  • 对象isa指向类对象isa指向元类

isa的走位图

上面我们已经分析了对象isa的走向,那么元类isa又指向哪里呢?结合LLDB来进行探索。

LLDB调试isa走位图.png
从图中可以得到:

  • 对象 isa --> 类 isa --> 元类 isa --> 根元类isa --> 根元类(自己)
  • 根类(NSObject) isa --> 根元类 isa --> 根元类(自己)
isa的流程图:
isa走位图.png

类、元类、根元类继承图

创建JCPerson,JCTeacher,NSObject的相关代码,探究一下它们的继承关系。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%@",class_getSuperclass(JCTeacher.class));
        NSLog(@"%@",class_getSuperclass(JCPerson.class));
        NSLog(@"%@",class_getSuperclass(NSObject.class));
        # NSObject实例对象
        NSObject *object1 = [NSObject alloc];
        # NSObject类
        Class class = object_getClass(object1);
        # NSObject元类
        Class metaClass = object_getClass(class);
        # NSObject根元类
        Class rootMetaClass = object_getClass(metaClass);
        # NSObject根根元类
        Class rootRootMetaClass = object_getClass(rootMetaClass);
        NSLog(@"\nNSObject实例对象  %p\nNSObject类  %p\nNSObject元类  %p\nNSObject根元类  %p\nNSObject根根元类  %p",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
        # NSObject 根类特殊情况
        Class nsuperClass = class_getSuperclass(NSObject.class);
        NSLog(@"%@ - %p",nsuperClass,nsuperClass);
        # 根元类 -> NSObject
        Class rnsuperClass = class_getSuperclass(metaClass);
        NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
        # JCPerson元类和元类的父类
        Class pMetaClass = object_getClass(JCPerson.class);
        Class psuperClass = class_getSuperclass(pMetaClass);
        NSLog(@"%@ - %p",psuperClass,psuperClass);
        # JCTeacher元类和元类的父类
        Class tMetaClass = object_getClass(JCTeacher.class);
        Class tsuperClass = class_getSuperclass(tMetaClass);
        NSLog(@"%@ - %p",tsuperClass,tsuperClass);
    }
    return 0;
}

类、元类的继承关系.png

源码分析中知道,NSObject的父类打印结果是nilNSObject根元类的父类的地址等于NSObject类的地址;JCPerson元类的父类的地址等于NSObject的元类。

  • JCTeacher-->JCPerson-->NSObject-->nil
  • JCTeacher元类-->JCPerson元类-->NSObject元类-->NSObject根元类-->NSObject
的继承图:
类的继承图.png
isa流程图和继承链:
isa流程图和继承图.png

内存偏移

在前面探究对象的底层实现,我们了解到对象属性getter方法底层实现是通过首地址+内存偏移的方式去获取内存中的变量。接下来我们来看一下内存偏移

基本指针
# 普通指针
int a = 10; //
int b = 10; //
JCNSLog(@"%d -- %p",a,&a);
JCNSLog(@"%d -- %p",b,&b);
==========: 10 -- 0x7ffeefbff4ec
==========: 10 -- 0x7ffeefbff4e8
  • a的地址是0x7ffeefbff4ecb的地址是0x7ffeefbff4e8,相差4个字节,int类型是4个字节的长度
  • a>b的地址,从高地址低地址偏移,这符合栈内存的分配原则
对象指针
# 对象
JCPerson *p1 = [JCPerson alloc];
JCPerson *p2 = [JCPerson alloc];
JCNSLog(@"%@ -- %p",p1,&p1);
JCNSLog(@"%@ -- %p",p2,&p2);
==========: <JCPerson: 0x1004075a0> -- 0x7ffeefbff4e8
==========: <JCPerson: 0x100408570> -- 0x7ffeefbff4e0
  • alloc开辟的内存在堆区指针地址栈区
  • 堆区是从地址 --> 地址,栈区是从地址 --> 地址
数组指针
int c[4] = {1,2,3,4};
int *d   = c;
JCNSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
JCNSLog(@"%p - %p - %p",d,d+1,d+2);

for (int i = 0; i<4; i++) {
     int value =  *(d+i);
     JCNSLog(@"----%d",value);
}
==========: 0x7ffeefbff4e0 - 0x7ffeefbff4e0 - 0x7ffeefbff4e4
==========: 0x7ffeefbff4e0 - 0x7ffeefbff4e4 - 0x7ffeefbff4e8
==========: ----1
==========: ----2
==========: ----3
==========: ----4
  • 数组的地址就是元素的首地址,即&c == &c[0]
  • 数组中每个元素的地址可以通过:首地址 + n*元素类型大小来获取,只需要数组中元素数据类型相同
  • 数组中的每个元素的地址间隔是通过当前元素的数据类型决定的
总结:
  • 内存偏移可以根据首地址 + 偏移值方式来获取各个数据的内存地址

类结构的分析

上面我们已经了解了内存偏移的知识,接下来我们来探究的底层结构。

类的内存地址.png
代码分析:在对象底层结构中存放在属性成员变量等数据;从图中打印可以看出是有内存的,那么它里面存放着什么呢?接下来分析底层的数据结构。

类的底层结构:

探索对象isa的过程中,我们已经知道isa在底层是Class类型,Class类型是objc_class *,所有的底层实现都是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;   # OBJC2不可用

通过上面这段代码我们发现在OBJC2中不可用,而现在的版本基本是在用OBJC2,所以这不是我们分析的,接下来在看一下如下代码:

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // 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_object,这说明也是对象,所谓万物皆对象。objc_class里面有一个隐藏成员变量isa,我们前面已经分析过了,下面还有三个成员变量superclass,cache,bits,我们知道首地址就是isa,那么我们可以通过首地址+偏移量的方式去获取成员变量的地址,然后获取值。

  • isa是结构体指针,占8字节
  • Class superclassClass类型,也是属于结构体指针,占8字节
  • cachecache_t结构体,结构体大小由内部的变量决定
  • bitsclass_data_bits_t结构体,如果知道前面三个成员变量的大小,那么就可以得到bits的地址

前面三个成员变量已经知道了前两个的大小,只要知道cache的内存大小,接下来看一下cache_t的内存大小

typedef unsigned long           uintptr_t;
#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 cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;  //8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;  //4
#if __LP64__
            uint16_t                   _flags;   //2
#endif
            uint16_t                   _occupied;  //2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; //8
    };
   ...
   下面的一些方法直接省略(static类型的是在内存的全局区,不在结构体里面的内存)

}

cache_t是一个结构体类型,内部包含了_bucketsAndMaybeMask和一个联合体.

  • _bucketsAndMaybeMaskuintptr_t类型,而uintptr_t是无符号长整型占8个字节
  • 联合体内存大小由成员变量中的最大变量的内存大小决定,该联合体由一个结构体_originalPreoptCache两个成员变量组成,由于联合体存在互斥的,所以只需要得到其中最大变量的内存大小
  • _originalPreoptCachepreopt_cache_t *结构体指针类型,占8个字节
  • 结构体中有_maybeMask,_flags,_occupied_maybeMaskmask_t类型,mask_t又是uint32_t类型,占4个字节,_flags_occupieduint16_t类型,占2个字节

综上所述cache_t的内存大小为16字节。

总结:
  • isa内存地址为首地址
  • superclass地址为首地址+0x08
  • cache_t地址为首地址+0x10
  • bits地址为首地址+0x20
    类的结构图.png

bits数据结构

上面已经了解的基本结构,isasuperclass已经探究过了,接下来我们先来研究一下成员变量bits存储了哪些信息?

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // 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 getSuperclass() const {...}
    void setSuperclass(Class newSuperclass) {...}

    class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }

bitsclass_data_bits_t类型,底层源码中还有一个data(),返回bits.data(),这有可能就是bits中存储的数据。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_or_rw_ext)->ro;
        }
        return v.get<const class_ro_t *>(&ro_or_rw_ext);
    }

    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_or_rw_ext)->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 *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->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 *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->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 *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }

class_rw_t是一个结构体,里面存储着方法,属性,协议列表,接下来验证是否存储在class_rw_t中。

属性探究( properties() )
@interface JCPerson : NSObject{
    NSString *subject;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *hobby;

- (void)sayNB;
+ (void)say666;
@end
类结构中属性分析.png
  • property_list_t中存储name,hobby属性
  • p $7.get(2)会提示数组越界,没有找到成员变量subject

问题:那么定义的subject成员变量存到哪里去了呢?

补充:成员变量

class_rw_t中除了有属性,方法,协议以外,还有class_ro_t结构体指针类型的ro(),在class_ro_t结构体中我们可以找到ivar_list_t类型的指针ivars成员变量会不会存在这里呢?

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

    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };

    explicit_atomic<const char *> name;
    // With ptrauth, this is signed if it points to a small list, but
    // may be unsigned if it points to a big list.
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
类结构中成员变量分析.png
源码和LLDB分析:
  • 成员变量底层实现是ivar_t,存储在class_ro_t成员变量列表
  • 系统是自动给属性添加_属性名的变量,存储在class_ro_t成员变量列表
方法探究( methods() )

类结构的方法分析.png

通过LLDB的方式,可以得到定义的方法。但是发现使用get(index)的方式无法得到,�使用get(index).big()才能获取,这是为什么呢?

struct property_t {
    const char *name;
    const char *attributes;
};

struct method_t {
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
    big &big() const {
        ASSERT(!isSmall());
        return *(struct big *)this;
    }

    SEL name() const {
        if (isSmall()) {
            return (small().inSharedCache()
                    ? (SEL)small().name.get()
                    : *(SEL *)small().name.get());
        } else {
            return big().name;
        }
    }
    const char *types() const {
        return isSmall() ? small().types.get() : big().types;
    }
    IMP imp(bool needsLock) const {
        if (isSmall()) {
            IMP imp = remappedImp(needsLock);
            if (!imp)
                imp = ptrauth_sign_unauthenticated(small().imp.get(),
                                                   ptrauth_key_function_pointer, 0);
            return imp;
        }
        return big().imp;
    }

源码分析:
属性和方法获取区别.png
  • 属性底层实现是property_t,在property_t结构体中定义了name等变量
  • 方法底层实现是method_t,在method_t结构体中定义了一个big(),通过big()获取SELIMP

接下来我们继续打印methods,如下图。

类结构的方法分析2.png

  • method_list_t中有对象方法,属性的setter方法getter方法
  • method_list_t中没有获取到类方法

问题:那么类方法存储到哪里去了呢?

补充:类方法

对象方法存储在中,那类方法可能存储在元类

类方法的分析.png

  • object_getClass获取到JCPerson的元类
  • 元类中method_list_t中存储着类方法
总结:
  • 类的结构主要由isa,superclass,cache,bits组成
  • bits中存储着属性,方法,协议
  • 属性存储在property_list_t中,而成员变量存储在class_ro_t-->ivar_list_t,系统为属性自动生成的_属性名的变量也存储在class_ro_t-->ivar_list_t
  • 方法存储在method_list_t中,method_list_t主要存储着对象方法,属性的setter方法getter方法,而类方法存储在元类中的method_list_t
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容