iOS底层探究 - 类结构剖析(类成员class_data_bits_t)

574 阅读11分钟

目录
1:类的本质
2:类的结构
3:类以及类的元类是什么时候创建的
4:lldb对类动态调试
5:总结

我们知道在面向对象的语言中,万物皆对象,在iOS系统中也是如此,由类派生出对象,而其实Class(类)本身也是一个对象。

一 :类的本质

在iOS系统中,NSObject是所有类的基类,我们一般创建的类都会基于它或者它的子类去派生。那么我们就从这个入口出发,去探索下在系统的底层,类是怎样的一种结构。

#import <Foundation/Foundation.h>
@interface China : NSObject
{
    NSString *gdp;
}
@property (nonatomic, strong) NSString *people;
@property (nonatomic, strong) NSString *area;

- (void)countPeople;
+ (void)gdpIncrease;
@end

@implementation China

- (void)countPeople{
    
}
+ (void)gdpIncrease{
    
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        China *object = [China alloc];
       //  **断点位置** 
    }
    return 0;
}

main函数里,我们alloc出来一个object对象,下面我们通过clang命令把代码转换成c++形式去查看,命令如下:

clang -rewrite-objc main.m -o main.cpp

转换成功之后我们打开main.cpp,天哪居然代码行数飙升到9000多行!不过没关系,其他代码我们先不看,我们直接搜索关键字China,发现也有好多的结果,仔细逐个查看之后发现了这个定义:

typedef struct objc_object China;

可以看出,我们创建的China类是一个通过typedef struct objc_objec定义的结构体。那么Class究竟是什么呢?我们知道,底层对象都是struct结构体表示而且类型都以objc_开头,我们把关键词切换成typedef struct objc_继续搜索,又出来一大堆结果,有点崩溃...我们通过Commmand + G翻阅,很幸运,翻阅了几次就找到了如下定义:

typedef struct objc_class *Class;

So,在底层Class是一个objc_class类型的结构体指针。我们把关键词改成typedef struct objc_class继续搜索,看看有没有具体的定义,结果发现结果为空,现在只能另辟蹊径,通过源码来搜索了。
这边我使用的是objc4-756的源码配置的工程,打开工程,全局搜索struct objc_class查看结果:

// 在objc-runtime-new.h这个文件发现了这段定义
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;           // 8
    cache_t cache;             // formerly cache pointer and vtable 16
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags 8

    class_rw_t *data() { 
        return bits.data();
    }
     //下面还有很多方法,在这里暂时我们不关注
};

可以发现,objc_class在底层是一个继承于objc_object的结构体,而objc_object又是什么?我们看下面两段源码大家就会明白了:

// 在objc.h文件中
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

有没有发现特别的相似?其实在底层,struct objc_object就是在OC中的NSObject派生的对象。而struct objc_class就是NSObject的Class类对象。

二 :类的结构

类的结构其实也很清晰,通过系统给成员的命名,大概就能猜出其成员的大概作用:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;           // 8
    cache_t cache;             // formerly cache pointer and vtable 16
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags 8

    class_rw_t *data() { 
        return bits.data();
    }
     //下面还有很多方法,在这里暂时我们不关注
};

2.1: isa-- isa指针

iOS底层探究-isa的初始化&走位分析中我们研究过对象内部是有个isa指针,是实现类和对象建立绑定的,而类中的isa是指向元类的,也就是说是其元类的实例。

2.2: Class superclass

指向父类的指针,它也是一个Class类型,如果已经是基类NSObject那么superclassnil

2.3: cache_t cache

cache是用来缓存方法的,后面会详细分析,曾经调用过的方法会缓存到这里,加速下一次的读取。

2.4: class_data_bits_t bits

用来保存类的信息,方法列表,属性列表,协议列表等等。

2.5: class_rw_t *data()

