iOS底层原理07:类 & 类结构分析

本文的主要目的是分析 类 & 类的结构,整篇都是围绕一个展开的一些探索

类的分析

类的分析主要是分析 isa的走向 以及 继承关系
准备工作
定义两个类

  • 继承自NSObject的类HTPerson
@interface HTPerson : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
- (void)sayHello;
+ (void)sayBye;
@end

@implementation HTPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end
  • 继承自HTPerson的类HTTeacher
@interface HTTeacher : HTPerson
@end

@implementation HTTeacher
@end
  • main中分别用两个定义两个对象:person & teacher
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HTPerson *person = [[HTPerson alloc] init];
        HTTeacher *teacher = [[HTTeacher alloc] init];
        NSLog(@"person:%@======teacher:%@", person, teacher);
    }
    return 0;
}

元类

首先,我们先通过一个案例的lldb调试先引入元类

  • 在main中HTTeacher部分加一个断点,运行程序
  • 开启lldb调试,调试的过程如下图所示
image

image

根据调试过程,我们产生了一个疑问:为什么图中的p/x 0x011d80010000828d & 0x00007ffffffffff8ULLp/x 0x0000000100008260 & 0x00007ffffffffff8ULL 中的类信息打印出来都是HTPerson

  • 0x011d80010000828dperson对象的isa指针地址,其&后得到的结果是 创建person的类HTPerson
  • 0x0000000100008260是isa中获取的类信息所指的类的isa的指针地址,即 HTPerson类的类 的isa指针地址,在Apple中,我们简称HTPerson类的类元类
  • 所以,两个打印都是HTPerson的根本原因就是因为元类导致的

元类的说明

下面来解释什么是元类,主要有以下几点说明:

  • 我们都知道 对象的isa 是指向类,类其实也是一个对象,可以称为类对象,其isa的位域指向苹果定义的元类
  • 元类是系统给的,其定义和创建都是由编译器完成,在这个过程中,类的归属来自于元类
  • 元类类对象,每个类都有一个独一无二的元类用来存储 类方法的相关信息
  • 元类本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称

下面通过lldb命令来探索元类的走向,也就是isa的走位,如下图所示,可以得出一个关系链:对象 --> 类 --> 元类 --> NSObject的元类, NSObject的元类 指向自身

image

总结

从图中可以看出

  • 对象isa 指向 (也可称为类对象)
  • isa 指向 元类
  • 元类isa 指向 根元类,即NSObject的元类
  • 根元类isa 指向 它自己

著名的 isa走位 & 继承关系 图

根据上面的探索以及各种验证,对象、类、元类、根元类的关系如下图所示

image

isa走位

isa的走向有以下几点说明:

  • 实例对象(Instance of Subclass)isa 指向 类(class)
  • 类对象(class) isa 指向 元类(Meta class)
  • 元类(Meta class)的isa 指向 根元类(Root meta class)
  • 根元类(Root meta class)isa 指向它自己本身,形成闭环,这里的根元类就是NSObject的元类

superclass走位

superclass(即继承关系)的走向也有以下几点说明:

  • 类之间的继承关系:
    • 类(subClass) 继承自 父类(superClass)
    • 父类(superClass) 继承自 根类(RootClass),此时的根类是指NSObject
    • 根类 继承自 nil,所以根类NSObject可以理解为万物起源,即无中生有
  • 元类也存在继承,元类之间的继承关系如下:
    • 子类的元类(meta SubClass) 继承自 父类的元类(meta SuperClass)
    • 父类的元类(meta SuperClass) 继承自 根元类(Root meta Class)
    • 根元类(Root meta Class) 继承于 根类(Root class),此时的根类是指NSObject
  • 【注意】实例对象之间没有继承关系类之间有继承关系

objc_class & objc_object

isa走位我们理清楚了,又来了一个新的问题:为什么 对象都有isa属性呢?这里就不得不提到两个结构体类型:objc_class & objc_object

  • 在objc4源码中搜索objc_class的定义,源码中对其的定义有两个版本
    • 旧版:位于runtime.h中,在OBJC2中已废弃

      image

    • 新版:位于objc-runtime-new.h中,我们后面的类的结构分析也是基于新版来分析的。

      image

从新版的定义中,可以看到 objc_class 结构体类型是继承自 objc_object

  • 在objc4源码中搜索objc_object的定义,发现也是有两个版本
    • 一个是位于objc.h中,从编译的main.cpp中可以看出,使用的是这个版本的objc_object

      image

    • 另外一个位于objc-privat.h

      image

以下是编译后的main.cpp中的objc_objectNSObject_IMPL的定义

struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

struct NSObject_IMPL {
    Class isa;
};

typedef struct objc_class *Class;

【问题】objc_class 与 objc_object 有什么关系?

通过上述的源码查找以及main.cpp中底层编译源码,有以下几点说明:

  • objc_class 继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa属性,所以objc_class也拥有了isa属性

  • mian.cpp底层编译文件中,objc_object中的isa在底层是由Class 定义的,其中Class的底层编码是struct objc_class *的别名(即结构体指针),所以NSObject也拥有了isa属性

  • NSObject是一个类,用它初始化一个实例对象objc,objc满足 objc_object的特性(即有isa属性),主要是因为isa 是由 NSObjectobjc_class继承过来的,而objc_class继承自objc_objectobjc_objectisa属性。所以对象都有一个 isa

  • objc_object(结构体)是当前的根对象所有的对象都有这样一个特性 objc_object,即拥有isa属性

总结

  • 所有的对象元类 都有isa属性
  • 所有的对象都是由objc_object继承来的
  • 简单概括就是万物皆对象,万物皆来源于objc_object,有以下两点结论:
    • 所有以objc_object为模板,创建的对象,都有isa属性
    • 所有以objc_class为模板,创建的,都有isa属性

类结构分析

接下来主要是分析类信息中存储了哪些内容

内存偏移

在分析类结构之前,需要先了解内存偏移,因为类信息中访问时,需要使用内存偏移

普通指针

// 普通指针
int a = 10; // 变量
int b = 11;
int c = 12;
int d = 13;
NSLog(@"%d -- %p", a, &a);
NSLog(@"%d -- %p", b, &b);
NSLog(@"%d -- %p", c, &c);
NSLog(@"%d -- %p", d, &d);

打印结果如下图所示:


image
  • 变量a、b、c、d存储在栈区,是连续的内存地址,栈区直接存储的是
  • a、b、c、d的地址之间都相差4字节,这取决于他们存储的类型,Int类型占4字节
  • 栈区的地址 比 堆区的地址 大
  • 栈是从高地址->低地址,向下延伸,由系统自动管理,是一片连续的内存空间
  • 堆是从低地址->高地址,向上延伸,由程序员管理,堆空间结构类似于链表,是不连续的

对象指针

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 对象指针
        HTPerson *p1 = [[HTPerson alloc] init];
        HTPerson *p2 = [[HTPerson alloc] init];
        NSLog(@"%@ -- %p", p1, &p1);
        NSLog(@"%@ -- %p", p2, &p2);
        NSLog(@"end");
    }
    return 0;
}

打印结果如下图所示:


image
  • alloc开辟的内存在堆区局部变量存储在栈区
  • p1、p2存储在栈区,p1、p2内存中存储的是 [HTPerson alloc]堆区地址

数组指针

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 数组指针
        int c[4] = {1,2,3,4};
        int *d = c;
        NSLog(@"%p - %p - %p ", &c, &c[0], &c[1]);
        NSLog(@"%p - %p - %p ", d, d+1, d+2);

        for (int i = 0; i<4; i++) {
            //(d+i) 取地址里面的值
            int value =  *(d+i);
            NSLog(@"value = %d",value);
        }
        NSLog(@"end");
    }
    return 0;
}

打印结果如下图所示:


image
  • &c&c[0] 都是取 首地址,即数组名等于首地址
  • &c&c[1]相差4字节,地址之间相差的字节数,主要取决于存储的数据类型,即步长
  • 可以通过 首地址+偏移量取出数组中的其他元素,其中偏移量等于步长 * 数组的下标

类结构组成

在前面的内容,我们可以得到的首地址,但是我们还不清楚类的结构是什么,里面都存储了什么东东

打开objc-818.2源码,在objc-runtime-new.h中找到 objc_class的定义,如下

typedef struct objc_class *Class;
struct objc_class : objc_object {
    ...
    // Class ISA;  // 8字节
    Class superclass; //8字节
    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,因此拥有isa属性,从源码我们可以看出,objc_class包含下列四个变量:

  • isa属性:结构体指针,继承自objc_objectisa,占8字节
  • superclass属性:结构体指针,指向父类的地址,占8字节
  • cache属性:是一个cache_t结构体类型,取决于cache_t内部结构体成员的大小,占16字节
  • bits属性:是一个class_data_bits_t结构体类型,占8字节

我们可以通过lldb+sizeof()查看cache_tclass_data_bits_t的内存大小,如下图所示:

image

【验证】objc_class的内存大小
定义两个类HTPersonHTTeacher,通过断点来查看元类的地址,寻找规律

image

