美文网首页
iOS 经典面试题解析

iOS 经典面试题解析

作者: 远方竹叶 | 来源:发表于2020-10-23 14:32 被阅读0次

题目:

  1. 下面代码 ⌘+R 后会 Compile Error 、Runtime Crash 或者 NSLog 输出?
  2. 如果 [(__bridge id)obj speak]; 能调用成功,输出什么?
@interface LCPerson : NSObject

@property (nonatomic, copy) NSString *username;

- (void)doSomthing;

@end

@implementation LCPerson

- (void)doSomthing {
    NSLog(@"%s -- %@", __func__, self.username);
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    id cls = [LCPerson class];
    void *obj = &cls;
    
    [(__bridge id)obj doSomthing];
}

首先,我们运行下,会有什么结果

从以上结果可以看到,不仅编译成功了,而且没有崩溃,还有 log 日志,why?

  • 为什么编译不会报错?运行后为什么没有崩溃?
  • 为什么打印的结果是 ViewController 对象?

下面针对上面两个问题进行解析

为什么能正常运行?

第一步

[LCPerson class] 返回的是 Class,实际类型为 struct objc_class *

typedef struct objc_class *Class;

id表示将其转换为一个对象指针,id 的实际类型为 struct objc_object *

struct objc_object {
    Class isa;
};

typedef struct objc_object *id;

从以上分析可以看出:clsid 类型指向, 即 struct objc_object *。 但本质还是 struct objc_class * 类型

  • 验证
id cls = [LCPerson class];
if (object_isClass(cls)) {
    NSLog(@"cls is Class");
}

输出结果为:

上面的打印验证了刚刚的结论

第二步

&cls 取的是 cls 的地址,相当于 [LCPerson class] 现在被一个指针的指针指向。

struct objc_object {
    Class isa;
};

objc_object 的结构我们知道,第一个属性是 isa,指向当前所属的类。就是说,我们如果有一个指向 Class 的地址的指针,相当于这个对象就已经可以使用了。而 cls 本质上就是 LCPerson,即 struct objc_class * 类型,void *obj = &cls; 相当于 obj 指针指向了 &cls,即 obj 变成了 LCPerson 的实例,就可以调用对象的实例方法了。

为什么打印的结果是 ViewController 对象?

这个我们先来了解点拓展知识

参数入栈顺序
  • 函数传参

先看一段代码,如下

void lcFunction(NSNumber *a, NSNumber *b, NSNumber *c) {
    NSLog(@"a == %p", &a);
    NSLog(@"b == %p", &b);
    NSLog(@"c == %p", &c);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    lcFunction(@(10), @(20), @(30));
}

在给函数传入参数时,参数会作为自动变量入栈

栈是由高地址向低地址分配内存,所以他们入栈的先后顺序为:a 先入栈,b 再入栈,c 最后入栈。

  • 初始化结构体

同样来一段代码

typedef struct myStruct {
    NSNumber *num1;
    NSNumber *num2;
    NSNumber *num3;
} myStruct;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    myStruct test = {@(10), @(20), @(30)};
    NSNumber *num4 = @(40);
    NSLog(@"num4: %p", &num4);
}

初始化一个结构体的时候,这个顺序是相反的:

他们入栈的先后顺序为:num3 先入栈,num2 再入栈,num1 最后入栈。

  • 隐藏参数

我们将前面题目的代码通过 clang 转换为 cpp 文件,查看源码实现。打开终端,cdViewController.m 所在的目录,执行以下命令

clang -rewrite-objc ViewController.m -o ViewController.cpp

找到 viewDidLoad的源码,如下:

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"));

    id cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("class"));
    void *obj = &cls;

    ((void (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("doSomthing"));
}

代码很繁杂,看的不太清晰,我们精简如下:

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    objc_msgSendSuper({self, class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));

    id cls = objc_msgSend(objc_getClass("LCPerson"), sel_registerName("class"));
    void *obj = &cls;

    objc_msgSend(obj, sel_registerName("doSomthing"));
}

可以看到,viewDidLoad 在底层会携带两个参数:self_cmd,这个大家应该很熟悉了。方法调用在底层都会转为消息发送

objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

objc_msgSend 会传入两个隐藏参数,而 objc_msgSendSuper 需要传入一个结构体 struct 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 */
};

上述源码中的 {self, class_getSuperclass(objc_getClass("ViewController"))} 就是在初始化一个结构体

栈结构

按照上面的规则,题目中代码的入栈顺序为:

    1. self_cmd 先后入栈;
    1. objc_msgSendSuper 初始化的结构体中两个参数按照从右到左的顺序先后入栈(即 class_getSuperclass(objc_getClass("ViewController")) 先入栈,self 后入栈);
    1. cls 最后入栈。

整体的入栈顺序为:self_cmdclass_getSuperclass(objc_getClass("ViewController"))selfcls。我们可以验证下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    id cls = [LCPerson class];
    void *obj = &cls;
    
    NSLog(@"开始");
    void *start = (void *)&self;
    void *end   = (void *)&obj;
    long count = (start - end) / 0x8;
    for (long i = 0; i < count; i++) {
        void *address = start - i * 0x8;
        if (i == 1) {
            NSLog(@"%p : %s", address, *(char **)(address));
        }
        else {
            NSLog(@"%p : %@", address, *(void **)(address));
        }
    }
    NSLog(@"结束");

    [(__bridge id)obj doSomthing];
}

打印结果如下:

栈区的顺序结果与我们预期的一样

获取 username 属性
  • 正常情况下,我们通过 [[LCPerson alloc] init] 初始化方法创建一个 LCPerson 实例对象,会在堆区分配内存。但现在,我们是使用栈区指针指向了 LCPerson 类对象地址,使编译器误认为是 LCPerson 的实例对象,首地址为 0x7ffee2fc7268(即 isa) 。

  • 获取对象的属性,通过 isa 指针的地址,进行内存平移得到属性的地址,也就是说 LCPerson 对象的 username 属性是根据 isa 的地址平移 8 字节得到。

  • 上述的打印中看到 obj 这个 伪对象 的地址 0x7ffee2fc7268,根据内存平移 8字节(0x7ffee2fc7268 + 0x8),得到 0x7ffee2fc7270,而根据打印的入栈顺序找到是 ViewController 对象,就是题目运行结果打印的

反思

通过这个题考查的点有:

  • 方法调用的本质

  • 编译器是如何判断当前对象是某个类的实例

  • 函数入栈&结构体入栈顺序

  • 对象属性的值是怎么获取的

总结

方法调用的本质是发送消息,在底层会调用 objc_msgSend 会携带两个隐藏参数(self、_cmd);Objective-C中的对象是一个指向 Class 地址的变量,即 id obj = &Class;函数参数入栈顺序是从左到右,而结构体参数入栈是反向的,从右到左;对象属性的获取是通过地址偏移(内存平移)

相关文章

网友评论

      本文标题:iOS 经典面试题解析

      本文链接:https://www.haomeiwen.com/subject/kfyvmktx.html