前言
IOS底层原理之类结构分析 对类结构
进行了整体大概流程的分析。今天我们对类结构
进行些补充,以及过程中出现的疑问进行说明,所以知识点比较散。
准备工作
WWDC 2020 - 类优化
WWDC
的重要性想必大家都知道,现在就类的优化这一块内容和大家一起分享一下,具体的链接已经给到大家,有兴趣的可以去看看
Clean Memory 和 Dirty Memory 区别
Clean Memory
clean memory
加载后不会发生改变的内存class_ro_t
就属于clean memory
,因为它是只读的不会,不会对齐内存进行修改clean memory
是可以进行移除的,从而节省更多的内存空间,因为如果你有需要clean memory
,系统可以从磁盘中重新加载
Dirty Memory
dirty memory
是指在进程运行时会发生改变的内存- 类结构一经使用就会变成
dirty memory
,因为运行时会向它写入新的数据。例如创建一个新的方法缓存并从类中指向它,初始化类相关的子类和父类 dirty memory
是类数据被分成两部分的主要原因
dirty memory
要比clean memory
昂贵的多,只要进行运行它就必须一直存在,通过分离出那些不会被改变的数据,可以把大部分的类数据存储为clean memory
,这是苹果追求的
class_rw_t
优化
当一个类首次被使用时,runtime
会为它分配额外的存储容量
,运行时分配的存储容量就是class_rw_t
。class_rw_t
用于读取-编写
数据,在这个数据结构中存储的是只有运行时
才会生成的新数据
图解如下
所有的类
都会链接成一个树状结构
这是通过firstSubclass
和nextSiblingClass
指针实现的,这样运行时会遍历当前使用的所有类
问题:为什么方法
,属性
在class_ro_t
中时,class_rw_t
还要有方法
,属性
呢?
- 因为它们可以在运行时进行更改
- 当
category
被加载时,它可以向类中添加新的方法
- 通过
runtime API
手动向类中添加属性
和方法
class_ro_t
是只读的,所以我们需要在class_rw_t
中来跟踪这些东西
问题:class_rw_t
结构在苹果手机中,占用很多的内存,那么如何去缩小这些结构呢?
- 我们在
读取—编写
部分需要这些东西,因为它们在运行时可以被修改
,但是大约10%
的类是需要修改它们的方法 - 而且只有在
swift
中才会使用这个demangledName
字段,但是swift
类并不需要这个字段,除非是访问它们Objective-C
名称时才需要。
因此我们可以拆除那些我们平时不常用的部分。图解如下
结果:这样class_rw_t
的大小会减少一半
对那些需要修改内存的,需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它滑到类中供其使用。图解如下
总结
class_rw_t
优化,其实就是对class_rw_t
不常用的部分进行了剥离。如果需要用到这部分就从扩展记录中分配一个,滑到类中供其使用。现在大家对类应该有个更清楚的认识。
类方法的探索方式
类比猜想
- 对象方法也就是实例方法是存放在类中的,那么
类
相对于元类
来说就是类对象
。在对象中的类方法
,就相当于是类对象
中的实例方法
,类对象
的实例方法应该存放在元类
中 - 在自定义的类中添加
方法名
相同的实例方法
和类方法
,编译运行不会报错。如果都是放在类中,那么编译器肯定会出问题,因为识别不了哪个是我需要的方法,我们猜想类方法
不在类中,那么可能在元类
中 - 有人可能觉着这种方法不靠谱,但是很多伟大的发现都是靠
猜想
+验证
的方式得出来的哦
通过这种类比猜想
+lldb
验证的方式。探究出原来类方法
确实是在元类
中,lldb
验证结果如下
runtime API
runtime
的方式是最暴力也是最直接的,下面提供几种runtime
验证方式
通过类的方法列表获取方法名
@interface LWPerson : NSObject
- (void)sayHello;
+ (void)sayHelloword;
@end
@implementation LWPerson
- (void)sayHello { NSLog(@"sayHello");}
+ (void)sayHelloword{NSLog(@"sayHelloword");}
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
objc_copyMethodList(LWPerson.class);
//获取元类
Class pMetaClass = object_getClass(LWPerson.class);
objc_copyMethodList(pMetaClass);
}
return 0;
}
void objc_copyMethodList(Class pClass){
unsigned int count = 0;
Method * methods = class_copyMethodList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Method const method = methods[i];
//获取方法名
NSString * key = NSStringFromSelector(method_getName(method));
NSLog(@"Method----- name: %@", key);
}
free(methods);
}
2021-06-19 21:54:10.953898+0800 testClass[10554:463640] Method----- name: sayHello
2021-06-19 21:54:10.953980+0800 testClass[10554:463640] Method----- name: sayHelloword
类中的方法是sayHello
方法,元类中的方法是sayHelloword
方法,因此类方法
是在元类
中的
方法名判断
int main(int argc, char * argv[]) {
@autoreleasepool {
objc_copyMethodList(LWPerson.class);
objc_MethodClass(LWPerson.class);
}
return 0;
}
void objc_MethodClass(Class pClass){
const char * className = class_getName(pClass);
//获取元类
Class metaClass = objc_getMetaClass(className);
//判断类中是否有sayHello方法
Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
//判断元类中是否有sayHello方法
Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));
//判断类中是否有sayHelloword方法
Method method3 = class_getInstanceMethod(pClass, @selector(sayHelloword));
//判断元类中是否有sayHelloword方法
Method method4 = class_getInstanceMethod(metaClass, @selector(sayHelloword));
NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
}
2021-06-19 22:27:48.240079+0800 testClass[11096:489432] 0x100008110-0x0-0x0-0x1000080a8
sayHello
方法是在类
中,sayHelloword
在元类
中
总结
- 实例方法在类中,
类方法
在元类
中 - 编译器自动生成
元类
,目的是存放类方法
变量及编码
成员变量和实例变量
- 在
Objective-C
中写在类声明的大括号中的变量称之为成员变量,例如int a
,NSObject *obj
- 成员变量用于类内部,无需与外界接触的变量
实例变量
- 变量的数据类型不是
基本数据
类型且是一个类
则称这个变量为实例变量,例如NSObject *obj
- 成员变量包含实例变量
成员变量和属性的区别
- 成员变量:在底层没有其他操作只是变量的声明
- 属性:系统会自动在底层添加了
_属性名
变量,同时生成setter
和getter
方法
IOS 底层原理之对象的本质&isa关联类 已经对属性和变量进行了底层的探究
编码
SEL
和IMP
关系
SEL
:方法编号IMP
:函数指针地址SEL
相当于书本的目录名称IMP
相当于书的页码
SEL
和IMP
关系图
官方类型编码
重要
:Objective-C
不支持long double
类型,@encode(long double)
返回d
,和double
类型的编码值一样(官方提供我就翻译下)
类型编码图的获取途径:打开xcode
--> command
+shift
+0
--> 搜索ivar_getTypeEncoding
--> 点击Type Encodings
代码实现类型编码
我们可以通过编译器指令@encode()
来获取一个给定类型的编码字符串,下表列举了各种类型的类型编码
NSLog(@"char --> %s",@encode(char));
NSLog(@"int --> %s",@encode(int));
NSLog(@"short --> %s",@encode(short));
NSLog(@"long --> %s",@encode(long));
NSLog(@"long long --> %s",@encode(long long));
2021-06-20 00:06:15.731450+0800 testClass[12583:564098] char --> c
2021-06-20 00:06:15.731509+0800 testClass[12583:564098] int --> i
2021-06-20 00:06:15.731537+0800 testClass[12583:564098] short --> s
2021-06-20 00:06:15.731560+0800 testClass[12583:564098] long --> q
2021-06-20 00:06:15.731581+0800 testClass[12583:564098] long long --> q
@encode()
获取一个给定类型的编码字符串,不用记,用到直接打印或者按图查找
setter
方法底层实现方式
在探究属性
和成员变量
的区别时,发现属性setter
方法有的是通过objc_setProperty
实现的,有的是直接内存偏移
获取变量地址,然后赋值
objc_setProperty
和内存偏移
首先定义一个LWPerson
类,自定义属性和成员变量,生成.cpp
文件。代码如下
@interface LWPerson : NSObject
{
NSString * newName;
NSObject * objc;
}
@property(nonatomic, copy)NSString * LWName;
@property(nonatomic,strong)NSString * LWNickname;
@property(nonatomic,assign)NSInteger age;
@end
@implementation LWPerson
@end
查看生成.cpp
文件。代码如下
LWName
属性底层是通过objc_setProperty
实现的,LWNickname
和age
是通过内存偏移实现的
setter
和getter
方法在编译期函数地址就已经确定,怎么确定是编译期呢?查看可执行文件的函数表
既然在编译期就已经确定,objc_setProperty
只能通过LLVM
源码查看,在LLVM
源码中全局搜索objc_setProperty
,然后查询重要信息
CGM.CreateRuntimeFunction(FTy, "objc_setProperty")
,创建了objc_setProperty
方法。为什么在getSetPropertyFn()
创建呢?由下层往上层推理,全局搜索getSetPropertyFn()
调用getSetPropertyFn()
的中间层是GetPropertySetFunction()
,因为需要判断是否走getSetPropertyFn()
,加一个中间层过度。全局搜索GetPropertySetFunction()
根据switch
条件PropertyImplStrategy
类型调用GetPropertySetFunction()
,PropertyImplStrategy
类型有两种GetSetProperty
或者SetPropertyAndExpressionGet
,下一步只要知道什么时候给策略赋值
copy
修饰的属性使用objc_setProperty
方式实现 。retain
修饰的属性也是使用objc_setProperty
方式实现。但是retain
一般在MRC
模式下使用,现在使用的基本是在ARC
模式,retain
这种修饰暂时忽略。通过实例再次确认
@interface LWPerson : NSObject
@property(nonatomic, copy)NSString * LWNameA;
@property(atomic, copy)NSString * LWNameB;
@property(atomic )NSString * LWNameC;
@property(nonatomic )NSString * LWNameD;
@end
@implementation LWPerson
@end
查看生成.cpp
文件。代码如下
LWNameA
和LWNameB
使用的objc_setProperty
方式实现,其它的属性通过内存偏移
实现赋值
objc_setProperty
函数的实现在哪里?objc4-818.2 全局搜索objc_setProperty
reallySetProperty
的源码实现,其原理就是新值retain
,旧值release
总结
copy
修饰的属性使用objc_setProperty
方式实现,其它属性使用内存偏移
实现- 苹果没有把所有的
setter
方法全部写在底层,因为如果底层需要维护,修改起来特别麻烦。搞了个适配器中间层,中间层的作用是供上层的setter
调用,中间层对属性的修饰符
进行判断走不同的流程,调用底层的方法实现 - 中间层的优点:
底层变化上层不受影响
,上层变化底层也不会受影响
层级关系图
getter
方法底层实现方式
在探究属性
和成员变量
的区别时,发现属性getter
方法,基本上通过内存偏移
获取变量地址获取值。
只有少数条件下才通过objc_getProperty
方式实现
objc_getProperty
和内存偏移
在LLVM
源码中全局搜索objc_getProperty
CGM.CreateRuntimeFunction(FTy, "objc_getProperty")
,创建了objc_getProperty
方法。为什么在getGetPropertyFn()
创建呢?由下层往上层推理,全局搜索getGetPropertyFn()
调用getGetPropertyFn()
的中间层是GetPropertyGetFunction()
,因为需要判断是否走getSetPropertyFn()
,加一个中间层过度。全局搜索GetPropertyGetFunction()
根据switch
条件PropertyImplStrategy
类型调用GetPropertyGetFunction()
,PropertyImplStrategy
类型是GetSetProperty
,下一步只要知道什么时候给策略赋值
copy
+atomic
修饰的属性使用objc_getProperty
方式实现 。retain
+ atomic
修饰的属性也是使用objc_getProperty
方式实现。但是retain
一般在MRC
模式下使用,现在使用的基本是在ARC
模式,retain
这种修饰暂时忽略。通过实例再次确认(但是还没找到方法调用和atomic
之间关联的地方,后面继续在找LLVM
寻找)
@interface LWPerson : NSObject
@property(nonatomic, copy)NSString * nonatomicName;
@property(atomic, copy)NSString * atomicName;
@property(nonatomic, retain)NSObject * retainStr;
@property(atomic, retain)NSObject * retainAtomicStr;
@end
@implementation LWPerson
@end
查看生成.cpp
文件。代码如下
atomicName
和retainAtomicStr
是通过objc_getProperty
方式实现,其它的属性通过内存偏移
获取值atomicName
和nonatomicName
的区别在于前者atomic
修饰的,后者是nonatomic
修饰的LLVM
中的代码显示如果你想要通过objc_getProperty
方法实现,ARC
下属性的修饰符必须是atomic
+copy
,MRC
下属性的修饰符必须是atomic
+retain
retain
修饰的属性setter
方法实现是通过objc_getProperty
方式
objc_getProperty
的底层实现
以及层级图
和objc_setProperty
的类似,在这省略。有兴趣的可以去探究下
总结
ARC
:atomic
+copy
修饰的属性使用objc_getProperty
方式实现,其它属性使用内存偏移
实现MRC
:atomic
+retain
修饰的属性使用objc_getProperty
方式实现,其它属性使用内存偏移
实现
总结
类越探究越多,越探究越细。总是有探究不完的感觉,但是探究过程中许多知识能连接起来了,豁然开朗的感觉。再接再厉,继续加油。
补充
objc_object
objc_class
对象
objc_class
继承objc_object
对象
的底层实现都是以objc_object
为模板创建的,对象
和objc_object
并不是继承
关系
面试题
通过一个经典面试题来探究下isKindOfClass
和isMemberOfClass
BOOL reg1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL reg2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL reg3 = [(id)[LWPerson class] isKindOfClass:[LWPerson class]];
BOOL reg4 = [(id)[LWPerson class] isMemberOfClass:[LWPerson class]];
NSLog(@" reg1 :%hhd reg2 :%hhd reg3 :%hhd reg4 :%hhd",reg1,reg2,reg3,reg4);
BOOL reg5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL reg6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL reg7 = [(id)[LWPerson alloc] isKindOfClass:[LWPerson class]];
BOOL reg8 = [(id)[LWPerson alloc] isMemberOfClass:[LWPerson class]];
NSLog(@" reg5 :%hhd reg6 :%hhd reg7 :%hhd reg8 :%hhd",reg5,reg6,reg7,reg8);
2021-06-20 15:41:30.839265+0800 KCObjcBuild[3949:139738] reg1 :1 reg2 :0 reg3 :0 reg4 :0
2021-06-20 15:41:30.839826+0800 KCObjcBuild[3949:139738] reg5 :1 reg6 :1 reg7 :1 reg8 :1
Xcode
中把macOS
的版本调到10.15
以下,或者iOS
的版本13.0
以下。查看下汇编代码
源码分析:isKindOfClass
和isMemberOfClass
底层的实现都是objc_msgSend
消息转发。通过SEL
找到对应的IMP
Xcode
中把macOS
的版本调到10.15
以上,或者iOS
的版本13.0
以上。查看下汇编代码
源码分析:isKindOfClass
底层的实现objc_opt_isKindOfClass
,class
底层的实现objc_opt_class
,isMemberOfClass
还是走消息转发
objc4-818.2 全局搜索isKindOfClass
、isMemberOfClass
、objc_opt_isKindOfClass
、objc_opt_class
isKindOfClass
底层实现
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
isMemberOfClass
底层实现
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
-
+ isKindOfClass
流程。 类的元类
vscls
(需要比较的类),不同继续比较 。元类的父类
vscls
,不同继续比较直到找到根元类
。根元类
vscls
,不同继续比较。根类(NSObject)
vscls
,如果还不相同则根类(NSObject)
的父类为nil
,跳出循环返回NO
-
- isKindOfClass
流程。获取当前对象所属类,类
vscls
,不同继续比较 。类的父类
vscls
,不同继续比较直到找到根类(NSObject)
。根类(NSObject)
vscls
,如果还不相同则根类(NSObject)
的父类为nil
,跳出循环返回NO
-
+ isMemberOfClass
流程。 类的元类
vscls
(需要比较的类),相同就返回YES
,否则返回NO
-
- isMemberOfClass
流程。类
vscls
(需要比较的类),相同就返回YES
,否则返回NO
objc_opt_isKindOfClass
底层实现
OBJC_EXPORT BOOL
objc_opt_isKindOfClass(id _Nullable obj, Class _Nullable cls)
OBJC_AVAILABLE(10.15, 13.0, 13.0, 6.0, 5.0);
isKindOfClass
的底层实现objc_opt_isKindOfClass
支持的版本masOS
大于10.15
,iOS
大于13.0
// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__ //现在基本上都用OBJC2版本
//slowpath(!obj) obj为空的是小概率事件基本不会发生
if (slowpath(!obj)) return NO;
// 获取类或者是元类:obj是对象就获取类,如果obj是类就获取元类
Class cls = obj->getIsa();
//fastpath(!cls->hasCustomCore()) (类或者父类中大概率没有默认的isKindOfClass方法)
if (fastpath(!cls->hasCustomCore())) {
for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
if (tcls == otherClass) return YES;
}
return NO;
}
#endif //OBJC版本直接走消息转发
return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}
基本上都用OBJC2
版本,obj->getIsa()
获取类
或者元类
:obj
是对象
就获取类
,obj
是类
就获取元类
然后就接着for
循环,for
里面的代码和isKindOfClass
逻辑一样就不进行分析了,可以直接看
isKindOfClass
的分析
objc_opt_class
底层实现
OBJC_EXPORT Class _Nullable
objc_opt_class(id _Nullable obj)
OBJC_AVAILABLE(10.15, 13.0, 13.0, 6.0, 5.0);
class
的底层实现objc_opt_class
支持的版本masOS
大于10.15
,iOS
大于13.0
// Calls [obj class]
Class
objc_opt_class(id obj)
{
#if __OBJC2__
if (slowpath(!obj)) return nil;
// 获取类或者是元类:obj是对象就获取类,如果obj是类就获取元类
Class cls = obj->getIsa();
//(类或者父类中大概率没有默认的class方法)
if (fastpath(!cls->hasCustomCore())) {
//cls是类 返回cls,如果cls是元类,obj是类,返回obj还是类
return cls->isMetaClass() ? obj : cls;
}
#endif
return ((Class(*)(id, SEL))objc_msgSend)(obj, @selector(class));
}
源码分析:objc_opt_class
的实现,其实就是获取类
,如果参数是对象
则返回类
,如果是类
就返回类
验证面试题
reg1
: 比较的是NSObject
vsNSObject
,返回1
reg2
: 比较的是根元类
vsNSObject
,返回0
reg3
: 比较的是NSObject
vsLWPerson
,返回0
reg4
: 比较的是LWPerson
的元类
vsLWPerson
,返回0
reg5
: 比较的是NSObject
vsNSObject
,返回1
reg6
: 比较的是NSObject
vsNSObject
,返回1
reg7
: 比较的是LWPerson
vsLWPerson
,返回1
reg8
: 比较的是LWPerson
vsLWPerson
,返回1
总结
+ isKindOfClass
方法:元类
-->元类的父类
-->直到找到根元类
-->根类(NSObject)
与cls
分别进行比较- isKindOfClass
方法:类
-->类的父类
-->直到找到根类(NSObject)
与cls
分别进行比较+ isMemberOfClass
方法:元类
vscls
- isMemberOfClass
方法:类
vscls
runtime API
问题
class_getClassMethod
int main(int argc, char * argv[]) {
@autoreleasepool {
obj_classToMetaclass(LWPerson.class);
}
return 0;
}
void obj_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
// - (void)sayHello;
Method method1 = class_getClassMethod(pClass, @selector(sayHello));
Method method2 = class_getClassMethod(metaClass, @selector(sayHello));
// + (void)sayHelloword;
Method method3 = class_getClassMethod(pClass, @selector(sayHelloword));
Method method4 = class_getClassMethod(metaClass, @selector(sayHelloword));
NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
}
2021-06-20 18:46:00.897204+0800 testClass[6574:263479] 0x0-0x0-0x1000080b8-0x1000080b8
源码分析:sayHello
怎么在LWPerson
类中怎么没有找到。实例方法不是在类中的吗?奇怪,是不是class_getClassMethod
的问题
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
Class getMeta() {
//如果是元类就返回
if (isMetaClassMaybeUnrealized()) return (Class)this;
//如果不是是元类,获取元类返回
else return this->ISA();
}
源码分析:class_getClassMethod
就是去元类
中,查找实例方法。打印结果sayHello
不存在元类
中,找不到。sayHelloword
在元类
中,可以找到。在底层不存在类方法,都是按普通方法进行查找。
class_getMethodImplementation
int main(int argc, char * argv[]) {
@autoreleasepool {
obj_IMPClassToMetaclass(LWPerson.class);
}
return 0;
}
void obj_IMPClassToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));
IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHelloword));
IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHelloword));
NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
}
2021-06-20 19:01:53.112778+0800 testClass[6782:274233] 0x100001a80-0x7fff69233580-0x7fff69233580-0x100001a90
源码分析:结果怎么都有函数指针地址,imp2
和imp3
是nil
才对啊。探究下class_getMethodImplementation
源码
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;
}
源码分析:当imp = nil
时,系统会返回_objc_msgForward
,所以imp2
和imp3
才有函数指针地址,而且地址相同