类的底层原理探索(下)

652 阅读7分钟

Xnip2022-04-21_21-53-23.jpg

1.成员变量的存储位置

1.1 class_ro_t

在class_rw_t 中找到如下方法,成员变量存放在类对象的class_ro_t结构体当中;

const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
        }
        return v.get<const class_ro_t *>(&ro_or_rw_ext);
    }

1.2获取成员变量的步骤

1.2.1步骤

  1. 步骤一: x/6gx p.class获取类对象内存-从而获取class_data_bits_t指针地址;

  2. 步骤二: p $1->data() 获取 class_rw_t 指针地址,在class_rw_t中有上面ro()方法;

  3. 步骤三: p $2->ro() 获取class_ro_t内存地址; image.png

  4. 步骤四: p *$3 获取class_ro_t存储内容;

image.png

  1. 步骤五: 在iOS中用ivar来表示成员变量 p $4.ivars 获取到const ivar_list_t指针;
  2. 步骤六: p *$5 获取const ivar_list_t 内容,可以看待其是一个entsize_list_tt,所以可以get()方法获取内容;
  3. 步骤七: $6.get(0) 获取成员变量。 image.png 为什成员变量存放在类对象中,而成员变量的值存放在实例对象中因为实例对象有很多,不同的实例对象存储的值不同;

2 ro、rw、rwe解析

官网相关地址链接

2.1 ro(class_ro_t)

class_ro_t是在编译的时候⽣成的。当类在编译的时候,类的属性,实例⽅法,协议这些内容就存在class_ro_t这个结构体⾥⾯了,这是⼀块纯净的内存空间,不允许被修改。

2.2 rw(class_rw_t)

class_rw_t是在运⾏的时候⽣成的,类⼀经使⽤就会变成class_rw_t,它会先将class_ro_t的内容"拿"过去,然后再将当前类的分类的这些属性、⽅法等拷⻉到class_rw_t⾥⾯。它是可读写的。

2.3 rwe(class_rw_ext_t)

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

class_rw_ext_t⽣成的条件:

  • 第⼀:⽤过runtime的Api进⾏动态修改的时候。
  • 第⼆:有分类的时候,且分类和本类都为⾮懒加载类的时候。实现了+load⽅法即为⾮懒加载类。

2.4 类加载到内存的结构变化

  1. 当类第一次从磁盘加载到内存时的结构: Xnip2022-04-24_20-33-33.jpg

  2. 当需要修改类信息的时候: Xnip2022-04-24_20-35-59.jpg

  3. 当需要动态更新的部分提取出来(满足class_rw_ext_t⽣成的条件),存储的到class_rw_ext_t中: Xnip2022-04-24_20-36-09.jpg

4.类的整体结构: Xnip2022-04-24_20-36-20.jpg

3.苹果为什么要设计元类?

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

4.类方法的存储位置

类方法存储在元类中;验证的步骤如下:

  1. 步骤一:x/4gx p.class获取类对象的isa指针;

  2. 步骤二:获取元类的内存地址(类对象的isa地址 & ISA_MASK) (在x86_64 环境下) p/x 0x0000000100008718 & 0x00007ffffffffff8ULL

  3. 步骤三:x/6gx 元类地址获取元类对象指针-从而获取class_data_bits_t指针地址;

  4. 步骤四:p (class_data_bits_t *)0x100008738获取class_data_bits_t

  5. 步骤五:获取class_rw_t$4->data()image.png

  6. 步骤六:获取(const method_array_t) p $5->methods()

  7. 步骤七:p $7.list获取const method_list_t_authed_ptr<method_list_t>

  8. 步骤八:获取method_list_t,p $8.ptr;

  9. 步骤九:获取method_list_t,*p $9,可以看到里面有一个类方法;

  10. 步骤十:$10.get(0).big(),获取类方法(我的是大端); image.png

5.通过runtime的API探索类的数据结构

5.1获取类的成员变量

