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和forwardInvocationsel是什么sel是⽅法编号,在read_images期间就加载进⼊了内存。它实际是objc_selector结构体。IMP是什么imp就是我们函数实现指针,找imp就是找函数实现的过程。sel与IMP的关系sel就相当于书本的⽬录titleimp就是书本的⻚码- 方法调用的时候首先根据
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