likes
comments
collection
share

OC底层面试题(一)附解题思路

作者站长头像
站长
· 阅读数 26

问题一:load方法在什么时候调用?

答:在load_images内部调用load方法。

  • 收集类的load方法 到loadable_classes表;
  • 收集分类的load方法添加到loadable_categories表,
  • 调用call_load_methods,内部调用两张load表里的load方法。

问题二:load_images在什么时候调用?

答:在dyly检测到新的镜像文件的时候,dyly会调用load_images。

问题三:load方法和initalize方法哪个先调用?

答:load方法更早。

  • load方法是在dyly链接镜像文件的时候就会去调用load_images,继而load_images会调用所有类和分类的load方法。

  • initalize是在第一次给对象发送消息的时候进行初始化调用。

问题四:load、initalize、C++这三个自起(自动调用)方法哪个先调用?

答:看C++定义在哪里。

  • C++方法如果写在objc工程里,那么执行顺序:C++方法 -> load方法 -> initalize方法

  • c++方法如果写在自己工程里,那么执行顺序:load方法 -> C++方法 -> initalize方法

补充:关于objc工程里的c++函数调用,在objc源码搜索_objc_init,可以看到内部调用了static_init函数。static_init函数是对C++函数静态函数的初始化

问题五:runtime是什么?它是底层吗?

答:runtime它是一种运行时机制,是由C或C++汇编写成的一套API,为OC面向对象运行时的一种功能,它不是底层。场景的运用场景:

  • Category分类属性、方法的加载;

  • addMethod(动态添加方法);

runtime的存在,让类的属性、方法在编译之后,还能够动态的添加绑定到类上。

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

答:不能向编译后的得到的类中增加实例变量;只要没注册到内存的类就还可以添加。

  • 编译好的实例变量存在于ro中,一旦编译完成,内存结构就完全确定,无法更改。
  • 关于向运行时创建的类中添加实例变量,这边有个案例和推导可以看下: 我们运行时创建类,代码是这样的:
Class Person = objc_allocateClassPair(NSObject.class, "Person", 0);

class_addIvar(Person, "_name", sizeof(NSString *), log2(sizeof(NSString *)), "@");

objc_registerClassPair(Person);

