OC底层原理(四)、isa走向与class_data_bits_

2020-09-20  本文已影响0人  默默_David

OC底层原理汇总

上一篇中,我们对isa的初始化、类与对象的底层结构以及属性进行了简单剥析。

在这一篇中,我们将进一步探析:

我们接下来逐一进行分析

一、isa指向

1.使用lldb命令分析isa指向

我们在main.m中,首先创建一个LWPerson类继承自NSObject,然后再创建一个LWStudent继承自LWPerson,并在main函数中创建两个类的实例,在创建后打一个断点,如下图所示:

创建两个类并添加断点

上一篇中我们知道,isa.bits & ISA_MASK就可以获得isa指向的类的地址,据此,我们使用lldb来调试查找isa的走位,如下面代码所示

//16进制打印student的地址
(lldb) p/x student 
(LWStudent *) $0 = 0x0000000100546690
//根据student的内容连续打印四段,第一段就是isa.bits
(lldb) x/4gx $0
0x100546690: 0x001d8001000023d5 0x00000000003c0001
0x1005466a0: 0x0000000100001030 0x0000000000000457
//得到isa指向的类的地址
(lldb) p/x 0x001d8001000023d5 & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x00000001000023d0
//po之后,可以看到指向LWStudent
(lldb) po $1
LWStudent

//继续找LWStudent的isa指向
(lldb) x/4gx $1
0x1000023d0: 0x00000001000023a8 0x0000000100002380
0x1000023e0: 0x00000001005466f0 0x0002802c00000007
(lldb) p/x 0x00000001000023a8 & 0x00007ffffffffff8ULL
(unsigned long long) $2 = 0x00000001000023a8
//po 之后发现,也是指向LWStudent
(lldb) po $2
LWStudent

//继续查找isa指向
(lldb) x/4gx $2
0x1000023a8: 0x00007fff991e60f0 0x0000000100002358
0x1000023b8: 0x0000000100546770 0x0003e03500000007
(lldb) p/x 0x00007fff991e60f0 & 0x00007ffffffffff8ULL
(unsigned long long) $3 = 0x00007fff991e60f0
//此处发现指向NSObject
(lldb) po $3
NSObject

//继续查找NSObject的isa指向
(lldb) x/4gx $3
0x7fff991e60f0: 0x00007fff991e60f0 0x00007fff991e6118
0x7fff991e6100: 0x0000000100704fb0 0x0004e03100000007
(lldb) p/x 0x00007fff991e60f0 & 0x00007ffffffffff8ULL
(unsigned long long) $4 = 0x00007fff991e60f0
(lldb) po $4
//发现,NSObject的isa还是指向NSObject
NSObject

//我们继续查找
(lldb) x/4gx $4
0x7fff991e60f0: 0x00007fff991e60f0 0x00007fff991e6118
0x7fff991e6100: 0x0000000100704fb0 0x0004e03100000007
(lldb) p/x 0x00007fff991e60f0 & 0x00007ffffffffff8ULL
(unsigned long long) $5 = 0x00007fff991e60f0
(lldb) po $5
//发现仍然是NSObject,并且地址都是0x00007fff991e60f0
NSObject

上方的lldb指令大致流程就是从查找studentisa指向出发,不断的查找指向的指向,其大致流程图如下
[图片上传失败...(image-e690ab-1600595328194)]
从案例中我们可以发现,对象studentisa指向Student,而Studentisa也是指向的StudentStudentisa指向NSObject,而NSObjectisa指向自己。
在这里,连续出现了两个Student,这两个的地址相差40个字节,并不是同一个,为什么会出现两个不同的Student呢?

为了分析这个问题,我们使用lldb命令,新创建一个NSObject类的对象,继续查找是否会出现两个NSObject,如下所示

//创建一个NSObject的对象
(lldb) expr [NSObject alloc]
(NSObject *) $6 = 0x000000010071b990
(lldb) po $6
<NSObject: 0x10071b990>

(lldb) x/4gx $6
0x10071b990: 0x001dffff991e6119 0x0000000000000000
0x10071b9a0: 0x656b6f54534e5b2d 0x7420646c6569466e
(lldb) p/x 0x001dffff991e6119 & 0x00007ffffffffff8ULL
//找到它的指向,地址为0x00007fff991e6118
(unsigned long long) $7 = 0x00007fff991e6118
(lldb) po $7
NSObject

