iOS底层原理之类的底层探索(下)

本文主要内容

1、探索ivar的存储位置

2、ro、rw、rwe解析

3、类方法的存储位置

4、关于元类的解释

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

一、探索ivar的存储位置

在上篇文章底层原理探索中我们知道了,实例对象isa指针指向类对象,类对象是一个objc_class的结构体,在这个结构体里面包含了ISA、superclass、cache、bits,我们在bits里面找到了实例方法、属性、协议,而这些内容都存储在一个class_rw_t结构体中。我们在LSPerson中有一个成员变量并没有在上一遍文章中看见?我们知道属性是成员变量 + set和get方法。那么成员变量存储在哪里了?我们通过objc4-838.1源码中发现class_rw_t里面有一个ro方法,我们按照验证实例方法的步骤来走一下?看是否在这个里面 image.png image.png 按照分析获取实例方法的步骤,最后我们知道里面有一个ivars,成员变量我们也经常叫ivars,最后我们得到一个ivar_list_t的结构体。此时我们输出ivars image.png 此时发现一个熟悉的结构entsize_list_tt,entsize_list_tt 我们上一篇在分析实例方法的时候知道他是一个容器/模版,跟前一篇文章一样继续让下走 image.png 到此我们拿到了LSPerson的成员变量,也知道了类对象里面存储了成员变量的name,成员变量的type,以及成员变量的size。

    总结:
    1、类里面的属性,系统会自动帮我们生成_xxx的成员变量+setter+getter方法。
    2、类的属性存储在类对象中
    3、实例对象里面只存储属性值
复制代码

二、ro、rw、rwe解析

在上面分析的时候我们经常遇到class_ro_t、class_rw_t等结构体,那么这几个代表什么意思了?

ro: 在编译时生成,app 在使用类时,是需要在磁盘中 app 的二进制文件中读取类的信息,二进制文件中的类存储了类的元类、父类、flags 和方法缓存,而类的额外信息(name、方法、协议和实例变量等)存储在 class_ro_t 中。class_ro_t 简称 roread only,将类从磁盘中读取到内存中就是对 ro 的赋值操作。由于 ro 是只读的,加载到内存后不会发生改变又称为 clean memory(干净内存)。

rw: 在运行时生成的,类一经使用,就会将ro中的内容剪切到rw中。

rwe:对应class_rw_ext_t结构体,runtime提供了动态为类添加方法和属性的api,这些方法、属性、协议存在只读不允许修改的ro中,如果想要修改方法和属性,需要把ro中的内容拷贝到rw中,这样rw中就会存在两份ro,增加内存消耗,所以苹果增加了rwe结构体来解决内存消耗问题,rwe里面存放要呗修改的内容。其中rwe产生的条件有两种:1:分类(分类和奔雷必须是懒加载类)2、使用runtime动态为类添加了属性、方法、协议。

类整体结构: image.png 仔细查看源码,我们能发现系统在获取方法、属性、协议的时候会先判断是否存在rwe,如果rew存在就会在其中找。如果不存在就会从ro中查找。

// 在class_rw_t结构体中
const method_array_t methods() const {
  auto v = get_ro_or_rwe();
  // 判断是class_rw_ext_t是否存在
  if (v.is<class_rw_ext_t *>()) {
    // 如果存在就从class_rw_ext_t查找
    return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
  } else {
    // 如果不存在就从ro中查找
    return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};
  }
}
复制代码

三、类方法的存储位置

在上一篇底层原理探索中探索bits里面找到了实例方法,但是没有找到类方法,那么类方法存储在哪里了? 我们知道实例对象isa指针指向类对象,类对象中保存了实例方法,类对象isa指针指向元类,元类也是一个对象, 那么类方法是否保存在元类的bits中?我们按照上一篇探索实例方法的存储位置的流程来验证一下。

image.png 结论:类方法存储在元类中。

四、关于元类的解释

在探索对象isa指向和获取对象类方法存储位置的时候我们知道了元类,那什么是元类?苹果为什么要设计元类呢? 苹果这样设计就是为了复用消息机制,用同一套消息机制。 [LSPerson alloc] 在编译的时候会被编译成objc_msgSend(消息的接受者,消息的方法名)函数。通过消息接受者的isa指针和消息的方法名找到方法的实现(imp)。如果消息接受者为实例对象,那就是通过实例对象的isa指针指向的类对象里面去找方法实现。如果消息接收者为类对象,那就是通过类对象的isa指针指向的元类中去找方法实现。

设想一下?如果没有元类了?那么类方法如何去查找了? 如果没有元类,那么在objc_msgSend里面首先需要判断消息的接受者是实例对象还是类对象,然后找实现的时候,也要去判断消息接受者的类型不同去不同的地方查找方法实现,而消息发送最重要的就是快速,如果增加这些判断就会影响查找效率。利用当前的消息机制只通过isa指针可以很快找到方法的实现,实例对象存储成员变量的值,类对象存储实例对象的方法,元类对象存储类对象的方法,也就是单一职责的原则,大大增加消息发送的效率,同时维护同一个消息机制(objc_msgSend函数)也更方便。

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

首先定义一个类LSPerson,声明如下:

@interface LSPerson : NSObject
 //isa指针                                     // 8字节  因为是对象,有一个isa指针所以默认有8个字节

@property (strong, nonatomic) NSString *name; // 8字节

