四:类原理之类的结构分析以及成员变量,属性,方法的探究

241 阅读9分钟

  通过之前的文字介绍,我们知道了对象是如何创建的,也知道了对象怎么通过 isa 找到类的,那么下一步我们将会对类进行进一步探索。

一:探索准备

通常我们探索OC的一些底层实现都是通过将OC转化为C++的代码,下面我们操作一下

以下为我们的测试代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        WYPerson *p = [WYPerson alloc];
        Class pClass = [p class];
        NSLog(@"%@ ---  %p",p,pClass);
    }
    return 0;
}

1.1 获取main.cpp

在MAC终端定位到 main.m 所在文件夹,输入:clang -rewrite-objc main.m

clang -rewrite-objc main.m

之后 main.m 所在文件夹会多出一个 main.cpp 的文件

1.2 查看main.cpp

打开这个 main.cpp,你会发现有10000多行的代码,我们把代码拉到最后,最终我们的测试代码会转化成如下形式

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        WYPerson *p = ((WYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("WYPerson"), sel_registerName("alloc"));
        Class pClass = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("class"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_x3_cw7r9gxj7rnfg6mvdl0tlksh0000gn_T_main_f5957c_mi_0,p,pClass);

    }
    return 0;
}

我们想看的是这个转化后的Class是怎么定义的,在main.cpp中我们发现其实Class就是一个objc_class类型

typedef struct objc_class *Class;

下面我们配合objc的源码来看下objc_class究竟为何物

二: objc_class结构源码分析

2.1 源码定义

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() { 
        return bits.data();
    }
    .............此处省略
}

2.2 成员分析

    1. isa 之前已经探究过,这里被注释说明是继承过来的,可以看到后面的objc_object,这也说明其实类也是个对象。
    1. superclass 父类指针。
    1. 类的相关缓存。
    1. class_data_bits_t 类型的bits,存储的是类的一些信息
    1. class_rw_t指针,其中的bits就是第四点中的bits

三:成员变量

通常我们会这样定义一个成员变量,那这个成员变量存在于哪里呢?

@interface WYPerson : NSObject
{
    NSString *_title;
}
@end

3.1 runtime的已知方法

我们用class_copyIvarList获取ivars并且打印出来

WYPerson *p1 = [WYPerson alloc];
        Class pClass = [p1 class];
        NSLog(@"%@ -- %p",p1,pClass);
        
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList(pClass, &count);
        for (unsigned int i = 0; i < count; i++) {
            Ivar const ivar = ivars[i];
            const char *tempIvarName = ivar_getName(ivar);
            NSString *ivarName = [NSString stringWithUTF8String:tempIvarName];
            NSLog(@"ivar ---  %@",ivarName);
        }

控制台打印出了我们定义的_title

2020-03-15 21:20:56.222653+0800 objc-test[87373:11596730] <WYPerson: 0x10182b7a0> -- 0x1000013a0
2020-03-15 21:20:56.223666+0800 objc-test[87373:11596730] ivar ---  _title

本着对知识刨根问底的精神我们不禁会想,class_copyIvarList怎么做到的呢?我们来通过源码看下。

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

其中(ivars = cls->data()->ro->ivars)非常关键,系统会去这里读取数据,cls->data()实际上是上文提到的class_rw_tro就是class_rw_tclass_ro_t 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_ro_t源码中的定义

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#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;
    ......
};

所以调用class_copyIvarList方法实则返回的就是class_ro_t中的ivars,这是一个ivar_list_t类型。

3.2 获取内存偏移

从上面的分析可以看出,只需要获取到bits,一切就将迎刃而解。 我们都知道数组指针的地址也是数组第一个元素的地址,既然如此我们获取到Class的地址,同样也是首个元素isa的地址,然后进行偏移,就能获取到class_rw_t,那偏移量是多少呢?

  • 第一个元素:isa,前面分析过,8字节。
  • 第一个元素:superclass,是个结构体指针,8字节。
  • 第一个元素:cache,是个结构体,16字节。

这里解释下16字节怎么来的。

struct cache_t {
    struct bucket_t *_buckets; // 结构体 8字节
    mask_t _mask;  // mask_t 是 uint32_t 4字节
    mask_t _occupied; // 4字节
    ......
}

第一个成员_buckets,是个指针,占8字节,第二个成员_mask和第三个成员_occupied,都是uint32_t类型,都是占4字节,总共就是16字节
总偏移量 = 8(isa) + 8(superclass) + 16(cache) = 32字节