主要用到了3个函数:

  • objc_allocateClassPair(获取成员变量Ivar
  • class_addIvar(动态添加成员变量
  • objc_registerClassPair(注册class

而且「class_addIvar」的执行是在objc_allocateClassPair之后在objc_registerClassPair之前。至于为什么是这样,我们可以通过源码来分析一下:

首先看下objc_allocateClassPair干了什么?
Class objc_allocateClassPair(Class superclass, const char *name, 
                             size_t extraBytes)
{
    ...
    objc_initializeClassPair_internal(superclass, name, cls, meta);
    ...
}

进入objc_initializeClassPair_internal

static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
{
    ...
     cls_rw_w->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
     meta_rw_w->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING | RW_META;
     ...
}

我们可以看到,objc_allocateClassPair对flag标记为RW_CONSTRUCTING。

看下class_addIvar干了什么?
BOOL 
class_addIvar(Class cls, const char *name, size_t size, 
              uint8_t alignment, const char *type)
{
    if (!cls) return NO; // 非空判断
    ...
    if (cls->isMetaClass()) {// 是元类就不能添加
            return NO;
    }
    // 如果类已经分配但未注册,就跳过判断
    if (!(cls->data()->flags & RW_CONSTRUCTING)) {
        return NO;
    }
    // 如果ro里面已经存在这个tIvar,就跳过判断
    if ((name  &&  getIvar(cls, name))  ||  size > UINT32_MAX) {
        return NO;
    }
    ...
}

/** 以上几个判断,说明满足以下几个条件才可以添加实例变量:
**
* 1、cls有值
* 2、cls不是元类
* 3、类已经分配但未注册(重点)
* 4、ro里面不存在这个tIvar
*/

我们要关注的点在这里:cls->data()->flags 什么时候发生变化?

看下objc_registerClassPair干了什么?
void objc_registerClassPair(Class cls)
{
    ...
    // cls已分配且已注册 或 cls的ISA指向已分配且已注册,就返回
    if ((cls->data()->flags & RW_CONSTRUCTED)  ||
    (cls->ISA()->data()->flags & RW_CONSTRUCTED)) 
    {
        _objc_inform("objc_registerClassPair: class '%s' was already "
                     "registered!", cls->data()->ro()->getName());
        return;
    }
    //(非)cls已分配但未注册 或 (非)cls的ISA指向已分配但未注册,就返回
    if (!(cls->data()->flags & RW_CONSTRUCTING)  ||  
        !(cls->ISA()->data()->flags & RW_CONSTRUCTING))
    {
        _objc_inform("objc_registerClassPair: class '%s' was not "
                     "allocated with objc_allocateClassPair!", 
                     cls->data()->ro()->getName());
        return;
    }
    /// 将状态改为RW_CONSTRUCTED,且清除之前的标记
    cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
    cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
    ...
}

所以,要执行objc_registerClassPair,需要满足已分配但未注册objc_registerClassPair函数一旦执行,flag标记就变成了RW_CONSTRUCTED,自然就不能再执行class_addIvar。

总结:
  • objc_allocateClassPair(flag设置为RW_CONSTRUCTING
  • class_addIvar(执行的条件之一是flag为RW_CONSTRUCTING
  • objc_registerClassPair(flag设置为RW_CONSTRUCTED

问题七: [self class] 和 [super class]的区别及原理是什么?

答:首先,我们写个测试案例打印一下:

/// DirectionChild.m内部代码:
- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"查看[self class] 和 [super class]区别---------%@->%@",[self class],[super class]);
    }
    return self;
}
/// main函数
int main(int argc, const char * argv[]) {
    DirectionChild *childClass = [[DirectionChild alloc] init];
}

OC底层面试题(一)附解题思路

我们发现两个打印的结果一样,这是为什么呢??

OC底层面试题(一)附解题思路

回到objc源码工程,command+单机进入class方法:

OC底层面试题(一)附解题思路

查看object_getClass

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

所以第一个self打印DirectionChild就不言而喻了。

接下来看[super class],

super不知道来自哪里,我们通过clang看一下编译后的源码是什么样的。指令如下:

  • cd到指定文件的上层文件夹目录
  • $xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc DirectionChild.m 打开得到的DirectionChild.mm,为了快速定位,搜索「DirectionChild」:

OC底层面试题(一)附解题思路

提取[super class]部分代码:

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

我们发现[super class]实际调用的是objc_msgSendSuper,

接下来,我们通过Debug查看下运行时的实际情况: OC底层面试题(一)附解题思路

OC底层面试题(一)附解题思路

当前运行的是mac工程,那么真机运行下,是否也是这样呢? 这里,我们新建了一个测试工程,运行看效果:

OC底层面试题(一)附解题思路

OC底层面试题(一)附解题思路

看到这里是不是有点懵逼,clang静态编译的时候调用objc_msgSendSuper动态运行的时候调用的居然是objc_msgSendSuper2

为了探索这个变化,我们

objc源码工程全局搜索objc_msgSendSuper

OC底层面试题(一)附解题思路

当前文件搜索L_objc_msgSendSuper2_body,发现它会进入_objc_msgSendSuper2里面:

OC底层面试题(一)附解题思路

回到前面的问题,[super class]打印:

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

简化一下代码:

((void *)objc_msgSendSuper)((__rw_objc_super){
            (id)self,
        (id)class_getSuperclass(objc_getClass("DirectionChild"))
        },sel_registerName("class"));

第一个参数是self,我们前面分析过,class最终返回的是class函数第一个参数的isa指向,self的isa指向是DirectionChild类,所以[self class][super class]都打印DirectionChild

问题七: 通常我们在给一个类写init初始化方法的时候,都会写self=[super init],这是为什么?

答:这样写是为了更好的继承父类定义的一些公共属性、方法、协议等等。

问题八:请问下面main函数运行会报错吗?

// Direction.h
#import <Foundation/Foundation.h>
@interface Direction : NSObject
@property (nonatomic , strong) NSString *hobby;
@end

// Direction.m
@implementation Direction
- (void)run{
    NSLog(@"run faster.");
}
@end

// main.m
int main(int argc, const char * argv[]) {

    Direction *dt = [Direction alloc];
    [dt run];

    Class cls = [Direction class];
    void *ssj = &cls;
    [(id)ssj run];
    
    return 0;
}

运行代码:

OC底层面试题(一)附解题思路

事实证明,可以正常运行,控制台也打印出来了东西。那么,为什么可以不通过实例化对象就调用类的实例方法呢?

  1. 首先,我们要知道,方法的调用本质上就消息的发送objc_msgSend
  2. objc_msgSend第一个参数是recever消息接收者,通过它找到Class类
  3. 然后再从Class类的data里,通过objc_msgSend的第二个参数SEL去找到对应的IMP并调用。
  4. dt作为一个Direction实例对象,通过它的isa可以找到Direction类
  5. ssj指针指向的就是Direction类地址,可以找到Direction类
  6. 所以对于objc_msgSend来说,只要能找到Class类,第一个参数是dt或ssj都可以。

下面是「dt」和「ssj」指向Direction类

OC底层面试题(一)附解题思路

OC底层面试题(一)附解题思路

拓展:对run方法进行改造,查看打印的数据。

// Direction.m
@implementation Direction
- (void)run{
    NSLog(@"run faster.->%@",self.hobby);
}
@end

运行:

OC底层面试题(一)附解题思路

  • 第一个打印null没什么疑问,因为没有对hobby赋值;

  • 第二个为什么打印的是Direction类呢?

答:dt作为一个实例对象,alloc开辟了空间,它有指针地址、也有内存。而ssj就是一个纯粹指向Direction类地址的指针,系统并没有为它开辟内存。因此hobby指向ssj指针地址的下一个地址(栈帧里的如栈顺序,也就是dt)。 我们来通过控制台打印,看下数据的变化:

OC底层面试题(一)附解题思路

然后在Directionrun方法下断点:

OC底层面试题(一)附解题思路

OC底层面试题(一)附解题思路

这时候,我们有个猜想

  • 通过类地址指针ssj访问类的属性,得到的是数据可能跟ssj指针所在栈帧有关系。

为了验证这个猜想,我们对代码进行了添加:

// Direction.h
#import <Foundation/Foundation.h>
@interface Direction : NSObject
@property (nonatomic , strong) NSString *firendNames;
@property (nonatomic , strong) NSString *firendNames2;
@property (nonatomic , strong) NSString *hobby;
@end


// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    Direction *dt = [Direction alloc];
    [dt run];
    /// 添加3个实例变量zo1、zo2、zo3
    Zoon *zo1 = [Zoon new];
    Zoon *zo2 = [Zoon new];
    Zoon *zo3 = [Zoon new];
    
    Class cls = [Direction class];
    void *ssj = &cls;
      [(__bridge id)ssj run];
}

@end

继续重复上面的控制台打印:

OC底层面试题(一)附解题思路

Directionrun方法下断点:

OC底层面试题(一)附解题思路

打印结果验证了上面的猜想:

  • 通过类地址指针ssj访问类的属性,得到的是数据跟ssj指针所在栈帧有关系,按照入栈顺序打印。

问题九:请问以下代码的压栈顺序是怎么样的?

结构体压栈
- (void)viewDidLoad {
    [super viewDidLoad];
    /// 测试插入顺序
    Direction *dt1 = [Direction alloc];

    struct SSJStruct sSJStruct = {@1,@22};
    
    Direction *dt2 = [Direction alloc];
    NSLog(@"测试结束");
}

答:

  • dt1、sSJStruct、dt2,以此由高地址向低地址压栈。

  • sSJStruct内部,低地址向高地址压栈。

运行代码:

OC底层面试题(一)附解题思路

参数压栈
// 自定义函数
int ssj_fun(id obj1,id obj2){
    return 0;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    /// 测试参数压栈
    Direction *dt1 = [Direction alloc];

    Direction *dt2 = [Direction alloc];
    
    ssj_fun(dt1, dt2);
    NSLog(@"测试结束");
}

下断点,运行代码:

OC底层面试题(一)附解题思路

分别打印dt1、dt2、obj1、obj2,其中dt1、dt2是指针地址,obj1、obj2是值的地址。 根据栈的压栈从高到低顺序,压栈顺序先obj1再obj2。

问题十:[super viewDidLoad] 里的super传的是当前Controller还是当前Controller的父类?

// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad]
    // 实际运行会调用函数  objc_msgSendSuper2
    // objc_msgSendSuper2定义如下
    objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
}

那么,这里的super传的是ViewController还是UIViewController? 带着这个疑问,我们准备实际操作打印一下:

OC底层面试题(一)附解题思路

下符号断点objc_msgSendSuper2

OC底层面试题(一)附解题思路

输入register read查看寄存器

OC底层面试题(一)附解题思路

控制台,打印x0,即第一个参数的数据:

OC底层面试题(一)附解题思路

代码:

链接: pan.baidu.com/s/1uPGdFij0… 密码: il7h --来自百度网盘超级会员V2的分享

转载自:https://juejin.cn/post/7000620015045050375
评论
请登录