一、类和元类的创建时机
这里先抛出结论:类和元类是在编译期创建的,即在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
通过以上对源码的分析,我们可以知道0x001d800100002411
是ISA
,0x0000000100b37140
是superclass
,0x00000001003da260
是cache
,0x0000000000000000
是bits
。
上文已经算出前三个属性占用内存偏移为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
: 十进制打印
以上
如有不当,欢迎指正,感谢。