3.3 通过内存偏移获取ivars

获取pClass(类)的地址

        WYPerson *p1 = [WYPerson alloc];
        Class pClass = [p1 class];
        NSLog(@"%@ -- %p",p1,pClass);
(lldb) p/x pClass
(Class) $0 = 0x00000001000013a0 WYPerson
(lldb)

上面我们算出要偏移32字节,将十进制32转化为十六进制就是0x20, 0x00000001000013a0 + 0x20 = 0x00000001000013A0,最终0x00000001000013A0就指向我们要找的bits。这里我们将其强转为class_data_bits_t

(lldb) p (class_data_bits_t *)0x00000001000013A0
(class_data_bits_t *) $1 = 0x00000001000013a0

得到的变量 $1 = 0x00000001000013a0 就是 bits 的地址,下一步是获取 bits.data(),其返回类型为 class_rw_t 的指针:

(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000100001378

下面我们打印出$2中的内容

(lldb) p *$2
(class_rw_t) $3 = {
  flags = 11526385
  version = 1933313
  ro = 0x0000000100afe0f0
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x000000010192a530
        arrayAndFlag = 4321355056
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x0000000100000003
        arrayAndFlag = 4294967299
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x000000010192a490
        arrayAndFlag = 4321354896
      }
    }
  }
  firstSubclass = 0x001d800100001379
  nextSiblingClass = NSObject
  demangledName = 0x000000010192a580 "\350I\332"
}

下一步取ro

(lldb) p $3.ro
(const class_ro_t *) $5 = 0x0000000100afe0f0

打印ro的内容

(lldb) p *$5
(const class_ro_t) $6 = {
  flags = 11526385
  instanceStart = 1933313
  instanceSize = 11526464
  reserved = 1
  ivarLayout = 0x00000001018061d0 "d\3439
  name = 0x0000000300000003 ""
  baseMethodList = 0x0000000101900130
  baseProtocols = 0x001d800100afe0f1
  ivars = 0x0000000100afe0f0
  weakIvarLayout = 0x00000001003a0290 ""
  baseProperties = 0x0000000000000000

其实到此为止我们已经找到ivars了,你以为结束了吗?当然......继续打印

(lldb) p $6.ivars
(const ivar_list_t *const) $8 = 0x0000000100afe0f0
(lldb) p *$8
(const ivar_list_t) $9 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 1
    first = {
      offset = 0x0000000100001308
      name = 0x0000000100000f76 "_title"
      type = 0x0000000100000fa1 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}

此时我们就看到_title了,这也就验证了类的成员变量保存在bits.data()->ro->ivars

四:方法

4.1 实例方法

定义一个方法

@interface WYPerson : NSObject
{
    NSString *_title;
}

- (void)eat;
@end

@implementation WYPerson
-(void)eat
{
    NSLog(@"eat");
}
@end

上面我们打印了ivars,里面除了ivars,还有其他的一些成员,那既然我们要找方法,不妨我们像上面一样直接打印baseMethodList

(lldb) p $8.baseMethodList
(method_list_t *const) $11 = 0x0000000100001240
(lldb) p *$11
(method_list_t) $12 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "eat"
      types = 0x0000000100000f99 "v16@0:8"
      imp = 0x0000000100000d10 (objc-test`-[WYPerson eat] at WYPerson.m:12)
    }
  }
}

一目了然,这样我们就找到了实例方法,它存储在bits.data()->ro-> baseMethodList

4.2 类方法

4.2.1 发现问题

定义一个方法

@interface WYPerson : NSObject
{
    NSString *_title;
}

- (void)eat;
+ (void)smile;
@end

@implementation WYPerson
-(void)eat
{
    NSLog(@"eat");
}
+ (void)smile{
    NSLog(@"smile");
}
@end

我们来继续打印

(lldb) p *$6
(method_list_t) $7 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "eat"
      types = 0x0000000100000f86 "v16@0:8"
      imp = 0x0000000100000be0 (objc-test`-[WYPerson eat] at WYPerson.m:12)
    }
  }
}
(lldb) p $7.get(0)
(method_t) $8 = {
  name = "eat"
  types = 0x0000000100000f86 "v16@0:8"
  imp = 0x0000000100000be0 (objc-test`-[WYPerson eat] at WYPerson.m:12)
}
(lldb) p $7.get(1)
(method_t) $9 = {
  name = ".cxx_destruct"
  types = 0x0000000100000f86 "v16@0:8"
  imp = 0x0000000100000cb0 (objc-test`-[WYPerson .cxx_destruct] at WYPerson.m:11)
}
(lldb) p $7.get(2)
Assertion failed: (i < count), function get, file /Users/wangyun/Desktop/iOS底层研究代码/OC对象原理1/资料/objc4-750/runtime/objc-runtime-new.h, line 117.
error: Execution was interrupted, reason: signal SIGABRT.