@property (strong, nonatomic) NSString *body; // 8字节

@property (assign, nonatomic) int age;        // 4字节

+ (void)testClassMethod;

- (void)test;

@end
复制代码

1、获取类的成员变量

通过runtime的class_copyIvarList函数拿到成员变量列表ivars,遍历即可,如下:

// 获取类的成员变量
- (void)getClassCopyIvarList:(Class)class {
    unsigned int count = 0;
    // 通过runtime获取成员变量
    Ivar *ivars = class_copyIvarList(class, &count);
    for (int i = 0; i < count; 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);
}
// 调用方法
[self getClassCopyIvarList:LSPerson.class
// 输出一下结果
// name = _age, type = i
// name = _name, type = @"NSString"
// name = _body, type = @"NSString"
复制代码
  1、当我们声明属性的时候系统会自动帮我们生成对应的_xxx的属性和setter、getter方法。
  2、type: i(int)、NSString代表成员变量的类型
复制代码

2、获取类的属性

通过runtime的class_copyPropertyList、property_getName等获取类的属性

// 获取类的属性
- (void)getClassCopyPropertyList:(Class)class {
    unsigned int count = 0;
    // 通过runtime的api获取类的属性列表
    objc_property_t *properies = class_copyPropertyList(class, &count);
    for (int i = 0; i < count; i++) {
        objc_property_t property = properies[i];
        // 遍历拿到属性的名字、属性的类型定义
        const char *cName = property_getName(property);
        const char *cAttributes = property_getAttributes(property);
        NSLog(@"name = %s, type = %s", cName, cAttributes);
    }
}
//调用方法
[self getClassCopyPropertyList:LSPerson.class]
// 输出结果
// name = name, type = T@"NSString",&,N,V_name
// name = body, type = T@"NSString",&,N,V_body
// name = age, type = Ti,N,V_age
复制代码
1、获取类的属性,属性名字就是我们声明的名字
2、属性里面的type是attributed类型 显示如属性name“T@'NSString',C,N,V_name”。其中"T"代表类型,
后面加"@‘NSString’即为字符串类型,"C"代表copy,"N"代表"nonatomic","V_name"代表成员变量_name. 
再如属性age”Ti,N,V_age“,"Ti"整体代表int类型,"N"代表"nonatomic","V_age"代表成员变量_age.
复制代码

属性类型编码说明官网地址:Declared property type encodings

3、获取类的方法

通过runtime的class_copyMethodList、method_getTypeEncoding等获取方法。

// 获取方法
- (void)getClassCopyMethodList:(Class)class {
    unsigned int count = 0;
    // 获取方法列表
    Method *methods = class_copyMethodList(class, &count);
    for (int i = 0; i < count; i++) {
        Method method = methods[i];
        // 获取方法的SEL
        SEL sel = method_getName(method);
        // 获取方法的type
        const char *cType = method_getTypeEncoding(method);
        NSLog(@"name = %@, type = %s", NSStringFromSelector(sel), cType);
    }
}
//方法调用
[self getClassCopyMethodList:LSPerson.class];
// 输出结果
// name = test, type = v16@0:8
// name = name, type = @16@0:8
// name = setName:, type = v24@0:8@16
// name = body, type = @16@0:8
// name = setBody:, type = v24@0:8@16
// name = age, type = i16@0:8
// name = setAge:, type = v20@0:8i16
// name = .cxx_destruct, type = v16@0:8
复制代码
打印属性类型解析: 显示如方法name类型“@16@0:8”.其中"@"代表返回值为对象类型,"16"代表方法参数的长度,
第2个"@"代表方法的接收者,8个长度,从0-7;":"代表方法名,8个长度,从8-15,所以总共16个长度. 
再如属性setHeight“v24@0:8q16”,"v"代表返回值为void类型,"24"代表方法参数的长度,
第2个"@"代表方法的接收者,8个长度,从0-7;":"代表方法名,8个长度,从8-15;还有个long类型的参数height,8个长度,
从16-23,所以总共24个长度.
复制代码

4、获取类方法

在3中我们获取到了类的实例方法,在上面我们说实例方法是通过类对象获取,类方法是元类获取,所以只需继续使用方法3,修改class的传入,如下

// 方法调用
[self getClassCopyMethodList:objc_getMetaClass("LSPerson")];
// 输出结果
// name = testClassMethod, type = v16@0:8
复制代码

5、获取方法的实现

通过runtime的class_getMethodImplementation获取方法的实现

// 获取方法的实现
- (void)getClassImplementation:(Class)class {
    Class metaClass = objc_getMetaClass(class_getName(class));
    IMP imp1 = class_getMethodImplementation(class, @selector(test));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(testClassMethod));
    NSLog(@"imp1: %p, imp2: %p", imp1, imp2);
}
//输出结果
// imp1: 0x????????, imp2: 0x????????,
复制代码
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;
}
通过在源码中查看class_getMethodImplementation的方法实现我们知道当IMP找不到的时候会走_objc_msgForward
复制代码

本文总结

1、类的属性、成员变量存储在类对象中,实例对象里面存储的是值。

2、实例方法存储在类对象中,类方法存储在元类中。

3、苹果设计元类就是位了复用消息机制,在底层里面也不需要区分是类方法还是实例方法。

猜你喜欢

转载自juejin.im/post/7108924714655383583