最近在群里看到有人发的一道面试题,题目以下:c++
@interface Spark : NSObject
@property(nonatomic,copy) NSString *name;
@end
@implementation Spark
- (void)speak {
NSLog(@"My name is:%@",self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Spark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
复制代码
问题:上述代码运行起来会:Complie error?|Runtime crash?|NSLog ?
git
最终问题就是这段代码的运行结果。github
第一眼看这个问题,我直接就想说,这个东西啊,确定是编译报错了、要不就是崩溃啊面试
因此我就跟着写了些代码,结果发现:objective-c
WTF? 怎么能运行,并且结果居然仍是bash
相信当你看到这个结果的时候会和我同样吃惊,不和逻辑啊,怎么居然能执行成功而且还打印出来当前controller了,不符合常理啊。数据结构
对于计算机而言,不存在什么魔法,若是一段代码能运行必然存在它的原理。app
咱们须要作的就是分析为何能成功。iphone
cls
的意思。cls
在C语言里,就是一个指针,这个指针的内容指向Spark类函数
当咱们经过void *obj = &cls;
这个语句执行后,获取的就是一个指向这个指针cls
的指针
事实上在这一步操做实现后,obj 这个指针就已经具备Object-c对象的功能了,为何呢?接下来咱们能够看看runtime实现原理了,这里我只说一点
//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
复制代码
引自: iOS Runtime详解-简书
上述简介中部分是错误的,由于这个只是在<objc/runtime.h>中的显示,可是以为直接删除又显现不出更改。于是在此专门写出,我会在下面给出正确的解释与数据来源
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
}
复制代码
数据来源: 苹果obj4开源代码 第1012行 用以替换 上述简述引用中的 objc_class
能够看到objc_object
这个对象的首字段是isa 指向一个Class
也就是说,咱们若是有一个指向Class的地址的指针,至关于这个对象就已经可使用了,只是像他的成员变量等等的一系列值都尚未被初始化。
因此接下来用(__bridge id)obj
,调用是不会产生问题的
这个问题就是由两个小部分组成的
1. name 这个属性是何时赋的值?
2. ViewController 这个对象是何时被传入的?
复制代码
首先咱们须要先了解一下,一个类对象的数据是如何存储的。
这里我就按照上文同样引用不少的论证了,咱们本身来探究
该上代码了:
@interface Cls : NSObject
@property(nonatomic,strong) NSString *test;
@property(nonatomic,strong) NSString *test1;
@end
@implementation Cls
- (void)printPrinter {
NSLog(@"self:%p",self);
NSLog(@"self.test:%p",&_test);
NSLog(@"self.test1:%p",&_test1);
}
@end
复制代码
接下来调用printPrinter
,打印一下对象指针地址:
能够发现,指针偏移量成员变量和指针首地址差8个字节,每一个成员变量与上一个成员变量偏移量也是8个字节。
完成到这一步,咱们仍然没有发现上述两个问题是应该怎么解释。可是咱们知道了,一个Object-C 对象的指针,和它的成员变量的指针确定是连续的。这就为接下来咱们的分析提供了一些思路。
下一步,我在本来的题目中增长一行代码:
[super viewDidLoad];
NSString *str = @"11111";
id cls = [Spark class];
复制代码
为啥要增长这行代码呢,这步是通过深(瞎)思(J)熟(B)虑(试),主要是考虑到函数内部的参数生成必然会须要地方存储,但这部分存储地址,咱们是不知晓的,它的实现是被系统隐藏的。而咱们的代码又没有明显的设置相关代码,那么必然是由这些条件实现的。因此当咱们增长了这一行代码后,不出意外的,打印结果变了
2018-11-29 20:49:39.254021+0800 test[1961:92498] My name is:11111
变成了 咱们 上述的值,这一切都和猜测的差很少
因而一个基本设想就出来了:
由于栈上的地址结构和本来类的需求地址结构高度重合了,同时全部地址都能访问到对应的值。咱们经过栈的默认行为生成了一个Spark对象!
为了验证,咱们打印一下cls
和str
的指针堆栈地址
NSLog(@"cls address:%p str address:%p",&cls,&str);
复制代码
2018-11-29 21:03:30.490989+0800 test[2129:122769] cls address:0x7ffeebf4fa00 str address:0x7ffeebf4fa08
咱们能够看到他们之间相差也正好是8,并且正好和对象结构体定义的如出一辙。因此这也正好能说明咱们上述的打印结果My name is:11111
为何会发生。
注:这个存在的缘由是由于函数内部变量采用的小端模式,也就是将参数地址由栈区从高地址依次向低地址分配,因此咱们打印cls
地址会比str
要小。
由此,第一个小问题就解决了,答案是由于咱们在生成堆栈参数的时候,拼凑出了Spark对象的地址数据结构格式,和真正的对象地址数据结构同样,因此self.name
就是在生成cls
的那一刻起内存地址就已经被赋值了。
接下来到下一个问题了ViewController 是何时传入的?
在这一步里咱们只能把目光向cls
对象生成前执行的操做来看,[super viewDidLoad];
咱们只执行了这一步操做,那必然是这个操做产生的结果。为了验证,咱们能够更改一下调用顺序
id cls = [Cls class];
[super viewDidLoad];
复制代码
当咱们进行这部操做后,会发现,执行speak方法时崩溃了,错误是EXC_BAC_ACCESS
,说明是咱们引用野指针了。
由此也能够证明,[super viewDidLoad];
确定作了一些骚操做,将ViewController的self
压入了栈区。
接下来咱们就须要探究究竟作了什么操做,咱们能够用以下的命令行代码将ViewController.m重写成c++代码,而后观看发生了什么。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.cpp
复制代码
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
复制代码
咱们能够发现本来这个方法里面会传入两个参数一个是self
,一个是_cmd
,当咱们调用[super viewDidLoad]
时,执行的方法中传入了参数self
,由此将self
作为一个值压入了栈中,可是_cmd
这个参数并未被使用,所以,没有被压入栈中。
至此,这个问题已经被解释出来了。
全部NSObject对象的首地址都是指向这个对象的所属类。这个条件是充要条件。反过来讲,若是一个地址指向某个类,咱们就能够把这个地址当成对象去用。因此编译是会经过的,也不会报unrecognized selector
的错误。
打印结果会是ViewController对象的缘由是由于cls
在栈上的数据结构符合了它做为真实的类时候的数据结构,cls.name
本来地址正好是栈上ViewController对象地址,所以NSLog能打印出<ViewController >
这类问题,考察的东西很深,而且结合了不少知识点。可是当咱们拿到面试题而且能进行思索的时候必定要好好的考虑,我对这道题的想法,也是在不断的试验中逐渐的完善,而且尝试了不少。其实找面试题为何是这个答案的过程和,找代码找bug的流程都是相似的,都是排除变量,逐步探索,最终将探索过程和概念结合。
也许答案不是很专业,但愿你们若是有更专业的答案,能够告诉我。顺便同步推广本身的博客:www.wdtechnology.club/
谢谢您的阅读
本文同步发行于本人博客 未经受权不得随意使用