iOS底层-类的三顾茅庐(二)

214 阅读10分钟

前言

上篇文章分析了objc_class里存储数据的bits,了解到方法和属性的存储的位置class_rw_t(以下简称rw)。本文将继续研究rw里包含的其他内容。

类数据的存储

书接上文,rw结构体,找到一个class_ro_t的结构体(以下简称ro)。

image-20220427150636259

代码验证:

// 声明
NS_ASSUME_NONNULL_BEGIN

@interface FFPhone : NSObject

@property (nonatomic, copy) NSString * name;

+ (void)phoneTest;

@end

NS_ASSUME_NONNULL_END
  
// 实现
@implementation FFPhone
{
    NSString * _privateProperty;
}

-(instancetype)init {
    if (self = [super init]) {
        self.name = @"init iPhone";
    }
    return self;
}

+ (void)phoneTest {
    NSLog(@"phoneTest");
}

@end

// 测试
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 掩码:0x00007ffffffffff8ULL
        FFPhone *p = [FFPhone alloc];
        
        NSLog(@"...");
    }
    return 0;
}

LLDB指令:

// (lldb) x/6gx p.class
// (lldb) p/x (class_data_bits_t *)0x100008200
// (lldb) p *$1
// (lldb) p $2.data()
// (lldb) p *$3
// (lldb) p $4.ro()

是能获取到到ro

image-20220427152403726

既然成员变量不在rw中,会不会在ro里?

成员变量

在iOS中,ivar表示成员变量,而ro结构体里正好有个ivars:

// (lldb) p *$5
// (lldb) p $6.ivars

如图:

image-20220427152518790

得到结构体ivar_list_t类型的指针

image-20220427152855264

也是继承自模板类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());
    }
};

通过get方法拿到第一个元素,正是私有的成员变量_privateProperty,说明属性生成的成员变量才会放在class_rw_t的属性列表properties()里,而单独的成员变量只会放在ro结构体里,因为成员变量无法从外部修改,看成是只读的

image-20220427153250225

越界也会报错:

image-20220427153340127

所以属性生成的成员变量_name也在ro生成了一份。

offset对应machO文件中的内存偏移量

可以确定成员对象放在类对象里,为什么它们的值放在实例对象里?

类的本质是结构体,相当于一个模板;类里有什么属性、方法等放在模板里就好了。而实例对象是根据这个模板生成的,每个对象的值可能不一样,当然只需要存放值。

rw和ro区别

rorw有什么区别?苹果的WWDC大会有个视频Advancements in the Objective-C runtime里解释过。ro放在纯净的内存空间(clean memory),是只读的。rw在运行生成,存放在可以读写的内存空间中(dirty memory),一经使用,ro就会成为rw的一部分(通过指针引用)。

runtime既然可以动态添加属性、协议等。而ro又不允许修改,怎么办?

拷贝一份再进行修改!这样存在了2份ro数据,岂不是内存浪费?

苹果解决的方式也在视频提到,对于没有使用到的ro,可以进行移除,需要时再分配。所以rw中可以只存储一部分信息,并且rw对于真正需要修改的内容,还会拆分出class_rw_ext_t;以下简称rwe

struct class_rw_ext_t {
    DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
    class_ro_t_authed_ptr<const class_ro_t> ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    char *demangledName;
    uint32_t version;
};

// 不包括方法
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;
}

// 不包括方法
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;
}

视频里提到,在苹果的测试中,实际只有大约不到10%的类真正更改了它们。

ro、rw、rwe的关系

初始时,只有ro。并且Swift和objc共享这一基础结构。

image-20220506113200422

当需要修改类信息的时候,rw引用ro,并拷贝一部分信息。

image-20220506113212980

将需要动态更新的部分提取出来,存⼊rwe:

image-20220506113330669

最终链接成这样:

image-20220506113406159

当前demo里没有rwe,因为没有进行动态修改(分类、runtime的api);除此以外,触发条件还包括:分类和本类都不是懒加载的类(以后单独讲分类)。

rwe不会将成员变量剪切过去,因为无法修改。

例如获取方法列表会判断rwe: 没有才从ro获取

image-20220427163416569

rwe作为属性跟着类一起释放;

根据链接图可以看到,方法列表放在robaseMethods数组里,之后拷贝到了rwe

image-20220506113559783

总结:程序加载时方法存在ro。当类第一次使用的时候,rw就会引用ro;如果动态修改,就会从ro拷贝到rwe;修改的时候也是去操作rwe

元类设计的初衷

复用消息机制。类调用方法,实际上就是发送一条信息。系统通过objc_msgSend()找到实现。

