iOS类的底层探索(下)

276 阅读7分钟

上篇文章我们在对类的底层分析探索得知了类的本质是objc_class的结构体,objc_class的结构体中包含有Class ISAClass superclasscache_t cacheclass_data_bits_t bits这些数据,class_data_bits_t结构体中提供了data()方法,用于获取class_rw_t,而实例对象的方法、属性、协议等就存储在class_rw_t结构体中。同时遗留了两个问题:

1、类的数据结构中的method_list_t里面并没有发现类里面的类方法,类方法存储在哪里呢?

2、实例对象里面存储的是isa指针 + 成员变量的值。真正的成员变量存储在哪里?

一、类的结构解析

苹果在wwdc2020(请使用Safari浏览器跳转观看)指出类的数据结构更改如下:

  • 当类第一次从磁盘加载到内存时的结构

截屏2022-05-06 下午1.40.19.png

  • 当一个类首次被使用

截屏2022-05-06 下午1.18.35.png

  • 需要额外信息的类:当类被动态修改时,例如使用runtime的API进⾏添加和修改类的方法

截屏2022-05-06 下午1.55.28.png

`clean memory & dirty memory` 

clean memory: 是指加载后不会发生更改的内存。
dirty memory: 是指在进程运行时会发生更改的内存

class_ro_t就属于clean memory,因为它是只读的。类结构一经使用就会变成dirty memory,因为运行时会向它写入新的数据。 如此调整的原因是优化内存使用,因为dirty memoryclean memory要昂贵的多,只要进程在运行,它就必须一直存在。另一方面clean memory可以进行移除,从而节省更多的内存空间,因为如果你需要clean memory,系统可以从磁盘中重新加载。因为iOS不使用swap,所以dirty memory在iOS中代价很大。dirty memory是这个类数据被分成两部分的原因,可以保持清洁的数据越多越好,通过分离出那些永远不会更改的数据,可以把大部分的类数据存储为clean memory

截屏2022-05-05 下午8.13.49.png

1、class_ro_t解析

class_ro_t在编译期生成的,ro即表示read only,这块内存空间是只读的,不允许被修改。class_ro_t存储了像类名称、方法、协议和实例变量的信息,像类里面的成员变量就存放在类对象的class_rw_t结构体中的class_ro_t结构体当中class_ro_t里面是没有分类的方法的。那些运行时添加的方法将会存储在运行时生成的class_rw_t中。可以通过class_rw_t结构体中提供的ro()方法获取class_ro_t

截屏2022-05-05 下午3.46.51.png

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;
    WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
    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;
        }
    }

    const char *getName() const {
        return name.load(std::memory_order_acquire);
    }

    class_ro_t *duplicate() const {
        bool hasSwiftInitializer = flags & RO_HAS_SWIFT_INITIALIZER;

        size_t size = sizeof(*this);
        if (hasSwiftInitializer)
            size += **sizeof**(_swiftMetadataInitializer_NEVER_USE[0]);

        class_ro_t *ro = (class_ro_t *)memdup(this, size);

        if (hasSwiftInitializer)
            ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];

#if __has_feature(ptrauth_calls)
        // Re-sign the method list pointer.
        ro->baseMethods = baseMethods;
#endif

        return ro;
    }

    Class getNonMetaclass() const {
        ASSERT(flags & RO_META);
        return nonMetaclass;
    }

    const uint8_t *getIvarLayout() const {
        if (flags & RO_META)
            return nullptr;
        return ivarLayout;
    }
};

class_ro_t的源码里发现其包含有const ivar_list_t * ivars,也就是说类里面的成员变量是存放在类对象的class_ro_t结构体当中

2、class_rw_t解析

class_rw_t生成在运行时,在编译期间,class_ro_t结构体就已经确定,objc_class中的bitsdata部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClassWithoutSwift方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。

截屏2022-05-06 上午11.13.06.png

realizeClassWithoutSwift运行之前:

截屏2022-05-06 下午12.03.42.png

realizeClassWithoutSwift运行之后:

截屏2022-05-06 下午12.05.13.png

class_rw_tclass_ro_t都存放着当前类的属性、实例变量、方法、协议等等。区别在于:class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容剪切过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容。

