类的底层原理探索(下)

2,842 阅读8分钟

在上一篇类的底层原理探索(上)中,我们遗留下了一个问题:类的成员变量和类方法存储在哪个地方?

知识点准备

clean memory

加载后不会发生变更的内存

dirty memory

在进程运行时会发生变更的内存

成员变量和类方法在类中的存储位置

成员变量

我们先继续查看class_rw_t的源码,看看成员变量和类方法是否存在这个结构体中其他地方。此时,我们发现了class_ro_t *ro()函数,返回一个class_ro_t类型的变量,接着查看class_ro_t源码。

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;

    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;

    const char *getName() const {
        return name.load(std::memory_order_acquire);
    }
    class_ro_t *ro = (class_ro_t *)memdup(this, size);
    // ....
};

我们除了找到了有些熟悉的baseMethodsbaseProtocolsbaseProperties之外,还发现了ivar_list_t * ivars;,这很可能就是我们要的东西。

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());
    }
};

struct ivar_t {
#if __x86_64__
    // *offset was originally 64-bit on some x86_64 platforms.
    // We read and write only 32 bits of it.
    // Some metadata provides all 64 bits. This is harmless for unsigned 
    // little-endian values.
    // Some code uses all 64 bits. class_addIvar() over-allocates the 
    // offset for their benefit.
#endif
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;

    uint32_t alignment() const {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
};

通过查看ivar_list_t的源码,发现它也跟之前的methods和properties一样,也是个容器,并且存放的ivar_t类型的数据也有nametype等字段。接下来同样通过控制台来查看这个是否就是我们要找的东西。

WechatIMG623.png

类方法

我们在class_ro_t中看到了baseMethod,既然类方法不在class_rw_tmethods中,那它会不会在class_ro_tbaseMethod中呢?我们同样通过控制台去获取baseMethod中的数据。

image.png 结果令人大失所望,class_ro_t中的baseMethod所存储的方法和class_ro_t中的baseMethod一模一样,我们仍然没有找到类方法所存储的位置。

到这里,似乎线索已经断掉了,但是根据先前的探索,我们知道类方法有一个isa指针指向元类,那是否能在元类中找到类方法呢?我们继续通过控制台来探索。

WechatIMG624.png

WechatIMG625.png

终于,我们在元类的methods中找到了我们定义的类方法。那Apple又是为什么要把类方法放在元类中呢?元类又到底是用来干什么的呢?

元类的设计理念

元类的设计主要是用来复用消息机制。iOS在调用方法时,都是通过objc_msgSend()函数来调用的,这个函数有两个默认的参数,一个是消息的接收者,一个是消息的方法名,通过这两个参数就可以找到所调用的方法对应的实现。

其中,调用该函数时,会通过消息的接收者这个参数的isa指针找到的对应的类,如果消息的接收者是实例对象,那就会在类对象中找方法实现,如果消息的接收者是类对象,那么就会在元类中找方法的实现。

自此,省去了在objc_msgSend()函数中判断消息接收者是何种类型,消息是合种方法(实例方法还是类方法)的操作,遵循了“单一职责”,增加了消息发送的效率。

实例变量存储成员变量的值,类对象存储实例方法和成员变量,元类对象存储类方法。

runtime获取成员变量及方法

成员变量

-(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);
}

image.png

属性

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

image.png

type后面的内容中,其中T代表类型,name是NSString类型,age是int类型,C代表的是copy,N代表的是nonatomic,V代表的是成员变量。

方法

//获取类的方法
-(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);
}

image.png

image.png type后的内容,以setAge为例,v代表的是返回值void,v后面的数字代表方法参数的长度,@代表对象,@0代表第一个参数是方法的调用者,从0开始,占8个长度,:代表方法selector(SEL),:8代表第二个参数SEL,从第8位开始,也占8个长度,i代表第三个参数是int类型,从第16个位置开始,长度为4,所以总的长度为20。

如果需要获取实例方法,参数传类对象,如果要获取类方法,参数传元类对象。

class_getInstanceMethod

-(void)methodTest:(Class)pClass {
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(instanceMethod));
    Method method2 = class_getInstanceMethod(metaClass, @selector(instanceMethod));
    Method method3 = class_getInstanceMethod(pClass, @selector(classMethod));
    Method method4 = class_getInstanceMethod(metaClass, @selector(classMethod));
    NSLog(@"%p - %p - %p - %p",method1,method2,method3,method4);
}

image.png

