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
和imp
sel
是⽅法编号 ~ 在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