1. WWDC 2020 class optimization
First, let's take a look at Apple's 2020 WWDC video . In the video, Apple's developers mainly made the following three modifications to the underlying data structure.
On disk, in your APP binary the class looks like this:
The class object itself, which contains the most frequently accessed information, is pointers to metaclasses, superclasses, and method caches . In addition, there is a pointer to more data. The place to store additional information is called class_ro_t
, ro
Represents read-only. This structure contains information about class names, methods, protocols, and instance variables , as shown in the following figure:
When classes are first loaded from disk into memory, they start out like this, but once used, they change, but before understanding these changes, it's worth understanding what is clean memory
and what dirty memory
the difference is:
clean memory
: Refers to memory that does not change after loading.dirty memory
: Refers to memory that changes while the process is running.
class_ro_t
belongs clean memory
, since it's read-only. The class structure changes as soon as it is used dirty memory
, as the runtime writes new data to it (eg: create a new method cache and point to it from the class). dirty memory
is much clean memory
more expensive than, as long as the process is running, it has to exist, on the other hand clean memory
it can be removed, saving more memory space. Because clean memory
the system can be reloaded from disk if you need to. macOS
There is an option to swap out dirty memory
, but since it iOS
's not used swap
, it 's expensive in the middle dirty memory
. That's why this class data is split in two, the more data that can be kept clean the better, by separating out the data that never changes, most of the class data can be stored as , while this data is enough to get us started, But the runtime needs to track more information about each class. So, when a class is used for the first time, the runtime allocates additional storage capacity for it. This runtime allocated storage capacity is used for reading-writing data, as shown in the following figure:iOS
dirty memory
clean memory
class_rw_t
First Subclass
In this data structure, we store new information ( , ) that is only generated at runtime Next Sibing Class
, eg: all classes are linked into a tree-like structure. This is achieved by using First Subclass
and Next Sibing Class
pointers. This allows the runtime to traverse all classes currently in use, which is useful for making methods cacheable. But why are methods and properties both in class_ro_t(只读数据)
and in class_rw_t(读写数据)
? Because class_rw_t
can be changed at runtime, when category
is loaded, it can add new methods to the class, and programmers can add them runtime API
dynamically . Since it class_ro_t
's read-only, we need class_rw_t
to keep track of these things in . But the result of the above is that it takes up quite a bit of memory, and there are many classes in use in any given device, so how do we shrink these structures? Remember, we class_rw_t
need these things in because these things can be changed at runtime. But by examining usage on actual devices, we found that only about 100 %10
classes actually changed their methods, and only Swift
classes would use the demangled name
field, and Swift classes don't need this field unless something asks for their Objective-C
name If needed, all we can remove those parts that are not normally used, which reduces class_rw_t
the size in half, as shown in the following image:
For those classes that do need extra information, we can assign one of these extension records and slide it into the class for its use, as shown in the following image:
大约 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
探索
The breakpoint found that the object method is also called directly isMemberOfClass
,
Find the implementation of this method:
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
类
vs cls
(classes that need to be compared), return if they are the same YES
, otherwise returnNO
Summarize:
-
+ isKindOfClass
process. Class元类
vscls
(the class that needs to be compared), the difference continues to compare. Metaclass父类
vscls
, diff Continue to compare until found根元类
.根元类
vscls
, the difference continues to compare.根类(NSObject)
vscls
, if it is not the same,根类(NSObject)
the parent class of isnil
, jump out of the loop and returnNO
-
- isKindOfClass
process. Get the class to which the current object belongs,类
vscls
, the difference continues to compare. Class父类
vscls
, diff Continue to compare until found根类(NSObject)
.根类(NSObject)
vscls
, if it is not the same,根类(NSObject)
the parent class isnil
, jump out of the loop and returnNO
-
+ isMemberOfClass
process. Class元类
vscls
(classes that need to be compared), return if they are the sameYES
, otherwise returnNO
-
- isMemberOfClass
process.类
vscls
(classes that need to be compared), return if they are the sameYES
, otherwise returnNO