这个函数用来获取该类的可读写信息,由第4点的bit->data()调用,返回的为class_rw_t类型:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;   // 方法列表
    property_array_t properties; // 属性列表
    protocol_array_t protocols;  // 协议列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
};

可以看出方法列表,属性列表,和协议列表都在这个结构体当中,我们知道在苹果runtime中,方法、属性、协议都可以动态添加,也就是说这三个属性都是可读可写的,class_rw_t中的rw可以理解为readwrite,也隐含着这个寓意。那大家可能就会有疑问了,那成员变量呢?在runtime中只读的成员变量放到哪里面去啦?大家可以留意这个成员const class_ro_t *ro其中的ro是不是代表着readonly呢?嘻嘻,我们一起看下class_ro_t这个结构体:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;//instance对象占用的内存空间
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;//类名
    method_list_t * baseMethodList; // 方法列表
    protocol_list_t * baseProtocols; // 协议列表
    const ivar_list_t * ivars; // 成员变量列表
    
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties; // 属性列表

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

const ivar_list_t * ivars这个便是成员变量列表。而且是被const修饰的,说明是不可修改,也印证了我们之前的猜测。但是在class_ro_t中我们发现了:

method_list_t * baseMethodList; // 方法列表
protocol_list_t * baseProtocols; // 协议列表
property_list_t *baseProperties; // 属性列表

class_ro_t中:method_list_t * baseMethodListprotocol_list_t * baseProtocolsproperty_list_t > *baseProperties这三个和我们在class_rw_t中看到的method_array_t methodsproperty_array_t propertiesprotocol_array_t protocols有什么区别呢?

class_rw_t中的methods方法列表,properties属性列表都是二维数组,是可读可写的,包含类的初始内容,分类内容。
class_ro_t里面的baseMethodList,baseProtocols,ivars,baseProperties是一维数组,是只读的,包含类的初始化内容.

三:类以及类的元类是什么时候创建的

我们知道,类派生的对象是alloc的时候创建的,而类包括元类本身是什么时候创建的呢?我们通过代码探索一下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
         //  **断点位置** 
        China *object = [China alloc];
      
    }
    return 0;
}

运行工程之后我们在控制台进行打印:

(lldb) p/x China.class
(Class) $1 = 0x00000001000012f8 China
// China Class
(lldb) x/4xg 0x00000001000012f8
0x1000012f8: 0x001d8001000012d1 0x0000000100afd140
0x100001308: 0x00000001003a0260 0x0000000000000000
// isa & MASK
(lldb) p/x 0x001d8001000012d1 & 0x00007ffffffffff8
(long) $3 = 0x00000001000012d0
// China metaClass
(lldb) x/4xg 0x00000001000012d0
0x1000012d0: 0x001d800100afd0f1 0x0000000100afd0f0
0x1000012e0: 0x0000000101f004e0 0x0000000200000003

我们看到,在进行 alloc 操作之前,类和元类就已经被编译器创建出来了。接下来用MachOView打开工程的mach-o文件 :

结论:类和元类是在编译期创建的。

四:lldb对类动态调试

上面我们已经通过源码,对类的结构体有了一个认知,但是还是停留在代码层面上的,下面我们通过lldb对类的结构进行调试打印,去印证底层代码的逻辑。我们还是以前面创建的工程为例:

目标:我们要通过调试,去发现类的属性,成员变量,方法,分别都存在了哪里?带着这个目标我们往下看。

#import <Foundation/Foundation.h>
@interface China : NSObject
{
    NSString *gdp;
}
@property (nonatomic, strong) NSString *people;
@property (nonatomic, strong) NSString *area;

- (void)countPeople;
+ (void)gdpIncrease;
@end

@implementation China

- (void)countPeople{
    
}
+ (void)gdpIncrease{
    
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        China *object = [China alloc];
       //  **断点位置** 
    }
    return 0;
}

运行工程程序停留在了断点的位置,此时object对象已经创建。我们通过lldb命令打印:

(lldb) x/6xg object.class
0x100001268: 0x001d800100001241 0x0000000100afd140
0x100001278: 0x000000010122d590 0x0000000200000003
0x100001288: 0x000000010122d424 0x0000000000000000

我们先看一张表格

类成员 占用内存
isa 8
superclass 8
cache 16

isasuperclass都是指针占用8个字节,而cache是个结构体,我们通过之前了解的内存对齐原则计算出其大小为16字节

很显然0x1000012680x100001278之前的16个字节分别是isasuperclass,而0x100001278到0x100001288是cache缓存,为了避免篇幅过长,cache这块我们在下篇文章去讨论,这里详细调试bits的结构。
调试bits我们需要从类的首地址平移0x20也就是0x100001288bits的首地址,接下来打印一下:

// 这里需要把地址强转成class_data_bits_t *类型
(lldb) p (class_data_bits_t *)0x100001288
(class_data_bits_t *) $4 = 0x0000000100001288
// 根据源码的函数调用返回class_rw_t结构体
(lldb) p $4->data()
(class_rw_t *) $5 = 0x000000010122d420
// 取一下结构体的值
(lldb) p *$5
(class_rw_t) $6 = {
  flags = 2148139008
  version = 0
  ro = 0x00000001000011d8
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x00000001000010b0
        arrayAndFlag = 4294971568
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x00000001000011b0
        arrayAndFlag = 4294971824
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSDate
  demangledName = 0x0000000000000000
}
// 我们先研究下properties
(lldb) p $6.properties
(property_array_t) $8 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x00000001000011b0
      arrayAndFlag = 4294971824
    }
  }
}
// 打印下数量
(lldb) p $8.list->count
(uint32_t) $10 = 2

//看看里面都有什么
(lldb) p $8.list[1]
(property_list_t) $13 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 3970
    count = 1
    first = (name = "T@\"NSString\",&,N,V_area", attributes = "")
  }
}
(lldb) p $8.list[0]
(property_list_t) $14 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 2
    first = (name = "people", attributes = "T@\"NSString\",&,N,V_people")
  }
}
// 我们看到area和people都存在了properties中,可是gdp这个成员变量呢?我们先保留这个疑问继续往下探索

// 打印一下methods
(lldb) p $6.methods
(method_array_t) $15 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000010b0
      arrayAndFlag = 4294971568
    }
  }
}

// 看一下方法的数量
(lldb) p $15.count()
(uint32_t) $17 = 6 // 里面有6个方法

// 看下都有哪些方法
(lldb) p $15.list
(method_list_t *) $19 = 0x00000001000010b0
(lldb)  p $19->get(0)
(method_t) $20 = {
  name = "countPeople"
  types = 0x0000000100000f3a "v16@0:8"
  imp = 0x0000000100000cf0 (LMTest`-[China countPeople] at main.m:21)
}
(lldb)  p $19->get(1)
(method_t) $21 = {
  name = "setPeople:"
  types = 0x0000000100000f4a "v24@0:8@16"
  imp = 0x0000000100000d20 (LMTest`-[China setPeople:] at main.m:13)
}
(lldb)  p $19->get(2)
(method_t) $22 = {
  name = ".cxx_destruct"
  types = 0x0000000100000f3a "v16@0:8"
  imp = 0x0000000100000dc0 (LMTest`-[China .cxx_destruct] at main.m:19)
}
(lldb)  p $19->get(3)
(method_t) $23 = {
  name = "people"
  types = 0x0000000100000f42 "@16@0:8"
  imp = 0x0000000100000d00 (LMTest`-[China people] at main.m:13)
}
(lldb)  p $19->get(4)
(method_t) $24 = {
  name = "setArea:"
  types = 0x0000000100000f4a "v24@0:8@16"
  imp = 0x0000000100000d80 (LMTest`-[China setArea:] at main.m:14)
}
(lldb)  p $19->get(5)
(method_t) $25 = {
  name = "area"
  types = 0x0000000100000f42 "@16@0:8"
  imp = 0x0000000100000d60 (LMTest`-[China area] at main.m:14)
}

