WWDC关于Runtime的优化
在WWDC2020大会上,苹果公告了对于Runtime的优化(https://developer.apple.com/videos/play/wwdc2020/10163/),本次优化在API层面未作改动,其优化的是隐藏在API后面的内部数据结构。
本次优化主要由三个方面的改动:
第一个改动是类的运行时数据结构的改变;
第二个改动是OC方法列表的变化;
第三改动是tagged pointer格式的变化。
这些优化可以极大地减少应用程序在运行的时候的内存占用,进而提升了应用程序的运行速度。
我在之前的文章中,是以objc4-756.2源码为研究对象进行研究的,而现在比较新的源码是objc4-818.2,这两份源码中的类的结构是不一样的,接下来也是以objc4-818.2源码为研究对象来研究一下类的成员变量、属性、方法等的存储。
类内存中的ro数据
如上图所示,断点后,我执行了三个lldb指令,作用分别是打印类的地址、获取到bits、获取到rw。
接下来打印rw的结构:
好像看不太懂诶,接下来咱们看一下class_rw_t的结构:
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
......
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
}
return v.get<const class_ro_t *>(&ro_or_rw_ext);
}
void set_ro(const class_ro_t *ro) {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro = ro;
} else {
set_ro_or_rwe(ro);
}
}
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
}
可以看到,rw中可以通过ro()、methods()、properties()、protocols()等方法获取到ro、methods、properties、protocols等数据。
接下来我们就实际操演一遍,来获取ro中的成员变量。
现在获取到ro了,接下来拿到其中的ivars:
这里是通过get()函数来获取成员变量数组中的各个元素。
方法的编码
现在有这样一个类:
我将其通过clang指令编译成c++源码,如下:
可以看到,属性在CPP底层都会被转换成成员变量。
在C++源码中,我们也可以看到这样的东西:
红框内的这些都叫做类型编码,关于类型编码,我在Runtime——消息转发流程中有过介绍,这里就不赘述了。
下面我总结了各种类型下的编码,可以直接拷贝运行:
#pragma mark - 各种类型编码
void lgTypes(void){
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));
NSLog(@"unsigned char --> %s",@encode(unsigned char));
NSLog(@"unsigned int --> %s",@encode(unsigned int));
NSLog(@"unsigned short --> %s",@encode(unsigned short));
NSLog(@"unsigned long --> %s",@encode(unsigned long long));
NSLog(@"float --> %s",@encode(float));
NSLog(@"bool --> %s",@encode(bool));
NSLog(@"void --> %s",@encode(void));
NSLog(@"char * --> %s",@encode(char *));
NSLog(@"id --> %s",@encode(id));
NSLog(@"Class --> %s",@encode(Class));
NSLog(@"SEL --> %s",@encode(SEL));
// 数组
int array[] = {1,2,3};
NSLog(@"int[] --> %s",@encode(typeof(array)));
// 结构体
typedef struct person{
char *name;
int age;
}Person;
NSLog(@"struct --> %s",@encode(Person));
// 联合体
typedef union union_type{
char *name;
int a;
}Union;
NSLog(@"union --> %s",@encode(Union));
int a = 2;
int *b = {&a};
NSLog(@"int[] --> %s",@encode(typeof(b)));
}
接下来咱来解释下红框内的每一个编码都代表什么意思:
第一个,nickName,是属性nickName的getter方法,其类型编码是@16@0:8。
第1个@指的是返回值是一个对象;
第2个16指的是总共所占用的内存;
第3个@代表的是第一个参数self是一个对象(每一个方法都有两个隐藏参数self和cmd);
第4个0代表的是从第0号位置开始;
第5个:代表的是第二个参数_cmd是一个SEL;
第6个8代表的是从第8位的位置开始。
接下来再来分析一下setNickName:,其类型编码是:v24@0:8@16。
第1个v指的是void,没有返回值;
第2个24指的是总共所占用的内存;
第3个@代表的是第一个参数self是一个对象(每一个方法都有两个隐藏参数self和cmd);
第4个0代表的是从第0号位置开始;
第5个:代表的是第二个参数_cmd是一个SEL;
第6个8代表的是从第8位的位置开始;
第7个@代表的是第三个参数,即传入的字符串是一个对象;
第8个16代表的是从第16位的位置开始。
setter方法的底层原理
如上图所示,在c++源码中,还可以看到,有些属性(nickName)的set方法是调用了objc_setProperty函数,而有些(name、aname)是直接通过内存偏移进行的赋值,这是为啥呢?
我们知道,每一个属性都会有一个对应的setter方法,比如属性name的setter方法是setName,属性age的setter方法是setAge。所有的setter方法的本质都是给某一块内存区域赋值,我们现在要讨论的就是如何找到对应的内存区域并且给其赋值。
现在我们知道,有些上层setter方法都会重定向调用objc_setProperty函数,将对应的ivar信息传递进去,该函数内部会有对应属性赋值的底层代码实现。那么objc_setProperty函数是怎么被调用的呢,接下来我们就来研究一下。
首先我们打开LLVM工程源码,搜索objc_setProperty
可以看到,在getSetPropertyFn函数中调用CreateRuntimeDunction函数生成objc_setProperty函数。
然后搜索getSetPropertyFn
发现是在GetPropertySetFunction中调用了getSetPropertyFn
然后搜索GetPropertySetFunction
......
可以看到,在GetSetProperty或者SetPropertyAndExpressionGet这两个case下才会调用GetPropertySetFunction函数,这说明GetPropertySetFunction函数的调用是跟PropertyImplStrategy枚举取值有关的。
接下来我们就来搜索探究下在哪里给PropertyImplStrategy枚举赋值。
搜索PropertyImplStrategy,首先看到PropertyImplStrategy可以有如下取值:
然后我们在看看其初始化赋值的地方:
可以看到,当采取copy内存管理策略的时候,就会给kind赋值为GetSetProprty,此时就会最终调用到objc_setProperty函数。
接下来我们验证一下:
clang指令编译成C++源码 :
可以看到,nickName和acnickName(采取了copy内存管理策略)的setter方法最终是调用了objc_setProperty函数,而nnickName和anickName的setter(采用了默认的strong内存管理策略)方法最终是通过位移的方式进行直接赋值的。
到这里,不知道大家有没有一个疑问,就是为啥必须要用LLVM源码进行研究呢?为什么不能直接在objc源码中进行研究?
通过上面的分析我们已经了解到,采取copy策略的setter方法在C++底层源码中都会被重定向到objc_setProperty函数,而这个重定向的过程就是在编译的时候进行的;在程序运行时,name属性(采取Copy内存管理策略)的设置方法会通过setName这个SEL找到对应的IMP,而这个对应的IMP就会被重定向到objc_setProperty函数中。
通过LLDB指令获取类方法
class_getClassMethod
看下面这个例子:
@interface LGPerson : NSObject
- (void)sayHello;
+ (void)sayHappy;
@end
// 执行如下代码:
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getClassMethod(pClass, @selector(sayHello));
Method method2 = class_getClassMethod(metaClass, @selector(sayHello));
Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
Method method4 = class_getClassMethod(metaClass, @selector(sayHappy)); // ?
NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
打印结果如下:
sayHello是实例方法,因此通过class_getClassMethod来查找肯定是找不到的,所以前两个都是0x0没有问题;
sayHappy是类方法,通过class_getClassMethod(pClass, @selector(sayHappy))能找到对应的方法,所以第三个打印0x1000022a0也没啥问题;
问题点就在于第四个,sayHappy是LGPerson的类方法,class_getClassMethod(metaClass, @selector(sayHappy))表示的是在LGPerson的原类中查找类方法,这怎么找到了呢?并且为啥找到的跟第三个打印(在LGPerson的类中查找类方法)是一模一样的呢?想要弄明白这个问题,就得看一下class_getClassMethod的源码了:
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
再来看cls->getMeta():
Class getMeta() {
if (isMetaClass()) return (Class)this;
else return this->ISA();
}
重点来了,当cls是元类的时候,cls->getMeta()返回自身(注意,并不是返回根元类哦!)
因此,上面的method3和method4打印是一样的。
class_getMethodImplementation
再来看这么一段代码:
/*
- (void)sayHello;
+ (void)sayHappy;
*/
LVPerson *person = [LVPerson alloc];
Class pClass = object_getClass(person);
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(sayHappy));
IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));
NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
打印结果如下:
按道理,imp1和imp4是有值的,这毋庸置疑;而imp2和imp3不应该有值啊,但是这里却有值,而且imp2和imp3的值还是一样的,这是为什么呢?
我们接下来看一下class_getMethodImplementation的源码实现:
可以看到,如果没有找到对应的IMP的话,就会直接返回_objc_msgForward函数。也就是说,这里的imp2和imp3打印出来的都是_objc_msgForward函数的地址。
以上。