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就相当于书本的⽬录tittleimp就是书本的⻚码
-
- 查找具体的函数就是想看这本书⾥⾯具体篇章的内容:
- 我们⾸先知道想看什么
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