Objective-C底层面试题
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
。@interface JSPerson : NSObject @end #import "JSPerson.h" @implementation JSPerson @end @interface JSStudent : JSPerson @end @implementation JSStudent - (instancetype)init{ self = [super init]; if (self) { NSLog(@"%@ - %@",[self class],[super class]); } return self; } @end
在
main
函数里实例化一个JSStudent
对象。int main(int argc, const char * argv[]) { @autoreleasepool { JSStudent *student = [[JSStudent alloc] init];; NSLog(@"%@",student); } return 0; }
发现打印结果为
JSStudent - JSStudent
。这是为什么呢,我们下面分析一下。首先,
JSPerson
和JSStudent
类都没有实现class
方法,根据消息发送查找流程,会调用NSObject
类的class
方法,它的实现为- (Class)class { return object_getClass(self); } //根据isa找到类 Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; }
class
方法的作用是返回当前的类,self
是调用的对象也就是student
实例。[self class]
打印的是JSStudent
很好理解,因为消息接受者就是JSStudent
的实例对象,通过isa
找到的就是JSStudent
类。[super class]
打印的也是JSStudent
就让人困惑了,我们打开汇编调试
看一下[super class]
的底层调用了什么
[super class]
实际调用的是objc_msgSendSuper2
方法,我们在源码看一下这个方法的定义:
// 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);
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
///old结构,我们可以忽略 !__OBJC2__使用
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
内存平移问题
@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
[person saySomething]
:这种方式没什么疑问,正常的方法调用。它的流程是通过person
对象的isa
指针找到类JSPerson
,首先通过内存平移找到cache
里查找,如果找不到,继续平移找到bits
查找方法列表查找,最后找到imp
调用。[(__bridge id)js saySomething]
:运行代码,我们这一行代码也正常执行了,原因是什么呢
void *js = &cls;
说明js
是一个指向JSPerson
类首地址的指针,它和对象的isa
指向的是同一个地址,通过内存平移也可以找到对应的方法。
拓展
@interface JSPerson : NSObject
@property (nonatomic, strong) NSString *js_name;
- (void)saySomething;
@end
@implementation JSPerson
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.js_name);
}
@end
-[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
。-
其中
隐藏参数会压入栈
,且每个函数都会有两个隐藏参数(id self,sel _cmd)
,这个我们前面探索过,可以通过clang
将oc
代码转成c++
代码查看。 -
隐藏参数压栈
的过程,其地址是递减
的,而栈是从高地址->低地址 分配
的,即在栈中,参数会从前往后一直压
-
前面还有一行
[super viewDidLoad];
,super
调用的压栈我们也需要研究一下,其实上一题我们研究过它实际调用的是objc_msgSendSuper2
,有两个参数_objc_super
和sel
。结构体的属性的压栈我们通过自定义一个结构体探索
-
struct 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
调试
(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>
的原因。
转载自:https://juejin.cn/post/6988688944154034190