前言
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
复制代码
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
条件调用GetPropertySetFunction()
,就是根据PropertyImplStrategy
策略的类型,下一步只要知道什么时候给策略赋值
copy
修饰的属性使用objc_setProperty
方式实现,现在再次确认下
@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
调用,中间层对属性的修饰符
进行判断走不同的流程,调用底层的方法实现 - 中间层的优点:
底层变化上层不受影响
,上层变化底层也不会受影响
层级关系图
总结
类越探究越多,越探究越细。总是有探究不完的感觉,但是探究过程中许多知识能连接起来了,豁然开朗的感觉。再接再厉,继续加油。
补充
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
复制代码
isKindOfClass
和isMemberOfClass
区别。objc4-818.2 全局搜索isKindOfClass
,isMemberOfClass
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
验证面试题
reg1
: 用的是+ isKindOfClass
方法,比较的是NSObject
vsNSObject
,返回1
reg2
: 用的是+ isMemberOfClass
方法,比较的是根元类
vsNSObject
,返回0
reg3
: 用的是+ isKindOfClass
方法,比较的是NSObject
vsLWPerson
,返回0
reg4
: 用的是+ isMemberOfClass
方法,比较的是LWPerson
的元类
vsLWPerson
,返回0
reg5
: 用的是- isKindOfClass
方法,比较的是NSObject
vsNSObject
,返回1
reg6
: 用的是- isMemberOfClass
方法,比较的是NSObject
vsNSObject
,返回1
reg7
: 用的是- isKindOfClass
方法,比较的是LWPerson
vsLWPerson
,返回1
reg8
: 用的是- isMemberOfClass
方法,比较的是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
才有函数指针地址,而且地址相同