ios底层原理-类的结构探索

100 阅读8分钟

接下来,深入探索类的结构

一、isa走位

对象的初始化过程中,学习了对象isa的初始化,并且isa中的shiftcls指向了对象所对应的类。

通过以下案例我们可以再次验证这一点,即对象内存结构的前八个字节是对象的isa,并且指向了p1对象类JHSPerson。见下图:

1.png

1.元类

在前面我们已经知道objc_class继承于objc_object,万物皆对象,也就是说,类也有一个isa指针,那么类isa指向什么呢? 接着上面,打印JHSPerson类中isa指针所指向的内容,发现也是JHSPerson!难道类isa指向自己吗? 不是,我们发现对象isa所指向的JHSPerson类的地址为。。。。。,而JHSPerson类isa所指向的JHSPerson地址为。。。。。,说明这是两个不同的类!见下图:

2.png

这里引入一个元类的概念,元类的定义和创建都是由编译器自动完成!可以理解为,为类的方法找到一个归属!其实这里类的isa指向了元类,这些操作是编译器完成的。

  • 总结

对象isa -> 类,类isa -> 元类。

2.根元类

在上面的案例中,我们通过类对象isa找到了元类对象元类既然也是对象,那么它的isa又指向谁呢? 依然是上面的案例,我们打印JhsPerson元类isa的指向,结果是NSObject!?见下图:

3.png 验证一下:NSObject类的地址JhsPerson元类isa所指向的NSObject是否相同?见下图:

4.png 从打印结果发现,并不相同!那么NSObject类isa又指向什么呢?

5.png 从结果中可以发现,NSObject类isa指向NSObject元类JhsPerson元类isa也指向NSObject元类。因为NSObject是所有类的根类,所以将NSObject元类称为根元类。 那么根元类isa又指向哪里呢?见下图:

6.png 通过打印根元类的内存空间,发现根元类isa指向了自己。

  • 总结

元类isa -> 根元类NSObject isa -> 根元类根元类 isa -> 自己

3.isa走位总结

补充:创建一个JhsTeacher类,继承JhsPerson类,同样按照上面的分析方式,发现JhsPerson类isa指向的是根元类JhsTeacher元类isa也指向根元类

通过上面的isa分析,可以得出isa的走位图:

7.png

二.superclass走位

在进行superclass走位分析之前,先要确定superclass指针所在位置,查看objc_class源码实现:

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_rw_t *data() const {

        return bits.data();

    }

    void setData(class_rw_t *newData) {

        bits.setData(newData);

    }
    
    ....
    ....
    
    等等......
 }

通过上面的源码可以确定,superclass指针在类的第二个8字节中

1.类superclass

引入一个案例JhsTeacher继承自JhsPersonJhsPerson继承NSObject,以我们现有的知识储备,很容理解superclass的走位,即:JhsTeacher superclass -> JhsPersonJhsPerson superclass -> NSObjectNSObject superclass -> nil

1.png

2.元类superclass

类的superclass我们很熟悉,也很好理解,那么元类的superclass走位是怎样的呢?

同样的思路,首先找到LGTeacher元类,然后根据元类superclass开始分析。

2.png

从上面的流程可以发现,JhsTeacher元类superclass指向JhsPersonJhsPersonsuperclass指向NSObjectNSObjectsuperclass指向NSObject根类。通过验证可以确定,这里的JhsPerson元类,并且其superclass指向的是NSObject元类

  • 总结

JhsTeacher元类 superclass -> JhsPerson元类JhsPerson元类 superclass -> NSObject元类NSObject元类 superclass -> NSObject类

3.superclass总结

根据上面的分析,可以确定以下superclass走位图:

3.png

  • isasuperclass总结,这里使用苹果官方的一走位图,将isasuperclass放在一起

isa流程图.png

三.类结构分析

在进行类的结构分析前,需要明确的是OC层c\c++层的对应关系。见下图:

4.png

  • OC层,万物皆对象,大部分的类均继承自NSObject
  • 使用clang查看源码,定义的别名NSObjectobjc_object类型。
  • c\c++层,类的定义为objc_class,继承自objc_object,与OC层一致,万物皆对象。

1.类的结构初步分析

libObjc.A.dylib库中,全局搜索obj_class,得到其源码实现:

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_rw_t *data() const {

        return bits.data();

    }

    void setData(class_rw_t *newData) {

        bits.setData(newData);

    }

  ....... 等等

 }  
 
 

类中包含四个重要的属性,Class ISAClass superclasscache_t cacheclass_data_bits_t bits

  • isasuperclass前面已经做了分析,这两个指针确定了类之间的关系。

  • cache中存储了运行中的一些缓存信息,比如消息缓存。从objc_class提供的方法可以看出,在获取一些数据时,优先从缓存中获取,如果没有缓存,则从bits中获取。这样设计的目的是保证响应速度

  • bits,类中相关的数据信息都存储在了bits中。

2.cache_t cache

cache_t源码如下:


private:

    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;

    union {

        struct {

            explicit_atomic<mask_t>    _maybeMask;

#if __LP64__

            uint16_t                   _flags;

#endif

            uint16_t                   _occupied;

        };

        explicit_atomic<preopt_cache_t *> _originalPreoptCache;

    };
    
    ......等等
 }
  • 结构体空间大小