//继续查找NSObject的isa指向
(lldb) x/4gx $7
0x7fff991e6118: 0x00007fff991e60f0 0x0000000000000000
0x7fff991e6128: 0x0000000100630340 0x0004801000000007
(lldb) p/x 0x00007fff991e60f0 & 0x00007ffffffffff8ULL
//找到NSObject的isa指向,地址为0x00007fff991e60f0
(unsigned long long) $8 = 0x00007fff991e60f0
(lldb) po $8
NSObject

测试后可以看出,NSObject也有两个,两者的地址相差也是40个字节,其最终指向的NSObject的地址为0x00007fff991e60f0,与我们之前指令得到的最终指向NSObject的地址相同。

2.通过调用runtime API来分析isa走向

我们在main函数中编辑代码如下:

int main(int argc, const char * argv[]) {
    
    LWStudent *student = [LWStudent alloc];
    
    Class class1 = object_getClass(student);
    NSLog(@"class1 is %@",class1);
    
    Class class2 = object_getClass(class1);
    NSLog(@"class2 is %@",class2);
    
    Class class3 = object_getClass(class2);
    NSLog(@"class3 is %@",class3);
    
    Class class4 = object_getClass(class3);
    NSLog(@"class4 is %@",class4);
    
    Class class5 = object_getClass(class4);
    NSLog(@"class5 is %@",class5);
    
    return 0;
}
//打印结果
class1 is LWStudent
class2 is LWStudent
class3 is NSObject
class4 is NSObject
class5 is NSObject

结果与lldb调试得到的结果一致

3.isa指向图、类对象、元类

综合以上分析,我们得到了这张经典图


isa指向经典图

在之前的测试中,我们得到两个类分别称之为类对象与元类,类对象是元类的对象,而NSObject的元类我们称之为根元类

这时,isa的指向我们可以总结为:对象->类对象->元类->根元类->根元类根元类继承于NSObject,它的isa指向自身

4.分析类结构体struct objc_class结构

同时,根据我们之前的测试,类对象的地址比它的元类高40字节,我们根据struct objc_class的结构进行分析。

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

    class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
}

其中cache_t是一个结构体,它内部所有的成员变量如下:

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#else
#error Unknown cache mask storage type.
#endif   
    uint16_t _flags;
    uint16_t _occupied;
}

其中,uintptr_tunsigned long的别名,占8个字节mask_tuint_32_t的别名,占4个字节
另外,我们看到,cache_t内部针对不同平台使用了不同的存储结构,我们可以在objc-runtime-new.h中看到对于各个存储类型的定义

#define CACHE_MASK_STORAGE_OUTLINED 1
#define CACHE_MASK_STORAGE_HIGH_16 2
#define CACHE_MASK_STORAGE_LOW_4 3

#if defined(__arm64__) && __LP64__
//arm64架构且是64位Unix系统,也就是我们现在的iOS设备
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#elif defined(__arm64__) && !__LP64__
//arm64架构但不是64位Unix系统,iphone 5s之前的设备都是32位
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
//其它,MAC_OS设备
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif

回到我们的cache_t结构体中,无论是哪种平台,根据内存对齐规则,它需要的存储空间都是16字节

我们再看class_data_bits_t的结构,它内部的结构比较简单

struct class_data_bits_t {
    uintptr_t bits;
}

所以,class_data_bits_t bits占8字节。

综上所述,struct objc_class所占的内存如下图所示,正好是40个字节

类结构字节占用图

所以,我们得出结论,在内存中,类对象元类的存储是紧密挨在一起的。

二、class_data_bits_t

接下来我们分析class_data_bits_t,在objc781源码中,class_data_bits_t和以前有了很大的改变,现在的源码中,它只有一个成员bits

1.地址偏移

在测试class_data_bits_t之前,我们先介绍一下什么是地址偏移

有如下C语言的代码:

int a[] = {0,1,2,3,4,5,6,7,8,9};
int temp1 = a[4];
int temp2 = *(a+4);

我们知道,C语言中,数组名就是指向数组首地址的指针,我们在获取数组元素的时候,可以使用下标获取a[4],也可以通过指针偏移的方式*(a+4),因为a有被定义为了int类型,所以,加一个偏移量就是偏移一个int的字节大小,在上例中,a[4]*(a+4)获取到的值相同,都是获取数组第5个元素的值。

内存中,我们要读取内存中的值也可以使用这样的方式,由于地址是没有类型的,所以我们的偏移量需要直接设置为多少字节

如我们有了struct objc_class的指针,根据我们之前对它内部结构的分析,我们获取它指向的内容地址后,可以如下得到各个成员的地址:

下面我们根据这个思想使用lldb找到class_data_bits_t,并且分析它内部的成员。

2.lldb分析class_data_bits_t

1).设置调试案例

我们打开可编译的源码,在main.m中添加代码如下:

@interface LWPerson : NSObject{
    NSInteger a;
    NSString *b;
    CGFloat c;
}
//姓名
@property (nonatomic,copy) NSString *name;
//年龄
@property (nonatomic,assign) short age;
//性别
@property (nonatomic,assign) BOOL isMan;

@end

@implementation LWPerson

- (void)sayHello{
    NSLog(@"%s",__func__);
}
- (void)sayByeBye{
    NSLog(@"%s",__func__);
}
+ (void)eat{
    NSLog(@"%s",__func__);
}
+ (void)drink{
    NSLog(@"%s",__func__);
}

@end

int main(int argc, const char * argv[]) {
    
    LWPerson *person = [[LWPerson alloc]init];
    person.name = @"Jobs";
    person.age = 77;
    person.isMan = YES;
    
    [person sayHello];
    [person sayByeBye];
    [LWPerson eat];
    [LWPerson drink];
    
    return 0;
}
2).lldb调试查看class_rw_t结构

我们在[LWPerson drink];前添加一个断点进行调试。

//获取person的类LWPerson的地址
(lldb) p/x person.class
(Class) $0 = 0x0000000100002368 LWPerson
//地址偏移32个字节获取class_data_bits_t,16进制下就是加上20
(lldb) p (class_data_bits_t *)0x0000000100002388
(class_data_bits_t *) $1 = 0x0000000100002388
//调用class_data_bits_t的成员方法data()获取到class_rw_t的指针
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000100655680
(lldb) p *$2
//查看class_rw_t的内容
(class_rw_t) $3 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975656
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
3).class_ro_tclass_rw_t

关于class_ro_tclass_rw_t的探索过程我们以后再去探究,关于这两个结构体,我们先大致解释一下:

class_rw_t的类结构中,有如下一些成员函数:

struct class_rw_t {

public:
    //获取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;
        }
        return v.get<const class_ro_t *>();
    }
    //设置class_ro_t
    void set_ro(const class_ro_t *ro) {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            v.get<class_rw_ext_t *>()->ro = ro;
        } else {
            set_ro_or_rwe(ro);
        }
    }
    //获取方法列表
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
        }
    }
    //获取属性列表
    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>()->baseProperties};
        }
    }
    //获取协议列表
    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
        }
    }
};

根据这些暴露出来的方法,我们继续使用lldb查看class_rw_t的内容。

4).寻找实例方法

我们继续进行lldb调试,先看看有哪些方法