//MARK: --获取类成员变量
- (void)showIvarsMethod:(Class)pClass{
    unsigned int counts = 0;
    Ivar *ivas = class_copyIvarList(pClass, &counts);
    NSLog(@"获取类对象的所有成员变量:");
    for (NSInteger i = 0; i < counts; ++i ) {
        Ivar ivar = ivas[i];
        const char *name = ivar_getName(ivar);
        const char *type = ivar_getTypeEncoding(ivar);
        NSLog(@"名字:%s, 类型:%s",name,type);
    }
    //由于class_copyIvarList中用malloc方法开辟了内存
    //ARC只能管理Object对象
    free(ivas);
}

输出: image.png

5.2 获取属性

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

输出: image.png

5.3 获取实例方法

//MARK:--获取实例方法
- (void)showMethodsMethod:(Class)pClass {
    unsigned int counts = 0;
    Method *methods = class_copyMethodList(pClass, &counts);
    NSLog(@"获取类实例方法:");
    for (NSInteger i = 0; i < counts; ++i ) {
        Method method = methods[i];
        NSString *name = NSStringFromSelector(method_getName(method));
        const char *type = method_getTypeEncoding(method);
        NSLog(@"名字:%@, 类型:%s",name,type);
    }
    free(methods);
}

输出: image.png

5.4 method验证苹果设计元类的原因?

- (void)methodTest:(Class)pClass {
    const char *name = class_getName(pClass);
    //获取元类
    Class metaClass = objc_getMetaClass(name);
    Method method1 = class_getInstanceMethod(pClass, @selector(inititialMethodShow));
    Method method2 = class_getInstanceMethod(metaClass, @selector(inititialMethodShow));
    Method method3 = class_getInstanceMethod(pClass, @selector(classMethodShow));
    Method method4 = class_getInstanceMethod(metaClass, @selector(classMethodShow));
    Method method5 = class_getClassMethod(pClass, @selector(classMethodShow));
    NSLog(@"%p -- %p --- %p -- %p -- %p", method1, method2, method3, method4, method5);
}

输出: 0x1044b3dc0 -- 0x0 --- 0x0 -- 0x1044b3d58 -- 0x1044b3d58 可以看出method4和method5的地址一样,一个从类方法中获取ClassMethod,一个从元类方法中获取InstanceMethod都可以获取到,源码class_getClassMethod 实现如下: image.png 也是调用的获取实例方法的,从而可以看出,苹果设计元类的目的并不是为了存储类方法,是为了复用消息机制;

5.5 IMP探索

- (void)impTest:(Class)pClass {
    const char *name = class_getName(pClass);
    //获取元类
    Class metaClass = objc_getMetaClass(name);
    IMP imp1 = class_getMethodImplementation(pClass, @selector(inititialMethodShow));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(inititialMethodShow));
    IMP imp3 = class_getMethodImplementation(pClass, @selector(classMethodShow));
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(classMethodShow));
    NSLog(@"%p -- %p --- %p -- %p", imp1, imp2, imp3, imp4);
}

输出: 0x1068dbbf0 -- 0x7fff20184080 --- 0x7fff20184080 -- 0x1068dbc20 可以看到从元类中获取实例方法imp2和从类对象中获取类方法imp3都有值并且相同,因为在class_getMethodImplementation 中当方法找不到的时候都会返回 _objc_msgForward(用来做消息转发的),如下图: image.png

6.总结

  1. 成员变量存放在类对象中,而成员变量的值存放在实例对象中,是因为实例对象有很多,不同的实例对象存储的值不同;
  2. 类在在底层是一个struct(objc_class),主要由四个成员变量组成:
  • isa: 指向元类,元类中存储了类方法;
  • superclass: 父类
  • cache:
  • class_data_bits_t:class_rw_t存储了属性、实例方法、协议、成员变量等,class_ro_t结构存储了成员变量,;
  1. 苹果设计元类的主要目的是为了复用消息机制;
  2. 在ARC环境下只能管理Object对象,malloc的对象需要手动释放;
  3. 在objc底层没有类⽅法和实例⽅法的区别,都是函数。