面试题分析

面试题一: [self class] & [super class]

以下打印输出什么?

@interface LGTeacher : LGPerson

@end

@implementation LGTeacher

- (instancetype)init {
    if (self = [super init]) {
        NSLog(@"%@ - %@", [self class], [super class]);
    }
    return self;
}

@end

直接运行,

  • [self class] 消息接收者为self,class的源码
- (Class)class {
    return object_getClass(self);
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

很简单,获取的是isa指针。

综上,self的isa指针指向谁?-->LGTeacher

  • [super class],注意super是关键字,那对应的底层代码是什么?-->clang一下

xcrun -sdk iphonesimulator clang -rewrite-objc LGTeacher.m


上图可知,[super class] --> objc_msgSendSuper,第一个参数是结构体__rw_objc_super,这是一个中间结构体,源码中搜不到,我们只能再搜索objc_msgSendSuper的定义

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

查看注释,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 */
};

objc_super有2个参数,分别是id类型的receiver变量和Class类型的父类变量。

再回到clang得到的C++代码
((void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("class"))

那么objc_super的receiver --> self,真正的消息接收者,第二个参数Class --> objc_getClass("LGTeacher")。

综上分析,

  1. objc_msgSendSuper的消息接收者还是self
  2. [super class]其实等价于[self class] --> LGTeacher

注意 事实真的是这样吗?
我们直接去汇编层查看[super class]

  1. 打开汇编调试: XCode菜单栏Debug-->Debug Workfiow-->勾上Always show Disassembly
  2. 只看[super class]
- (instancetype)init{
    self = [super init];
    if (self) {
        NSLog(@"%@",[super class]);
    }
    return self;
}

进入汇编层


红框处是NSLog那句代码的汇编,发现[super class]对应的是objc_msgSendSuper2
再看objc_msgSendSuper2源码

// objc_msgSendSuper2() takes the current search class, not its superclass.
OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);

看注释,objc_msgSendSuper2() takes the current search class, not its superclass,是不是很明白了!

至此,这道面试题完整答案如下:

  • [self class]:本质是 发送消息objc_msgSend,消息接收者是self,class是拿isa指针,self的isa指针指向类LGTeacher,所以是LGTeacher

  • [super class] :super是一个关键字,本质是调用objc_msgSendSuper2,其消息接收者和[self class]是一模一样的,都是self,所以返回的也是LGTeacher

面试题二:经典的 内存平移 问题

@interface LGPerson : NSObject
@property (copy, nonatomic) NSString *name;
- (void)sayHello;
@end

@implementation LGPerson
- (void)sayHello {
    NSLog(@"%s - %@", __func__, self.name);
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Class cls = [LGPerson class];
    void *kc = &cls;
    [(__bridge id)kc sayHello];
}

@end

以上打印输出什么?
直接运行,查看结果

-[LGPerson sayHello] - 

why?我们一行一行的查看代码:

  • Class cls = [LGPerson class]; --> 很简单,cls就是类LGPerson。
  • void *p = &cls; --> 指针p指向cls的地址,也就是类LGPerson的地址。
  • [(__bridge id)p sayHello]; --> 因为p是指针,是C++底层结构,要调用OC的方法,所以要桥接一下,即(__bridge id)。

回到问题,为什么能运行成功,并且打印的结果是ViewController?

我们先看看一个普通方法的调用过程:

LGPerson *person = [LGPerson alloc];
[person sayHello];
  • [person sayHello]中,person是消息接收者,然后person的isa指针指向类LGPerson,那么person的首地址就是类LGPerson的首地址。
  • 然后根据类LGPerson的首地址进行内存平移,找到缓存cache,在cache中查找方法sayHello。


再看void *p = &cls;, p是一个指针,也指向类LGPerson的首地址


上图可知,指针p和person的isa指针一样,其实都是指向类LGPerson的首地址,所以[(__bridge id)p sayHello] ==> [person sayHello],所以[(__bridge id)p sayHello]可正常运行。

接下来看-->为什么打印的值是ViewController呢?

//下面这两种方式调用
//方式一
Class cls = [LGPerson class];
void  *kc = &cls; 
[(__bridge id)kc sayHello]; 
 
