iOS两道经典的面试题(二)

920 阅读6分钟

我们前面文章讲了App加载的有关内容,我将文章进行了总结。OC底层原理系列,OC底层系列讲了不少,这边文章就来说说比较经典的相关面试题。

[self class]和[super class]的区别

准备代码,预测结果

我们准备下代码

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGTeacher *teacher = [[LGTeacher alloc] init];;
        NSLog(@"%@",teacher);
    }
    return 0;
}

我们预测的答案应该是LGTeacher和NSObject(LGTeacher的父类是NSObject)。是这样吗?我们验证下 我们发现两个答案的都是LGTeacher,并没有我们想要的NSObject。这是为什么呢?

分析,答疑

上面都是调用class,调用class,我们看下class的实现

我们看到调用class结果就是如歌obj存在就拿到obj的isa,[self class]很好理解,因为self的isa就是LGTeacher。所以[self class]就是LGTeacher,[super class]中的super是什么?其实是关键字,来自寄存器底层的指令。

我们看下[super class]在C++下是什么样呢?我们用clang去看下,我们寻找一下init方法 我们看到NSLog打印的时候发送了两个消息,分别是

((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))

((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("class")))

他们相对应的就是[self class]和[super class],我们看到他们区别在于[self class]发送消息用的是objc_msgSend,而[super class]发送消息用的是objc_msgSendSuper,那么objc_msgSendSuper方法实现是什么样的呢?

我们看到objc_super是个结构体类型,receiver是消息接受者,而super_class只是一个类型,我们再看[super class]在C++的代码,objc_msgSendSuper传入的第一个参数为(id)self,所以这个消息的接收者就是self,就是LGTeacher。补充:objc_super的class和super_calss是为了快速查找(是去类对象class中查找还是直接去super_class中查找)

我们验证下,我们在class方法内打断点,运行代码,看下进来的self是否都为LGTeacher 符合期望

总结

上面的问题我们知道

  • 1.super是个关键字
  • 2.[super class] -> (class)(id self , sel_cmd)其中self,cmd是隐藏参数
  • 3.self->isa 就是LGPerson类
  • 4.[self class]的消息接受者是self,[super class]的消息接收者也是self。

指针平移

在网上看过一道面试题,准备代码

// ViewController
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Class cls = [LGPerson class];
    void  *kc = &cls;
    [(__bridge id)kc saySomething];
    LGPerson *person = [LGPerson alloc];
    [person saySomething];
}
@end

// LGPerson.h
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lg_name;
@property (nonatomic, copy) NSString *lg_hobby;
- (void)saySomething;
@end
// LGPerson.m
@implementation LGPerson
- (void)saySomething { 
    NSLog(@"%s",__func__);
}
@end

问题回事代码运行会怎么样?我们看下代码,saySomething是对象方法,我们一般都是通过对象去调用的对象方法,而cls是类名,所以应该会报错。(__bridge id)是桥接,因为kc带*。

下面我们运行代码 我们发现没有报错,很正常的执行了方法,为什么能够调用成功? 首先我们要明白方法的调用,我们创建LGPerson对象,对象结构中bits存有saySomething方法,创建person对象,它的isa指向了LGPerson对象,通过指针平移找到saySomething方法进行调用,如下图: 同理Class cls = [LGPerson class];创建类对象,void *kc = &cls的指向到LGPerson,就跟也去查找方法进行调用。

拓展

上面我们知道为什么[(__bridge id)kc saySomething]方法能够调用成功,下面我们进行扩展,对LGPerson类的saySomething方法进行扩展,改为下面样子

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

我们打印一下self.lg_name,因为lg_name并没有赋值,猜测应该打印null。下面我们运行代码验证下: 看到上面方法打印的结果是ViewController,这是什么原因呢?下面我们就解释一下

内存平移

我们知道person对象调用saySomething方法,self.lg_name是通过指针平移8字节得到的,如下图: 我们看对kc进行打印分析 我们对kc打印,发现奇怪的东西,kc是LGPerson类的对象,他的isa指针指向了cls,这个时候就好理解了吧!kc调用saySomething,跟对象调用一样,通过isa指针找到类,在类中找到方法调用,self.lg_name是首地址进行偏移8字节的到的值,我们偏移8字节就是0x0000000103dab670,我们进行打印,发现就是ViewController。我们发现kc虽然是LGPerson对象,但是内存里存的和我们常看到LGPerson对象不同。这是什么情况呢?

我们知道方法执行的时候,都会将方法,对象入栈,我们知道参数会从前往后压入。我们下面看下结构体是怎样的一个压栈情况。看到下面方法

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

- (void)viewDidLoad {
    [super viewDidLoad];
    struct lg_struct lgs = {@(10),@(20)};
    LGPerson *person = [LGPerson alloc];
    NSLog(@"%p",&person);
}

我们创建一个结构体kc_struct,然后在viewDidLoad进行初始化,我们在NSLog上打断点,看下入栈的顺序,运行代码在断点进行打印: 通过我们的打印可以知道结构体入栈是从低到高压入的。下面我们写如下代码:

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

我们先推测下栈内都有什么,我们知道每个方法都有2个隐藏参数self,_cmd。 所以调用viewDidLoad就会有self,cmd方法,紧接着调用[super viewDidLoad]就会有(id)class_getSuperclass(objc_getClass("ViewController"))和self(看上面super调用方法的c++代码),接着就会调用[LGPerson class],这个时候就会有cls以及person对象。 下面我们写代码进行验证。准备如下代码:

  void *sp  = (void *)&self;
  void *end = (void *)&person;
  long count = (sp - end) / 0x8;

  for (long i = 0; i<count; i++) {
      void *address = sp - 0x8 * i;
      if ( i == 1) {
          NSLog(@"%p : %s",address, *(char **)address);
      }else{
          NSLog(@"%p : %@",address, *(void **)address);
      }
  }

这几句代码的意思:拿到self的地址作为首地址,拿到person的地址作为尾地址,因为地址是连续的,都是8字节。将首地址减去尾地址在除以8得到存了多少内容。然后开始遍历,将指针地址取出来,进行打印,因为第0个是self也就是ViewController,是字符串形式。这几句代码将所有入栈信息都打印出来

我们运行代码,看下打印结果,如下图 我们上面分析的是对的。而且连续。推测:kc是个特殊的Person,它的存储是入栈信息,它的isa指向创建的Person类cls

扩展

我们对LGPerson修改代码

@interface LGPerson : NSObject
@property (nonatomic, assign) int kc_name;
@property (nonatomic, copy) NSString *kc_hobby;
- (void)saySomething;
@end

然后运行,我们发现报错了,原是因为int是4字节,所以再平移4字节是是找不到内容的,所以会报错!

总结

这两道面试题,第一个将的self跟super调用class的区别,通过cpp的代码发现接收者都为self,super为关键字,只是super会让编译期直接去父类查找,节约时间。 第二题是内存平移,以及入栈的顺序问题,也看看到kc为特殊LGPerson,类似入栈顺序(我这部分没有弄清kc到底是什么东西,如果有人知道可以说一下)。