我们知道,实例方法存在类对象中,类方法存放在元类中。method1获取类对象到了pClass的实例方法,method2通过元类对象去获取实例方法得到的结果为空0x0,method3通过类对象获取类方法为空0x0,这些都合情合理。但为什么通过元类获取类方法可以使用class_getInstanceMethod函数呢?

我们查看class_getClassMethod函数的实现。

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

class_getClassMethod的实现中,最终也是调用class_getInstanceMethod函数,只不过传入的第一个参数是cls的元类。这就意味着在OC的底层中,类方法也是元类对象的实例方法。那么在本质上,其实并不区分类方法和实例方法,只不过这些方法所存储在的对象不同,方便开发者表述和区分,才有了类对象和实例对象的名义上的区分。

class_getClassMethod

刚才看了class_getClassMethod的实现,里面调用的是cls->getMeta()。根据之前的探索,我们知道元类的isa指针都是指向根源类,那根源类是不存在我们定义的类方法的,那为什么元类通过 getMeta函数获取metaClass后再调用class_getInstanceMethod能获取到类方法呢?我们再看一下getMeta()的实现。

Class getMeta() {
    if (isMetaClassMaybeUnrealized()) return (Class)this;
    else return this->ISA();
}

bool isMetaClassMaybeUnrealized() {
    static_assert(offsetof(class_rw_t, flags) == offsetof(class_ro_t, flags), "flags alias");
    static_assert(RO_META == RW_META, "flags alias");
    if (isStubClass())
        return false;
    return data()->flags & RW_META;
}

经过测试,如果是调用getMeta的对象是类对象,那么就会return this->ISA();返回类对象的isa,即元类。如果调用调用getMeta的对象是元类对象,那么就会return (Class)this;即返回元类对象自己。那么元类对象通过getMeta函数来获取类方法,就解释的痛了。

接下来通过代码再继续验证一下。

Method method5 = class_getClassMethod(pClass, @selector(instanceMethod));
Method method6 = class_getClassMethod(metaClass, @selector(instanceMethod));
Method method7 = class_getClassMethod(pClass, @selector(classMethod));
Method method8 = class_getClassMethod(metaClass, @selector(classMethod));
NSLog(@"%p - %p - %p - %p",method5,method6,method7,method8);

执行后的结果为

image.png

instanceMethod在类对象中,method5通过用pClass的metaClass去获取instanceMethod肯定是获取不到的,同样,metaClass中也没有实例方法,所以method6也是0x0。method7中类对象通过getMeta()获取到元类对象,再通过class_getInstance获取到classMethod。method8通过元类调用getMeta()得到的依然是元类对象,再调用class_getInstance获取到classMethod

class_getMethodImplementation

同样地,我们测试一下class_getMethodImplementation函数

const char *className = class_getName(LGPerson.class);
Class metaClass = objc_getMetaClass(className);
    
IMP imp1 = class_getMethodImplementation(pClass, @selector(instanceMethod));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(instanceMethod));
IMP imp3 = class_getMethodImplementation(pClass, @selector(classMethod));
IMP imp4 = class_getMethodImplementation(metaClass, @selector(classMethod));
NSLog(@"%p - %p - %p - %p",imp1,imp2,imp3,imp4);

最后的打印结果如下。 image.png

imp1通过类对象获取到instanceMethod的实现,imp4通过元类获取到classMethod的实现,这两个都合情合理。但是imp2和imp3分别通过元类对象获取instanceMethod、类对象获取classMethod,却依然有值,这就有点奇怪了。

我们查看class_getMethodImplementation函数的实现。

__attribute__((flatten))
IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    lockdebug_assert_no_locks_locked_except({ &loadMethodLock });

    imp = lookUpImpOrNilTryCache(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

发现如果没有找到imp,就会return _objc_msgForward;。这个就是消息的转发机制,后续在objc_msgSend时再详细探索。

ro、rw、rwe

我们在探索的过程中发现,有些东西在ro中发现,有的在rw中发现,另外还发现了又rwe,那这些东西又是什么关系呢?

ro顾名思义就是readonly,它在内存中属于clean memory,在编译的时候产生。而rw就是readwrite,它在内存中的dirty memory,在运行的时候生成的。

image.png

根据WWDC20的介绍视频,通过分离出那些永远不会被改变的数据可以把大部分的类数据存储为clean memory。当一个类首次被使用,runtime会为它分配额外的存储容量,并将class_ro的内存存放到这个存储容量中,这个存储容量就是class_rw_t。在class_rw_t中,还存储了只有在运行时才会生成的新信息。

生成的信息包括有:通过使用First Subclass和Next sibling Class将类链接成一个树状结构、当category被加载时,它可以向类中添加新方法、可以动态添加属性和协议等。此时,class_rw_t中间即含有不可变的内容,也包含了动态添加的内容。 image.png

接下来Apple继续将class_rw_t进行分割,将可变的部分与不可变的部分分开来,这可以大大减小class_rw_t所占的内存空间,对于那些确实需要额外信息的类,将额外的信息放到class_rw_ext_t中。class_rw_ext_t生成的条件有两个,一是用过runtime的Api进行动态修改的时候;二是有分类的时候,且分类和本类都为非懒加载类的时候。实现了+load方法即为非懒加载类。

image.png

最终的类的结构就如下所示。类中包含一份完整的class_ro_t,当内存空间不足时,可以将这部分的内容从内存中移除,有需要时再从磁盘中加载。class_rw_t会将class_ro_t的内容放到自己里面,然后再将当前类的category里的方法、属性、协议也放到自己中。最后,再将那些会可变的部分与不可变的部分分开来。

image.png

至此,我们对类的探索就完成了。接下来做个总结。

总结

类对象

类也是对象,类对象有且只有⼀个。类对象本质为objc_class结构体。类对象⾥⾯存储了类的⽗类、属性、实例⽅法、协议、成员变量、⽅法缓存等等。成员变量存放在类对象的class_ro_t结构体当中。类⽅法存在元类当中。

isa指向关系

实例对象的isa->类对象
类对象的isa->元类
元类的isa->根元类
根元类的isa->根元类⾃⼰

元类的继承关系

⽗类的元类就是元类的⽗类。根元类的⽗类就是NSObjectNSObject是万类之祖。

.cxx_destruct

.cxx_destruct方法是在ARC模式下用于释放成员变量的。只有当前类拥有实例变量时这个方法才会 出现,property生成的实例变量也算,且父类的实例变量不会导致子类拥有这个方法。

苹果为什么设计元类?

主要的⽬的是为了复⽤消息机制。在OC中调⽤⽅法,其实是在给某个对象发送某条消息。消息的发送在编译的时候编译器就会把⽅法转换为objc_msgSend这个函数。

id objc_msgSend(id self, SEL op, ...) 这个函数有俩个隐式的参数:消息的接收者,消息的⽅法名。通过这俩个参数就能去找到对应⽅法的实现。

objc_msgSend函数就会通过第⼀个参数消息的接收者的isa指针,找到对应的类,如果我们是通过实例对象调⽤⽅法,那么这个isa指针就会找到实例对象的类对象,如果是类对象,就会找到类对象的元类对象,然后再通过SEL⽅法名找到对应的imp,然后就能找到⽅法对应的实现。

那如果没有元类的话,那这个objc_msgSend⽅法还得多加俩个参数,⼀个参数⽤来判断这个⽅法到底是类⽅法还是实例⽅法。⼀个参数⽤来判断消息的接受者到底是类对象还是实例对象。消息的发送,越快越好。

那如果没有元类,在objc_msgSend内部就会有有很多的判断,就会影响消息的发送效率。所以元类的出现就解决了这个问题,让各类各司其职,实例对象就⼲存储属性值的事,类对象存储实例⽅法列表,元类对象存储类⽅法列表,符合设计原则中的单⼀职责,⽽且忽略了对对象类型的判断和⽅法类型的判断可以⼤⼤的提升消息发送的效率,并且在不同种类的⽅法⾛的都是同⼀套流程,在之后的维护上也⼤⼤节约了成本。

所以这个元类的出现,最⼤的好处就是能够复⽤消息传递这套机制。不管你是什么类型的⽅法,都是同⼀套流程。

在objc底层没有类⽅法和实例⽅法的区别,都是函数。

ro,rw,rwe

class_ro_t是在编译的时候⽣成的。当类在编译的时候,类的属性,实例⽅法,协议这些内容就存在class_ro_t这个结构体⾥⾯了,这是⼀块纯净的内存空间,不允许被修改。class_rw_t是在运⾏的时候⽣成的,类⼀经使⽤就会变成class_rw_t,它会先将class_ro_t的内容"拿"过去,然后再将当前类的分类的这些属性、⽅法等拷⻉到class_rw_t⾥⾯。它是可读写的。

class_rw_ext_t可以减少内存的消耗。苹果在wwdc2020⾥⾯说过,只有⼤约10%左右的类需要动态修改。所以只有10%左右的类⾥⾯需要⽣成class_rw_ext_t这个结构体。这样的话,可以节约很⼤⼀部分内存