//方式二:常规调用
LGPerson *person = [LGPerson alloc];
 [person sayHello];

运行查看结果


why?

  1. [person sayHello]的方式获取属性name的值: 本质是由于self指向person的内存结构,然后通过内存平移8字节(平移8字节是因为self的首地址是isa指针,指针占8字节大小),取出name,即self指针首地址平移8字节获得,但是属性name没有赋值,所以打印为null。
  2. [(__bridge id)kc sayHello]的方式:因为kc是指针,self.name就相当于kc指针平移8字节去查找name,那么kc的地址是什么?平移8字节后又是多少?
    我们还是要通过clang查看ViewDidLoad对应的底层C++代码:
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"));

    Class cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("class"));
    void *p = &cls;
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHello"));

    LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
}
  • 首先我们知道,kc指针是局部变量,内存中是在栈区栈区先进后出,后进先出的原则,参数的传入就是一个不断压栈的过程,压栈的意思就是,先传进来的地址高,后传进来的地址低,栈在释放时,先释放地址低的变量(后进先出),再释放地址高的变量(先进后出)。
  • 根据上面C++代码,我们看到ViewDidLoad有两个入参(ViewController * self, SEL _cmd),这是隐藏参数,就好比消息的发送是objc_msgSend,它有两个参数receiver 和 方法的sel。而方法的入参压栈的过程和先进后出,后进先出的原则一样,所以也是从高地址->低地址分配的。
  • [super viewDidLoad]; -->
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
    objc_msgSendSuper的第一个入参是结构体__rw_objc_super,它里面也有2个参数selfreceiver 和 (id)class_getSuperclass(objc_getClass("ViewController"))SEL,那结构体中的成员变量的地址是如何分配的呢?
结构体中成员变量的内存地址分配

举例验证:
首先定义结构体

struct kc_struct{
    NSNumber *num1;
    NSNumber *num2;
} kc_struct;

然后我们在ViewDidLoad中这么调用

struct kc_struct kcStruct = {@(10), @(20)};
LGPerson *person = [LGPerson alloc];

接着我们lldb查看内存栈区地址


上图我们发现,num1和num2这两个成员变量的内存地址是从低到高进行分配的,与栈的从高到低相反,即结构体内部的成员是反向压栈

回到之前clang得出的C++源码,目前为止,栈区中所有变量地址从高到低的顺序是:
self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person

  • self_cmdviewDidLoad方法的两个隐藏参数,是高地址->低地址正向压栈的。
  • objc_msgSendSuper2中的结构体成员 selfclass_getSuperClass,是低地址->高地址反向压栈的。
  • 剩下的cls - kc - person就是单纯的栈区变量,前面的高地址,后面的低地址,也是正向压栈
验证以上结论

self是地址最高,而person是地址最低,我们直接取它们的地址,进行一个for循环,看看

    Class cls = [LGPerson class];
    void  *kc = &cls;
    LGPerson *person = [LGPerson alloc];
    NSLog(@"%p - %p",&person,kc);
    // 隐藏参数 会压入栈帧
    void *sp  = (void *)&self;
    void *end = (void *)&person;
    long count = (sp - end) / 0x8;

    for (long i = 0; i
[(__bridge id)kc sayHello]的值是ViewController的原因

根据以上我们的分析,知道了kc指针在底层其实和person的性质是一样的,都是指向类LGPerson的首地址,[(__bridge id)kc sayHello]中调用self.name,kc是,是一个实例对象,那么此时的操作与普通的LGPerson对象是一致的,其中调用self.name, 即LGPerson的首地址内存平移8字节 --> 0x7ffeeccfc0d8 + 0x80 = 0x7ffeeccfc0e0,看上图,0x7ffeeccfc0e0对应的就是

小结

由于kc是指向LGPerson的关系,编译器会认为 kc也是LGPerson的一个实例化对象,即kc相当于isa指向LGPerson,即类LGPerson首地址,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即kc也有属性name,然后由于person查找name是通过内存平移8字节,所以kc也是通过内存平移8字节,正好是objc_msgSendSuper2的入参self,而objc_msgSendSuper2是在当前类中查找的,所以内存平移后的值就是类ViewController。

你可能感兴趣的:(面试题分析)