前言
“这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战”
底层面试题
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
方法;
附:详细底层原理:load和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
。