那道值得思考的iOS面试题

前言

记得去年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位。。。这点目前我还没想通,个中原委还望知道的大神指点一二。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,081评论 1 32
  • 把网上的一些结合自己面试时遇到的面试题总结了一下,以后有新的还会再加进来。 1. OC 的理解与特性 OC 作为一...
    AlaricMurray阅读 2,538评论 0 20
  • 一直喜欢李敖这首描写爱情的小诗《只爱一点点》:不爱那么多,只爱一点点,别人的爱情像海深,我的爱情浅。不爱那么多,只...
    弯月牙阅读 3,018评论 29 132
  • 晚霞弯月极美,星辰亦是! 沙滩椅上坐着仰望满穹碎钻,倾听海浪来来回回的音律,想起你牵着我手走过的每一个地方,谢谢你...
    mo清夜无尘阅读 84评论 1 1
  • 姓名:韩文祥 单位:上海晋名实业有限公司 六项精进361期【日精进打卡第115天】 【知~学习】 背诵《六项精进》...
    祥子oboz阅读 126评论 0 0