//获取方法列表
(lldb) p $3.methods()
(const method_array_t) $4 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000020f0
      arrayAndFlag = 4294975728
    }
  }
}
//读取实际的方法列表(list)
(lldb) p $4.list
(method_list_t *const) $5 = 0x00000001000020f0
//读取list内部的结构
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 9
    first = {
      name = "sayHello"
      types = 0x0000000100000ebb "v16@0:8"
      imp = 0x0000000100000b90 (LWObjc`-[LWPerson sayHello] at main.m:27)
    }
  }
}
//获取第一个方法,看得出来是- (void)sayHello
(lldb) p $6.get(0)
(method_t) $7 = {
  name = "sayHello"
  types = 0x0000000100000ebb "v16@0:8"
  imp = 0x0000000100000b90 (LWObjc`-[LWPerson sayHello] at main.m:27)
}
//获取第二个方法,看得出来是- (void)sayByeBye
(lldb) p $6.get(1)
(method_t) $8 = {
  name = "sayByeBye"
  types = 0x0000000100000ebb "v16@0:8"
  imp = 0x0000000100000bc0 (LWObjc`-[LWPerson sayByeBye] at main.m:30)
}
////获取第三个方法,看得出来是isMan的getter方法
(lldb) p $6.get(2)
(method_t) $9 = {
  name = "isMan"
  types = 0x0000000100000efd "c16@0:8"
  imp = 0x0000000100000c90 (LWObjc`-[LWPerson isMan] at main.m:21)
}
//获取第四个方法,看得出来是isMan的setter方法
(lldb) p $6.get(3)
(method_t) $10 = {
  name = "setIsMan:"
  types = 0x0000000100000f05 "v20@0:8c16"
  imp = 0x0000000100000cb0 (LWObjc`-[LWPerson setIsMan:] at main.m:21)
}
//获取第五个方法,看得出来是c++析构方法
(lldb) p $6.get(4)
(method_t) $11 = {
  name = ".cxx_destruct"
  types = 0x0000000100000ebb "v16@0:8"
  imp = 0x0000000100000cd0 (LWObjc`-[LWPerson .cxx_destruct] at main.m:25)
}
//获取第六个方法,看得出来是name的getter方法
(lldb) p $6.get(5)
(method_t) $12 = {
  name = "name"
  types = 0x0000000100000ed7 "@16@0:8"
  imp = 0x0000000100000bf0 (LWObjc`-[LWPerson name] at main.m:17)
}
//获取第七个方法,看得出来是name的setter方法
(lldb) p $6.get(6)
(method_t) $13 = {
  name = "setName:"
  types = 0x0000000100000edf "v24@0:8@16"
  imp = 0x0000000100000c20 (LWObjc`-[LWPerson setName:] at main.m:17)
}
//获取第八个方法,看得出来是age的getter方法
(lldb) p $6.get(7)
(method_t) $14 = {
  name = "age"
  types = 0x0000000100000eea "s16@0:8"
  imp = 0x0000000100000c50 (LWObjc`-[LWPerson age] at main.m:19)
}
//获取第九个方法,看得出来是age的setter方法
(lldb) p $6.get(8)
(method_t) $15 = {
  name = "setAge:"
  types = 0x0000000100000ef2 "v20@0:8s16"
  imp = 0x0000000100000c70 (LWObjc`-[LWPerson setAge:] at main.m:19)
}
//获取第十个方法时提示越界
(lldb) p $6.get(9)
Assertion failed: (i < count), function get, file .../runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

从结果我们可以看出,所有的实例方法和属性的settergetter方法都存放在类对象的class_rw_t的方法列表中

方法types v16@0:8的解释:
v表示方法返回值是void16表示方法所有参数一共占16字节,由于所有OC方法有两个默认的参数id selfSEL _cmd@表示对象类型,也就是id self0表示这个参数从0字节位置开始,:表示方法,也就是SEL _cmd8表示这个参数从第8个字节位置开始

5).探索属性列表

我们再看看属性列表

//获取属性列表
(lldb) p $3.properties()
(const property_array_t) $16 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x0000000100002298
      arrayAndFlag = 4294976152
    }
  }
}
//读取实际的属性列表
(lldb) p $16.list
(property_list_t *const) $17 = 0x0000000100002298
//我们在读取属性列表的时候被打断了,没有权限
(lldb) p *$17
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
6).探索成员变量列表

在前面的我们提到,成员变量在class_ro_t中,而不在class_rw_t中,以为编译结束类的结构就已经确定了,不能够再被改变。

所以,我们先从class_rw_t获取到class_ro_t,再查找ivars

