美文网首页iOS 底层原理 iOS 进阶之路
OC底层原理二十:OC底层面试题

OC底层原理二十:OC底层面试题

作者: markhetao | 来源:发表于2020-10-27 21:03 被阅读0次

    OC底层原理 学习大纲

    目录:

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

    准备工作:


    1. 关联对象AssociationsManager是否唯一

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

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

    image.png
    • 加入测试代码,创建2个manager,都调用get(),发现2个读取的associations相同地址
    • 证明AssociationsHashMap在内存中是独一份的,而manager只是外层包装,可以创建多个。
      image.png

    2. 分类方法覆盖本类方法吗?

    • 分类方法会调用attachLists,将分类方法插入了本类方法前面,全都存储起来。并不是覆盖本类方法。

    详细的attachLists绑定流程: OC底层原理十八:类的加载(中) SEL & 分类的加载 中 4.4 attachLists 绑定数据


    3. 所有分类方法优先本类吗?

    • 除了+load方法,其他分类方法都会优先本类
      调用所有本类+load方法,顺序调用分类+load

    1. +load之外的方法,分类方法都会插入本类方法前面

    2. +load方法的顺序为何是本类前面

    代码验证:

    • 创建HTPerson类、HTStudent类、HTPeroson(CatA)分类、HTPeroson(CatB)分类,都实现+load方法:
    +(void)load { NSLog(@"%s", __func__); }
    

    打印顺序如下:

    image.png
    • 打印了所有本类+load方法,顺序调用分类+load

    源码分析

    • 我们知道所有+load方法,都是在app启动过程中,dyld库调用objc库_objc_init中,load_images函数内进行的实现 (不熟悉的请看 【OC底层原理 学习大纲】 第5部分:dyld加载 & 类的加载

    • 打开objc4源码,搜索load_images

      image.png
    • 找到prepare_load_methods准备load函数和call_load_methods调用load函数两部分:

    2.1 准备load函数

    • 进入prepare_load_methods:

      image.png
    • 这里分别记录本类分类load函数,先进入schedule_class_load:

      image.png
    • 这里有递归执行,添加操作是在add_class_to_loadable_list中:

      image.png
    • 这里进行了读取检查扩容存储操作。我们检查getLoadMethod读取:

      image.png
    • getLoadMethod中从ro中读取了函数列表,寻找并返回load函数的Imp
      ------ 所有本类load的函数列表已准备完毕 ------

    • 回到上面add_category_to_loadable_list,检查分类的load列表的准备:

      image.png
    • 检查分类的读取_category_getLoadMethod

      image.png
      这里也从分类类方法列表中找到load函数并返回了imp
      ------ 所有分类load的函数列表已准备完毕 ------

    2.2 调用load函数

    • 进入call_load_methods:

      image.png
      这里的do-while循环内部,调用本类load函数时,使用了while循环。 所以load的调用顺序是:
      先调用所有本类+load方法,所有本类都没load方法时,才调用分类的load方法
    • 所以我们打印的结果,不仅HTPerson本类CatACatB分类前面调用+load方法HTStudent本类也在他们前面调用

    image.png

    4. Runtime是什么

    • CC++汇编实现的一套API,为OC语言加入了面向对象运行时的功能

    • 运行时(Runtime)是指数据类型确定,由编译器推迟到运行时
      (比如Extentioncategory的区别,extension编译期就确定了,但是懒加载category是在运行时动态加入的)


    5. 方法的本质,SEL是什么?IMP是什么?两者之间关系是什么?

    • 方法本质: 发送消息
      消息有以下几个流程:
    1. 快速查找(objc_msgSend)~ cache_t缓存信息中读取(汇编
    2. 慢速查找(lookUpImpOrForward)~ 递归自己/父类,从methodlist中查找
    3. 查找不到消息时: 动态方法解析 ~ resolveInstanceMethod
    4. 消息快速转发 ~ forwardTargetForSelector (更换实现对象)
    5. 消息慢速转发 ~ methodSignatureForSelector & forwardInvocation (自定义实现,或飘走不管了~)
    • sel方法编号 ~ 在read_images期间,编译进入内存
      sel带地址字符串方法排序是依据SEL地址值进行的排序

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

    SELIMP的关系

    • 如同查字典,你想查的意思,首先在目录通过niu找到对应页数,在页数内有关于详细解释
      (sel就是niuimp就是页码,而imp指针指向的内容,就是详细的解释)

    6. 编译后能否添加实例变量

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

    7. 能否向运行时创建添加实例变量?

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

    8. [self class][super class]区别原理分析

    • [self class]就是发送消息objc_msgSend,消息接受者是self,方法编号(SEL)是class

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

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

    测试代码:

    @interface HTPerson : NSObject
    @end
    @implementation HTPerson
    - (instancetype)init {
       if (self = [super init]) {
           NSLog(@"%@ %@", [self class], [super class]);
       }
       return self;
    }
    @end
    
    int main(int argc, const char * argv[]) {
       @autoreleasepool {
           HTPerson * person = [[HTPerson alloc] init];
       }
       return 0;
    }
    
    • 打印结果: 都是HTPerson
    image.png
    • 1. 查看class实现

      image.png
    • 进入object_getClass,发现是getIsa()读取的:

      image.png
    • 进入getIsa,其实就是从指针优化isa中,进行位运算取出类cls

      image.png

    想知道cls是什么,我们必须确定消息接收对象是谁,是谁的isa

    • clang生成cpp编译文件(clang -rewrite-objc main.m -o main.cpp),打开main.cpp文件:

      • 搜索HTPerson_init,找到实例化方法的编译结果
        image.png
    • 可以看到[self class],是使用objc_msgSend发送的消息,第1个参数是self,第2个参数是class方法编号,执行对象是self,测试代码中selfHTPerson实例
      所以读取到的是selfisa指向的类HTPerson类[self class]打印结果为HTPerson

    • [super class]编译后,是使用objc_msgSendSuper发送消息,第1个参数是__rw_objc_super结构体,第2个参数是class方法编号。
      执行对象第1个参数,所以我们在当前文件中搜索__rw_objc_super:

      image.png

    其中objectselfsuperClass(id)class_getSuperclass(objc_getClass("HTPerson"))

    • 我们在objc4源码中搜索objc_msgSendSuper(,查看其格式:

      image.png
    • 发现第1个参数是struct objc_super结构,我们搜索struct objc_super:

      image.png
    • 在这里,可以看到第1个参数是receive接收者,第二个是super_class父类。

    到这,我们就已经完整的弄清楚了[super class]实际调用对象__rw_objc_super结构中的第1个入参self,所以调用class方法时,传入的是selfselfisa指向的类HTPerson类

    • 所以[super class]打印结果为HTPerson

    补充:

    1. super虽然可以直接调用方法,但是它并不是对象只是语法关键字。真正的调用者,是当前对象

    2. 虽然clang编译中我们看到[super class]被编译为发送objc_msgSendSuper消息,但实际断点检查汇编流程时,发现它callq调用的是objc_msgSendSuper2:

      image.png
    • 搜索objc_msgSendSuper2objc_super 说明了就是当前搜索类,并不是父类

      image.png
    • 汇编源码:直接调用CacheLookup,从superclass中的cache快速查找方法

      image.png

    9. 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
    image.png

    10. runtime Associate方法关联对象,是否需要在dealloc中释放?

    • 不需要。因为设置关联对象时,会将isahas_assoc设置为true。当持有对象释放时,会调用c++析构函数,此时会将关联对象全部释放

    指针优化的isa中的has_assoc记录了是否有关联属性,在析构函数触发时,会检查是否有关联属性主动释放

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

    相关文章

      网友评论

        本文标题:OC底层原理二十:OC底层面试题

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