  • 从上图可以发现,这几个类地址之间相差0x2840字节),正好和objc_class结构体中成员变量所占的内存相同,都是40字节

探究类里面的各个成员变量,成员变量的内存地址如下

  • isa的内存地址是类首地址
  • superclass的内存地址是首地址+0x8
  • cache的内存地址是首地址+0x10
  • bits的内存地址是首地址+0x20

isa属性

objc_class继承自objc_object,因此拥有isa属性,isa是结构体指针,占8字节

  • 类对象(class) isa 指向 元类(Meta class)
  • 元类(Meta class)的isa 指向 根元类(Root meta class)
  • 根元类(Root meta class)isa 指向它自己本身,形成闭环,这里的根元类就是NSObject的元类

superclass属性

Class superclass; //8字节也是结构体指针,占8字节

  • 类之间的继承关系:
    • 类(subClass) 继承自 父类(superClass)
    • 父类(superClass) 继承自 根类(RootClass),此时的根类是指NSObject
    • 根类 继承自 nil,所以根类NSObject可以理解为万物起源,即无中生有
  • 元类也存在继承,元类之间的继承关系如下:
    • 子类的元类(meta SubClass) 继承自 父类的元类(meta SuperClass)
    • 父类的元类(meta SuperClass) 继承自 根元类(Root meta Class)
    • 根元类(Root meta Class) 继承于 根类(Root class),此时的根类是指NSObject
  • 【注意】实例对象之间没有继承关系类之间有继承关系

cache属性

cache_t结构体大小

首先,我们来探究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属性和方法,并不影响结构体的内存大小,主要是因为static类型的属性 不存在结构体的内存中
}

cache_t是结构体类型,有两个成员变量:_bucketsAndMaybeMask和一个联合体

  • _bucketsAndMaybeMaskuintptr_t类型,占8字节
  • 联合体里面有两个成员变量:结构体_originalPreoptCache,联合体由最大的成员变量的大小决定
    • _originalPreoptCachepreopt_cache_t *结构体指针,占8字节
    • 结构体中有_maybeMask_flags_occupied三个成员变量。
      • _maybeMask的大小取决于mask_tuint32_t,占4字节
      • _flagsuint16_t类型,占2字节
      • _occupieduint16_t类型,占2字节

所以cache_t的大小等于 8+8或者8+4+2+2,即16字节

👇我们看几个重要的函数

fastInstanceSize函数

size_t fastInstanceSize(size_t extra) const {}记录实例对象需要分配的内存大小(16字节对齐),我们在iOS底层原理02:alloc & init & new 源码分析已经分析过了

size_t fastInstanceSize(size_t extra) const
{
    ASSERT(hasFastInstanceSize(extra));

    // Gcc的内建函数 __builtin_constant_p 用于判断一个值是否为编译时常数,如果参数EXP 的值是常数,函数返回 1,否则返回 0
    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    } else {
        size_t size = _flags & FAST_CACHE_ALLOC_MASK;
        // remove the FAST_CACHE_ALLOC_DELTA16 that was added
        // by setFastInstanceSize
        // 删除由setFastInstanceSize添加的FAST_CACHE_ALLOC_DELTA16 8个字节
        return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    }
}
insert函数

void insert(SEL sel, IMP imp, id receiver);函数用来插入selimp,方法缓存,我们会在后面的篇章进行分析

bits属性

bits属性是class_data_bits_t结构体类型,有一个成员变量bits,占8字节,结构如下

struct class_data_bits_t {
    friend objc_class; // 友元:objc_class可以调用 class_data_bits_t中的私有属性和方法
    
    // Values are the FAST_ flags above.
    uintptr_t bits; // 8字节,类似于isa,通过位域来存储数据
    // ...
public:
    // 通过data()获取数据
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    // ...方法较多
}
  • 继续查看class_rw_t结构体
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif

    explicit_atomic<uintptr_t> ro_or_rw_ext;

    Class firstSubclass;
    Class nextSiblingClass;
    
    // ...
    
    const class_ro_t *ro() const {...}
    
    void set_ro(const class_ro_t *ro) {...}
    
    const method_array_t methods() const {...}
    
    const property_array_t properties() const {...}
    
    const protocol_array_t protocols() const {...}
}
image.png

现在我们对objc_class的结构有了初步的认识,在后面的篇章中继续深入分析bitscache两个属性

类结构分析传送门:

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

推荐阅读更多精彩内容