解读cache_t源码,提供了两个属性,_bucketsAndMaybeMask和一个联合体,其中_bucketsAndMaybeMaskuintptr_t泛型,占8个字节。联合体中包含一个结构体和一个指针,所以联合体也占用8个字节,所以cache_t一共占用16字节的内存空间。

2.class_data_bits_t bits

class_data_bits_t是一个结构体,objc_class为其提供了两个重要的方法,data()setData()

  // objc_class - 类
   class_rw_t *data() const {

        return bits.data();

    }

    void setData(class_rw_t *newData) {

        bits.setData(newData);

    }
    
   // class_data_bits_t - rw 
   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)));
        uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;

        atomic_thread_fence(memory_order_release);

        bits = newBits;

    }

在类的实现过程中,会创建一个class_rw_t,会将MachO文件中的class_ro_t数据拷贝一份到class_rw_t中,并设置到类中。在class_rw_t中提供了一些方法,可以获取类的属性、协议、分类、方法等。

四.类结构探索

引入一个案例,来探索类的结构,定义了JhsPerson类,包括了两个属性,一个成员变量,一个对象方法和一个类方法,见下面代码:

@interface JhsPerson : NSObject{

    NSString *subject;

}
@property (nonatomic, copy) NSString *name;

@property (nonatomic, copy) NSString *hobby;

- (void)sayNB;

+ (void)say666;

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {

        // 0x00007ffffffffff8

        JhsPerson *p = [[JhsPerson alloc]init];

    

        JhsTeacher *t = [[JhsTeacher alloc]init];

        NSLog(@"%@",p);   

    }
    
    return 0;
 }

设置断点,运行程序,获取JhsPerson类的地址,并答应类的内存结构。见下图:

5.png

很显然0x0000000100008278为指向元类的isa0x0000000100379140superclass,那么如果要探索bits数据,应该查看那部分地址呢?

从上面的源码结构可以知道,isa占用8字节superclass占用8字节cache占用16字节,所以只要类地址平移32字节即可获取bits的首地址!

其中类的首地址为0x1000082a0,那么bits首地址 = 0x1000082a0 + 0x20。 通过上面的分析,将bits首地址强制转换为class_data_bits_t *即可成功获取class_data_bits_t数据结构。见下图:

1.png

2.png

此时bits不等于空,所以类中已经存储了相关的数据信息。继续探索,我们上面已经说明,class_data_bits_t结构体中提供了data()方法,用于获取class_rw_tclass_rw_t是在类初始化过程中已经被创建了,并且class_rw_t的相关数据来自MachO文件ro数据!

下面获取class_rw_t内容:

3.png

4.png

我们已经探索到了类的class_rw_t中,在这里我们可以获取相关的地方、属性等内容。

1.探索方法

查看源码可知,在class_rw_t中提供了const method_array_t methods()方法,在这里寻找突破口。

5.png

至此获取到了method_list_t,并且里面有count = 5个方法,只要便利该列表就可以拿到相关方法了。打印输出第一个元素:

6.png 很遗憾没有输出结果,为空?查看method_t的源码实现,内部有一个big结构体,该结构体包括了方法编号SEL方法type encoding,和方法实现

struct method_t {

    static const uint32_t smallMethodListFlag = 0x80000000;

    method_t(const method_t &other) = delete;

    // The representation of a "big" method. This is the traditional

    // representation of three pointers storing the selector, types

    // and implementation.

    struct big {

        SEL name;

        const char *types;

        MethodListIMP imp;

    };
    .....等等
 }

打印输出big数据,见下图:

缺一张图

成功输出了五个方法!其中namehobby属性会自动生成get\set方法,但是没有类方法,全是对象方法!!!类+say666()方法在哪里呢?

2.探索属性

和探索方法一样的方式,通过调用class_rw_t中的properties()方法,可以获取属性列表。运行结果如下:

8.png

成功找到了namehobby两个属性,那么成员变量subject放在哪里呢,name和hobby对应的_成员变量又放在那呢?``

3.class_ro_t谜底

走读class_rw_t的源码,并没有发现存储变量的相关属性,也没有获取变量的相关方法,但是在const class_ro_t *ro()方法获取的class_ro_t结构体中有一个属性const ivar_list_t * ivars;,并且是一个常量!说明在编译之初已经确定,运行时也不会修改!

下面从ro()方法中寻找突破口!见下图:

9.png

class_ro_t中,包括方法列表、协议列表、变量列表等,成员变量是否存储在这里呢?进去看看:

10.png

完美,成功找到了JhsPerson的三个成员变量,subject_name_hobby均已找到!!!

4.类方法 --- 待补充

五.ro rw rwe补充

  • ro属于clean memory,在编辑时即确定的内存空间,只读,加载后不会发生改变的内存空间,包括类名称、方法、协议和实例变量的信息;

  • rw的数据空间属于dirty memoryrw是运行时的结构,可读可写,由于其动态性,可以往类中添加属性、方法、协议。在运行时会发生变更的内存。

  • rwe类的额外信息。在rw中只有10%的类真正的更改了他们的方法,并不是每一个类都需要插入数据,进行修改的类很少,避免资源的消耗,所以就有了rwe