隐藏参数
首先由下面这个常见的面试题入手。MyObject是自定义的一个类,父类是NSObject。接下来的问题是:下面的代码中打印的结果是什么?为什么?
@implementation MyObject
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"%@,%@", [self class], [super class]);
}
return self;
}
@end
我们都知道打印的结果是一样的,都是MyObject。但是为什么呢?接下来我们通过xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc MyObject.m -o MyObject.cpp编译成c++查看底层原理:
static instancetype _I_MyObject_init(MyObject * self, SEL _cmd) {
self = ((MyObject *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyObject"))}, sel_registerName("init"));
if (self) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_91163dcd57j_zw_xyry904bc0000gn_T_main_da396f_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")), ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyObject"))}, sel_registerName("class")));
}
return self;
}
这里我们看到[self class]和[super class]在底层分别变成了:
objc_msgSend((id)self, sel_registerName("class"));
objc_msgSendSuper((__rw_objc_super, sel_registerName("class"));
__rw_objc_super的数据结构是objc_super:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
这里我们就可以发现[super class]中的super并不是消息的接收者,它的实际接收者是self,在调用过程中会生成一个结构体objc_super,objc_super中的receiver就是self。所以实际上是跟[self class]一样的像self发送消息。super只是调用了objc_msgSendSuper,绕过本类方法列表,直接去父类寻找方法实现。其实这里想说是,不管[self class]还是[super class],我们代码中并没有加入任何参数,但是转化成objc_msgSend和objc_msgSendSuper确多了两个参数,这其实就是所谓的隐藏参数。
隐藏参数和指针平移
OC中,当我们调用方法时,在底层会转化成消息发送的形式,调用objc_msgSend向这个调用者发送消息。这里的消息就是我们调用的方法,而消息的接收者就是调用的对象。它们都转化成了objc_msgSend的参数(objc_msgSendSuper也是同理):
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
接下来看下如下demo,我们创建一个自定义类,并定义一个属性name和一个实例方法printName,demo如下:
@interface MyObject : NSObject
@property (nonatomic, copy) NSString *name;
- (void)printName;
@end
@implementation MyObject
- (instancetype)init
{
self = [super init];
if (self) {
}
return self;
}
- (void)printName
{
NSLog(@"%s:%@", __func__, self.name);
}
@end
然后我们进行如下操作:
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [MyObject class];
void *objc = &cls;
[(__bridge id)objc printName];
MyObject *objc1 = [[MyObject alloc] init];
[objc1 printName];
}
运行,最终打印结果为:
2021-08-12 22:29:57.308903+0800 Superclass&isaDemo[8273:557040] name:<ViewController: 0x105e06300>
2021-08-12 22:29:57.309032+0800 Superclass&isaDemo[8273:557040] name:(null)
这里有两个问题:
1、objc为什么能调用printName方法?
2、为什么objc调用printName打印的name是<ViewController: 0x105e06300>,而objc1打印的是null?
解答1:这里objc之所以能调用printName方法,是因为objc是一个指针,对象本身也是通过指针访问的,而且对象底层结构的首地址是isa指针,指向的是它的类,而这里objc指向的地址正好MyObject这个类结构的地址,objc会被认为是MyObject一个对象,当我们通过 [(__bridge id)objc printName]这种方式调用时,会进入消息发送流程,通过objc的isa指针(这里正好是objc指针指向的地址)找到它的类MyObject,并从这个类的方法列表里面找到printName这个方法调用。
2、由1可知,objc是一个指向类结构的指针,因此在访问name属性时,它是通过指针平移的方式读取,而实际上它不是一个真正的对象,而因为隐藏参数的原因和函数压栈的原因,它平移之后刚好访问到函数隐藏参数self。而objc1是经过类结构创建的真实对象,它的name属性本来就没有赋值,所以为空。关于函数压栈详情请看下文:
什么是函数压栈?
为了了解函数压栈,我们进行如下demo进行试验。msg_send模拟的是objc_msgSend。它的两个参数objc和sel在调用msg_send时会被按循序压栈:
void msg_send(id objc, SEL sel)
{
NSLog(@"msg_send中objc地址:%p", &objc);
NSLog(@"msg_send中objc地址:%p", &sel);
}
打印结果:
2021-08-12 23:04:59.176589+0800 Superclass&isaDemo[8429:566293] msg_send中objc地址:0x16d979a28
2021-08-12 23:04:59.176663+0800 Superclass&isaDemo[8429:566293] msg_send中objc地址:0x16d979a20
可以看出,它们地址是连续的,地址由高到低。而且相差8位。压栈不只是函数参数,函数里的临时变量的地址都会被进行压栈。为了进一步了解函数压栈,继续往下看。
结构体压栈
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [MyObject class];
void *objc = &cls;
[(__bridge id)objc printName];
MyObject *objc1 = [[MyObject alloc] init];
void *start = (void *)&self;
void *end = (void *)&objc1;
long count = (start - end)/8;
for (int i = 0; i < count; i++) {
void *addr = start - 8*i;
if (i == 1) {
NSLog(@"第%d个地址:%p, value:%s", i+1, addr, *(void **)addr);
}else{
NSLog(@"第%d个地址:%p, value:%@", i+1, addr, *(void **)addr);
}
}
NSLog(@"self:%@", self);
NSLog(@"cls的地址:%p", &cls);
NSLog(@"objc的地址:%p", &objc);
}
打印结果:
第1个地址:0x16d10dac8, value:<ViewController: 0x113d07af0>
第2个地址:0x16d10dac0, value:viewDidLoad
第3个地址:0x16d10dab8, value:ViewController
第4个地址:0x16d10dab0, value:<ViewController: 0x113d07af0>
第5个地址:0x16d10daa8, value:MyObject
第6个地址:0x16d10daa0, value:<MyObject: 0x16d10daa8>
self:<ViewController: 0x113d07af0>
cls的地址:0x16d10daa8
objc的地址:0x16d10daa0
从上面的打印结果中可以看出第1个地址和第4个地址都是当前ViewCntroller即self,第2个是方法viewDidLoad,第3个是当前self的类ViewController,第5个是cls,第6个是objc的地址。所以这个方法里面压栈的顺序是:
self->(SEL)viewDidLoad->ViewController->self->cls->objc,而且地址是由高到低的。
这里就可以回答2的问题了。因为objc被认为是MyObject类的对象,所以会按照MyObject的结构去访问属性name。访问的方式是通过指针平移,而MyObject的第一个属性是isa,这也是objc能调用MyObject的方法的原因。但是当objc继续访问name时,它要平移8个字节(指针大小为8个字节),而由打印结果可知objc的地址0x16d10daa0,所以objc平移8个字节之后刚好越过cls,直接到达上面的第四个地址0x16d10dab0的区域,这个地址刚好指向的是self的内存,所以在这里objc本来是访问name,实际确访问了self。
如果前面的代码改成在cls前面定义别的临时变量,name访问的数据将会改变:
- (void)viewDidLoad {
[super viewDidLoad];
// int a = 123;
Class cls2 = [MyObject class];
Class cls = [MyObject class];
void *objc = &cls;
[(__bridge id)objc printName];
MyObject *objc1 = [[MyObject alloc] init];
[objc1 printName];
}
如果demo中cls前面加个cls2,那么这个这回objc访问的name指向的应该是cls2,打印的是MyObject, 而如果前面换成加了一个整形变量int a,那么这回访问的name将是一个异常的,因为name是8字节的,a是4字节的,结果就是访问的地址出现偏差,程序crash。