本文主要内容
1、探索ivar的存储位置
2、ro、rw、rwe解析
3、类方法的存储位置
4、关于元类的解释
5、通过runtime的 API探索类的数据结构
一、探索ivar的存储位置
在上篇文章底层原理探索中我们知道了,实例对象isa指针指向类对象,类对象是一个objc_class的结构体,在这个结构体里面包含了ISA、superclass、cache、bits,我们在bits里面找到了实例方法、属性、协议
,而这些内容都存储在一个class_rw_t结构体
中。我们在LSPerson中有一个成员变量并没有在上一遍文章中看见?我们知道属性是成员变量 + set和get方法。那么成员变量存储在哪里了?我们通过objc4-838.1
源码中发现class_rw_t里面有一个ro方法
,我们按照验证实例方法的步骤来走一下?看是否在这个里面
按照分析获取实例方法的步骤,最后我们知道里面有一个
ivars
,成员变量我们也经常叫ivars,最后我们得到一个ivar_list_t的结构体
。此时我们输出ivars 此时发现一个熟悉的结构
entsize_list_tt
,entsize_list_tt 我们上一篇在分析实例方法的时候知道他是一个容器/模版,跟前一篇文章一样继续让下走 到此我们拿到了LSPerson的成员变量,也知道了类对象里面存储了成员变量的name,成员变量的type,以及成员变量的size。
总结:
1、类里面的属性,系统会自动帮我们生成_xxx的成员变量+setter+getter方法。
2、类的属性存储在类对象中
3、实例对象里面只存储属性值
复制代码
二、ro、rw、rwe解析
在上面分析的时候我们经常遇到class_ro_t、class_rw_t
等结构体,那么这几个代表什么意思了?
ro:
在编译时生成,app 在使用类时,是需要在磁盘中 app 的二进制文件中读取类的信息,二进制文件中的类存储了类的元类、父类、flags 和方法缓存,而类的额外信息(name、方法、协议和实例变量等)存储在 class_ro_t
中。class_ro_t
简称 ro
,read only
,将类从磁盘中读取到内存中就是对 ro
的赋值操作。由于 ro
是只读的,加载到内存后不会发生改变又称为 clean memory
(干净内存)。
rw:
在运行时生成的,类一经使用,就会将ro中的内容剪切到rw中。
rwe:
对应class_rw_ext_t结构体,runtime提供了动态为类添加方法和属性的api,这些方法、属性、协议存在只读不允许修改的ro中,如果想要修改方法和属性,需要把ro中的内容拷贝到rw中,这样rw中就会存在两份ro,增加内存消耗,所以苹果增加了rwe结构体来解决内存消耗问题,rwe里面存放要呗修改的内容。其中rwe产生的条件有两种:1:分类(分类和奔雷必须是懒加载类)2、使用runtime动态为类添加了属性、方法、协议。
类整体结构: 仔细查看源码,我们能发现系统在获取方法、属性、协议的时候会先判断是否存在rwe,如果rew存在就会在其中找。如果不存在就会从ro中查找。
// 在class_rw_t结构体中
const method_array_t methods() const {
auto v = get_ro_or_rwe();
// 判断是class_rw_ext_t是否存在
if (v.is<class_rw_ext_t *>()) {
// 如果存在就从class_rw_ext_t查找
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
// 如果不存在就从ro中查找
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};
}
}
复制代码
三、类方法的存储位置
在上一篇底层原理探索中探索bits里面找到了实例方法,但是没有找到类方法,那么类方法存储在哪里了?
我们知道实例对象isa指针指向类对象,类对象中保存了实例方法
,类对象isa指针指向元类,元类也是一个对象, 那么类方法是否保存在元类的bits中?我们按照上一篇探索实例方法的存储位置的流程来验证一下。
结论:
类方法存储在元类中。
四、关于元类的解释
在探索对象isa指向和获取对象类方法存储位置的时候我们知道了元类,那什么是元类?苹果为什么要设计元类呢?
苹果这样设计就是为了复用消息机制
,用同一套消息机制。 [LSPerson alloc] 在编译的时候会被编译成objc_msgSend(消息的接受者,消息的方法名)函数
。通过消息接受者的isa指针和消息的方法名找到方法的实现(imp)。如果消息接受者为实例对象,那就是通过实例对象的isa指针指向的类对象里面去找方法实现。如果消息接收者为类对象,那就是通过类对象的isa指针指向的元类中去找方法实现。
设想一下?如果没有元类了?那么类方法如何去查找了? 如果没有元类,那么在objc_msgSend里面首先需要判断消息的接受者是实例对象还是类对象,然后找实现的时候,也要去判断消息接受者的类型不同去不同的地方查找方法实现,而消息发送最重要的就是快速
,如果增加这些判断就会影响查找效率。利用当前的消息机制只通过isa指针可以很快找到方法的实现,实例对象存储成员变量的值,类对象存储实例对象的方法,元类对象存储类对象的方法,也就是单一职责的原则
,大大增加消息发送的效率,同时维护同一个消息机制(objc_msgSend函数)
也更方便。
五、通过runtime的 API探索类的数据结构
首先定义一个类LSPerson,声明如下:
@interface LSPerson : NSObject
//isa指针 // 8字节 因为是对象,有一个isa指针所以默认有8个字节
@property (strong, nonatomic) NSString *name; // 8字节
@property (strong, nonatomic) NSString *body; // 8字节
@property (assign, nonatomic) int age; // 4字节
+ (void)testClassMethod;
- (void)test;
@end
复制代码
1、获取类的成员变量
通过runtime的class_copyIvarList
函数拿到成员变量列表ivars,遍历即可,如下:
// 获取类的成员变量
- (void)getClassCopyIvarList:(Class)class {
unsigned int count = 0;
// 通过runtime获取成员变量
Ivar *ivars = class_copyIvarList(class, &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
// 获取成员变量名字
const char *cName = ivar_getName(ivar);
// 获取成员变量类型
const char *cType = ivar_getTypeEncoding(ivar);
NSLog(@"name = %s, type = %s", cName, cType);
}
free(ivars);
}
// 调用方法
[self getClassCopyIvarList:LSPerson.class
// 输出一下结果
// name = _age, type = i
// name = _name, type = @"NSString"
// name = _body, type = @"NSString"
复制代码
1、当我们声明属性的时候系统会自动帮我们生成对应的_xxx的属性和setter、getter方法。
2、type: i(int)、NSString代表成员变量的类型
复制代码
2、获取类的属性
通过runtime的class_copyPropertyList、property_getName
等获取类的属性
// 获取类的属性
- (void)getClassCopyPropertyList:(Class)class {
unsigned int count = 0;
// 通过runtime的api获取类的属性列表
objc_property_t *properies = class_copyPropertyList(class, &count);
for (int i = 0; i < count; i++) {
objc_property_t property = properies[i];
// 遍历拿到属性的名字、属性的类型定义
const char *cName = property_getName(property);
const char *cAttributes = property_getAttributes(property);
NSLog(@"name = %s, type = %s", cName, cAttributes);
}
}
//调用方法
[self getClassCopyPropertyList:LSPerson.class]
// 输出结果
// name = name, type = T@"NSString",&,N,V_name
// name = body, type = T@"NSString",&,N,V_body
// name = age, type = Ti,N,V_age
复制代码
1、获取类的属性,属性名字就是我们声明的名字
2、属性里面的type是attributed类型 显示如属性name“T@'NSString',C,N,V_name”。其中"T"代表类型,
后面加"@‘NSString’即为字符串类型,"C"代表copy,"N"代表"nonatomic","V_name"代表成员变量_name.
再如属性age”Ti,N,V_age“,"Ti"整体代表int类型,"N"代表"nonatomic","V_age"代表成员变量_age.
复制代码
属性类型编码说明官网地址:Declared property type encodings
3、获取类的方法
通过runtime的class_copyMethodList、method_getTypeEncoding
等获取方法。
// 获取方法
- (void)getClassCopyMethodList:(Class)class {
unsigned int count = 0;
// 获取方法列表
Method *methods = class_copyMethodList(class, &count);
for (int i = 0; i < count; i++) {
Method method = methods[i];
// 获取方法的SEL
SEL sel = method_getName(method);
// 获取方法的type
const char *cType = method_getTypeEncoding(method);
NSLog(@"name = %@, type = %s", NSStringFromSelector(sel), cType);
}
}
//方法调用
[self getClassCopyMethodList:LSPerson.class];
// 输出结果
// name = test, type = v16@0:8
// name = name, type = @16@0:8
// name = setName:, type = v24@0:8@16
// name = body, type = @16@0:8
// name = setBody:, type = v24@0:8@16
// name = age, type = i16@0:8
// name = setAge:, type = v20@0:8i16
// name = .cxx_destruct, type = v16@0:8
复制代码
打印属性类型解析: 显示如方法name类型“@16@0:8”.其中"@"代表返回值为对象类型,"16"代表方法参数的长度,
第2个"@"代表方法的接收者,8个长度,从0-7;":"代表方法名,8个长度,从8-15,所以总共16个长度.
再如属性setHeight“v24@0:8q16”,"v"代表返回值为void类型,"24"代表方法参数的长度,
第2个"@"代表方法的接收者,8个长度,从0-7;":"代表方法名,8个长度,从8-15;还有个long类型的参数height,8个长度,
从16-23,所以总共24个长度.
复制代码
4、获取类方法
在3中我们获取到了类的实例方法,在上面我们说实例方法是通过类对象获取,类方法是元类获取,所以只需继续使用方法3,修改class的传入,如下
// 方法调用
[self getClassCopyMethodList:objc_getMetaClass("LSPerson")];
// 输出结果
// name = testClassMethod, type = v16@0:8
复制代码
5、获取方法的实现
通过runtime的class_getMethodImplementation
获取方法的实现
// 获取方法的实现
- (void)getClassImplementation:(Class)class {
Class metaClass = objc_getMetaClass(class_getName(class));
IMP imp1 = class_getMethodImplementation(class, @selector(test));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(testClassMethod));
NSLog(@"imp1: %p, imp2: %p", imp1, imp2);
}
//输出结果
// imp1: 0x????????, imp2: 0x????????,
复制代码
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
lockdebug_assert_no_locks_locked_except({ &loadMethodLock });
imp = lookUpImpOrNilTryCache(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}
通过在源码中查看class_getMethodImplementation的方法实现我们知道当IMP找不到的时候会走_objc_msgForward
复制代码
本文总结
1、类的属性、成员变量存储在类对象中,实例对象里面存储的是值。
2、实例方法存储在类对象中,类方法存储在元类中。
3、苹果设计元类就是位了复用消息机制,在底层里面也不需要区分是类方法还是实例方法。