iOS底层-面试题解析
前言
前面讲了类相关的知识,针对这些知识,再结合一些面试题综合的理解下,也能从中发现一些新的东西。
相关题目
1. Asssociate关联的对象在什么时候释放
分析:
- 在
objc4-812
源码中,进入objc_setAssociatedObject
的代码实现时,发现有一个objc_removeAssociatedObjects
函数
objc_removeAssociatedObjects
源码如下
// Removes all associations for a given object.
objc_removeAssociatedObjects(**id** **_Nonnull** object)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object, /*deallocating*/false);
}
}
- 根据函数定义处的注释,可知它的作用就是
移除关联
,全局搜索,但发现并没有其他地方调用,然后再去看_object_remove_assocations
方法:
_object_remove_assocations
void
_object_remove_assocations(id object, bool deallocating)
{
ObjectAssociationMap refs{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
refs.swap(i->second);
// If we are not deallocating, then SYSTEM_OBJECT associations are preserved.
bool didReInsert = false;
if (!deallocating) {
...
}
...
}
}
}
- 注释说如果不是
deallocating
,则系统的关联将会保留。从objc_removeAssociatedObjects
方法进入,传入的deallocating
参数为false
,说明必定不是这个入口解除关联。现在可以定位到是移除的核心是_object_remove_assocations
方法,使用反推法
,去推导它的调用时机。 - 再去搜索
_object_remove_assocations
,找到了在objc_destructInstance
中调用了它
objc_destructInstance
objc_destructInstance
实现如下:
- 在代码发现此处的
deallocating
为true
,说明找的地方是对的,再搜索objc_destructInstance
的调用,得知到object_dispose
方法里调用
object_dispose
- 再搜索
object_dispose
在哪里被调用,发现有两处的rootDealloc
调用了object_dispose
rootDealloc
两处实现分别为下:
- 但两处的函数名一样,再搜索
rootDealloc
的调用,
_objc_rootDealloc
找到了在方法_objc_rootDealloc
里调用了rootDealloc
,它的源码如下
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
dealloc
最后搜索_objc_rootDealloc
,就找到了dealloc
- (void)dealloc {
_objc_rootDealloc(self);
}
- 也就是说在类走
dealloc时会进行解除关联
。流程确定后,再来看看解除关联的步骤
解除关联
从上面分析得知调用_object_remove_assocations
方法实现的解除关联:
- 从代码中得知:
- 先创建一个空的
ObjectAssociationMap
, - 再创建
AssociationsManager
,然后获取全局的HashMap
表,再根据object
找到装ObjectAssociationMap
的bucket
, - 如果
bucket
不是最后一个,将空的ObjectAssociationMap
与bucket
中的ObjectAssociationMap
交换,如果deallocating
为false
则遍历ObjectAssociationMap
中的bucket
,在i->second处,插入满足条件的bucket
,并将didReInsert置为true
。 - 然后判断
didReInsert
,为false
时,AssociationsHashMap
会执行清除关联
操作erase
- 然后创建一个
SmallVector<ObjcAssociation *, 4>
类型向量,用来存储ObjcAssociation
,然后遍历ObjectAssociationMap
中的bucket
,当满足条件时,往laterRefs
添加ObjcAssociation
,条件不满足时,得到的ObjcAssociation
则执行_value
释放 - 最后遍历
laterRefs
中的ObjcAssociation
,然后执行_value
释放。
- 先创建一个空的
流程图
- 整个过程的流程如下:
结论:
在
dealloc
后,系统才会解除关联。
2. +load
方法的调用原则, load
和initialize
哪个先调用
load
方的调用load
方法是在dyld
完成调用,是在main
函数之前调用的load
方法的调用顺序是父类
->子类
->分类
- 多个类和多个分类的调用顺序都是以编译顺序为主,可以在
build Phases
中调整
initialize
方法的调用initialize
在第一次消息发送
的时候调用。所以load
先于initialize
调用。- 调用
initialize
时,会优先调用父类,再子类
C++
构造函数- 如果
C++
构造方法写在objc
中,系统会通过static_init()
方法直接调用,此时的顺序为:C++
->+load
->main
- 如果写在
main
或者自己的代码中,则调用顺序是为:+load
->C++
->main
- 如果
3. Runtime是什么
-
Runtime
是由C和C++汇编
实现的一套Api
,为OC
语言加入了面向对象和运行时的功能
-
Runtime
是指将类型的确定由编译时推迟到运行时,如类扩展和分类的区别
-
- 平时写的
OC
代码在程序运行过程中,最终会转成Runtime
的C
语言代码,它是Objective-C
的幕后工作者
- 平时写的
4. 方法的本质是什么?sel和IMP是什么,二者的关系是怎样的?
-
- 方法的本质是:
消息发送
,消息有以下几个流程
- 快速查找:
(objc_msgSend) ~ cache_t
缓存查找 - 慢速查找:递归自己|父类
lookUpImpOrForward
- 查找不到:动态方法决议
resolveInstanceMethod
- 消息快速转发:
forwardingTargetForSelector
- 消息慢速转发:
methodSignatureForSelector & forwardInvocation
- 方法的本质是:
-
sel
和IMP
sel
是⽅法编号 ~ 在read_images
期间就编译进⼊了内存imp
就是我们函数实现指针 ,找imp
就是找函数的过程sel
就相当于书本的⽬录tittle
imp
就是书本的⻚码
-
- 查找具体的函数就是想看这本书⾥⾯具体篇章的内容:
- 我们⾸先知道想看什么
tittle
也就是(sel)
- 根据⽬录找到对应的⻚码也就是
(imp)
- 翻到具体的内容
方法实现
5. 能否向编译后得到的类中添加实例变量?能否向运行时创建的类中添加实例变量
-
不能
向编译后的得到的类中增加实例变量
- 我们编译好的实例变量存储的位置在
ro
,⼀旦编译完成,内存结构就完全确定,就⽆法修改 - 可以通过分类向类中添加方法和属性(关联对象)
-
- 只要类
没有注册到内存
(没有执行objc_registerClassPair
操作)是可以添加
的。
- 只要类
6.[self class]
和[super class]
的区别以及原理
定义一个WSPerson类
,然后定义一个WSTeacher
类继承WSPerson
,再在WSTeacher
的init
方法中打印[self class]
和[super class]
:
在main
中调用WSTeacher
的alloc init
方法,打印的结果都是WSTeacher
,[self class]
我们知道,但[super class]
不是应该WSPerson
么?带着问题去研究下他们的底层。
-
先来看看
class
方法的源码:- (Class)class { return object_getClass(self); } Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; }
- 由于消息的底层都是
objc_msgSend
,而objc_msgSend
的第一个参数是消息的接收者receiver
,也就是说class
中隐参self
就是消息的接收者receiver
,就是WSTeacher
,此时[self class]
就比较好理解了,self
是WSTeacher
对象,而对象的isa指向类
,所以最终打印结果WSTeacher
。 - 但
super
不是隐参,这个逻辑就走不通了,那么[super class]
是怎样的,通过汇编来看看: - 此时出现了个
objc_msgSendSuper2
函数,头文件如下:
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);
- 说明
super
只是个关键字,再看objc_super代码:
- 由于消息的底层都是
-
objc_super
由receiver
和Class
两个参数构成,而receiver
是WSTeacher
类的实例,所以调用class
方法时最终得到WSTeacher
。
7. 指针平移问题
案例
先来看下如下代码:
WSPerson *person = [WSPerson alloc];
person.name = @"wushaung";
[person sayNB];
Class pClass = [WSPerson class];
void *ws = &pClass;
[(__bridge id)ws sayNB];
- 这里
WSPerson
里有个sayNB
的实例方法,首先person
能调用成功是毋庸置疑的,那么ws
能调用成功吗?
- 打印结果显示,两个都可以调用成功,为什么呢?
分析
-
- 首先方法是存在类里的,
person
能调用是因为person
里的isa
指向类,person
能调用类的方法实质是指针平移的结果,所以person
可以访问到类里的方法。
- 首先方法是存在类里的,
-
void *ws = &pClass
,实质是将pClass
地址赋值给ws
指针,此时ws
也指向类,进而能访问到方法。
压栈
- 将代码调整下,在
sayNB
打印中加上name
的打印:
//打印
- (void)sayNB {
NSLog(@"🎈🎈🎈 %s name: %@", __func__, self.name);
}
运行结果如下:
-
居然出现了
<WSPerson: 0x600003d74270>
,再来分析下:person
取值实质通过地址0x8
得到name
的值:- 也就是通过首地址
0x6000021d81b0
平移0x8
后得到name的地址:0x6000021d81b8
,进而得到值,此时ws
也去模仿首地址平移0x8
: - 这样刚好得到了
person
的地址,所以打印结果为person
的值:
-
再在
WSPerson
中name
前面加一个属性hobby
:
@interface WSPerson : NSObject
@property (nonatomic, retain) NSString *hobby;
@property (nonatomic, copy) NSString *name;
@end
- 此刻访问到
name
要经过hobby
属性,就是要平移0x10
,再打印看看:
此刻出现了
ViewController
,这是怎么回事,再打印地址分析下:
- 此时
pClass
地址平移0x10
后变为0x00007ffee3052180
,也就是比person
地址还高,再就只有当前的ViewDidLoad
函数了,我们知道super
有个objc_super
结构体,结构体的成员为receiver和class
,而ViewDidLoad
有两个隐藏参数self和_cmd
:
- 但是结构体的参数压栈不确定,所以可以写个结构体测试下
结构体压栈
- 定义一个结构体:
- 然后打印各个下地址:
- 这里
pClass
平移0x10
后为0x00007ffee4ce2170
,刚好是cat的color地址
,所以打印为black
,此时person, cat, pClass
的栈结构分布为:
- 所以可以解释前面打印
ViewController
的原因,是因为访问到了super中的结构体class信息
由此可见:结构体的压栈方向是
由低地址往高地址方向压
参数压栈
定义一个函数:
- 然后运行查看打印
由此可见:函数参数的压栈方向是
由高地址往底地址方向压
未完待续...
转载自:https://juejin.cn/post/6990330990463827981