iOS底层学习-类的原理分析(上)

440 阅读7分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

一、isa分析到元类

上一篇isa分析,我们分析出对象的isa & ISA_MASK得到LRPerson类,那类中是否有isa呢,类的isa又指向什么呢? 接上篇对这个LRPerson类再x/4gx查看内存,并打印出类的isa & ISA_MASK,也得到了LRPerson类。也就是说0x0000000100008388 0x0000000100008360都指向LRPerson

可以看出类也开辟了内存,存放isa和其他信息。那么我们不妨猜想一下:类和对象一样可以开辟内存,并且不止有一个类。下面尝试验证一下

查看下面这段代码打印: 可以看出类对象指向同一个内存地址0x100008388,那上面类的isa & ISA_MASK得到的0x0000000100008360是什么呢?

此时借助MachOView查看可执行文件 在符号表中可以看到有个METACLASS,这就是元类元类是系统生成编译的

到此可以分析出这个流程:对象isa -> 类isa -> 元类

二、isa走位图和继承链

1、isa走位

上面分析到元类,那么元类的isa以及后续流程呢? 通过lldb一步步分析打印,观察出以下结论:

  • 对象isa ->
  • 类isa -> 元类
  • 元类isa -> 根元类
  • 根元类isa -> 根元类
  • 根类isa -> 根元类

通过代码验证下:

// NSObject实例对象
NSObject *object1 = [NSObject alloc];
// NSObject类
Class cass = object_getClass(object1);
// NSObject元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n-%p 实例对象\n-%p 类\n-%p 元类\n-%p 根元类\n-%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);

得到打印结果:

-0x10104e840 实例对象
-0x100357140-0x1003570f0 元类
-0x1003570f0 根元类
-0x1003570f0 根根元类

结合分析得出isa走位图:

2、isa继承链

OC中,类是有继承关系的,那么元类 根元类有继承关系吗?下面探索一下,先创建一个类LRTeacher继承于LRPerson,在观察下面代码打印

// LRPerson元类
Class pMetaClass = object_getClass(LRPerson.class);
Class psuperClass = class_getSuperclass(pMetaClass);
NSLog(@"LRPerson元类的父类:%@ - %p",psuperClass,psuperClass);

// LRTeacher -> LRPerson -> NSObject
// 元类也有一条继承链
Class tMetaClass = object_getClass(LRTeacher.class);
Class tsuperClass = class_getSuperclass(tMetaClass);
NSLog(@"LRTeacher元类的父类:%@ - %p",tsuperClass,tsuperClass);

// NSObject 根类特殊情况
Class nsuperClass = class_getSuperclass(NSObject.class);
NSLog(@"根类的父类:%@ - %p",nsuperClass,nsuperClass);

// 根元类 -> NSObject
Class rnsuperClass = class_getSuperclass(object_getClass(NSObject.class));
NSLog(@"根元类的父类:%@ - %p",rnsuperClass,rnsuperClass);

打印结果如下:

LRPerson元类的父类:NSObject - 0x1003570f0
LRTeacher元类的父类:LRPerson - 0x100008360
根类的父类:(null) - 0x0
根元类的父类:NSObject - 0x100357140

根据打印结果分析:

  • LRPerson元类继承于NSObject元类根元类
  • LRTeacher元类继承于LRPerson元类
  • NSObject根类继承于nil
  • NSObject根元类继承于NSObject根类

通过上面分析的isa走位图和继承链,结合起来就是苹果官网上经典的一张图

三、源码分析类的结构

对象的内存中有isa成员变量,那么类的内存中有什么呢?我们在源码中可以看到类的根本是objc_class,是一个结构体。 objc_class结构体中有这四个成员变量:隐藏成员变量ISA,继承于objc_objectsuperclass父类、cachebits。前两个我们已经比较熟悉了,那么cachebits是什么呢?

先看bits后边注释中提到了class_rw_t,进入class_rw_t可以看到包含方法 属性 协议等等

struct class_rw_t {
    ...
    const method_array_t methods() const {...}
    const property_array_t properties() const {...}
    const protocol_array_t protocols() const {...}
}

现在我们只走到objc_class,怎么从类中获取这些内存数据呢?

四、指针和内存平移

目前还不知道如何获取当前内存的结构,这里先看一下指针和内存平移