这个函数id objc_msgSend(id self, SEL op, ...) 有2个参数:消息的接收者self,消息的方法名op

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

由于在oc中类方法和实例方法可以同名,通过消息接收者的isa指针来查找。类方法通过类对象的isa找到元类对象,实例方法通过实例对象的isa找到类对象。

如果没有元类的话,那这个objc_msgSend方法还得多加俩个参数,一个参数用来判断这个方法到底是类方法还是实例方法。一个参数用来判断消息的接受者到底是类对象还是实例对象。在方法内部就会有有很多的判断,影响发送效率。消息的发送,总是越快越好。

所以还是得用isa。根据单一职责,元类对象存储类方法,类对象存储实例方法。并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。所以这个元类的出现,最大的好处就是能够复用消息传递这套机制

结合之前的探究的实例方法和类方法,说明在objc底层没有区别,都是函数,通过消息机制调用。只不过存放位置的不同,类方法存储在元类对象里,实例方法存储在类对象里。

runtime的api尝试

既然说到动态修改方法,runtime提供了很多api,这里就随便试试。

成员变量列表

通过class_copyIvarList获取成员变量列表:

// Class的成员变量
void ff_class_copyIvarList(Class ffClass) {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList(ffClass, &count);
    for (int i = 0; i < count; i ++) {
        Ivar ivar = ivars[i];
        const char *name =  ivar_getName(ivar);
        const char *type = ivar_getTypeEncoding(ivar);
        NSLog(@"ivar name = %s; ivar type = %s", name, type);
    }
    free(ivars);
}

这里为什么要通过free(ivars);手动释放变量?查看方法的源码可以看到:


/***********************************************************************
* class_copyIvarList
* fixme
* Locking: read-locks runtimeLock
**********************************************************************/
Ivar *
class_copyIvarList(Class cls, unsigned int *outCount)
{
    const ivar_list_t *ivars;
    Ivar *result = nil;
    unsigned int count = 0;

    if (!cls) {
        if (outCount) *outCount = 0;
        return nil;
    }

    mutex_locker_t lock(runtimeLock);

    ASSERT(cls->isRealized());
    
    if ((ivars = cls->data()->ro()->ivars)  &&  ivars->count) {
        // 开辟了内存,需要手动释放。
        result = (Ivar *)malloc((ivars->count+1) * sizeof(Ivar));
        
        for (auto& ivar : *ivars) {
            if (!ivar.offset) continue;  // anonymous bitfield
            result[count++] = &ivar;
        }
        result[count] = nil;
    }
    
    if (outCount) *outCount = count;
    return result;
}

通过malloc开辟了内存,由于ARC只会管理OC对象,所以需要手动释放。运行结果:

image-20220428113206260

type这里对应编码类型简称:i = int,c = char, d = double, s = short;

属性列表

通过class_copyPropertyList获取属性列表:

// Class的属性
void ff_class_copyPropertyList(Class ffClass) {
    unsigned int count = 0;
    objc_property_t *perperties = class_copyPropertyList(ffClass, &count);
    for (int i = 0; i < count; i++) {
        objc_property_t property = perperties[i];
        const char *name = property_getName(property);
        const char *type = property_getAttributes(property);
        NSLog(@"property name = %s; property type = %s", name, type);
    }
    free(perperties);
}

运行结果:

image-20220428113423263

这里编码怎么理解呢,以T@"NSString",C,N,V_goodsName为例:

  • T代表类型,后面接类型名称@"NSString";
  • C代表属性的Copy关键字,是复制的;
  • N代表nonatomic,该属性是原子性的;
  • V_goodsName代表属性生成的带下划线的成员变量_goodsName

完整编码见扩展

方法列表

通过class_copyMethodList获取方法列表:

// Class的Method
void ff_class_copyMethodList(Class ffClass) {
    unsigned int count = 0;
    Method *methods = class_copyMethodList(ffClass, &count);
    for (int i = 0; i < count; i++) {
        Method method = methods[i];
        NSString *name = NSStringFromSelector(method_getName(method));
        const char *type = method_getTypeEncoding(method);
        NSLog(@"method name = %@; method type = %s",name,type);
    }
    free(methods);
}
// 方法名称是SEL类型
SEL method_getName(Method mSigned)
{
    if (!mSigned) return nil;

    method_t *m = _method_auth(mSigned);
    ASSERT(m->name() == sel_registerName(sel_getName(m->name())));
    return m->name();
}

获取到的是Method 名称是SEL类型,所以要打印的话还需要解析:NSStringFromSelector

image-20220428152111953