3、class_rw_ext_t解析

class_rw_ext_t可以减少内存的消耗。苹果在wwdc2020⾥⾯说过,只有⼤约10%左右的类需要动态修改。所以只有10%左右的类⾥⾯需要⽣成class_rw_ext_t这个结构体。这样的话,可以节约很⼤⼀部分内存。对于动态修改的类可以通过class_rw_t结构体中提供的ext()方法获取class_rw_ext_t

class_rw_ext_t⽣成的条件:
1、⽤过`runtime`API进⾏动态修改的时候。
2、有分类的时候,且分类和本类都为⾮懒加载类的时候。在iOS开发中,一般类都是懒加载的,只有用到类的时
   候才会将类加载到内存中,在类中添加+load方法可以将类变成非懒加载类。

截屏2022-05-06 下午2.20.01.png

二、案例验证

@interface MyClass : NSObject {
    NSString * _hobby;
}

@property (nonatomic, copy) NSString * name;
@property (nonatomic, assign) int age;

- (void)instanceMethod;
+ (void)classMethod;

@end


@implementation MyClass
- (instancetype)init {
    self = [super init];
    if (self) {
    
    }
    return  self;
}

- (void)instanceMethod {
    NSLog(@"%s", __func__);
}

+ (void)classMethod {
    NSLog(@"%s", __func__);
}

@end

设置断点,运行程序,获取MyClass类对象的内存地址。

截屏2022-05-05 下午1.35.16.png

1、类的成员变量存放在class_ro_t结构体中

通过调用class_data_bits_t结构体中提供的data()方法class_rw_t中的ro()方法可以得到class_ro_t类型的指针$4,如下图所示:

截屏2022-05-05 下午4.06.56.png

通过对$4进行取值发现ivars的内存地址

截屏2022-05-05 下午4.07.26.png

ivars取值发现,ivar_list_t继承自entsize_list_tt

struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
    bool containsIvar(Ivar ivar) const {
        return (ivar >= (Ivar)&*begin()  &&  ivar < (Ivar)&*end());
    }
};

截屏2022-05-05 下午4.08.45.png

调用entsize_list_tt中提供的get(uint32_t i),果然获取到了类里面的成员变量

截屏2022-05-05 下午4.10.15.png

2、类方法存放在元类里面

截屏2022-05-06 下午5.26.32.png

通过上图中一系列的查找,在元类对象数据的class_rw_t结构体中的method_list_t结构体中查找到了MyClass的类方法classMethod

二、通过runtime的API探索类的数据结构

1、获取类的成员变量

//获取类的成员变量
- (void)class_copyIvarList:(Class)pClass {
    unsigned int outCount = 0;
    Ivar * ivars = class_copyIvarList(pClass, &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        const char *cName = ivar_getName(ivar);
        const char *cType = ivar_getTypeEncoding(ivar);
        NSLog(@"name = %s type = %s", cName, cType);
    }
    free(ivars);
}

截屏2022-05-06 下午7.03.48.png

2、获取类的属性

//获取类的属性
- (void)class_copyPropertyList:(Class)pClass {
    unsigned int outCount = 0;
    objc_property_t *properties = class_copyPropertyList(pClass, &outCount);
    for (int i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        const char *cName = property_getName(property);
        const char *cType = property_getAttributes(property);
        NSLog(@"name = %s type = %s", cName, cType);
    }
    free(properties);
}

截屏2022-05-06 下午7.03.18.png

3、获取类的方法

//获取类的方法
- (void)class_copyMethodList:(Class)pClass {
    unsigned int outCount = 0;
    Method *methods = class_copyMethodList(pClass, &outCount);
    for (int i = 0; i < outCount; i++) {
        Method method = methods[i];
        NSString *name = NSStringFromSelector(method_getName(method));
        const char *cType = method_getTypeEncoding(method);
        NSLog(@"name = %@ type = %s", name, cType);
    }
    free(methods);
}

截屏2022-05-06 下午7.18.40.png

4、objc底层没有类⽅法和实例⽅法的区别

在objc底层没有类⽅法和实例⽅法的区别,都是函数。以下方式进行验证:

  • 1.方法:

截屏2022-05-07 上午9.19.09.png