// 普通指针
int a = 10;
int b = 10;
NSLog(@"%d -- %p",a,&a);
NSLog(@"%d -- %p",b,&b);

// 对象指针
LRPerson *p1 = [LRPerson alloc];
LRPerson *p2 = [LRPerson alloc];
NSLog(@"%@ -- %p",p1,&p1);
NSLog(@"%@ -- %p",p2,&p2);

观察以上代码,得到打印结果如下:

10 -- 0x7ffeefbff4cc
10 -- 0x7ffeefbff4c8
<LRPerson: 0x100714f90> -- 0x7ffeefbff4c0
<LRPerson: 0x10070e660> -- 0x7ffeefbff4b8

分析:普通指针2个地址不同,但指向的值是同一个,可以理解为指向同一个内存空间。这就是值copy。而两个对象指针的地址不同,指向的内存空间也不同。

再看下数组指针

// 数组指针
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);

打印结果:

分析:&c&c[0]地址相同,说明数组的首地址就是第一个元素的地址。内存地址是连续的并且相差4字节,代表当前元素类型为4字节指针d+1代表平移一个步长(即当前元素类型的大小),这里表示平移4字节,如果数组中是指针或对象那么平移8字节

根据分析结论,我们知道可以通过指针地址的平移来做取值操作,代码验证下 打印结果:

1
2
3
4

有了这个验证结果,那类是否也可通过内存平移的方式获取其中的数据呢?

五、类的结构内存计算

打印查看类的的内存情况 根据之前源码分析类的结构,可以得知首地址0x0000000100008360isa,紧跟着应该是superclass(结构体指针占8字节),截图中已验证0x0000000100357140NSObject。那下面是cachebits吗?今天先来看bits,后续单独篇章探究cache

从类的结构中得知isasuperclass都是Class类型即结构体指针占8字节, 想要通过内存平移到bits,首先要知道cache的大小。下面跟进cache_t源码: cache_t是结构体,结构体大小要看其内部成员变量的大小。我们发现cache_t结构体中有很多东西,其中方法在方法区、全局变量在全局区不占用结构体内存

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字节
    };
    ...
}

最终影响结构体大小的成员变量有两个:一个是_bucketsAndMaybeMask,类型是explicit_atomic<uintptr_t>explicit_atomic结构体是一个泛型,其真实大小取决于uintptr_t8字节;另一个是联合体,联合体内又包含一个结构体和_originalPreoptCache,由于联合体是互斥的,经分析联合体也是8字节

所以cache_t大小为16字节

到此获取bits需要从类的首地址偏移32字节0x20(isa(8) + superclass (8) + cache (16))。 首地址偏移0x20后强转为class_data_bits_t *类型就得到了bits的地址

六、lldb分析类的属性

上面已经获取到bits的地址,我们在objc_class结构体中看到下面这行代码 尝试一下获取bits.data(),由于是指针需要用->调用 我们发现$2->data()得到了class_rw_t *类型的变量。再查看$3的内容发现没有之前提到的方法 属性 协议等。这里需要通过$4.properties()获取,lldb分析流程如下:

七、lldb分析类的方法

上面分析了类的属性,修改下LRPerson,添加一些属性和方法

继续通过lldb分析类的方法 在分析方法时,还原method_list_t后,通过get(index)无法获取方法,这里和属性有所区别

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

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

property_t结构体中有两个成员变量和打印信息相符,而method_t结构体中有个big结构体,尝试打印big结构体即得到我们想看到的信息。

还原method_list_t后可以看到count = 5,即有5个方法,打印出这5个方法: 我们发现分别是saySomethinghobbynameget set方法,LRPerson的类方法say666却没有,因为类方法存储在元类中的方法列表里。

八、lldb分析类的协议

先写一个协议,LRPerson遵守这个协议并实现协议的方法

class_rw_t结构体中有个protocols方法

lldb分析类的协议 当获取到protocol_list_t时,发现和属性property_list_t、方法method_list_t结构有所区别。查看protocol_list_t结构体 protocol_list_t结构体中list[0]protocol_ref_t类型,获取到protocol_ref_t后直接强转为protocol_t,打印出详细信息就可以看到instanceMethodsinstanceMethods即协议的方法列表,在这个方法列表中我们找到了先前写的协议的方法。