欢迎阅读iOS底层系列(建议按顺序)
iOS底层 - alloc和init探索iOS底层 - 类的本质分析 iOS底层 - cache_t流程分析 iOS底层 - 方法的本质和查找流程分析 iOS底层 - dyld是如何加在app的
1.本文概述
本文旨在通过
类&元类的创建时机
,类的结构及相关属性
,添加的类信息
等分析类在内存中的实际存在,并分享一些关于类的经典面试题
2.类&元类的创建时机
上文说到,对象通过isa
和类关联,同个类型的对象可以多次创建,所以对象可以有多个。那么类呢,根据开发经验,很容易得出类在内存中只有一个,那究竟要怎么实锤呢。提供验证方式:
- command + b ,通过
machoView
查看
新建一个项目,并创建一个CJPerson
类,在Products
目录下.app
结尾的文件还没有编译是报红的,
command + b
下变黑,Show In Finder
后显示包内容,把可执行文件拖入machoView
,
machoView
中显示如下
可以看到,在DATA
段的_objc_classrefs
内已经加载了CJPerson
类,并指定了内存地址,说明类的创建是在编译时期,并且只有一份。
元类也是在编译期,由系统创建的,在我的理解,元类的作用是:
1.承接isa走向
2.缓存类方法
总觉得苹果大费周章引入元类的概念,还有其他的作用,至于是什么,希望可以指导我下
2.类的本质
在main.m
下初始化一个CJPerson
对象
int main(int argc, const char * argv[]) {
@autoreleasepool {
CJPerson * person = [[CJPerson alloc]init];
NSLog(@"%@",person.class);
}
return 0;
}
用clang
编译下main.m
clang -rewrite-objc main.m -o main.cpp
打开编译后的main.cpp
文件,直接来到最后面,会看到初始化时候是调用objc_getClass
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
CJPerson * person = ((CJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((CJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJPerson"), sel_registerName("alloc")), sel_registerName("init"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_fg_3995175d2_v6g81lknk8jdwh0000gn_T_main_e6fa2f_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("class")));
}
return 0;
}
在文件内搜索objc_getClass
,看到
__OBJC_RW_DLLIMPORT struct objc_class *objc_getClass(const char *);
再搜索objc_class
,找到重定义
typedef struct objc_class *Class;
原来类是objc_class
重定义的,感觉这个objc_class
很熟悉,好像在objc源码里面有看到过,那就搜索试试
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;
};
-
objc_class结构体内
ISA
是被注释的,不代表结构体内不包含它,因为objc_class
继承objc_object
,所以ISA
是来自于父类 -
同样使用
clang
编译下NSObject
可以得到objc_object
-
isa
的结构是isa_t
,可是这里使用class
接收是可以的,内部通过shift_class
得到class
总结:类的真正类型为objc_class,继承于objc_object(也就是NSObject的底层结构),说明万物皆对象,类也是一个对象,CJPerson严格来说是类对象
3.类的数据结构
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
}
根据objc_class
结构体可以清楚看到,类有4个属性,分别是isa
,superclass
,cache
,bits
1.Class isa
isa
在(iOS底层-包罗万象的isa)中已经分析过了,占有8个字节,只是这里的isa是指向元类
2.Class superclass
superclass
就是指向的父类
typedef struct objc_class *Class;
superclass
的结构同样是objc_class
,因为它是结构体指针
,占有8个字节
3.cache_t cache
cache
是方法缓存,方法缓存涉及到方法查找流程
,缓存策略
,动态扩容
等,下一章详细说明,先看下cache_t
和bucket_t
结构
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
struct bucket_t {
private:
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
...
}
-
buckets
是装方法的桶子,里面放着方式实现imp
,根据方法编号sel
生成的key
-
mask
是扩容因子,决定扩容的时机 -
occupied
是当前占用的容量 -
bucket_t
是结构体指针,占用8个字节,mask_t
就是int
,占用4个字节,所以mask
和occupied
各占4个字节 -
cache_t
是个结构体,结构体大小是内部所有大小的和,所以cache_t
占有16个字节
4.class_data_bits_t bits
bits
是数据存放的地方,bits
里面有data
,data
是从macho
里面读取的class_rw_t
class_rw_t *data() {
return bits.data();
}
struct class_rw_t {
...
const class_ro_t *ro;
method_array_t methods;//方法列表
property_array_t properties;//属性列表
protocol_array_t protocols;//协议列表
...
}
class_rw_t
有方法列表,属性列表,协议列表等等
class_rw_t
里面有个class_ro_t
struct class_ro_t {
..
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;
}
};
class_ro_t
也有方法列表,属性列表,协议列表等,但是多了ivar_list_t实例变量列表。下面探索下两者等区别
4.类添加的属性方法
类中添加实例变量sex
,属性age
,对象方法work
,类方法play
@interface CJPerson : NSObject{
NSString * sex;
}
@property (nonatomic , copy , readonly) NSString * age;
- (void)work;
+ (void)play;
object_getClass
输出下类对象
CJPerson * person = [CJPerson alloc];
Class cls = object_getClass(person);
NSLog(@"%@",cls);
x/4gx
打印cls
的内存地址如下:
通过类的数据结构分析,bits
位于ISA,superclass,cache
之后,这三者分别是8,8,16字节,根据内存偏移,bits
在isa
地址后的32字节,
0x100001218 + 32字节 = 0x100001238
po
和p
直接打印都不行,那么需要用class_data_bits_t
类型强转打印
取出bits
内的class_rw_t
class_rw_t的methods
看到methods
中有3个方法,分别输出下
-
.cxx_destruct 系统添加c++的析构方法
-
work 对象方法
-
age 属性生成的age方法
-
method_t中的types,在苹果开发者文档中有详细说明
结论:有对象方法work
,居然没有类方法play
class_rw_t的properties
如法炮制,输出下属性列表properties
结论:符合预期,只有一个添加的age
属性
那继续看下class_ro_t
class_ro_t的ivar_list_t
结论:ivar_list_t
中有添加的实例变量sex
,还有一个_age
,这也符合常规认知,属性在底层会生成带下划线的实例变量
class_ro_t的baseMethodList
结论:和class_rw_t
中的methods
一致
class_ro_t的baseProperties
结论:和class_rw_t
中的properties
一致
搜索疑点
目前添加的实例变量,属性,对象方法都在内存中寻找到了,唯独缺少类方法,不过根据经验可以知道,类方法是缓存在元类
中,那尝试去元类
搜索
通过isa_mask
找到元类,然后一步步找到baseMethodList
,果然在其中找到类方法play
结论
- 成员变量存放在
class_ro_t
中的ivar_list_t
- 属性在
class_rw_t
中的property_array_t
和class_ro_t
中的的property_list_t
都存着一份,并且会生成实例变量,和对应的方法 - 方法在
class_rw_t
中的method_array_t
和class_ro_t
中的的method_list_t
都存着一份 - 对象方法存放在
类
里面 - 类方法存放在
元类
里面
class_ro_t和class_rw_t内容大部分相同的原因:
class_ro_t
存储了当前类在编译期就已经确定的属性
、方法
以及遵循的协议,``class_rw_t
是在运行时才确定,它会先将class_ro_t
的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。class_rw_t
是class_ro_t
的超集。
5.类的面试题
这是一道比较经典的面试题,CJPerson
继承NSObject
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; //
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; //
BOOL re3 = [(id)[CJPerson class] isKindOfClass:[CJPerson class]]; //
BOOL re4 = [(id)[CJPerson class] isMemberOfClass:[CJPerson class]]; //
NSLog(@"%hhd%hhd%hhd%hhd",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; //
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; //
BOOL re7 = [(id)[CJPerson alloc] isKindOfClass:[CJPerson class]]; //
BOOL re8 = [(id)[CJPerson alloc] isMemberOfClass:[CJPerson class]]; //
NSLog(@"%hhd%hhd%hhd%hhd",re5,re6,re7,re8);
输出为**1000,**1111
re1是NSObject调用类方法isKindOfClass
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
递归查找NSObject的元类是否存在,存在就继续找元类的父类,直到根元类指向NSObject的时候,返回YES
re2是NSObject调用类方法isMemberOfClass
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
只对比一次,NSObject的元类是否等于NSObject,返回NO
re3是CJPerson调用类方法isKindOfClass
第一步就到CJPerson元类,然后一直递归下去,不会回到CJPerson类,返回NO
re4是CJPerson调用类方法isMemberOfClass
只对比一次CJPerson元类和CJPerson类,返回NO
re5是NSObject对象调用对象方法isKindOfClass
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
递归查找对象的父类是否等于NSObject,总有一次等于NSObject,返回YES
re6是NSObject对象调用对象方法isMemberOfClass
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
只对比一次,NSObject对象的类是否等于NSObject,刚好继承关系满足
re7是CJPerson对象调用对象方法isKindOfClass
递归查找CJPerson对象的类是否等于CJPerson,第一次就满足继承关系
re8是CJPerson对象调用对象方法isMemberOfClass
只对比一次,CJPerson对象的类是否等于CJPerson,刚好继承关系满足
写在最后
以上就是关于类的探索,下一章是cache_t
流程分析引出消息发送流程,后续继续更新类的底层结构,block,锁,多线程等底层探索,还有应用程序加载,启动优化,内存优化等相关知识点,敬请关注。