前言
记得去年7,8月份的时候就看到过这么一篇文章,当时没花多少时间,看得懵懵懂懂的。结果昨天机缘巧合又在cocoachina上看到有关这个题目的另一篇帖子.这两篇文章解释的都是sunnyxx出的神经病院objc runtime入院考试中的第四道题。看完之后鄙人更懵逼了,可能是作者确实没有描述清楚,又或者是本人没有get到作者的点,反正对第二步的理解就是感觉不得要领,没有那种清楚知道的感觉。所以花了点时间好好梳理了一下,这里做下记录。
题目我就直接摘抄了:
//MNPerson
@interface MNPerson : NSObject
@property (nonatomic, copy)NSString *name;
- (void)print;
@end
@implementation MNPerson
- (void)print{
NSLog(@"self.name = %@",self.name);
}
@end
---------------------------------------------------
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [MNPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}
问输出结果是啥,会不会崩溃。
自己实验一下,就知道不会崩溃,打印结果是
2019-04-13 12:09:40.678424+0800 ZZTest[5431:86117] self.name = <ViewController: 0x7feaf2d0d060>
那么分析可以分两步进行:
- 1.为啥能正常调用,不会崩溃?
- 2.为啥打印的会是当前视图控制器?
一、先研究第1步:
先定义一个概念,组合键点击 = 按住control + command + 鼠标左键点击;
组合键点击[MNPerson class]
中的class
,可以看到这个类方法返回的是Class:
+ (Class)class OBJC_SWIFT_UNAVAILABLE("use 'aClass.self' instead");
再组合键点击Class
,可以看到这些:
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif
其实就是类和对象的基本定义了。可以看到id是objc_object结构体指针,而结构体本身只包含一个Class类型的isa成员变量。而Class又是objc_class结构体指针。再组合键点击objc_class
,看看它的具体定义:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
为什么id是通用对象类型,可以强转任何对象?因为一个实例包含的全部信息都存储在它所属的类里,而每个类也是对象,即isa指针。也就是说isa指针如果指向A,那么它就是A类实例,如果指向B,那么它就是B实例。那么题目里的第二句代码:
id cls = [MNPerson class];
就可以解释为:将MNPerson类对象赋值给了cls。
题目的后两句句代码:
void *obj = &cls;
[(__bridge id)obj print];
理解为:C语言的obj指针指向了cls即MNPerson类对象,经过桥接转换为OC的id类型后调用print实例方法。我们知道一个类的实例对象的实例方法都是存在当前类中的,既然有了指向这个类对象的指针,那么调用类对象中存储的实例方法自然是顺理成章的了。
接下来分析第2步,为啥会打印当前视图控制器。
这里我们再分两小步进行分析:
- 2.1实例方法print中,访问成员变量name的逻辑是怎么样的。
- 2.2怎么访问到的self,即当前控制器变量。
这里说一个前提,一个类定义完成以后,那么它的内存布局就已经确定了。这也就是为什么不能给类别增加属性的原因。
2.1那么一个类定义的属性或者成员变量是怎么访问到的呢?这里做个小的试验:
@interface MNPerson : NSObject
@property (nonatomic, copy)NSString *name;
@property (nonatomic, copy)NSString *name2;
- (void)print;
- (void)test;
@end
@implementation MNPerson
- (void)print{
NSLog(@"self.name = %@",self.name);
}
- (void)test {
NSLog(@"self:%p",self);
NSLog(@"self.name:%p",&_name);
NSLog(@"self.name2:%p",&_name2);
}
给MNPerson新增一个name2属性和test实例方法,在viewDidLoad方法中访问:
- (void)viewDidLoad {
[super viewDidLoad];
MNPerson *p = [[MNPerson alloc] init];
[p test];
// NSString *test = @"777";
// id cls = [MNPerson class];
//
// void *obj = &cls;
// NSLog(@"test:%p", &test);
// NSLog(@"cls:%p", &cls);
// NSLog(@"obj:%p", obj);
// [(__bridge id)obj print];
}
运行结果为:
2019-04-13 15:15:38.910967+0800 ZZTest[7755:188927] self:0x600000d86180
2019-04-13 15:15:38.911093+0800 ZZTest[7755:188927] self.name:0x600000d86188
2019-04-13 15:15:38.911159+0800 ZZTest[7755:188927] self.name2:0x600000d86190
可以看到对象属性的内存地址跟对象的内存地址是连续的,每个占8字节,这里通过alloc生成的OC对象是占用的堆空间。这里访问属性的过程可以做一个合理的抽象,即通过当前对象的内存地址偏移n*8位去访问第n个成员变量。假如当前对象地址是0x600000d86180,那么它第一个成员变量的内存地址就是0x600000d86180 + 1 * 8 = 0x600000d86188。实例方法中访问成员变量的逻辑我们已经理清楚了。接着看2.2。
2.2 接下来,得另外做一个小试验。在[super viewDidLoad]
后面插入一个test变量,将viewDidLoad方法中改成:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *test = @"777";
id cls = [MNPerson class];
void *obj = &cls;
NSLog(@"test:%p", &test);
NSLog(@"cls:%p", &cls);
NSLog(@"obj:%p", &obj);
[(__bridge id)obj print];
}
打印结果为:
2019-04-13 15:47:33.303760+0800 ZZTest[8215:213054] test:0x7ffee7118308
2019-04-13 15:47:33.303911+0800 ZZTest[8215:213054] cls:0x7ffee7118300
2019-04-13 15:47:33.303983+0800 ZZTest[8215:213054] obj:0x7ffee7118300
2019-04-13 15:47:33.304062+0800 ZZTest[8215:213054] self.name = 777
我们知道函数或者方法中的局部变量是在栈中生成的,可以看到test,cls,obj地址是连续递减的,obj中装的是cls的内存地址,所以最后一句代码[(__bridge id)obj print];
的意思就是将cls的内存地址作为入参调用print实例方法,根据第1步得出的经验,print实例方法中的访问逻辑是根据入参内存地址偏移即增加8位去访问,那么访问的地址就是0x7ffee7118300 + 8 = 0x7ffee7118308,即test变量。所以最后打印self.name = 777就解释通了。
那么回到最原始的题目,为什么cls的内存地址偏移8位会访问到self呢??
到目前为止,第一句代码[super viewDidLoad];
还没有深究,那它的底层做了什么东西呢?
底层 - objc_msgSendSuper,
objc_msgSendSuper({ self, [ViewController class] },@selector(ViewDidLoad)),
等价于
struct temp = {
self,
[ViewController class]
}
objc_msgSendSuper(temp, @selector(ViewDidLoad))
所以等于有个局部变量 - 结构体 temp,
结构体的地址 = 他的第一个成员的内存地址,这里的第一个成员是self。所以现在栈内元素内存地址由高到低就是:temp = self, cls,obj。根据cls的地址偏移8位,访问到的就是self。那么整个问题就解释通了。
cls变量直接指向了MNPerson类对象,所以能够调用到print实例方法;而实例方法中访问成员变量的逻辑是根据入参内存地址偏移8*n位来访问第n个成员变量;cls是在栈内生成的,栈的内存地址是从高位向低位排列。根据cls的地址偏移8位,刚好访问到了[super viewDidLoad]的隐藏参数局部变量结构体temp,temp的内存地址等于它的第一个成员变量的内存地址,即self的内存地址。所以最终访问到了self。
最后的疑惑
2019-04-13 16:22:34.490522+0800 ZZTest[8647:237822] cls:0x7ffeedb20308
2019-04-13 16:22:34.490632+0800 ZZTest[8647:237822] obj:0x7ffeedb20300
2019-04-13 16:22:34.490734+0800 ZZTest[8647:237822] self.name = <ViewController: 0x7f888161bd90>
可以看到self的内存地址跟cls的内存地址并不是偏移8位。。。这点目前我还没想通,个中原委还望知道的大神指点一二。