我们打印出了所有的方法,发现有·eat方法,以及系统在底层添加的.cxx_destruct方法,唯独没有我们的smile。怎么办?
此时你会不会这样想:既然对象的属性和方法存储在类里面,那么作为类,它也是个对象,那么它的方法会不会存在于元类当中呢?

4.2.2 验证猜想

我们先获取到元类

(lldb) x/4xg pClass
0x100001498: 0x001d800100001471 0x0000000100afe140
0x1000014a8: 0x0000000101862840 0x0000000100000003
(lldb) p/x 0x001d800100001471 & 0x00007ffffffffff8
(long) $1 = 0x0000000100001470

然后去获取元类的baseMethodList

(lldb) p $6.baseMethodList
(method_list_t *const) $7 = 0x0000000100001228
(lldb) p *$7
(method_list_t) $8 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "smile"
      types = 0x0000000100000f86 "v16@0:8"
      imp = 0x0000000100000c10 (objc-test`+[WYPerson smile] at WYPerson.m:17)
    }
  }
}

皇天不负有心人,我们在 元类baseMethodList里面找到了smile的方法,而且我们发现不同于类,元类中系统底层不会额外添加方法。

五:属性

5.1 @property的本质

@interface WYPerson : NSObject
{
    NSString *_title;
}
@property(nonatomic,copy) NSString * name;

- (void)eat;
@end

@property(nonatomic,copy) NSString * name;这行代码会做什么事情呢?

    1. 生成setter方法。
    1. 生成getter方法。
    1. 生成带下划线的成员变量,也就是_name

5.2 验证

按照上面的指令打印

(lldb) p $5.ivars
(const ivar_list_t *const) $6 = 0x00000001000012b8
(lldb) p *$6
(const ivar_list_t) $7 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x0000000100001380
      name = 0x0000000100000f61 "_title"
      type = 0x0000000100000fa5 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}

没有_name,但是发现返回ivar_list_t类型,它继承自entsize_list_tt,我们可以调用get(),传递索引来获取.

(lldb) p $7.get(0)
(ivar_t) $8 = {
  offset = 0x0000000100001380
  name = 0x0000000100000f61 "_title"
  type = 0x0000000100000fa5 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $7.get(1)
(ivar_t) $9 = {
  offset = 0x0000000100001388
  name = 0x0000000100000f68 "_name"
  type = 0x0000000100000fa5 "@\"NSString\""
  alignment_raw = 3
  size = 8
}

到这里我们发现的确生成了成员变量_name.

(lldb) p $11.get(2)
(method_t) $14 = {
  name = "name"
  types = 0x0000000100000f92 "@16@0:8"
  imp = 0x0000000100000c50 (objc-test`-[WYPerson name] at WYPerson.h:16)
}
(lldb) p $11.get(3)
(method_t) $15 = {
  name = "setName:"
  types = 0x0000000100000f9a "v24@0:8@16"
  imp = 0x0000000100000c80 (objc-test`-[WYPerson setName:] at WYPerson.h:16)
}

到这里也验证了的确生成了settergetter方法。

5.3 属性与成员变量对比

此时我们不妨再来打印下baseProperties

(lldb)  p $5.baseProperties
(property_list_t *const) $16 = 0x0000000100001300
(lldb) p *$16
(property_list_t) $17 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
  }
}

由此我们可以推断,当添加完属性,除了生成setter方法,getter方法,_name成员变量之外,好会将属性保存在class_ro_t中。而添加的成员变量不会。

六: 总结

  • 成员变量:保存在类的 ivars 中:objc_class->bits.data()->ro->ivars
  • 属性:自动添加成员变量保存在ivars中:objc_class->bits.data()->ro->ivars
  • gettersetter 方法保存在baseMethodList中:objc_class->bits.data()->ro-> baseMethodList
  • 属性信息还另外单独保存在baseProperties中:objc_class->bits.data()->ro-> baseProperties
  • 实例方法:保存在类的baseMethodList中:objc_class->bits.data()->ro-> baseMethodList
  • 类方法:保存在元类的baseMethodList中:metaclass->bits.data()->ro-> baseMethodList