类的结构分析

一、类和元类的创建时机

这里先抛出结论:类和元类是在编译期创建的,即在alloc之前,下面我们通过两种方式来验证:

1、LLDB打印:
  • 断点在int main()处:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 0x00007ffffffffff8
        Person *person = [Person alloc];
    }
    return 0;
}
  • 这个时候还没走[Person alloc];,我们来打印一下:
(lldb) p/x Person.class
(Class) $0 = 0x0000000100001100 Person // 类

(lldb) x/4gx 0x0000000100001100
0x100001100: 0x001d8001000010d9 0x0000000100b35140
0x100001110: 0x00000001003d8280 0x0000000000000000
(lldb) p/x 0x001d8001000010d9 & 0x00007ffffffffff8
(long) $1 = 0x00000001000010d8
(lldb) po $1
Person // 元类

(lldb) x/4gx 0x00000001000010d8
0x1000010d8: 0x001d800100b350f1 0x0000000100b350f0
0x1000010e8: 0x0000000101f3faf0 0x0000000200000003
(lldb) p/x 0x001d800100b350f1 & 0x00007ffffffffff8
(long) $2 = 0x0000000100b350f0
(lldb) po $2
NSObject  // 根元类

(lldb) x/4gx 0x0000000100b350f0
0x100b350f0: 0x001d800100b350f1 0x0000000100b35140
0x100b35100: 0x0000000101f400c0 0x0000000400000007
(lldb) p/x 0x001d800100b350f1 & 0x00007ffffffffff8
(long) $3 = 0x0000000100b350f0
(lldb) po $3
NSObject  // 根根元类
2、Macho工具:
  • command + B进行编译,然后将可执行文件拖入Macho中查看。
    (没图,我的Macho打不开了)

二、类结构分析

1、objc_class源码跟踪

以下源码来自objc-runtime-new.h文件中,从源码我们可以看到类的本质是结构体,继承自objc_object对象。

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();
    }
  // 省略代码
};

再继续查看objc_object:

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

两个说明:

  • OC底层实现是C语言,所以objc_object即是NSObject的表现形式。
  • isa的类型为Class的原因有三点:
    • 万物皆对象,isa是可以由Class来接受的。
    • 早期调用isa是用来返回类的,后面是通过nonpointer区分纯净isa和优化的isa
    • 源码:return (Class)(isa.bits & ISA_MASK),进行了Class类型强转。
2、objc_class分析

1)内存占用分析:

Class ISA;   :// 8字节 
Class superclass;  :// 8字节
cache_t cache; :// 16字节

前两个很好理解,我们顺着源码点进去可以看到都是结构体指针,我们看一下cache_t源码:

struct cache_t {
    struct bucket_t *_buckets;  // 8字节 注意这里是结构体指针,指针占8字节
    mask_t _mask; // 4字节 mask_t类型为uint32_t ,int占4字节
    mask_t _occupied; // 4字节 mask_t类型为uint32_t ,int占4字节
    // 省略代码
};

以上,我们可以得出前三个属性的内存偏移量是8+8+16=32字节,这里32字节是在10进制下,在16进制下则是20字节。

2)bits存储信息探索

在开始探索之前我们看一下Person文件的代码:

Person.h
@interface Person : NSObject{
    NSString *hobby;
}

@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, copy) NSString *sex;

- (void)sayHello;
+ (void)sayHappy;

@end

Person.m
@implementation LGPerson

- (void)sayHello{
    NSLog(@"Person say : Hello!!!");
}

+ (void)sayHappy{
    NSLog(@"Person say : Happy!!!");
}

NSLog处断点,通过x/4gx打印地址

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person alloc];
        Class pClass = object_getClass(person); 
        NSLog(@"-------");
}

打印结果:

(lldb) x/4gx pClass
0x100002438: 0x001d800100002411 0x0000000100b37140
0x100002448: 0x00000001003da260 0x0000000000000000

通过以上对源码的分析,我们可以知道0x001d800100002411ISA0x0000000100b37140superclass0x00000001003da260cache0x0000000000000000bits

上文已经算出前三个属性占用内存偏移为20字节,为了得到class_data_bits_t的地址,我们需要做一下地址偏移运算:0x100002438 + 0x20 = 0x100002458

  • 首先取到class_ro_t:
(lldb) x/4gx pClass
0x100002438: 0x001d800100002411 0x0000000100b37140
0x100002448: 0x00000001003da260 0x0000000000000000
(lldb) p (class_data_bits_t *)0x100002458 // 类型转换
(class_data_bits_t *) $1 = 0x0000000100002458
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000100f55d30
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148139008
  version = 0
  ro = 0x0000000100002388
  methods = {
    list_array_tt = {
       = {
        list = 0x0000000100002260
        arrayAndFlag = 4294976096
      }
    }
  }
  properties = {
    list_array_tt = {
       = {
        list = 0x0000000100002360
        arrayAndFlag = 4294976352
      }
    }
  }
  protocols = {
    list_array_tt = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
  demangledName = 0x0000000000000000
}
(lldb) p $3.ro
(const class_ro_t *) $4 = 0x0000000100002388
(lldb) p *$4
(const class_ro_t) $5 = {
  flags = 388
  instanceStart = 8
  instanceSize = 32
  reserved = 0
  ivarLayout = 0x0000000100001f81 "\x03"
  name = 0x0000000100001f78 "LGPerson"
  baseMethodList = 0x0000000100002260
  baseProtocols = 0x0000000000000000
  ivars = 0x00000001000022f8
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000100002360
}
  • 属性:我们的属性是存放在baseProperties中的:
(lldb) p $5.baseProperties
(property_list_t *const) $6 = 0x0000000100002360
(lldb) p *$6
(property_list_t) $7 = {
  entsize_list_tt = {
    entsizeAndFlags = 16
    count = 2
    first = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
  }
}

到这里我们就取到了nickName,但是count为2,这里只看到first,那我们来尝试取一下另一个属性:

(lldb) p $7.get(1)
(property_t) $13 = (name = "sex", attributes = "T@\"NSString\",C,N,V_sex")
  • 成员变量:存放在ivars中:
(lldb) p $5.ivars
(const ivar_list_t *const) $8 = 0x00000001000022f8
(lldb) p *$8
(const ivar_list_t) $9 = {
  entsize_list_tt = {
    entsizeAndFlags = 32
    count = 3
    first = {
      offset = 0x00000001000023f8
      name = 0x0000000100001e34 "hobby"
      type = 0x0000000100001f9e "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}

同样取到其他两个元素:

(lldb) p $8->get(1)
(ivar_t) $20 = {
  offset = 0x0000000100002400
  name = 0x0000000100001e3a "_nickName"
  type = 0x0000000100001f9e "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $8->get(2)
(ivar_t) $21 = {
  offset = 0x0000000100002408
  name = 0x0000000100001e44 "_sex"
  type = 0x0000000100001f9e "@\"NSString\""
  alignment_raw = 3
  size = 8
}

如上,可以看到生成的成员变量也是存在ivars中的。

  • 方法:接下来看一下baseMethodList里存放的是啥。
(lldb) p $5.baseMethodList
(method_list_t *const) $10 = 0x0000000100002260
(lldb) p *$10
(method_list_t) $11 = {
  entsize_list_tt = {
    entsizeAndFlags = 26
    count = 6
    first = {
      name = "sayHello"
      types = 0x0000000100001f83 "v16@0:8"
      imp = 0x0000000100001ad0 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
    }
  }
}

意料之中我们取到了sayHello方法,但是这里count为什么是6呢?我们来继续打印:

(lldb) p $11.get(1)
(method_t) $23 = {
  name = "sex"
  types = 0x0000000100001f8b "@16@0:8"
  imp = 0x0000000100001ba0 (LGTest`-[LGPerson sex] at LGPerson.h:18)
}
(lldb) p $11.get(2)
(method_t) $24 = {
  name = "setSex:"
  types = 0x0000000100001f93 "v24@0:8@16"
  imp = 0x0000000100001bd0 (LGTest`-[LGPerson setSex:] at LGPerson.h:18)
}
(lldb) p $11.get(3)
(method_t) $25 = {
  name = ".cxx_destruct"
  types = 0x0000000100001f83 "v16@0:8"
  imp = 0x0000000100001c10 (LGTest`-[LGPerson .cxx_destruct] at LGPerson.m:11)

一一取完发现系统生成的setter/getter方法以及.cxx_destruct析构函数都是存放在baseMethodList中的,,但是并没有看到类方法+ (void)sayHappy;。那么类方法是存在哪里呢?

在isa探索一文中我们已经对类方法的存储做了分析,类方法是存在元类中的。

这里有两种方式可以验证:
1)按照上面的方法继续用lldb来打印分析。

(lldb) x/4gx pClass
0x100002438: 0x001d800100002411 0x0000000100b37140
0x100002448: 0x00000001003da260 0x0000000000000000
// 通过isa & mask 取到 pClass的元类
(lldb) p/x 0x001d800100002411 & 0x00007ffffffffff8
(long) $29 = 0x0000000100002410
(lldb) x/4gx $29
0x100002410: 0x001d800100b370f1 0x0000000100b370f0
0x100002420: 0x0000000100f55d90 0x0000000100000003
(lldb) p (class_data_bits_t *)0x100002430
(class_data_bits_t *) $30 = 0x0000000100002430
(lldb) p $30->data()
(class_rw_t *) $32 = 0x0000000100f55cf0
(lldb) p $32->ro
(const class_ro_t *) $35 = 0x0000000100002218
(lldb) p *$35
(const class_ro_t) $36 = {
  flags = 389
  instanceStart = 40
  instanceSize = 40
  reserved = 0
  ivarLayout = 0x0000000000000000
  name = 0x0000000100001f78 "Person"
  baseMethodList = 0x00000001000021f8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000000000000
}
(lldb) p $35.baseMethodList
(method_list_t *const) $37 = 0x00000001000021f8
  Fix-it applied, fixed expression was: 
    $35->baseMethodList
(lldb) p $35->baseMethodList
(method_list_t *const) $38 = 0x00000001000021f8
(lldb) p *$38
(method_list_t) $39 = {
  entsize_list_tt = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "sayHappy"
      types = 0x0000000100001f83 "v16@0:8"
      imp = 0x0000000100001b00 (Test`+[Person sayHappy] at Person.m:17)
    }
  }
}

2)利用runtime提供的API来调用分析。

void testInstanceMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));

    Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    NSLog(@"%s",__func__);
}

输出结果:

2020-03-16 15:44:57.692317+0800 Test[85991:2268904] 0x100002268-0x0-0x0-0x100002200
2020-03-16 15:44:57.692674+0800 Test[85991:2268904] testInstanceMethod_classToMetaclass

sayHello在类中是有值的,在元类中是没有值的。
sayHappy在类中是没有值的,在元类中是有值的。

三、总结:

  • 类创建时机是在编译期。
  • 类的本质是对象(万物皆对象)。
  • class_rw_t是可以在运行时来拓展类的一些属性、方法和协议等内容。
  • class_ro_t 是在编译时就已经确定了的,存储的是类的成员变量、属性、方法和协议等内容。
  • 实例方法存在类中。
  • 类方法存在元类中。

lldb命令:

  • p/t: 二进制打印
  • p/o: 八进制打印
  • p/x: 十六进制打印
  • p/d: 十进制打印

以上
如有不当,欢迎指正,感谢。

你可能感兴趣的:(类的结构分析)