有上图打印结果可以看出,通过runtime获取实例方法的class_getInstanceMethod方法可以正确获取到类方法classMethod。去源码里查找可以看出获取类方法的class_getClassMethod方法底层是调用元类的class_getInstanceMethod方法。这也验证了我们之前所说的类对象是元类的实例对象

截屏2022-05-07 上午9.21.51.png

  • 2.函数实现:

截屏2022-05-07 上午9.33.53.png

有上图打印结果可以看出从元类里找实例方法instanceMethod的实现和从类里面找类方法classMethod的实现是有值的。也就是类方法和实例方法在底层的查找执行的是同一套查找流程。(值一样是因为在类或元类里面找不到方法实现会执行消息转发)

三、编码值的含义

//编码值 含意
//c 代表char类型
//i 代表int类型
//s 代表short类型
//l 代表long类型,在64位处理器上也是按照32位处理
//q 代表long long类型
//C 代表unsigned char类型
//I 代表unsigned int类型
//S 代表unsigned short类型
//L 代表unsigned long类型
//Q 代表unsigned long long类型
//f 代表flfloat类型
//d 代表double类型
//B 代表C++中的bool或者C99中的_Bool
//v 代表void类型
//* 代表char *类型
//@ 代表对象类型
//# 代表类对象 (Class)
//: 代表⽅法selector (SEL)
//[array type]   代表array
//{name=type…}   代表结构体
//(name=type…)   代表union
//bnum     一个位域,由数字位组成
//^type    指向类型的指针
//?        未知类型(其中,此代码用于函数指针)

截屏2022-05-06 下午7.20.25.pngsetAge:方法的typev20@0:8i16为例: OC底层方法的调用会转化成消息的发送objc_msgSend,其默认自带两个参数(消息的接收者,消息的方法名)

`v`  --> 代表方法的返回值,v = void
`20` --> 参数所占字节总长度,@(占8字节) + :(占8字节) + i(占4字节) = 20
`@`  --> 方法参数1,消息的接收者。对象类型,占8个字节
`0`  --> 参数消息的接收者所占字节的起始位置,从第0个字节开始
`:`  --> 方法参数2,代表方法名(SEL),占8字节
`8`  --> 参数消息的方法名所占字节的起始位置,从第8个字节开始
`i`  --> 方法参数age,int类型。占4字节
`16` --> age参数所占字节起始位置,从第16个字节开始

扩展内容

1、苹果为什么设计元类?

主要的⽬的是为了复⽤消息机制。在OC中调⽤⽅法,其实是在给某个对象发送某条消息。
消息的发送在编译的时候编译器就会把⽅法转换为objc_msgSend这个函数。
id objc_msgSend(id self, SEL op, ...) 这个函数有俩个隐式的参数:消息的接收者,消息
的⽅法名。通过这俩个参数就能去找到对应⽅法的实现。
objc_msgSend函数就会通过第⼀个参数消息的接收者的isa指针,找到对应的类,如果我们是通过
实例对象调⽤⽅法,那么这个isa指针就会找到实例对象的类对象,如果是类对象,就会找到类对
象的元类对象,然后再通过SEL⽅法名找到对应的imp,然后就能找到⽅法对应的实现。
那如果没有元类的话,那这个objc_msgSend⽅法还得多加俩个参数,⼀个参数⽤来判断这个⽅法
到底是类⽅法还是实例⽅法。⼀个参数⽤来判断消息的接受者到底是类对象还是实例对象。
消息的发送,越快越好。那如果没有元类,在objc_msgSend内部就会有有很多的判断,就会影响
消息的发送效率。
所以元类的出现就解决了这个问题,让各类各司其职,实例对象就⼲存储属性值的事,类对象存储
实例⽅法列表,元类对象存储类⽅法列表,符合设计原则中的单⼀职责,⽽且忽略了对对象类型的
判断和⽅法类型的判断可以⼤⼤的提升消息发送的效率,并且在不同种类的⽅法⾛的都是同⼀套流
程,在之后的维护上也⼤⼤节约了成本。
所以这个元类的出现,最⼤的好处就是能够复⽤消息传递这套机制。不管你是什么类型的⽅法,都
是同⼀套流程。