这是我参与更文挑战的第7天,活动详情查看: 更文挑战
在上一篇文章中我们通过
lldb分析来获取了类的属性和对象方法,却没有找到成员变量和类方法的存储位置;今天我们先来说一下成员变量的问题,既然属性和成员变量的存储没有在一起,那么说明属性和成员变量是不一样的,那么他们有什么区别呢?
成员变量和属性
我们将上一篇文章中的Person类修改如下:
@interface Person : NSObject{
NSString *nickName;
NSObject *objc;
}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) NSString *realName;
@property (nonatomic, assign) NSInteger age;
- (void)run;
+ (void)eat;
@end
@implementation Person
- (void)run {
}
+ (void)eat {
}
@end
在Person中存在:
- 属性:
name、realName和age - 成员变量:
nickName - 实例变量:
objc,实例变量是一种特殊的成员变量(以对象为类型) - 方法:
- (void)run和+ (void)eat
我们使用xcrun命令来生成Person的cpp文件类分析一下:
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc Person.m -o Person.cpp
我们在cpp文件中找到类Person实现的地方:
我们可以发现,在cpp文件中所有的属性都被注释掉了,生成对应的带_的成员变量,并且生成了对应的setter和getter方法
属性=成员变量+setter+getter
除此之外,我们还在cpp文件中发现类如下代码:
那么v16@0:8和@16@0:8这样的字符串我们称为编码(Apple自带的),那么这些编码在什么地方呢?我们在工程中使用快捷键command + shift +0打开如下窗口:
搜索ivar_getTypeEncoding:
打开如图所示链接:
以下代码,大家可自行打印输出,查看结果:
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)));
接下来我们以@16@0:8为例解释一下编码的含义:
@: 是一个id类型,因为getter方法是有返回值的16: 整串编码所占用的内存@: 参数id, id self 默认第一个参数0: 从0号位置开始:: 表示SEL,SEL _cmd 默认第二个参数8: 表示从8号位置开始
还记得我们生成的cpp文件中,getter和setter方法:
static NSString * _Nonnull _I_Person_name(Person * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_name)); }
static void _I_Person_setName_(Person * self, SEL _cmd, NSString * _Nonnull name) { (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_name)) = name; }
static NSString * _Nonnull _I_Person_realName(Person * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_realName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_Person_setRealName_(Person * self, SEL _cmd, NSString * _Nonnull realName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _realName), (id)realName, 0, 1); }
static NSInteger _I_Person_age(Person * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_Person$_age)); }
static void _I_Person_setAge_(Person * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_Person$_age)) = age; }
这里面分为了两种赋值方式:
objc_setProperty((char *)self + OBJC_IVAR_$_Person$_name)) = name内存平移赋值
接下来我们结合llvm来分析一下这两种赋值方式的区别:
objc_setProperty分析
LLVM源码分析
我们知道,setter方法的本质是对内存进行赋值,我们可以给一个类设置任何属性,但是最终底层的代码实现是不变的,那么就需要有一个封装层来动态处理这些属性赋值的过程,objc_setProperty方法我们可以看做是对这个赋值过程进行的一个封装;
一个正常的setter方法我们可以这样理解:对于ivar的setter方法,有一个sel和imp是对应的,那么现在通过一个入口使得sel绑定的imp重定向到了objc_setProperty方法上,这个过程是在编译阶段LLVM完成的;
接下来我们通过LLVM来分析一下objc_setProperty:
我们通过逆向推导的方式来分析objc_setProperty的实现:
首先,我们可以在LLVM的源码中搜索objc_setProperty,定位到如下代码:
系统在getSetProperty方法中通过CreateRuntimeFunction来创建运行时方法objc_setProperty,那么我们可以继续探索什么地方调用了getSetPropertyFn方法:
可以看到有两处调用了getSetPropertyFn方法,最终都指向GetPropertySetFunction,我们继续搜索:
这里是创建了一个PropertyImplStrategy类型的strategy,然后根据strategy.getKind()返回的枚举值做不通的处理,那么我们只需要去找到在PropertyImplStrategy中Kind的赋值情况即可:
通过搜索PropertyImplStrategy,我们定位到如下代码:
根据上图中红框部分的解释:当有property有copy描述的情况时,会在setter方法中出现objc_setProperty进行赋值的情况出现
验证
我们将Person类进行如下修改:
@interface Person : NSObject{
NSString *nickName;
NSObject *objc;
}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) NSString *realName;
@property (atomic, copy) NSString *acRealName;
@property (atomic) NSString *aRealName;
@property (nonatomic, retain) NSString *nrRealName;
@property (atomic, retain) NSString *arRealName;
@property (retain) NSString *rRealName;
@property (nonatomic, assign) NSInteger age;
- (void)run;
+ (void)eat;
@end
@implementation Person
- (void)run {
}
+ (void)eat {
}
@end
然后使用xcrun命令生成cpp文件如下:
结论
objc_setPropertynonatomic和copy修饰的属性,生成的setter方法会调用objc_setPropertyatomic和copy修饰的属性,生成的setter方法会调用objc_setPropertynonatomic和retain修饰的属性,生成的setter方法会调用objc_setPropertyatomic和retain修饰的属性,生成的setter方法会调用objc_setProperty- 仅用
retain修饰的属性,生成的setter方法会调用objc_setProperty
objc_getPropertyatomic和copy修饰的属性,生成的getter方法会调用objc_getPropertyatomic和retain修饰的属性,生成的getter方法会调用objc_getProperty- 仅用
retain修饰的属性,生成的getter方法会调用objc_getProperty
使用
copy或者retain修饰的属性,生成的setter方法会调用objc_setProperty
使用
copy或者retain且使用非nonatomic修饰的属性,生成的getter方法会调用objc_getProperty
其他均采用内存平移赋值/取值
注意: 此处真机添加
objc_getProperty符号断点时,nonatomic, copy修饰的属性也会触发objc_getProperty,猜测Clang和真机运行有差异;
类的成员变量的存储
WWDC 2020中关于runtime的优化分析
成员变量的存储分析
在上篇文章中,我最终留下了一个疑问:类的成员变量存储到哪里去了?WWDC 2020针对运行时进行了优化。在优化过程中,将ivar存在了ro中,那么我们分析一下源码能否找到
ro的相关操作:
那么我们通过lldb来分析一下ro:
继续分析:
最终我们找到了成员变量nickName,以及属性生成的带_的成员变量
那么我们来看一下baseProperties里边存放的什么东西?
我们最后得出结论:baseProperties和properties()存放的都是属性
类方法的存储
所谓的对象方法或者是类方法都是我们在OC层面的概念,在C/C++底层统称为函数。那么如果我们在OC中同时定义了-(void)run和+(void)run,那么我们在底层代码中放在一起存储的话,我们如何区分呢?最大可能就是我们没有找到的类方法,被存储在了别处,那么在什么地方呢,那就是我们之前引入的元类,类方法就存储在元类中,接下来我们用lldb来验证一下:
协议方法的获取
在Person类中添加协议
@protocol PersonDelegate <NSObject>
- (void)delegatMethod;
@end
@interface Person : NSObject<PersonDelegate>{
NSString *nickName;
NSObject *objc;
}
按照获取方法和属性的方式,lldb调试至此:
返回了protocol_list_t类型的数据,通过查看源码:
按照之前的逻辑,我们应该找到protocol_ref_t类型的数据,查看protocol_list_t的源码发现:
全局搜索protocol_ref_t找到了其类型转换的代码:
经过查看protocol_t类型的源码发现其很有可能就是我们最后要找的数据存储的地方:
lldb打印:
类的懒加载
我们创建一个继承自Person的类Teacher:
@interface Teacher : Person
@end
@implementation Teacher
@end
此时我们使用lldb进行打印调试:
我们在lldb中加载了一下Teacher类,这里就能打印出firstSubclass侧面验证了我们类的加载是一个懒加载的过程
类方法存储的API方法解析
除了以上使用lldb的方法分析,我们还可以使用API分析方法的存储
第一种分析方案
void testCopyMethodList(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 *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"-->%@", methodName);
}
}
Person *person = [Person alloc];
Class cls = object_getClass(person);
testCopyMethodList(cls); // 打印Person的方法列表
NSLog(@"------分割线-------");
const char *className = class_getName(cls);
Class metaClass = objc_getMetaClass(className);
testCopyMethodList(metaClass); // 打印Person的元类的方法列表
打印结果:
-->run
-->name
-->.cxx_destruct
-->setName:
-->age
-->setAge:
-->realName
-->setRealName:
------分割线-------
-->eat
在类
Person中打印出了属性的setter和getter方法,对象方法以及一个C++的析构方法,在Person的元类中打印出了类方法eat
第二种分析方案
void testInstanceMethodPlace(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(pClass, @selector(run));
Method method2 = class_getInstanceMethod(metaClass, @selector(run));
Method method3 = class_getInstanceMethod(pClass, @selector(eat));
Method method4 = class_getInstanceMethod(metaClass, @selector(eat));
NSLog(@"-->%p", method1);
NSLog(@"-->%p", method2);
NSLog(@"-->%p", method3);
NSLog(@"-->%p", method4);
}
Person *person = [Person alloc];
Class cls = object_getClass(person);
testInstanceMethodPlace(cls);
打印结果:
-->0x1000080e8
-->0x0
-->0x0
-->0x100008080
结论分析:
- 第一行打印结果:
Person类有对象方法run - 第二行打印结果:
Person的元类没有对象方法run--run存储在Person类 - 第三行打印结果:
Person类没有对象方法eat--eat是Person类的类方法,存储在Person的元类 - 第四行打印结果:
Person的元类有对象方法eat--eat在Person的元类中以对象方法的形式存在
第三中分析方案
void testClassMethodPlace(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getClassMethod(pClass, @selector(run));
Method method2 = class_getClassMethod(metaClass, @selector(run));
Method method3 = class_getClassMethod(pClass, @selector(eat));
Method method4 = class_getClassMethod(metaClass, @selector(eat));
NSLog(@"-->%p", method1);
NSLog(@"-->%p", method2);
NSLog(@"-->%p", method3);
NSLog(@"-->%p", method4);
}
打印结果:
-->0x0
-->0x0
-->0x1000081e8
-->0x1000081e8
结论分析:
第一行打印结果: Person类没有类方法run -- run为Person类的对象方法
第二行打印结果: Person的元类没有类方法run -- run为Person类的对象方法
第三行打印结果: Person类有类方法eat -- eat在Person类中以类方法存在
第四行打印结果: Person的元类有类方法eat -- ???
疑惑:
为什么第四行打印结果显示:Person的元类会有类方法eat呢,eat在元类中不是应该以对象方法的形式存在么,此时我们结合objc源码分析:
原来在class_getClassMethod的底层实现是调用了class_getInstanceMethod,接下来我们看一下getMeta()方法的实现:
结合判断条件的注释是否是元类,我们可以知道getMeta()就是判断传进来的类是否是元类来返回不同结果的,而此处明显传进来的是Person的元类,所以getMeta()就返回了this元类自己,那么最终class_getClassMethod还是返回了元类的对象方法,即返回了Person元类的对象方法eat,这就是为什么第三行和第四行打印地址一样的原因;
在底层没有类方法,都是对象方法
第四种分析方案
void testMethodImpClass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
IMP imp1 = class_getMethodImplementation(pClass, @selector(run));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(run));
IMP imp3 = class_getMethodImplementation(pClass, @selector(eat));
IMP imp4 = class_getMethodImplementation(metaClass, @selector(eat));
NSLog(@"-->%p", imp1);
NSLog(@"-->%p", imp2);
NSLog(@"-->%p", imp3);
NSLog(@"-->%p", imp4);
}
打印结果:
-->0x1000038a0
-->0x1002eb100
-->0x1002eb100
-->0x100003890
结论分析:
- 第一行打印结果:
Person类有run方法的实现 - 第二行打印结果:
Person的元类有run方法的实现 -- ??? - 第三行打印结果:
Person类有eat方法的实现 - 第四行打印结果:
Person的元类有eat方法的实现
疑惑:
我们知道寻找imp的过程就是sel映射imp的过程(方法的查找过程)
为什么Person的元类会有类的对象方法run的实现呢?我们结合objc的源码进行分析
当需要映射的imp不存在时,底层方法反悔了一个_objc_msgForward,这就是最终出现Person的元类存在run方法实现结果的原因,那么_objc_msgForward是什么呢
(补充)消息转发机制: _objc_msgForward
我们调用对象的方法在底层都是通过_objc_msgSend方法来给当前对象发送消息,但是它并不是直接发送消息的,而是由一个_objc_msgForward的imp触发了消息转发机制,那么_objc_msgForward做了什么呢?
_objc_msgForward其实是一个imp类型,是一个具体的方法实现(Apple并未公布),其主要作用就是用来进行消息转发,如果一个消息通过_objc_msgSend发送之后没有找到,那么_objc_msgForward就会尝试做消息转发;
具体_objc_msgForward是如何进行消息转发的,我们后续会在runtime中一窥究竟......