iOS底层 -- 类的本质分析
2020/7/3 23:27:16
本文主要是介绍iOS底层 -- 类的本质分析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
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的method_list_t
结论:和class_rw_t
中的method_array_t
一致
class_ro_t的method_list_t
结论:和class_rw_t
中的property_array_t
一致
搜索疑点
目前添加的实例变量,属性,对象方法都在内存中寻找到了,唯独缺少类方法,不过根据经验可以知道,类方法是缓存在元类
中,那尝试去元类
搜索
通过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,锁,多线程等底层探索,还有应用程序加载,启动优化,内存优化等相关知识点,敬请关注。
这篇关于iOS底层 -- 类的本质分析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值