iOS 底层面试题

作者: Y丶舜禹 | 来源:发表于2020-11-05 16:53 被阅读0次

    前言

    我们类的底层探索已经告一段落,我们梳理一下常见的面试题,希望对你有些帮助。

    问题

    • 1.runtime是什么?
    • 2.runtime如何实现weak,为什么可以自动置为nil?
    • 3.runtime Associate方法关联的对象,是否需要在dealloc中释放?
    • 4.关联对象AssociationsManager是否唯一?
    • 5.分类方法会覆盖本类方法吗?
    • 6.所有分类方法都优先于本类吗?
    • 7.方法的本质,SEL是什么?IMP是什么?两者之间关系是什么?
    • 8.编译后的类能否添加实例变量?能否向运行时创建的类添加实例变量?
    • 9.[self class][super class]区别和原理分析
    • 10.内存平移问题
    问题一:runtime是什么?

    runtime 是由CC++汇编实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能。
    运⾏时(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时.
    举个例⼦: extension - category 的区别(extension是编译期就确定了,但是懒加载的category是在运行时动态加入的)。
    平时我们编写的OC代码,在程序运⾏过程中,其实最终会转换成RuntimeC语⾔代码,Runtime 是Object-C 的幕后⼯作者

    问题二:runtime如何实现weak,为什么可以自动置为nil?
    1. 通过SideTable找到我们的weak_table
    2. weak_table 根据referent 找到或者创建 weak_entry_t
    3. 然后append_referrer(entry, referrer)将我的新弱引用的对象加进去entry
    4. 最后weak_entry_insertentry加入到我们的weak_table

    底层源码调用流程如下图所示

    weak底层调用
    问题三:runtime Associate方法关联的对象,是否需要在dealloc中释放?

    当我们创建的对象释放时,会调用dealloc方法,其中的大致流程如下:

    • 1、C++函数释放 :objc_cxxDestruct
    • 2、移除关联属性:_object_remove_assocations
    • 3、将弱引用自动设置nil:weak_clear_no_lock(&table.weak_table, (id)this);
    • 4、引用计数处理:table.refcnts.erase(this)
    • 5、销毁对象:free(obj)

    所以,关联对象不需要我们手动移除,会在对象析构即dealloc时释放

    dealloc 源码

    dealloc的源码查找路径为:dealloc -> _objc_rootDealloc -> rootDealloc -> object_dispose(释放对象)-> objc_destructInstance -> _object_remove_assocations

    • 在objc源码中搜索dealloc的源码实现
    // Replaced by NSZombies
    - (void)dealloc {
        _objc_rootDealloc(self);
    }
    
    • 进入_objc_rootDealloc源码实现,主要是对对象进行析构
    void
    _objc_rootDealloc(id obj)
    {
        ASSERT(obj);
    
        obj->rootDealloc();
    }
    
    • 进入rootDealloc源码实现,发现其中有关联属性时设置bool值,当有这些条件时,需要进入else流程

      image
    • 进入object_dispose源码实现,主要是销毁实例对象

    /***********************************************************************
    * object_dispose
    * fixme
    * Locking: none
    **********************************************************************/
    id 
    object_dispose(id obj)
    {
        if (!obj) return nil;
    
        objc_destructInstance(obj);    
        free(obj);
    
        return nil;
    }
    
    
    • 进入objc_destructInstance源码实现,在这里有移除关联属性的方法
    /***********************************************************************
    * objc_destructInstance
    * Destroys an instance without freeing memory. 
    * Calls C++ destructors.
    * Calls ARC ivar cleanup.
    * Removes associative references.
    * Returns `obj`. Does nothing if `obj` is nil.
    **********************************************************************/
    void *objc_destructInstance(id obj) 
    {
        if (obj) {
            // Read all of the flags at once for performance.
            bool cxx = obj->hasCxxDtor();
            bool assoc = obj->hasAssociatedObjects();
    
            // This order is important.
            if (cxx) object_cxxDestruct(obj);
            if (assoc) _object_remove_assocations(obj);
            obj->clearDeallocating();
        }
    
        return obj;
    }
    
    • 进入_object_remove_assocations源码,关联属性的移除,主要是从全局哈希map中找到相关对象的迭代器,然后将迭代器中关联属性,从头到尾的移除
    // Unlike setting/getting an associated reference,
    // this function is performance sensitive because of
    // raw isa objects (such as OS Objects) that can't track
    // whether they have associated objects.
    void
    _object_remove_assocations(id object)
    {
        ObjectAssociationMap refs{};
    
        {
            AssociationsManager manager;
            AssociationsHashMap &associations(manager.get());
            //获取迭代器
            AssociationsHashMap::iterator i = associations.find((objc_object *)object);
            //从头到尾逐个移除
            if (i != associations.end()) {
                refs.swap(i->second);
                associations.erase(i);
            }
        }
    
        // release everything (outside of the lock).
        for (auto &i: refs) {
            i.second.releaseHeldValue();
        }
    }
    
    问题四:关联对象AssociationsManager是否唯一?

    AssociationsManager结构中,manager只是对外代言人,并不是唯一的,AssociationsHashMap哈希表才是唯一的。

    1. 运行验证:
    移除锁,这样可以同时存在2个manager了。

    image
    • 加入测试代码,创建2个manager,都调用get(),发现2个读取的associations相同地址
    • 证明AssociationsHashMap在内存中是独一份的,而manager只是外层包装,可以创建多个。
    测试
    问题五:分类方法会覆盖本类方法吗?
    • 分类方法会调用attachLists,将分类方法插入了本类方法前面,全都存储起来。并不是覆盖本类方法,这个在我们之前的文章中 iOS-类的加载(下)有详细的解释。
    问题六:所有分类方法都优先于本类吗?

    类的方法 和 分类方法 重名,如果调用,是什么情况?

    • 如果同名方法是普通方法,包括initialize -- 先调用分类方法

      • 因为分类的方法是在类realize之后 attach进去的,插在类的方法的前面,所以优先调用分类的方法(注意:不是分类覆盖主类!!)

      • initialize方法什么时候调用? initialize方法也是主动调用,即第一次消息时调用,为了不影响整个load,可以将需要提前加载的数据写到initialize

    • 如果同名方法是load方法 -- 先 主类load,后分类load(分类之间,看编译的顺序)

    image
    问题七:方法的本质,SEL是什么?IMP是什么?两者之间关系是什么?

    方法的本质:发送消息,消息会有以下几个流程

    • 快速查找(objc_msgSend) -cache_t缓存消息中查找
    • 慢速查找 - 递归自己|父类 -lookUpImpOrForward
    • 查找不到消息:动态方法解析 -resolveInstanceMethod
    • 消息快速转发 - forwardingTargetForSelector
    • 消息慢速转发 - methodSignatureForSelector & forwardInvocation

    sel是方法编号 - 在read_images期间就编译进了内存

    imp是函数实现指针 ,找imp就是找函数的过程

    打个比方:加入你要从一本字典中查找某个字,那么sel相当于 字典的目录titleimp 相当于 字典的页码。

    问题八:编译后的类能否添加实例变量?能否向运行时创建的类添加实例变量?

    1、不可以。 因为编译好的实例变量存放的位置在类的ro,一旦编译完成,内存结构就完全确定了,无法修改。

    2、运行时在register注册前,可以添加。但是调用运行时register注册后,就完成了内存的注入,内存结构确定了,无法修改。

    问题九:[self class][super class]区别和原理分析
    • [self class]就是发送消息objc_msgSend,消息接受者是self,方法编号(SEL)是class

    • [super class]本质是objc_msgSendSuper,消息接受者还是self,方法编号是class

    实际运行时,[super class]在汇编层执行的是objc_msgSendSuper2,直接从superclass父类开始搜索,节约了一轮查找资源

    测试代码:

    @interface ZGPerson : NSObject
    @end
    @implementation ZGPerson
    - (instancetype)init {
     if (self = [super init]) {
           NSLog(@"%@ %@", [self class], [super class]);
       }
       return self; }
    @end
    
    int main(int argc, const char * argv[]) {
       @autoreleasepool {
           ZGPerson * person = [[ZGPerson alloc] init];
      }
       return 0;
    }
    
    
    • 打印结果: 都是ZGPerson
      结果
      结果与我想的不一样,为什么不是ZGPersonNSObject呢?我们查看源码分析一下

    我们查看 [self class]中的class源码

    - (Class)class {
        return object_getClass(self);
    }
    
    👇
    Class object_getClass(id obj)
    {
        if (obj) return obj->getIsa();
        else return Nil;
    }
    

    其底层是获取对象的isa,当前的对象是ZGPerson,其isa是同名的ZGPerson,所以[self class]打印的是ZGPerson

    [super class]中,其中super 是语法的 关键字,可以通过clangsuper的本质,clang生成cpp编译文件(clang -rewrite-objc ZGPerson.m -o ZGPerson.cpp),打开main.cpp文件:

    ZGPerson.cpp

    底层源码中搜索__rw_objc_super,是一个中间结构体

    __rw_objc_super

    objc中搜索objc_msgSendSuper,查看其隐藏参数

    objc_msgSendSuper

    搜索struct objc_super

    objc_super

    通过clang的底层编译代码可知,当前消息的接收者 等于 self,而self 等于 LGTeacher,所以 [super class]进入class方法源码后,其中的self是init后的实例对象,实例对象的isa指向的是本类,即消息接收者是LGTeacher本类

    • 我们再来看[super class]在运行时是否如上一步的底层编码所示,是objc_msgSendSuper,打开汇编调试,调试结果如下

      image
      • 搜索objc_msgSendSuper2,从注释得知,是从 类开始查找,而不是父类

        objc_msgSendSuper2
      • 查看objc_msgSendSuper2的汇编源码,是从superclass中的cache中查找方法

    ENTRY _objc_msgSendSuper2
    UNWIND _objc_msgSendSuper2, NoFrame
    
    ldp p0, p16, [x0]       // p0 = real receiver, p16 = class 取出receiver 和 class
    ldr p16, [x16, #SUPERCLASS] // p16 = class->superclass
    CacheLookup NORMAL, _objc_msgSendSuper2//cache中查找--快速查找
    
    END_ENTRY _objc_msgSendSuper2
    
    总结:
    • [self class]方法调用的本质是 发送消息,调用class的消息流程,拿到元类的类型,在这里是因为类已经加载到内存,所以在读取时是一个字符串类型,这个字符串类型是在map_imagesreadClass时已经加入表中,所以打印为ZGPerson

    • [super class]打印的是ZGPerson,原因是当前的super是一个关键字,在这里只调用objc_msgSendSuper2,其实他的消息接收者和[self class]是一模一样的,所以返回的是ZGPerson

    问题十:runtime是什么?内存平移问题
    Class cls = [LGPerson class];
    void  *kc = &cls;  //
    [(__bridge id)kc saySomething];
    
    

    LGPerson中有一个属性 kc_name 和一个实例方法saySomething,通过上面代码这种方式,能否调用实例方法?为什么?

    代码调试

    • 我们在日常开发中的调用方式是下面这种
    LGPerson *person = [LGPerson alloc];
    [person saySomething];
    
    
    • 通过运行发现,是可以执行的,打印结果如下

      image
    • [person saySomething]的本质是对象发送消息,那么当前的person是什么?

      • personisa指向类LGPersonperson的首地址 指向 LGPerson的首地址,我们可以通过LGPerson的内存平移找到cache,在cache中查找方法

        image
    • [(__bridge id)kc saySomething]中的kc是来自于LGPerson 这个类,然后有一个指针kc,将其指向LGPerson的首地址

      image

    所以,person是指向LGPerson类的结构,kc也是指向LGPerson类的结构,然后都是在LGPerson中的methodList中查找方法

    image

    修改:saySomething里面有属性 self.kc_name 的打印

    代码如下所示

    - (void)saySomething{
        NSLog(@"%s - %@",__func__,self.kc_name);
    }
    
    //下面这两种方式调用
    //方式一
    Class cls = [LGPerson class];
    void  *kc = &cls; 
    [(__bridge id)kc saySomething]; 
    
    //方式二:常规调用
    LGPerson *person = [LGPerson alloc];
     [person saySomething];
    
    
    • 查看这两种调用方式的打印结果,如下所示
      • kc方式的调用打印的kc_name<ViewController: 0x7fe29170b560>

      • person方式的调用打印的kc_name(null)

        image

    为什么会出现打印不一致的情况?

    • 其中person方式的kc_name 是由于 self指向person的内存结构,然后通过内存平移8字节,取出去kc_name,即self指针首地址平移8字节获得

      image
    • 【方式一】其中kc指针中没有任何,所以kc表示8字节指针self.kc_name的获取,相当于 kc首地址的指针也需要平移8字节找kc_name,那么此时的kc的指针地址是多少?平移8字节获取的是什么?

      • kc是一个指针,是存在中的,栈是一个先进后出的结构,参数传入就是一个不断压栈的过程,
        • 其中隐藏参数会压入栈,且每个函数都会有两个隐藏参数(id self,sel _cmd),可以通过clang查看底层编译

        • 隐藏参数压栈的过程,其地址是递减的,而栈是从高地址->低地址 分配的,即在栈中,参数会从前往后一直压

        • super通过clang查看底层的编译,是objc_msgSendSuper,其第一个参数是一个结构体__rw_objc_super(self,class_getSuperclass),那么结构体中的属性是如何压栈的?可以通过自定义一个结构体,判断结构体内部成员的压栈情况

          • p &person3

          • p *(NSNumber **)0x00007ffee83a8090

          • p *(NSNumber **)0x00007ffee83a8098

            image

            所以图中可以得出 20先加入,再加入10,因此结构体内部的压栈情况是 低地址->高地址递增的,栈中结构体内部的成员是反向压入栈,即低地址->高地址,是递增的,

    • 所以到目前为止,栈中从高地址到低地址的顺序的:self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person

      • self_cmdviewDidLoad方法的两个隐藏参数,是高地址->低地址正向压栈

      • class_getSuperClassselfobjc_msgSendSuper2中的结构体成员,是从最后一个成员变量,即低地址->高地址反向压栈

    可以通过下面这段代码打印下栈的存储是否如上面所说

    void *sp  = (void *)&self;
    void *end = (void *)&person;
    long count = (sp - end) / 0x8;
    
    for (long i = 0; i<count; i++) {
        void *address = sp - 0x8 * I;
        if ( i == 1) {
            NSLog(@"%p : %s",address, *(char **)address);
        }else{
            NSLog(@"%p : %@",address, *(void **)address);
        }
    }
    
    

    运行结果如下

    image

    其中为什么class_getSuperclassViewController,因为objc_msgSendSuper2返回的是当前类,两个self,并不是同一个self,而是栈的指针不同,但是指向同一片内存空间

    • [(__bridge id)kc saySomething]调用时,此时的kc是 LGPerson: 0x7ffeec381098,所以saySomething方法中传入的self 还是LGPerson,但并不是我们通常认为的LGPerson,使我们当前传入的消息接收者,即LGPerson: 0x7ffeec381098,是LGPerson的实例对象,此时的操作与普通的LGPerson是一致的,即LGPerson的地址内存平移8字节
      • 普通person流程:person -> kc_name - 内存平移8字节

      • kc流程:0x7ffeec381098 + 0x80 -> 0x7ffeec3810a0,即为self,指向<ViewController: 0x7fac45514f50>,如下图所示

        image

    其中 personLGPerson的关系是 person是以LGPerson为模板的实例化对象,即alloc有一个指针地址,指向isa,isa指向LGPerson,它们之间关联是有一个isa指向

    而kc也是指向LGPerson的关系,编译器会认为 kc也是LGPerson的一个实例化对象,即kc相当于isa,即首地址,指向LGPerson,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即kc也有kc_name。由于person查找kc_name是通过内存平移8字节,所以kc也是通过内存平移8字节去查找kc_name

    哪些东西在栈里 哪些在堆里

    • alloc的对象 都在

    • 指针、对象中,例如person指向的空间中,person所在的空间在栈中

    • 临时变量

    • 属性值,属性随对象是在

    注意:

    • 是从小到大,即低地址->高地址
    • 栈是从大到小,即从高地址->低地址分配
    *   函数隐藏参数会`从前往后`一直压,即 `从高地址->低地址 开始入栈`,
        
        
    *   结构体内部的成员是`从低地址->高地址`
    
    • 一般情况下,内存地址有如下规则
    *   `0x60` 开头表示在 `堆`中
        
        
    *   `0x70` 开头的地址表示在 `栈`中
        
        
    *   `0x10` 开头的地址表示在`全局区域`中
    

    以上就是全部的内容了,如有错误,还望指正。

    相关文章

      网友评论

        本文标题:iOS 底层面试题

        本文链接:https://www.haomeiwen.com/subject/tpwbvktx.html