方法编码的含义又是什么?举个栗子:v20@0:8i16

  • @代表对象类型,@0从第0个字节开始存放;
  • :代表方法SEL类型,:8表示从第8个字节开始存放;
  • i代表int类型;i16表示从第16字节开始存放;
  • v代表void无返回值类型,v20表示方法总共占20字节;

v20@0:8i16正好对应这三种类型,加起来占20字节。

其他编码含义:

//编码值 含意
//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 代表float类型
//d 代表double类型
//B 代表C++中的bool或者C99中的_Bool
//v 代表void类型
//* 代表char *类型
//@ 代表对象类型
//# 代表类对象 (Class)
//: 代表⽅法selector (SEL)
//[array type] 代表array
//{name=type…} 代表结构体
//(name=type…) 代表union
//bnum A bit field of num bits
//^type A pointer to type
//? An unknown type (among other things, this code is used for function pointers)

试试打印元类的方法,只有一个类方法,这也能证明元类只存放类方法。

image-20220428153137003

实例方法

类对象和元类对象,通过class_getInstanceMethod获取实例方法的方式,来获取一下实例方法和类方法(正常应该不能)。

// 获取类对象和元类的实例方法
void ff_class_getInstanceMethod(Class ffClass) {
    const char *className = class_getName(ffClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method cInstanceMethod = class_getInstanceMethod(ffClass, @selector(testInstancePrint));
    Method mInstanceMethod = class_getInstanceMethod(metaClass, @selector(testInstancePrint));
    Method cClassMethod = class_getInstanceMethod(ffClass, @selector(testClassPrint));
    Method mClassMethod = class_getInstanceMethod(metaClass, @selector(testClassPrint));
    
    NSLog(@"类对象的实例方法: %p", cInstanceMethod);
    NSLog(@"元类对象的实例方法: %p", mInstanceMethod);
    NSLog(@"类对象的类方法: %p", cClassMethod);
    NSLog(@"元类对象的类方法: %p", mClassMethod);
}

运行效果:

image-20220428154204093

为什么元类能通过class_getInstanceMethod获取类方法?正常应该是class_getClassMethod函数来获取的

查看源码可知,本质还是class_getInstanceMethod只是参数变成了元类:cls->getMeta()

/***********************************************************************
* class_getClassMethod.  Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
    return class_getInstanceMethod(cls->getMeta(), sel);
}

那么类方法其实也是实例方法?iOS底层根本没有类方法和实例方法之分。本质都是函数,而runtime通过消息机制调用函数。

元类存放类方法,是为了复用消息机制。满足单一职责的设计原则,同时对类的结构体模板进行复用。

IMP

通过class_getMethodImplementation获取方法的实现(IMP):

// 获取方法实现IMP
void ff_class_getMethodImpl(Class ffClass) {
    const char *className = class_getName(ffClass);
    Class metaClass = objc_getMetaClass(className);
    
    IMP cInstanceMethodImpl = class_getMethodImplementation(ffClass, @selector(testInstancePrint));
    IMP mInstanceMethodImpl = class_getMethodImplementation(metaClass, @selector(testInstancePrint));
    IMP cClassMethodImpl = class_getMethodImplementation(ffClass, @selector(testClassPrint));
    IMP mClassMethodImpl = class_getMethodImplementation(metaClass, @selector(testClassPrint));
    
    NSLog(@"类对象的实例方法IMP: %p", cInstanceMethodImpl);
    NSLog(@"元类对象的实例方法IMP: %p", mInstanceMethodImpl);
    NSLog(@"类对象的类方法IMP: %p", cClassMethodImpl);
    NSLog(@"元类对象的类方法IMP: %p", mClassMethodImpl);
}

运行结果:

image-20220428160316043

元类里面也能找到实例方法的实现?而且mInstanceMethodImplcClassMethodImpl一样内存地址...

查找源码:

__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;
}

if (!imp)表示没找到方法时,会返回_objc_msgForward;这个就是用来做消息转发的。

以前的文章,用到.big()来获取方法,查看big结构体可知,包含SELIMP

image-20220428160755831

总结

成员变量

成员变量存放在类对象的class_ro_t结构体里,因为它们不能被外部修改。

为什么设计元类?

  • 复用消息机制。
  • 符合设计原则中的单一职责。
  • 不同种类的方法走的都是同一套流程,易于维护。

类方法和实例方法的区别

在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这个结构体。这样的话,可以节约很大一部分内存。

class_rw_ext_t生成的条件:

  • 用过runtime的api进行动态修改的时候。
  • 有分类的时候,且分类和本类都为非懒加载类 (实现了+ load方法)。

扩展

类型编码

官方文档:runtime类型编码

方法的类型:

image-20220506151926795

属性的类型:

image-20220506152039060