oc
底层探索了很多了,今天主要总结一下一些相关的面试题。
load
方法什么时候调用。
这个问题很多同学应该都知道,就是main
函数之前,但是main
之前的哪一步执行的,可能有些同学就疑惑了,,同时还有一个方法的调用时机也会被经常问到就是initialize
,我们分别讨论。
load
方法
load
方法是在应用程序加载过程中调用的,确实是在main
函数之前调用。- 具体是
_dyld_objc_notify_register
方法的第二个参数load_images
回调的。 - 通过
prepare_load_methods
递归查找load
方法添加到一个load
方法的加载表loadable_classes
里,注意父类会比子类先加入到表中,查找完类的load
方法之后,查找分类的load
也会添加到一个loadable_categories
表中。 - 最后是
call_load_methods
调用load
方法,先从loadable_classes
表里循环调用类的load
方法,然后从loadable_categories
表里循环调用分类的load
方法。 - 因为是顺序遍历表调用
load
方法的。所以load
方法的调用次序是父类
>本类
>分类
。 - 如果有多个分类都有
load
方法,其调用顺序会根据编译的顺序调用。编译顺序可以在Compiles Sources
里调整。 load
方法过多会影响到应用的启动速度。
initialize
方法。
initialize
方法是在第一次objc_msgSend
的时候调用的,它的调用时机晚于load
。- 分类的方法是在类
realize
之后attachCategorys
进去的,会在类的方法前面。如果分类实现了initialize
方法,会优先调用分类的方法。
Runtime
是什么
runtime
是由c
、c++
、汇编
实现的一套API
,为oc
语言加入面向对象运行时功能。运行时
是指讲数据类型的确定有编译时
推迟到了运行时
。- 我们写的
oc
代码,在程序运行过程中,最终都会转换成runtime
的c
语言代码。
⽅法的本质,sel
是什么?IMP
是什么?两者之间的关系是什么
⽅法的本质
⽅法的本质是消息发送,即objc_msgSend
,它的流程是:
- 快速查找 (
objc_msgSend
)~cache_t
缓存消息 - 慢速查找~ 递归⾃⼰或⽗类 ~
lookUpImpOrForward
- 查找不到消息: 动态⽅法解析 ~
resolveInstanceMethod
- 消息快速转发 ~
forwardingTargetForSelector
- 消息慢速转发 ~
methodSignatureForSelector
和forwardInvocation
sel
是什么
sel
是⽅法编号,在read_images
期间就加载进⼊了内存。它实际是objc_selector
结构体。
IMP
是什么
imp
就是我们函数实现指针,找imp
就是找函数实现的过程。
sel
与IMP
的关系
sel
就相当于书本的⽬录title
imp
就是书本的⻚码- 方法调用的时候首先根据
sel
找到imp
最后到具体函数的实现,完成调用。
能否向编译后的类中增加实例变量?能否向运⾏时创建的类中添加实例变量?
不能向编译后的得到的类中增加实例变量:
- 我们编译好的实例变量存储的位置在
ro
(read only),⼀旦编译完成,内存结构就完全确定。 - 我们可以通过
分类
向类中添加方法
和属性
(通过关联对象)。
- 我们编译好的实例变量存储的位置在
可以向运行时创建的类中添加实例变量,只要类没有注册到内存还是可以添加。
这里
运行时创建的类
指的是通过objc_allocateClassPair
方法,创建的类
,在调用objc_registerClassPair
方法之前是可以添加实例变量
的。
[self class]和[super class]区别
先定义两个类JSPerson
和JSStudent
,其中JSStudent
继承于JSPerson
。
1 | @interface JSPerson : NSObject |
在main
函数里实例化一个JSStudent
对象。
1 | int main(int argc, const char * argv[]) { |
发现打印结果为JSStudent - JSStudent
。这是为什么呢,我们下面分析一下。
首先,JSPerson
和JSStudent
类都没有实现class
方法,根据消息发送查找流程,会调用NSObject
类的class
方法,它的实现为
1 | - (Class)class { |
class
方法的作用是返回当前的类,self
是调用的对象也就是student
实例。
[self class]
打印的是JSStudent
很好理解,因为消息接受者就是JSStudent
的实例对象,通过isa
找到的就是JSStudent
类。[super class]
打印的也是JSStudent
就让人困惑了,我们打开汇编调试
看一下[super class]
的底层调用了什么[super class]
实际调用的是objc_msgSendSuper2
方法,我们在源码看一下这个方法的定义:1
2
3
4// objc_msgSendSuper2() takes the current search class, not its superclass.
OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);其实看
objc_msgSendSuper2
的注释就可以看出来,方法查找的是本类
而不是它的父类
。继续看,
super
是方法的第一个参数,也就是objc_super *
它的结构是:1
2
3
4
5
6
7
8
9
10
11
12
13
14struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
/* For compatibility with old objc-runtime.h header */
///old结构,我们可以忽略 !__OBJC2__使用
__unsafe_unretained _Nonnull Class class;
__unsafe_unretained _Nonnull Class super_class;
/* super_class is the first class to search */
};objc_super
的成员变量有receiver
它是当前的调用的实例也就是student
,super_class
是当前实例对象的类的父类
。所以说[super class]
的消息接受者self
还是student
,所以打印的是JSStudent
,super
是一个关键字。内存平移问题
案例说明:
JSPerson
类中有一个saySomething
方法,在ViewController
类的viewDidLoad
通过两种方式调用,详细看代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19@implementation JSPerson
- (void)saySomething{
NSLog(@"%s",__func__);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
JSPerson *person = [JSPerson alloc];
[person saySomething];
Class cls = [JSPerson class];
void *js = &cls;
[(__bridge id)js saySomething];
}
@end问题是这两个调用
saySomething
的语句是否有问题。[person saySomething]
:这种方式没什么疑问,正常的方法调用。它的流程是通过person
对象的isa
指针找到类JSPerson
,首先通过内存平移找到cache
里查找,如果找不到,继续平移找到bits
查找方法列表查找,最后找到imp
调用。[(__bridge id)js saySomething]
:运行代码,我们这一行代码也正常执行了,原因是什么呢void *js = &cls;
说明js
是一个指向JSPerson
类首地址的指针,它和对象的isa
指向的是同一个地址,通过内存平移也可以找到对应的方法。
拓展
在
saySomething
方法里增加属性self.js_name
的打印,其他不变1
2
3
4
5
6
7
8
9
10
11
12
13@interface JSPerson : NSObject
@property (nonatomic, strong) NSString *js_name;
- (void)saySomething;
@end
@implementation JSPerson
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.js_name);
}
@end再次运行程序发现打印结果为:
1
2-[JSPerson saySomething] - (null)
-[JSPerson saySomething] - <JSPerson: 0x600003a00380>[person saySomething]
:因为我们没有对js_name
赋值,[person saySomething]
打印(null)
正常。[(__bridge id)js saySomething]
:这里打印了<JSPerson: 0x600003a00380>
很困惑。我们首先要清楚
self.js_name
是怎么找到js_name
并打印的,它是从person
内存地址中平移8位(isa
是8位)找到第一个属性js_name
。类比
js
也需要平移8位找js_name
,由于js
是一个指针,存在栈
上的,栈是一个先进后出
的数据结构,每次参数传入就会压栈。其中
隐藏参数会压入栈
,且每个函数都会有两个隐藏参数(id self,sel _cmd)
,这个我们前面探索过,可以通过clang
将oc
代码转成c++
代码查看。隐藏参数压栈
的过程,其地址是递减
的,而栈是从高地址->低地址 分配
的,即在栈中,参数会从前往后一直压
前面还有一行
[super viewDidLoad];
,super
调用的压栈我们也需要研究一下,其实上一题我们研究过它实际调用的是objc_msgSendSuper2
,有两个参数_objc_super
和sel
。结构体的属性的压栈我们通过自定义一个结构体探索1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct js_struct{
NSNumber *num1;
NSNumber *num2;
} js_struct;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
JSPerson *person1 = [JSPerson alloc];
struct js_struct jst = {@(1),@(3)};
JSPerson *person = [JSPerson alloc];
[person saySomething];
Class cls = [JSPerson class];
void *js = &cls;
[(__bridge id)js saySomething];
}我们在图示位置添加断点调试
使用
lldb
调试1
2
3
4
5
6
7
8
9
10
11
12
13
14(lldb) p &person1
(JSPerson **) $0 = 0x00007ffeed1d8118
(lldb) p &jst
(js_struct *) $1 = 0x00007ffeed1d8108
(lldb) p &person
(JSPerson **) $2 = 0x00007ffeed1d8100
(lldb) p jst.num1
(__NSCFNumber *) $3 = 0xbab63c269bab4904 (int)1
(lldb) p &$3
(NSNumber **) $4 = 0x00007ffeed1d8108
(lldb) p jst.num2
(__NSCFNumber *) $5 = 0xbab63c269bab4924 (int)3
(lldb) p &$5
(NSNumber **) $6 = 0x00007ffeed1d8110发现
num1
的地址<num2
的地址,说明num2
先入栈。也就是结构体是从后向前入栈的
。总结来说题中压栈的顺序是
self
->_cmd
->superClass
->self
->person
->cls
->js
。地址空间是由高到低。所以这个地方js
向高地址平移8字节找到的是person
也就是打印是<JSPerson: 0x600003a00380>
的原因。