//得到`class_ro_t`
(lldb) p $3.ro()
(const class_ro_t *) $19 = 0x00000001000020a8
//查看`class_ro_t`结构
(lldb) p *$19
(const class_ro_t) $20 = {
  flags = 388
  instanceStart = 8
  instanceSize = 48
  reserved = 0
  ivarLayout = 0x0000000100000e4b "\x11!"
  name = 0x0000000100000e42 "LWPerson"
  baseMethodList = 0x00000001000020f0
  baseProtocols = 0x0000000000000000
  ivars = 0x00000001000021d0
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000100002298
  _swiftMetadataInitializer_NEVER_USE = {}
}
//获得ivars
(lldb) p $20.ivars
(const ivar_list_t *const) $21 = 0x00000001000021d0
//查看ivars
(lldb) p *$21
(const ivar_list_t) $22 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 6
    first = {
      offset = 0x0000000100002310
      name = 0x0000000100000e58 "a"
      type = 0x0000000100000ec3 "q"
      alignment_raw = 3
      size = 8
    }
  }
}
//获取第一个成员变量,这个是a
(lldb) p $22.get(0)
(ivar_t) $23 = {
  offset = 0x0000000100002310
  name = 0x0000000100000e58 "a"
  type = 0x0000000100000ec3 "q"
  alignment_raw = 3
  size = 8
}
//获取第二个成员变量,这个是b
(lldb) p $22.get(1)
(ivar_t) $24 = {
  offset = 0x0000000100002318
  name = 0x0000000100000e5a "b"
  type = 0x0000000100000ec5 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
//获取第三个成员变量,这个是c
(lldb) p $22.get(2)
(ivar_t) $25 = {
  offset = 0x0000000100002320
  name = 0x0000000100000e5c "c"
  type = 0x0000000100000ed1 "d"
  alignment_raw = 3
  size = 8
}
//获取第四个成员变量,这个是_isMan
(lldb) p $22.get(3)
(ivar_t) $26 = {
  offset = 0x0000000100002328
  name = 0x0000000100000e5e "_isMan"
  type = 0x0000000100000ed3 "c"
  alignment_raw = 0
  size = 1
}
//获取第五个成员变量,这个是_age
(lldb) p $22.get(4)
(ivar_t) $27 = {
  offset = 0x0000000100002330
  name = 0x0000000100000e65 "_age"
  type = 0x0000000100000ed5 "s"
  alignment_raw = 1
  size = 2
}
//获取第六个成员变量,这个是_name
(lldb) p $22.get(5)
(ivar_t) $28 = {
  offset = 0x0000000100002338
  name = 0x0000000100000e6a "_name"
  type = 0x0000000100000ec5 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
//获取第七个报错,提示我们越界了
(lldb) p $22.get(6)
Assertion failed: (i < count), function get, file .../runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

3.类方法存储位置的探索

在上面的案例探索中,我们都没有发现类方法+ (void)eat+ (void)drink,这是为什么呢?

第一部分中,我们谈了isa指向的问题,在我们的方法查找的过程中,我们的实例方法存放在类对象中,这部分我们已经验证了。而类方法保存在元类方法列表中。

我们接下来对此进行验证

//得到LWPerson的元类
(lldb) p/x object_getClass(person.class)
(Class) $0 = 0x0000000100002340
(lldb) po $0
LWPerson
//加上32字节,16进制下就是加20,得到class_data_bits_t
(lldb) p (class_data_bits_t *)0x0000000100002360
(class_data_bits_t *) $1 = 0x0000000100002360
//获取class_rw_t
(lldb) p $1->data()
(class_rw_t *) $2 = 0x000000010111a7c0
//查看class_rw_t结构
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2684878849
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975528
  }
  firstSubclass = nil
  nextSiblingClass = 0x00007fff8b25dcd8
}
//获取方法列表
(lldb) p $3.methods()
(const method_array_t) $4 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002070
      arrayAndFlag = 4294975600
    }
  }
}
//获取实例的方法列表list
(lldb) p $4.list
(method_list_t *const) $5 = 0x0000000100002070
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "eat"
      types = 0x0000000100000ebb "v16@0:8"
      imp = 0x0000000100000b30 (KCObjc`+[LWPerson eat] at main.m:33)
    }
  }
}
//获取第一个方法,我们发现是类方法eat
(lldb) p $6.get(0)
(method_t) $7 = {
  name = "eat"
  types = 0x0000000100000ebb "v16@0:8"
  imp = 0x0000000100000b30 (KCObjc`+[LWPerson eat] at main.m:33)
}
//获取第二个方法,我们发现是类方法drink
(lldb) p $6.get(1)
(method_t) $8 = {
  name = "drink"
  types = 0x0000000100000ebb "v16@0:8"
  imp = 0x0000000100000b60 (KCObjc`+[LWPerson drink] at main.m:36)
}
//获取第三个方法时提示越界了
(lldb) p $6.get(2)
Assertion failed: (i < count), function get, file .../runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

这就是验证了类方法存放在元类方法列表

三、总结

在本篇中,我们研究了isa的走向以及对class_data_bits_t的结构进行了分析。

下一篇我们继续分析关于类结构中的cache_t的作用于内部结构与算法。

上一篇下一篇

猜你喜欢

热点阅读