iOS底层-面试题解析

1,210 阅读7分钟

前言

前面讲了类相关的知识,针对这些知识,再结合一些面试题综合的理解下,也能从中发现一些新的东西。

相关题目

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实现如下: 截屏2021-07-27 17.51.49.png

  • 在代码发现此处的deallocatingtrue,说明找的地方是对的,再搜索objc_destructInstance的调用,得知到object_dispose方法里调用
object_dispose

截屏2021-07-27 17.56.02.png

  • 再搜索object_dispose在哪里被调用,发现有两处的rootDealloc调用了object_dispose
rootDealloc

两处实现分别为下:

截屏2021-07-27 17.58.42.png

截屏2021-07-27 17.58.50.png

  • 但两处的函数名一样,再搜索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方法实现的解除关联: 截屏2021-07-28 07.23.33.png

  • 从代码中得知:
    • 先创建一个空的ObjectAssociationMap
    • 再创建AssociationsManager,然后获取全局的HashMap表,再根据object找到装ObjectAssociationMapbucket
    • 如果bucket不是最后一个,将空的ObjectAssociationMapbucket中的ObjectAssociationMap交换,如果deallocatingfalse则遍历ObjectAssociationMap中的bucket,在i->second处,插入满足条件的bucket,并将didReInsert置为true
    • 然后判断didReInsert,为false时,AssociationsHashMap执行清除关联操作erase
    • 然后创建一个SmallVector<ObjcAssociation *, 4>类型向量,用来存储ObjcAssociation,然后遍历ObjectAssociationMap中的bucket,当满足条件时,往laterRefs添加ObjcAssociation,条件不满足时,得到的ObjcAssociation则执行_value释放
    • 最后遍历laterRefs中的ObjcAssociation,然后执行_value释放。

流程图

  • 整个过程的流程如下: class18.jpg

结论:

dealloc后,系统才会解除关联。

2. +load方法的调用原则, loadinitialize哪个先调用

  • 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是什么

    1. Runtime是由C和C++汇编实现的一套Api,为OC语言加入了面向对象和运行时的功能
    1. Runtime是指将类型的确定由编译时推迟到运行时,如类扩展和分类的区别
    1. 平时写的OC代码在程序运行过程中,最终会转成RuntimeC语言代码,它是Objective-C的幕后工作者

4. 方法的本质是什么?sel和IMP是什么,二者的关系是怎样的?

    1. 方法的本质是:消息发送,消息有以下几个流程
    • 快速查找:(objc_msgSend) ~ cache_t缓存查找
    • 慢速查找:递归自己|父类lookUpImpOrForward
    • 查找不到:动态方法决议resolveInstanceMethod
    • 消息快速转发:forwardingTargetForSelector
    • 消息慢速转发:methodSignatureForSelector & forwardInvocation
    1. selIMP
    • sel是⽅法编号 ~ 在read_images期间就编译进⼊了内存
    • imp就是我们函数实现指针 ,找imp就是找函数的过程
    • sel就相当于书本的⽬录tittle 
    • imp就是书本的⻚码
    1. 查找具体的函数就是想看这本书⾥⾯具体篇章的内容:
    • 我们⾸先知道想看什么tittle也就是(sel)
    • 根据⽬录找到对应的⻚码也就是(imp)
    • 翻到具体的内容方法实现

5. 能否向编译后得到的类中添加实例变量?能否向运行时创建的类中添加实例变量

    1. 不能向编译后的得到的类中增加实例变量
    • 我们编译好的实例变量存储的位置在ro,⼀旦编译完成,内存结构就完全确定,就⽆法修改
    • 可以通过分类向类中添加方法和属性(关联对象)
    1. 只要类没有注册到内存(没有执行objc_registerClassPair操作)是可以添加的。

截屏2021-07-28 18.48.31.png

6.[self class][super class]的区别以及原理

定义一个WSPerson类,然后定义一个WSTeacher类继承WSPerson,再在WSTeacherinit方法中打印[self class][super class]

截屏2021-07-29 07.06.12.png

main中调用WSTeacheralloc 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]就比较好理解了,selfWSTeacher对象,而对象的isa指向类,所以最终打印结果WSTeacher
    • super不是隐参,这个逻辑就走不通了,那么[super class]是怎样的,通过汇编来看看: 截屏2021-07-29 10.09.54.png
    • 此时出现了个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代码:

    截屏2021-07-29 10.15.40.png

  • objc_superreceiverClass两个参数构成,而receiverWSTeacher类的实例,所以调用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能调用成功吗?

截屏2021-07-29 14.38.22.png

  • 打印结果显示,两个都可以调用成功,为什么呢?

分析

    1. 首先方法是存在类里的,person能调用是因为person里的isa指向类,person能调用类的方法实质是指针平移的结果,所以person可以访问到类里的方法。
    1. void *ws = &pClass,实质是将pClass地址赋值给ws指针,此时ws也指向类,进而能访问到方法。

截屏2021-07-29 15.22.07.png

压栈

  • 将代码调整下,在sayNB打印中加上name的打印:
//打印
- (void)sayNB {
    NSLog(@"🎈🎈🎈 %s  name: %@", __func__, self.name);
}

运行结果如下: 截屏2021-07-29 16.31.36.png

  • 居然出现了<WSPerson: 0x600003d74270>,再来分析下:

    • person取值实质通过地址0x8得到name的值: 截屏2021-07-29 16.38.28.png
    • 也就是通过首地址0x6000021d81b0平移0x8后得到name的地址:0x6000021d81b8,进而得到值,此时ws也去模仿首地址平移0x8截屏2021-07-29 16.46.56.png
    • 这样刚好得到了person的地址,所以打印结果为person的值: 截屏2021-07-29 16.48.49.png
  • 再在WSPersonname前面加一个属性hobby

@interface WSPerson : NSObject

@property (nonatomic, retain) NSString *hobby;
@property (nonatomic, copy) NSString *name;

@end
  • 此刻访问到name要经过hobby属性,就是要平移0x10,再打印看看:

截屏2021-07-29 20.01.02.png
此刻出现了ViewController,这是怎么回事,再打印地址分析下:

截屏2021-07-29 20.03.29.png

  • 此时pClass地址平移0x10后变为0x00007ffee3052180,也就是比person地址还高,再就只有当前的ViewDidLoad函数了,我们知道super有个objc_super结构体,结构体的成员为receiver和class,而ViewDidLoad有两个隐藏参数self和_cmd

截屏2021-07-29 20.07.04.png

  • 但是结构体的参数压栈不确定,所以可以写个结构体测试下
结构体压栈
  • 定义一个结构体: 截屏2021-07-29 20.10.36.png
  • 然后打印各个下地址:

截屏2021-07-29 20.14.46.png

  • 这里pClass平移0x10后为0x00007ffee4ce2170,刚好是cat的color地址,所以打印为black,此时person, cat, pClass的栈结构分布为:

截屏2021-07-29 20.39.33.png

  • 所以可以解释前面打印ViewController的原因,是因为访问到了super中的结构体class信息

由此可见:结构体的压栈方向是由低地址往高地址方向压

参数压栈

定义一个函数:

截屏2021-07-29 20.49.30.png

  • 然后运行查看打印

截屏2021-07-29 20.51.34.png

由此可见:函数参数的压栈方向是由高地址往底地址方向压

未完待续...