//我们通过上边的打印看到
1、我们自己写的对象方法
2、C++构造方法
3、是@property 自动生成的属性的get和set方法

到此为止总结一下,类的结构存储了isasuperClasscachebits,对象方法和属性都存在bits.data()中的methodsproperties,那成员变量和类方法都去哪里了?不着急我们继续探索下ro的结构

探索ro:

(lldb) p $6.ro
(const class_ro_t *) $7 = 0x00000001000011f8
(lldb) p *$7
(const class_ro_t) $8 = {
  flags = 388
  instanceStart = 8
  instanceSize = 32
  reserved = 0
  ivarLayout = 0x0000000100000edc "\x03"
  name = 0x0000000100000ed6 "China"
  baseMethodList = 0x00000001000010d0
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000100001168
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000011d0
}
// 我们来看下baseProperties都存了哪些
(lldb) p $8.baseProperties
(property_list_t *const) $9 = 0x00000001000011d0
(lldb) p *$9
(property_list_t) $10 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 2
    first = (name = "people", attributes = "T@\"NSString\",&,N,V_people")
  }
}
(lldb) p $10.get(0)
(property_t) $11 = (name = "people", attributes = "T@\"NSString\",&,N,V_people")
(lldb) p $10.get(1)
(property_t) $12 = (name = "area", attributes = "T@\"NSString\",&,N,V_area")
// 奇怪了为什么还是只有area和people呢,gdp去哪里了?
// 这个ivars看起来比较可疑,我们看一下
(lldb) p $8.ivars
(const ivar_list_t *const) $13 = 0x0000000100001168
(lldb) p *$13
(const ivar_list_t) $14 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 3
    first = {
      offset = 0x0000000100001248
      name = 0x0000000100000f24 "gdp"
      type = 0x0000000100000f51 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}
(lldb) p $14.get(0)
(ivar_t) $15 = {
  offset = 0x0000000100001248
  name = 0x0000000100000f24 "gdp"
  type = 0x0000000100000f51 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $14.get(1)
(ivar_t) $16 = {
  offset = 0x0000000100001250
  name = 0x0000000100000f28 "_people"
  type = 0x0000000100000f51 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $14.get(2)
(ivar_t) $17 = {
  offset = 0x0000000100001258
  name = 0x0000000100000f30 "_area"
  type = 0x0000000100000f51 "@\"NSString\""
  alignment_raw = 3
  size = 8
} // 原来gdp在这里,而且还有属性生成的_area,_people两个成员变量

// 我们在看下方法baseMethodList
(lldb) p $8.baseMethodList
(method_list_t *const) $18 = 0x00000001000010d0
(lldb) p *$18
(method_list_t) $19 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 6
    first = {
      name = "countPeople"
      types = 0x0000000100000f36 "v16@0:8"
      imp = 0x0000000100000cd0 (LMTest`-[China countPeople] at main.m:24)
    }
  }
}
(lldb) p $19.get(0)
(method_t) $20 = {
  name = "countPeople"
  types = 0x0000000100000f36 "v16@0:8"
  imp = 0x0000000100000cd0 (LMTest`-[China countPeople] at main.m:24)
}
(lldb) p $19.get(1)
(method_t) $21 = {
  name = "setPeople:"
  types = 0x0000000100000f46 "v24@0:8@16"
  imp = 0x0000000100000d10 (LMTest`-[China setPeople:] at main.m:13)
}
(lldb) p $19.get(2)
(method_t) $22 = {
  name = ".cxx_destruct"
  types = 0x0000000100000f36 "v16@0:8"
  imp = 0x0000000100000db0 (LMTest`-[China .cxx_destruct] at main.m:22)
}
(lldb) p $19.get(3)
(method_t) $23 = {
  name = "people"
  types = 0x0000000100000f3e "@16@0:8"
  imp = 0x0000000100000cf0 (LMTest`-[China people] at main.m:13)
}
(lldb) p $19.get(4)
(method_t) $24 = {
  name = "setArea:"
  types = 0x0000000100000f46 "v24@0:8@16"
  imp = 0x0000000100000d70 (LMTest`-[China setArea:] at main.m:14)
}
(lldb) p $19.get(5)
(method_t) $25 = {
  name = "area"
  types = 0x0000000100000f3e "@16@0:8"
  imp = 0x0000000100000d50 (LMTest`-[China area] at main.m:14)
}
// 和rw_t中的一样,那么我们的类方法呢?

研究到这里我们的属性,成员变量,对象方法都在内存里面找到了,可是类方法仍然没有头绪。类方法究竟是怎样存储的,接下来我们用 Runtime 的 API 来实际测试一下。
首先我们在测试项目里面添加一个函数:

 void testInstanceMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(gdpIncrease));
    Method method2 = class_getInstanceMethod(metaClass, @selector(gdpIncrease));
    
    NSLog(@"%p-%p",method1,method2);
    NSLog(@"%s",__func__);
}

然后在main()里面调用这个函数:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        China *object = [China alloc];
        testInstanceMethod_classToMetaclass([object class]);
    }
    return 0;
}

运行之后我们看控制台打印:

2020-01-16 20:22:15.724385+0800 LMTest[55630:25157174] 0x0-0x1000010d0
2020-01-16 20:22:15.724493+0800 LMTest[55630:25157174] testInstanceMethod_classToMetaclass

首先 testInstanceMethod_classToMetaclass 方法测试的是分别从类和元类去获取类方法的结果。由打印结果我们可以知道:
对于元类对象来说,gdpIncrease 是元类对象的实例方法,所以存在元类中。

我们把函数内部的class_getInstanceMethod改成class_getClassMethod在进行测试

 void testInstanceMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(gdpIncrease));
    Method method2 = class_getClassMethod(metaClass, @selector(gdpIncrease));
    
    NSLog(@"%p-%p",method1,method2);
    NSLog(@"%s",__func__);
}

2020-01-16 20:32:47.612936+0800 LMTest[55674:25167897] 0x1000010d0-0x1000010d0
2020-01-16 20:32:47.613052+0800 LMTest[55674:25167897] testInstanceMethod_classToMetaclass

WHAT?为什么类和元类都会有值?可是我们在类结构中并没有发现呀?
为了解开谜题我进入了class_getClassMethod这个函数内部看看实现:

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

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

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

很明显,class_getClassMethod 方法底层其实调用的是 class_getInstanceMethod,而 cls->getMeta() 方法底层就是通过判断最后返回的元类。这样就可以理解为什么调用class_getClassMethod类和元类都会有结果了。

通过lldb可以对元类的内存进行打印,最终找到类方法:
1:通过isa()找到元类
2:找到元类的ro
3:打印baseMethodsList

具体过程笔者就不在赘述。

五:总结

  • 万物皆对象,类也是对象

  • 类的本质是一个结构体

  • 属性 = getter + setter + _成员变量

  • class_rw_t 是在运行时来拓展类的一些属性、方法和协议等内容,class_ro_t 是在编译时就已经确定了的,存储的是类的成员变量、属性、方法和协议等内容

  • 类和元类的创建时机是在编译期

  • 类在 class_ro_t 结构中存储了编译时确定的属性、成员变量、方法和协议等内容

  • 实例方法存在类中而类方法存在该类的元类中

我们完成了对 iOS 中类的底层探索,可是还是有个遗留,就是类中的cache,也就是方法缓存,在下篇文章中我会和大家一起探索,感谢阅读~