iOS 底层探究-----OC底层面试
前言
底层面试题
load 方法的调用、C++构造函数、initialize 之间的对比

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是由C、C++、 汇编实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能而已,并不是底层哦; -
运⾏时(
Runtime)是指将数据类型的确定由编译时推迟到了运⾏时,就好比:类扩展(extension)和分类(category)的区别;更加具备运行时,因为我们所有的类是在编译时,就加载完毕了,当加了Runtime这些api的使用,就把类里面的方法推迟到运行时,才加载,如:rwe,不在根据编译得到的machO进行获取数据,而是可以进行动态的处理,使我们面向对象能够面向切面。 -
平时编写的
OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代码,Runtime是Object-C的幕后工作者。
⽅法的本质,sel 是什么?IMP 是什么?两者之间的关系⼜是什么?
- ⽅法的本质:发送消息,消息会有以下⼏个流程
- 1:快速查找(
objc_msgSend)~cache_t缓存消息; - 2:慢速查找 ~ 递归⾃⼰|⽗类 ~
lookUpImpOrForward; - 3:查找不到消息:动态⽅法解析 ~
resolveInstanceMethod; - 4:消息快速转发 ~
forwardingTargetForSelector; - 5:消息慢速转发~
methodSignatureForSelector&forwardInvocation;
- 1:快速查找(
sel和impsel是⽅法编号 ~ 在read_images期间就编译进⼊了内存;imp就是我们函数实现指针,找imp就是找函数的过程;sel就相当于书本的⽬录tittle;imp就是书本的⻚码;
- 查找具体的函数就是想看这本书⾥⾯具体篇章的内容:
- 1:我们⾸先知道想看什么 ~
tittle(sel); - 2:根据⽬录对应的⻚码(
imp); - 3:翻到具体的内容;
- 1:我们⾸先知道想看什么 ~
能否向编译后的得到的类中增加实例变量?能否想运⾏时创建的类中添加实例变量
- 1、不能向编译后的得到的类中增加实例变量:
- 我们编译好的实例变量存储的位置在
ro,⼀旦编译完成,内存结构就完全确定; - 可以通过分类向类中添加方法和属性(关联对象);
- 我们编译好的实例变量存储的位置在
- 2、只要没有注册到内存还是可以添加的(没有执行
objc_registerClassPair),见下面代码:

未执行了objc_registerClassPair,可以添加,执行了,就添加不了了。
[self class]和 [super class] 的区别以及原理分析
创建两个类 LGPerson 和 LGTeacher,其中 LGTeacher 继承于 LGPerson,然后在LGTeacher里面打印类,如下图:

然而,根据打印结果,是两个LGTeacher,为什么了?
我们可以通过终端指令, clang -rewrite-objc LGTeacher.m -o LGTeacher.cpp得到LGTeacher.cpp,再在里面找到 [self class] 和 [super class] 的C++代码,如下:

既然在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 的执行情况:

根据汇编,知道真正调用的是objc_msgSendSuper2,那么我们看下他的结构:
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
可以发现,是和 objc_msgSendSuper 结构一样的。那么他们之间有什么联系了,我们直接去 objc 源码的汇编层去看:

-
可以看到当调用的是
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。
其实,self 和 super 本质是不一样的,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];
}
然而,打印的结果:

可以看到,两种情况都能打印出方法,第一种常规的初始化 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类。
- 图解如下:

内存偏移 案例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];
}
运行结果:

根据打印结果,发现person获取到了属性kc_name,而kc只是输出了其在栈中的数据信息。这是问什么了?
因为,常规初始化,LGPerson *person = [[LGPerson alloc] init]是开辟了内存空间的。所以 person 是有内存空间的,还有内存地址,而 kc 只有内存地址,没有内存空间。
那么 person 是怎么访问的 kc_name 的了?
我们都知道,属性的调用,都是存在 setter 和 getter 方法的。常规的 retain 调用,就是 self + offerset(偏移量),找到 kc_name 的地址位置,然后再读取。这也就是内存的平移。
我们可以通过终端,获取 LGPerson 类的 C++ 代码: clang -rewrite-objc LGPerson.m -o LGPerson.cpp得到LGPerson.cpp文件,在里面找到 kc_name 的 setter 和 getter 方法位置:

从底层 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);
}
}
}
看看打印结果:

我们再对这个打印结果分析下:
<ViewController: 0x7fa4ebf079b0>为- (void)viewDidLoad的id self压栈。viewDidLoad为- (void)viewDidLoad的SEL _cmd压栈。ViewController为结构体的class压栈。<ViewController: 0x7fa4ebf079b0>为结构体的receiver压栈。LGPerson为ssl压栈。<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