一. WWDC 2020 类优化
首先,我们先来看看苹果2020年WWDC视频,视频中苹果开发者主要对底层数据结构做了如下的三个修改。
在磁盘上,在你的APP二进制文件中类是这样的,如下所示:
这个类对象本身,它包含了最常被访问的信息,就是 指向元类、超类以及方法缓存的指针,除此之外,还有一个指向更多数据的指针,存储额外信息的地方叫做class_ro_t
,ro
代表只读,这结构中,包含类名、方法、协议和实例变量 的信息,如下图所示:
当类第一次从磁盘加载到内存中时,它们一开始也是这样的,但是一经使用,它们就会发生变化,但是在了解这些变化之前,我们有必要了解一下什么是clean memory
以及dirty memory
的区别:
clean memory
:指加载后不会发生更改的内存。dirty memory
:指在进程运行时会发生更改的内存。
class_ro_t
就属于clean memory
,因为它是只读的。类结构一经使用就会变成dirty memory
,因为运行时会向它写入新的数据(例如:创建一个新的方法缓存并从类中指向它)。dirty memory
比clean memory
要昂贵得多,只要进程在运行,它就必须一直存在,另一方面clean memory
可以进行移除,从而节省更多的内存空间。因为如果你需要clean memory
,系统可以从磁盘中重新加载。macOS
可以选择换出dirty memory
,但因为iOS
不使用swap
,所以dirty memory
在iOS
中代价很大。dirty memory
是这个类数据被分成两部分的原因,可以保持清洁的数据越多越好,通过分离出那些永远不会更改的数据,可以把大部分的类数据存储为clean memory
,虽然这些数据足以让我们开始,但运行时需要追踪每个类的更多信息。所有,当一个类首次被使用,运行时会为它分配额外的存储容量,这个运行时分配的存储容量是class_rw_t
用于读取-编写数据,如下图所示:
在这个数据结构中,我们存储了只有在运行时才会生成的新信息(First Subclass
,Next Sibing Class
),例如:所有的类都会链接成一个树状结构。这是通过使用First Subclass
和Next Sibing Class
指针实现的。这允许运行时遍历当前使用的所有类,这对于使方法缓存非常有用。但为什么方法和属性既在class_ro_t(只读数据)
,又在class_rw_t(读写数据)
中呢?因为 class_rw_t
可以在运行时进行更改,当 category
被加载时,它可以向类中添加新的方法,而且程序员可以使用 runtime API
动态地添加它们。因为class_ro_t
是只读的,所以我们需要在class_rw_t
中追踪这些东西。 但是以上做法导致的结果是会占用相当多的内存,在任何给定的设备中都有许多类在使用,那么我们如何缩小这些结构呢?记住,我们在class_rw_t
中需要这些东西,因为这些东西可以在运行时进行更改。但是通过检查实际设备上的使用情况,我们发现大约只有%10
的类真正地更改了它们的方法,而且只有Swift
类会使用demangled name
字段,并且Swift类并不需要这一字段,除非有东西询问它们的Objective-C
名称时才需要,所有我们可以拆掉那些平时不用的部分,这将class_rw_t
的大小减少了一半,如下图所示:
对于那些确实需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它滑到类中供其使用,如下图所示:
大约 90%
的类从来不需要这些扩展数据,在系统范围内可节省大约14MB
的内存,这些内存可以用于更有效的用途,比如存储你的App
数据。这对于dirty memory
来说,这是真正的节省内存,现在,很多从类中获取数据的代码,必须同时处理那些有扩展数据和没有扩展数据的类,当然,运行时会为你处理这一切,并且从外部看,一切都像往常一样工作,只是使用更少的内存,之所以会这样,是因为读取这些结构的代码,都在运行时内并且还会同时进行更新,坚持使用这些API
真的很重要,因为任何视图直接访问这些数据结构的代码都将在今年的OS
版本中停止工作,如下图所示:
因为数据结构已经发生了变化,而且该代码不知道新的布局,除了你自己的代码,也要注意那些依赖的外部代码,你可能正把它们带入你的 app
中,它们可能会在你没有意识到的情况下,访问这些数据结构。这些结构中的所有信息,都可以通过官方API
获取,一些API
如下图所示:
当你使用这些API
访问信息时,无论Apple
在后台进行什么更改,这些APIS
都将继续工作,所有的API
都可以在Objective-C
运行时说明文档中找到,而这个文档在developer.apple.com
中。
总结
class_rw_t
优化,其实就是对class_rw_t
不常用的部分进行了剥离。如果需要用到这部分就从扩展记录中分配一个,滑到类中供其使用。现在大家对类应该有个更清楚的认识。
二. 类方法的探索方式
// YJPerson.h
@interface YJPerson : NSObject
- (void)sayHello;
+ (void)eat;
@end
// YJPerson.m
@implementation YJPerson
- (void)sayHello {}
+ (void)eat {}
@end
2.1 lldb 读取类方法
通过 类的底层探究上 我们知道 对象方法也就是实例方法是存放在类中的,那么 类
相对于 元类
来说就是 类对象
,由此猜想类对象的方法是不是在 元类中呢?下面我们来通过实例探索:
2.1 runtime API
读取类的方法列表
/// 输出类的方法列表
/// @param cls 类
void yj_copyMehtodList(Class cls)
{
unsigned int count = 0;
Method *methods = class_copyMethodList(cls, &count);
for (unsigned int i=0; i < count; i++) {
Method const method = methods[i];
// 获取方法名
NSString *key = NSStringFromSelector(method_getName(method));
YJLog(@"Method, name: %@", key);
}
free(methods);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
YJPerson *person = [YJPerson alloc];
YJLog(@"类的方法列表:")
// 类
Class pCls = object_getClass(person);
// 获取类的 方法列表
yj_copyMehtodList(pCls);
YJLog(@"\n元类的方法列表:")
// 元类
const char *className = class_getName(pCls);
Class metaCls = objc_getMetaClass(className);
// 获取元类的 方法列表
yj_copyMehtodList(metaCls);
}
return 0;
}
输出:
类中的方法是 sayHello
方法,元类中的方法是 eat
方法,因此 类方法
是在 元类
中的
三. 编码
3.1 官方编码说明
3.2 通过代码获取编码信息
@encode()
获取一个给定类型的编码字符串
四. setter
方法底层实现
在 OC对象本质与 isa 1.2.4 中发现属性setter
方法有的是通过objc_setProperty
实现的,有的是直接内存偏移
获取变量地址,然后赋值。这是为什么呢?下面我们通过实例来探索
在 main.m
中创建 YYJPerson
代码为:
// main.m
@interface YYJPerson : NSObject
{
NSString *yj_name;
NSObject *yj_objc;
}
@property (nonatomic, copy) NSString *c_name;
@property (nonatomic, strong) NSString *s_name;
@property (nonatomic, assign) int age;
@end
@implementation YYJPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
使用clang
命令:clang -rewrite-objc main.m -o main.cpp
生成 main.cpp
文件:
我们发现,虽然这两个属性 c_name
和 s_name
都是 NSString
类型的,但是编译后的源码,c_name
属性的 setter
方法中使用的是objc_setProperty
函数赋值,而s_name
属性的setter
方法中使用的确实指针偏移赋值,这是为什么呢?
首先,我们先来探究一下为什么会需要 objc_setProperty
这个函数,因为在你编写类的代码时,底层并不知道你会定义什么样的属性以及方法,但是属性的setter
、getter
方法又要调用底层的接口,这就形成了一个矛盾,那该如何解决这个问题呢?由于所有属性的setter
方法只有第二个参数_cmd
是不一样的,因此,就出现了objc_setProperty
函数,用来作为中间层,替换了属性setter
方法的imp
实现,这样就保证了底层代码不会因为属性的增多而增加多余的代码,如下图所示:
接着,我们还需要探究的是objc_setProperty()
调用流程,但是现在就出现了一个问题,就是我们是去那里找objc_setProperty()
这个函数的调用流程呢,如果你去objc
的源码中查找,就会发现objc
的源码中有objc_setProperty()
函数的声明以及实现,但问题是,这里是我们自定义的类中的属性的setter
方法里面调用的objc_setProperty()
,这肯定不是objc
源码调用的,而是llvm
在编译的时候调用的,但是为什么要在编译时刻如此修改呢?为什么不在运行时修改呢?因为要知道,在一个项目工程中,我们自定义的类是很多的,类中的属性也是很多的,如果我们全部交给运行时处理,效率就会很低,因此放在编译时修改属性的setter
方法的imp
是最明智的选择。现在使用VS Code
打开llvm
源码,搜索objc_setProperty
,我们找到了如下的代码:
全局搜索发现 其实是调用了 getSetPropertyFn()
这个函数,根据这个函数的名字,可以判断这个函数是用来获取 setProperty
这个函数的,因此我们需要知道在哪里调用了 getSetPropertyFn()
这个函数,然后我们全局搜索getSetPropertyFn()
,发现了以下的代码:
全局搜索发现 其实是 GetPropertySetFunction
函数调用了GetSetPropertyFn
这个函数,接着我们全局搜索GetPropertySetFunction()
,看看GetPropertySetFunction
在哪里调用了,然后发现了如下代码:
GetPropertySetFunction
函数的实际上调用的是generateObjCSetterBody
函数,在这个函数中对strategy.getKind()
函数调用后的枚举值进行比较,当枚举值为GetSetProperty
或SetPropertyAndExpressionGet
并且UseOptimizedSetter(CGM)
这个函数调用结果不为真的时候将会调用GetPropertySetFunction
函数获取到setProperty
这个函数,并在下面的函数中进行调用,因此我们需要知道strategy
的getKind
函数是如何实现的,其代码如下:
getKind()
函数实际上是获取了类PropertyImplStrategy
中的成员变量Kind
的值,因此我们再来看看成员变量Kind
是在何时进行赋值的,最后我们在PropertyImplStrategy
的构造方法中找到了其成员变量Kind
的赋值,代码如下所示:
分析上图代码,我们就可以明白,当设置属性为Copy
时,就会给Kind
赋值为GetSetProperty
,这就是objc_setProperty()
函数底层的调用流程。 尽管底层是如此实现的,但是我们还是想做一个验证,编写代码如下:
// main.m
@interface YYJPerson : NSObject
@property (nonatomic, copy) NSString *n_c_name;
@property (atomic, strong) NSString *a_c_name;
@property (nonatomic) NSString *n_name;
@property (atomic) NSString *_name;
@end
@implementation YYJPerson
@end
再次生成 main.cpp
:
可以发现,只有使用 copy
修饰的属性才会在其setter
方法中调用objc_setProperty
函数。
五. 面试题
通过一个经典面试题来探究下isKindOfClass
和isMemberOfClass
// iskindOfClass & isMemberOfClass 类方法调用
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[YJPerson class] isKindOfClass:[YJPerson class]];
BOOL re3 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re4 = [(id)[YJPerson class] isMemberOfClass:[YJPerson class]];
NSLog(@"\n re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n", re1, re2, re3, re4);
// iskindOfClass & isMemberOfClass 实例方法调用
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re6 = [(id)[YJPerson alloc] isKindOfClass:[YJPerson class]];
BOOL re7 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re8 = [(id)[YJPerson alloc] isMemberOfClass:[YJPerson class]];
NSLog(@"\n re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
输出结果:
5.1 类方法 isKindOfClass
探索
我们来探索底层实现,首先在这儿打断点
开启 Always Show Disassembly
运行项目:
通过断点调试,我们发现 isKindOfClass
底层调用的是 objc_opt_isKindOfClass
。既然知道了调用的哪个方法,我们直接找到它:
BOOL objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
// obj为空的是小概率事件基本不会发生
if (slowpath(!obj)) return NO;
// 获取 obj 的 isa
// 如果 obj 是实例对象,则 cls 就是类
// 如果 obj 是类,则 cls 就是元类
Class cls = obj->getIsa();
// fastpath(!cls->hasCustomCore()) (类或者父类中大概率没有默认的isKindOfClass方法
if (fastpath(!cls->hasCustomCore())) {
// 先用 isa 返回的 cls 去比较,相等 return yes
// 不相等 再循环获取 cls 的 superClass 去比较
for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
if (tcls == otherClass) return YES;
}
return NO;
}
#endif
// 非OBJC2版本直接走消息转发
return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}
现在用的基本都是 OBJC2
版本。obj->getIsa()
获取 类
或者元类
:obj
是对象
就获取类
,obj
是类
就获取元类
然后就接着for
循环,先用 getIisa
返回的 cls
去比较,相等就 return yes;不相等循环 获取 cls
的 superClass
去比较
objc_opt_class
底层实现:
Class objc_opt_class(id obj)
{
#if __OBJC2__
// obj为空的是小概率事件基本不会发生
if (slowpath(!obj)) return nil;
Class cls = obj->getIsa();
// 类或者父类中大概率没有默认的class方法
if (fastpath(!cls->hasCustomCore())) {
// 如果 cls 是元类就返回 obj,非元类就返回 cls
return cls->isMetaClass() ? obj : cls;
}
#endif
return ((Class(*)(id, SEL))objc_msgSend)(obj, @selector(class));
}
objc_opt_class
其实就是获取 类
,如果参数是 对象
则返回 类
,如果是 类
就返回类
5.2 对象方法 isKindOfClass
探索
断点发现,对象方法 isKindOfClass
在底层也是调用的 objc_opt_isKindOfClass
这里就不重复分析了,参考 5.1
5.3 类方法 isMemberOfClass
探索
打断点:
运行项目:
发现这里是直接调用了 isMemberOfClass
,找到这个方法实现:
+ (BOOL)isMemberOfClass:(Class)cls {
// 获取自己的 isa,这里是类方法,isa 返回的肯定是元类
// 用自己的元类 和 cls 比较
return self->ISA() == cls;
}
直接获取元类
vs cls
(需要比较的类),相同就返回YES
,否则返回NO
5.4 对象方法 isMemberOfClass
探索
断点发现对象方法也是直接调用了 isMemberOfClass
,
找到这个方法的实现:
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
类
vs cls
(需要比较的类),相同就返回YES
,否则返回NO
总结:
-
+ 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