likes
comments
collection
share

iOS 底层探究-----OC底层面试

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

前言

底层面试题

load 方法的调用、C++构造函数、initialize 之间的对比

iOS 底层探究-----OC底层面试

load的调用

  • load方法是在应用程序加载过程中(也就是dyld的加载流程中)被调用,而且是发生在 main 函数之前的。其中load方法在dyld加载流程中被调用的流程是:doModInitFunctions -> libSystem_initializer -> _objc_init -> _dyld_objc_notify_register ->load_images ->... 。

  • 在底层_objc_init的过程中进行注册,回调load_images,加载两个load加载表,先是类的 load 表,再是分类的 load 表;

  • objc 底层中,在对类的load方法进行处理时,是进行了递归操作的,目的是为了保证父类优先被处理, 所以load方法的调用顺序:父类 --> 子类 --> 分类;然而在分类中,load方法的调用顺序根据编译顺序为准;

initialize的调用

  • objc 中,initialize的调用,是在第一次消息发送的时候 lookupimporforward,所以说调用顺序,是load方法优先于initialize方法;

  • 分类的⽅法是在类realize之后attach进去的插在前⾯,所以如果分类中有initialize这个方法,会优先调⽤分类的这个方法,值得注意的是,并不是分类覆盖主类哦;

  • 同样的,如果子类没有实现initialize方法,就会查找并调用父类的initialize方法,并且会调用两次(isa 的走位图),如果子类和父类都实现了initialize,那么会优先调用父类的,再调用子类的(递归);

C++ 构造函数的调用

  • 如果C++构造函数是在objc源码中,那么他的调用流程:doModInitFunctions -> libSystem_initializer -> _objc_init -> static_init -> getLibobjcInitializers。而 load 方法 是在 _dyld_objc_notify_register函数后调用的,所以此时,是先调用 C++ 构造函数,再调用 load 方法;

  • 如果写在main函数里面,或者自己的代码中,那么则是先调用+load方法,再调用C++函数,再调用main函数;

小结

所以,

  • 如果C++构造函数是在objc源码中,那么三者的调用顺序是:C++ 构造函数 --> load 方法 --> initialize 方法;
  • 如果只是普通的C++函数,那么调用顺序是:load 方法 --> C++ 函数 --> initialize 方法;

Runtime 是什么?

  • 其实 runtime 是由 CC++、 汇编实现的⼀套 API,为 OC 语⾔加⼊了⾯向对象,运⾏时的功能而已,并不是底层哦;

  • 运⾏时(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时,就好比:类扩展(extension)和分类(category)的区别;更加具备运行时,因为我们所有的类是在编译时,就加载完毕了,当加了Runtime这些 api的使用,就把类里面的方法推迟到运行时,才加载,如:rwe,不在根据编译得到的 machO 进行获取数据,而是可以进行动态的处理,使我们面向对象能够面向切面。

  • 平时编写的OC代码,在程序运行过程中,其实最终会转换成RuntimeC语言代码,RuntimeObject-C 的幕后工作者。

⽅法的本质,sel 是什么?IMP 是什么?两者之间的关系⼜是什么?

  • ⽅法的本质:发送消息,消息会有以下⼏个流程
    • 1:快速查找(objc_msgSend)~ cache_t 缓存消息;
    • 2:慢速查找 ~ 递归⾃⼰|⽗类 ~ lookUpImpOrForward
    • 3:查找不到消息:动态⽅法解析 ~ resolveInstanceMethod
    • 4:消息快速转发 ~ forwardingTargetForSelector
    • 5:消息慢速转发~ methodSignatureForSelector&forwardInvocation
  • selimp
    • sel 是⽅法编号 ~ 在 read_images 期间就编译进⼊了内存;
    • imp 就是我们函数实现指针,找 imp 就是找函数的过程;
    • sel 就相当于书本的⽬录 tittle
    • imp 就是书本的⻚码;
  • 查找具体的函数就是想看这本书⾥⾯具体篇章的内容:
    • 1:我们⾸先知道想看什么 ~ tittle(sel);
    • 2:根据⽬录对应的⻚码(imp);
    • 3:翻到具体的内容;

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

  • 1、不能向编译后的得到的类中增加实例变量:
    • 我们编译好的实例变量存储的位置在ro,⼀旦编译完成,内存结构就完全确定;
    • 可以通过分类向类中添加方法和属性(关联对象);
  • 2、只要没有注册到内存还是可以添加的(没有执行objc_registerClassPair),见下面代码:

iOS 底层探究-----OC底层面试

未执行了objc_registerClassPair,可以添加,执行了,就添加不了了。

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

创建两个类 LGPersonLGTeacher,其中 LGTeacher 继承于 LGPerson,然后在LGTeacher里面打印类,如下图:

iOS 底层探究-----OC底层面试

然而,根据打印结果,是两个LGTeacher,为什么了?

我们可以通过终端指令, clang -rewrite-objc LGTeacher.m -o LGTeacher.cpp得到LGTeacher.cpp,再在里面找到 [self class][super class]C++代码,如下:

iOS 底层探究-----OC底层面试

既然在C++里面,[super class]对应的是objc_msgSendSuper函数,那么我们去 objc 源码里面查找 objc_msgSendSuper,看一下它的结构:

objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

objc_super 的结构是:

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};

通过objc_super函数能找到 receiver(接收者)和 super_class(父类),作为 objc_msgSendSuper的参数,传入进去。

我们可以通过设置断点,看汇编,来跟踪 objc_super 的执行情况:

iOS 底层探究-----OC底层面试

根据汇编,知道真正调用的是objc_msgSendSuper2,那么我们看下他的结构:

objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

可以发现,是和 objc_msgSendSuper 结构一样的。那么他们之间有什么联系了,我们直接去 objc 源码的汇编层去看:

iOS 底层探究-----OC底层面试

  • 可以看到当调用的是objc_msgSendSuper2函数,而传入的是当前类 (receiver --> self),通过汇编源码来获取父类,再调用CacheLookup函数;

  • 如果调用的是objc_msgSendSuper函数,直接传入的就是获取好的父类,然后跳到L_objc_msgSendSuper2_body也调到了CacheLookup函数。

所以[super class]是从父类开始查找方法,但是最终拍板的还是要看class的方法实现。

- (Class)class { 
    return object_getClass(self); 
} 

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

所以说,self是我们传入的隐藏参数也就是LGTeacher对象,那么它的isa自然就是LGTeacher类,从而 [super class] 打印出来的,还是 LGTeacher

其实,selfsuper 本质是不一样的,self 是一个形参名,而 super 却是个关键字(编译器的指示器)。比如说,self = [super init]就是为了把父类的一些内容给带过来,子类好继承并使用。

内存偏移 案例一

在此案例中,先创建LGPerson类,再实现一个实例方法saySomething,然后在控制请 ViewController 里面,用两种方式来调用LGPerson类。

  • LGPerson 中;
@implementation LGPerson 
- (void)saySomething{ 
    NSLog(@"%s - %@",__func__); 
} 
@end
  • ViewController 中:
- (void)viewDidLoad {    
    [super viewDidLoad];
    //正常实现
     LGPerson *person = [LGPerson alloc]; 
     [person saySomething]; 
     
     //桥接实现
     Class cls = [LGPerson class]; 
     void  *kc = &cls; 
     [(__bridge id)kc saySomething]; 
  }

然而,打印的结果:

iOS 底层探究-----OC底层面试

可以看到,两种情况都能打印出方法,第一种常规的初始化 LGPerson类,调用 saySomething 方法,是容易理解的,其本质就是消息的发送 ---- objc_msgSend

但是桥接的方法,也能调用动 saySomething 方法,就有点意思了。

  • 消息发送,通过对象的isa指针,找到类地址,然后再进行地址平移,通过sel找到对应的方法的imp

正如 [person saySomething]的原理----首先是根据person对象的isa指针,找到其对应的类LGPerson,然后在类中进行地址平移,先在缓存(cache_t)中快速查找,如果找不到,就在方法列表中查找(子类和父类的方法列表),二分法查找,找到之后,就返回调用。

[(__bridge id)kc saySomething] 了? &kc 的地址是指向 class 的,而cls就是class,指向一个objc_class的指针,所以 kc 也指向LGPerson类。

  • 图解如下: iOS 底层探究-----OC底层面试

内存偏移 案例2

  • 修改 LGPerson 类的设置:
@interface LGPerson : NSObject 
// .h
@property (nonatomic, copy) NSString *kc_name;
- (void)saySomething; 

@end 

// .m
@implementation LGPerson 

- (void)saySomething { 
    NSLog(@"%s - %@",__func__,self.kc_name); 
  } 
@end
  • viewDidLoad中也更改代码:
- (void)viewDidLoad { 
[super viewDidLoad]; 

    LGPerson *person = [[LGPerson alloc] init];         
    person.kc_name = @"name123";
    [person saySomething];
    
    Class cls = [LGPerson class]; 
    void *ssl = &cls;
    [(__bridge id)ssl saySomething]; 
}

运行结果:

iOS 底层探究-----OC底层面试

根据打印结果,发现person获取到了属性kc_name,而kc只是输出了其在栈中的数据信息。这是问什么了?

因为,常规初始化,LGPerson *person = [[LGPerson alloc] init]是开辟了内存空间的。所以 person 是有内存空间的,还有内存地址,而 kc 只有内存地址,没有内存空间。

那么 person 是怎么访问的 kc_name 的了?

我们都知道,属性的调用,都是存在 settergetter 方法的。常规的 retain 调用,就是 self + offerset(偏移量),找到 kc_name 的地址位置,然后再读取。这也就是内存的平移。

我们可以通过终端,获取 LGPerson 类的 C++ 代码: clang -rewrite-objc LGPerson.m -o LGPerson.cpp得到LGPerson.cpp文件,在里面找到 kc_namesettergetter 方法位置:

iOS 底层探究-----OC底层面试

从底层 C++ 代码里面,就知道,kc_name 的获取,是通过内存地址偏移得到。其实,就是用 person 的地址,加上偏移量,就得到 kc_name 的地址。

内存偏移 案例3

  • LGPerson 类再添加一个 kc_hobby 属性:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *kc_name;
@property (nonatomic, copy) NSString *kc_hobby;

- (void)saySomething;
@end
  • viewDidLoad 重新设置
// 1: 参数 会从前往后一直压
// 2: 结构体的属性 是怎么一个压栈情况 self superclass
struct kc_struct{
    NSNumber *num1;
    NSNumber *num2;
} kc_struct;

- (void)viewDidLoad {
    [super viewDidLoad];

    // ViewController 当前的类
    // self cmd (id)class_getSuperclass(objc_getClass("LGTeacher")) self cls kc person

    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<count; i++) {
        void *address = sp - 0x8 * i;
        if ( i == 1) {
            NSLog(@"%p : %s",address, *(char **)address);
        }else{
            NSLog(@"%p : %@",address, *(void **)address);
        }
    }
}

看看打印结果:

iOS 底层探究-----OC底层面试

我们再对这个打印结果分析下:

  • <ViewController: 0x7fa4ebf079b0>- (void)viewDidLoadid self压栈。
  • viewDidLoad- (void)viewDidLoadSEL _cmd压栈。
  • ViewController为结构体的class压栈。
  • <ViewController: 0x7fa4ebf079b0>为结构体的receiver压栈。
  • LGPersonssl压栈。
  • <LGPerson: 0x7ffee0a6f158>person压栈。

压栈,地址从大到小,新进去的地址大

说到压栈,那什么东西才会压栈了?

  • 方法的可使用参数,就可以进行压栈,如viewDidLoad(id self, SEL _cmd),这些是隐藏参数。参数在压栈时是根据参数的顺序进行的,第一个参数先入栈,然后依次压栈;
  • objc_msgSendSuper,内部会自动创建 一个 objc_super 的临时变量,相当于创建一个 person 对象一样,所以会压栈。还有就是对于objc_super这种结构体参数,如下面代码:
struct objc_super {     
    __unsafe_unretained _Nonnull id receiver;           
    __unsafe_unretained _Nonnull Class super_class; 
 };
转载自:https://juejin.cn/post/6992603560596750366
评论
请登录