iOS底层原理18:OC底层面试题分享

994 阅读6分钟

这是我参与8月更文挑战的第7天,活动详情查看: 8月更文挑战

loadinitialize方法的调用原则和顺序?

load方法

  • load方法是在应用程序加载过程中,在main函数之前,也就是dyld加载过程中完成调用的;
  • 在底层load_images方法被调用时,维护了两张表格,一张用来存放load方法,另一张用来存放分类load方法;
  • 优先调用load方法;
  • 在对类的load方法处理时,进行递归调用来确保父类的方法能被优先处理;
  • load方法调用顺序为:父类子类分类
  • 分类中load方法的调用顺序有变异顺序决定;

initialize方法

  • initialize在第一次消息发送时进行调用,所以load方法比initialize方法先调用;
  • 分类的initialize方法是会插在最前面,以确保分类的initialize优先调用;

C++构造函数的调用

  • dyld流程分析的时候,我们知道其调用顺序为:loadC++构造函数main
  • 如果是在源码工程objc中的C++构造函数,在objc_init()的时候,会通过static_init方法优先调用C++构造函数;

runtime是什么?

  • runtime是由CC++汇编实现的一套Api,为OC语言加入了面向对象运行时的功能;
  • 运行时是指将数据类型的确定由编译时推迟到了运行时;比如extension和category的区别;
  • 平时编写的OC代码,在程序运行过程中,其实最终会转换成RuntimeC语言代码,RuntimeObject-C的幕后工作者;

方法的本质是什么?sel是什么?imp是什么?它们之前有什么关系?

方法的本质是发送消息,流程如下:

  • 1.快速查找(objc_msgSend)~cache_t缓存消息;
  • 慢速查找~递归自己|父类~lookUpImpOrForward
  • 查找不到消息:动态方法解析~resolveInstanceMethod;
  • 消息快速转发~forwardingTargetForSelector
  • 消息慢速转发~methodSignatureForSelector & forwardInvoation

sel是方法编号,在read_images期间就编译进入内存; imp就是函数实现的指针,找imp就是找函数的过程; 举个例子:sel相当于书本的目录,imp就是书本的页码;查找具体的函数就是想看这本书里边具体篇章的内容; 1.我们想看什么?sel 2.根据目录找到页码;imp 3.翻到内容所在页;

能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?

  • 不能向编译后得到的类中增加实例变量
  • 只要类没有被注册到内存中就可以添加实例变量 原因:编译好的实例变量存储在ro中,它是只读的,一旦编译完成,内存结构就完全确定,无法修改;可以增加属性和方法;

[self class][super class]的区别及原理分析?

我们创建一个Person类,及其子类Teacher;然后在Teacherinit方法中作如下打印:

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

查看打印结果:

按照我们正常的理解,应该打印一个Teacher和一个Person才对,但是为什么打印了两个Teacher呢?

接下来,我们来看一下class方法的实现:

最终调用的是objc_getClass(self),此处的self是从隐藏参数中(id self, sel _cmd)中传递过来的,也就是消息发送objc_msgSend(id receiver, ...)中的receiver,所以当调用[self class]objc_getClass(self)中的self指的是Teacher

接下来看objc_getClass的实现:

其实现是返回类的isa,因为TeacherisaTeacher,所以返回[self class]打印的是:Teacher

那么为什么[super class]打印的也是Teacher呢?我们通过clang看一下Teacher.mcpp文件:

我们发现起底层调用的是objc_msgSendSuper方法,我们在源码工程中查看objc_msgSendSuper方法:

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

起第一个参数是个结构体类型的objc_super,我们查看其结构:

其实简化之后,就是id receiverClass super_class两个成员,根据注释super_class is the first class to search可以知道super_class表示第一个要查找的类,而不是父类;那么整个调用逻辑就清晰了:

Teacher中调用[super class]其底层会调用objc_msgSendSuper方法,传入参数receiverTeachersuper_class是第一个要查找的类,也就是Teacher类的父类Person;因为[super class]receiverTeacher,所以最终打印的是Teacher

其调用objc_msgSendSuper时,在底层最终调用的是objc_msgSendSuper2

内存平移

我们先来看一段代码的执行结果:

两种方式都能调用saySomething方法,这是为什么呢?

第一种我们正常调用可以理解,那么为什么第二种方式也能调用呢?

要想解决这个疑问,首先需要明白,为什么person能调用saySomething方法?

通过我们之前的知识积累可以明白person之所以能调用saySomething方法,是因为能够通过person对象的isa找到对应的类,在类中进行地址平移找到cache_t,然后进行方法的查找;

此处kc指针指向LGPerson类的首地址,那么就依然能够通过内存平移找到方法;

接下来,我们修改一下saySomething方法的打印:

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

运行,查看打印结果:

因为没有给kc_name属性进行赋值,所以[person saySomething]打印个null;但是为什么调用[(__bridge id)kc saySomething]的时候,打印的是person对象呢?虽然都是调用了saySomething,但是结果不同,说明personkc是有区别的。

person是一个对象,它开辟了内存空间,里边存放了isa成员变量,而kc只是一个指针地址,没有内存空间。如下:

person访问kc_name的时候,是通过内存平移的方式访问的,也就是self+kc_name的偏移量offset来找到kc_name的地址,然后获取值;也就是person的收地址向下偏移8获取到kc_name得值;

那么我们修改代码验证一下:

那我们看一下kc的地址偏移8之后是什么?

所以打印的是person对象;

接下来,我们修改代码如下:

kc_name属性上方,再加一个属性kc_hobby,然后运行打印:

只有kc_name一个属性的时候,person偏移8,现在在kc_name之前加了个kc_hobby,就需要偏移16也就是0x10才能够访问到kc_name的值,代码验证:

那么,kc偏移0x10之后怎么找到了ViewController的对象呢?

要解决这个问题,首先我们需要分析一下压栈逻辑,根据前文中的结论,我们知道[super viewDidLoad]此处也是一个结构体,那么结构体是如何亚栈的呢?接下来我们来自定义结构体验证一下;

自定义一个结构体:

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

修改代码如下:

打印内存地址,可以分析出栈的内存如下:

在看一下结构体数据的存放地址:

说明num1低地址num2在高地址;

那么回到我们最初的代码:

kc偏移0x10之后其实是找到了[super viewDidLoad]对应的结构体中的class,而class指的就是ViewController,其receiver就是ViewController对象;

{num1, num2}结构体压栈,num1在低地址,num2在高地址

那么参数是怎么压栈的呢?

(id p1, id p2)参数压栈,p1在高地址,p2在低地址

接下来,通过一段测试代码验证一下我们的结论: