美文网首页iOS 底层原理 iOS 进阶之路
OC底层原理十八:类的加载(中) SEL & 分类的加载

OC底层原理十八:类的加载(中) SEL & 分类的加载

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

    OC底层原理 学习大纲

    上一节,我们了解了map_images的整体结构 & 非懒加载类,了解了APP启动时,所有都已记录哈希表中(仅类名字地址)。

    • 实现类+load方法非懒加载类,会在启动时,实现类的加载,从macho中读取原始数据存放到rw
    • 懒加载类则是在被第一次调用时,通过消息机制触发类的实现
      两种类的加载方式,最终都是调用realizeClassWithoutSwift完成实现。

    上节回顾:

    上节回顾

    我们上一节留下了2个问题:rwe何时加载?分类如何加载?

    • 现在不急着回答,本节结束后,我相信你就完全懂了。

    本节尽可能讲得详细一些:

    1. sel注册
    2. 分类的本质
    3. 分类的数据加载
    4. attachCategories详解
    5. attachCategories的调用

    准备工作:

    1. sel注册

    我们在前面学习msgSend消息机制时,慢速查找阶段中,在类的函数列表查找方法时,是使用二分查找(👉流程图)。

    Q: 二分查找必须是有序的,那排序依据是什么,如何排序

    • 上一节我们分析map_images流程时,在第2步 修复预编译阶段的SEL的混乱问题时,就需要将SEL插入到nameSelectors哈希表中。
    image.png
    • 其中_getObjc2SelectorRefsmacho__objc_selrefs,存储的内容是SEL:
      image.png
    • 遍历从macho__objc_selrefs读取SEL,其中的sels包含的是带地址sel(后面证明)。
    • 循环注册sel,检查sel地址,如果不同,就重新赋值sel地址

    进入sel_registerNameNoLock:

    image.png
    • 进入__sel_registerName:
    image.png
    • 一般是可以通过name搜索到result,直接返回result
    • 但如果特殊情况name搜索不到,就重新创建,再返回sel

    我们进入search_builtins来了解查询路径:

    image.png
    • 发现_dyld_get_objc_selectorextern申明在dyld中:
    // Called only by objc to see if dyld has uniqued this selector.
    // Returns the value if dyld has uniqued it, or nullptr if it has not.
    // Note, this function must be called after _dyld_objc_notify_register.
    //
    // Exists in Mac OS X 10.15 and later
    // Exists in iOS 13.0 and later
    extern const char* _dyld_get_objc_selector(const char* selName);
    
    • 打开dyld源码,搜索_dyld_get_objc_selector(const
    image.png
    • 进入getObjCSelector
    image.png
    • 发现是调用getString方法在读取内容,所以我们反向搜索getString(const,检查函数的实现:
    image.png
    • 通过这里,我们就明确知道了:

    sel虽然是函数名(字符串),但同时它是有地址值的。

    拓展:

    1. 函数地址完全随机,是由它所在的段基础地址偏移值确定的。程序每次运行,函数地址可能变化
    2. 判断两个函数是否相等,是通过地址值进行判断
      两个不同类相同名称函数,但函数地址不同,是两个独立的函数
    3. 函数列表排序,是依据SEL地址进行排序。所以排序后,可使用二分查找。

    2.分类的本质

    • main.m文件加入测试代码
    // 本类
    @interface HTPerson : NSObject
    
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, assign) int age;
    
    - (void)func1;
    - (void)func3;
    - (void)func2;
    
    + (void)classFunc;
    
    @end
    
    @implementation HTPerson
    
    + (void)load { NSLog(@"%s",__func__); };
    
    - (void)func1 { NSLog(@"%s",__func__); };
    - (void)func3 { NSLog(@"%s",__func__); };
    - (void)func2 { NSLog(@"%s",__func__); };
    
    + (void)classFunc { NSLog(@"%s",__func__); };
    
    @end
    
    // 分类 CatA
    @interface HTPerson (CatA)
    
    @property (nonatomic, copy) NSString *catA_name;
    @property (nonatomic, assign) int catA_age;
    
    - (void)func1;
    - (void)func3;
    - (void)func2;
    
    + (void)classFunc;
    
    @end
    
    @implementation HTPerson (CatA)
    
    + (void)load { NSLog(@"%s",__func__); };
    
    - (void)func1 { NSLog(@"%s",__func__); };
    - (void)func3 { NSLog(@"%s",__func__); };
    - (void)func2 { NSLog(@"%s",__func__); };
    
    + (void)classFunc { NSLog(@"%s",__func__); };
    
    @end
    
    int main(int argc, const char * argv[]) {
        return 0;
    }
    

    检查格式的方式:1. clang 2. 官方帮助文档

    • 2.1 clang
      cdmain.m所在文件夹,输入clang -rewrite-objc main.m -o main.cpp,打开main.cpp文件,搜索分类_CatA

    • 分类的实例方法类方法

      image.png
    • 分类的属性:

      image.png
    • 分类的结构

      image.png
    • 我们搜索struct _category_t,可看到分类的完整格式
      image.png

    发现编译期HTPerson(CatA)nameHTPersoncls也是HTPerosn类

    • 分类的实现:
      image.png

    本类属性分类属性的区别:

    • 本类属性:在clang编译环节,会自动生成并实现对应的set和get方法

    • 分类属性:会存在set、get方法,但是没有实现需要runtime设置关联属性)。

      易混淆点: 分类属性存在setget方法,但没有实现
      检验方式: 使用person对象可以快捷访问到catA_age,并可以赋值。但是程序运行时crash。 这是因为方法存在,但找不到对应的imp实现

      image.png
      Q: 1. 分类属性为何存在setget方法? 2. 如何让它不crash(关联属性的动态实现)
    • 第1个问题在本节后续探索中,会得到很清晰的答案。 第2个问题,我们下一节专门讲解关联属性

    • 2.2 官方帮助文档

    打开官方文档 (快捷键:shift + command + 0),搜索Categor:

    image.png
    • 切换语言为Objective-C:

      image.png
    • 发现类型是objc_category,在objc4源码中搜索:

    image.png
    • 💣 格式不一样?name呢?cls呢?
    • 😂 注意看后面的声明:OBJC2_UNAVAILABLE, objc2不可用。文档已过期的。这个时候,我们要以真实运行的代码为准

    了解了分类数据格式,那分类的数据如何加到HTPerson的呢?

    3. 分类的加载

    如何研究呢?

    • 已知的信息出发先找一条抵达目的地的路径,找到核心方法,再反向搜索核心方法被调用的地方,进行全面推理

    我们上一节分析_read_images结构时,第9步 实现非懒加载类->methodizeClass内部有对分类的处理

    • methodizeClass中加入测试代码:
    // >>>> 测试代码
        const char *mangledName = cls->mangledName();
        const char * HTPersonName = "HTPerson";
        if (strcmp(HTPersonName, mangledName) == 0 ) {
            if (!isMeta) {
                printf("%s - 精准定位: %s\n", __func__, mangledName);
            }
        }
        // <<<< 测试代码
    
    • printf打印处加入断点,运行程序
    image.png
    • 发现进入了HTPerosn类,查看ro信息,发现其中baseMethods只有8个,分别打印查看,都是HTPerosn本类实例函数。 从信息栏可以看rwe此时为Null

    ro的读取:

    image.png
    • 单步往下运行,发现最终会到达attachToClass处:
    image.png

    methodizeClass的内容是:

    • 读取函数(已排序)存到list -> 读取属性存到proplist -> 读取协议存到protolist -> 分类添加到类attachToClass

    有个细节,我们发现initialize在这里被添加到根元类函数列表了。根元类拥有initialize方法,所有继承NSObject的类,都将拥有initialize方法。

    我们知道+load方法会将懒加载类转变为非懒加载类,在app启动前完成了所有非懒加载类加载。但是app启动环节加载过多内容,会影响app的启动时长

    • Q:有些准备必须在类初始化之前就完成,如果不写+load方法内,怎么做到提前准备呢?
    • A:写在initialize内,因为每个类都继承自NSObject,所以都自带了initialize函数,而initialize函数是在类第一次发送消息时,就触发。 所以可以做到提前准备
    • 进入attachToClass,加入测试代码:
    image.png

    看到了关键的attachCategories函数:绑定分类

    • 如果是元类,需要分别绑定对象类方法。否则,只需要绑定对象方法。

    注意,此时测试代码中HTPersonHTPerson(CatA)都必须实现+load方法,才会进入attachCategories代码区域) 具体原因,后面第5部分 本类与分类的+load区别 会详细讲解。

    下面,我们详细分析一下attachCategories

    4. attachCategories详解

    进入attachCategories,加入定位测试代码

    image.png

    开辟了64个空间大小的mlistsproplistsprotolists容器,分别用于存储函数属性协议

    image.png

    attachCategories流程:

    • 首先,开辟空间,对rwe进行初始化
    • 然后,遍历所有的分类
      entry记录当前分类,entry.cat是category_t结构,存储了分类所有数据。
      从分类中读取函数属性协议信息,存放指定容器内。
    • 最后,将容器内数据,分别添加rwe指定属性中。

    此处分为3小部分讲解:

    1. rwe的初始化
    2. 数据读取
    3. prepareMethodLists函数排序
    4. attachLists 绑定数据

    4.1 rwe的初始化

    哈哈哈 😃 走过千山万水,终于找到你,我的rwe

    • 进入 extAllocIfNeeded
    image.png
    • 进入extAlloc:
    image.png

    此时,rwe才完成了初始化工作。各项属性完备。(关于attachLists赋值操作,在4.3小部分进行讲解)

    关于rwe何时加载的问题:
    我们现在知道分类加载会进行rwe初始化加载数据。那还有其他地方触发rwe的加载吗?

    • rwe的加载,是执行了extAlloc方法,所以我们反向搜索,查看谁调用extAlloc方法:
    image.png

    只有extAllocIfNeededdeepCopy调用了。

    • deepCopy深拷贝: 搜索deepCopy(,发现只被objc_duplicateClass调用,而是objc_duplicateClass开放使用的API接口,并没自动调用的地方。 所以此处不做考虑。

    • extAllocIfNeeded: 搜索extAllocIfNeeded(,发现有以下7处调用了它:

      image.png
    • 发现都是动态添加(函数、属性、协议、分类等)时,才会创建rwe

    还记得上面ro的读取吗?

    • rwe存在时:表示这个类有数据被修改了,所以需要从rwe返回数据
    • 而如果rwe不存在,表明这个类的数据没有动态修改过,所以可以直接从macho拷贝一份ro返回即可。

    附上WWDC2020视频Advancements in the Objective-C runtime,回顾官方对于rwe的解释,会理解得更深刻。

    4.2 数据读取和prepareMethodLists函数排序

    初始化rwe后,我们读取分类数据

    image.png
    • 查看entry.cat结构:
    image.png
    • 查看category_t结构,发现存储了分类所有数据。
      image.png

    所以分类的数据都是从entry.cat进行读取。

    • 我们在上面定位测试代码打印处加上断点,运行代码,到达断点后,往下进入循环内:
      image.png
    • 发现此时name已从编译时的HTPerson变成了CatA,而我们的cls仍旧是HTPerson
      (类地址在内存中是唯一的,地址相同表示是一个类)
      image.png
    • 下面以函数读取为例,(属性、协议的读取和赋值方式一样):
      image.png

    将分类的methods函数列表读取到mlist,如果存在:

    • 如果数组是否已满(64),将mlist内部排序后,调用attachLists存到rwemethods中,并将mcount归零。
    • mlist倒序插入到mlists

    属性协议也是相同的操作方式,只是读取的内容和存入的容器不同而已。

    image.png
    • 至此,已遍历分类,将分类的函数、属性、协议都分别存储到mlistsproplistsprotolists中了。

    接下来,是将他们赋值给rwe对应属性:

    image.png

    4.3 prepareMethodLists函数排序

    函数在插入前,都会预先进行一轮排序,进入prepareMethodLists

    image.png
    • 进入fixupMethodList:
    image.png
    • 执行完prepareMethodLists函数后,我们p mlists打印容器,p $7[63]取出刚才存放在最后的mlistp $8->get(index)打印数据:
    image.png

    发现排序后的顺序为: [ func1, func3 , func2 ] ,确实不是根据sel字符串进行的排序。

    • 我们使用p/x $8->get(0),打印SEL地址:
    image.png
    • 0x0000000100003e12 < 0x0000000100003e18 < 0x0000000100003e1e,发现我们SEL地址确实是从小到大排列的。

    所以验证了:
    函数的排序:不是根据SEL字符串排序,也不是通过imp进行排序,而通过SEL地址进行排序

    • 排序后,我们通过attachLists完成数据的绑定

    4.4 attachLists 绑定数据

    • 进入attachLists:
    image.png

    拓展函数:

    • memcpy(开始位置,放置内容,占用大小)内存拷贝
    • memmove(开始位置,移动内容,占用大小)内存平移

    LRU算法:

    • Least Recently Used的缩写,最近最少使用算法,越容易被调用(访问)的放前面

    • 回想一下,不管我们是动态插入函数,还是添加分类,一定是有需求时才这么操作。而新加入的数据,明显访问频率高于默认模板内容。所以我们addedLists使用LRU算法,将旧数据放在最后面新数据永远插入最前面。 这样可以提高查询效率减少运行时资源的占用

    这里有3种情况:

    - 0->1: 首次加入,直接将addedLists[0]赋值给list,是一维数组
    (首次加载是本类数据在extAllocIfNeeded时,从macho读取ro中的对应数据加入)

    image.png

    - 1->多: 此时扩容为二维数组旧数据插入后面新数据插入前面:
    将数组扩容newCount大小
    -> array()count记录个数
    -> 如果有旧数据插入lists容器尾部
    -> 调用memcpy内存拷贝,从array()首地址开始,将addedLists插入,占用addedCount个元素大小。

    image.png

    - 多 -> 更多: 类似于1->多的操作,也是旧数据移到后面新数据插入前面
    将数组扩容newCount大小
    -> array()count记录个数
    -> 调用memmove内存评议,从array()首地址偏移addedCount个元素位置开始,移动array()旧数据,占用oldCount个元素大小
    -> 调用memcpy内存拷贝,从array()首地址开始,将新数据addedLists插入,占用addedCount个元素大小。

    image.png

    所以这里rwe函数、属性、协议都是attachLists进行处理后完成的赋值。

    image.png

    5. attachCategories的调用

    此时,我们通过一条线,完整熟悉了attachCategories分类数据添加到rwe中的整个流程和细节。

    • 我们可以反过来搜索attachCategories被哪些地方调用:
    image.png

    我们发现,除了我们已分析的attachToClass函数,就只有load_categories_nolock函数调用了attachCategories

    • 进入load_categories_nolock,加入测试代码:
    const char *mangledName = cls->mangledName();
    const char * HTPersonName = "HTPerson";
    if (strcmp(HTPersonName, mangledName) == 0 ) {
        auto ht_ro = (const class_ro_t *)cls->data();
        auto ht_isMeta = ht_ro->flags & RO_META;
        if (!ht_isMeta) {
            printf("%s - 精准定位: %s\n", __func__, mangledName);
        }
    }
    
    • 再检查load_categories_nolock在哪里被调用:

    第一处被调用:loadAllCategories

    image.png

    继续搜索loadAllCategories,发现在load_images被调用:

    image.png

    第二处被调用:_read_images第8步 分类的加载

    image.png
    • _read_images的加载,是从map_images过来的。

    总结:
    分类的加载,总得来说有2个大的调用路径

      1. map_images -> map_images_nolock -> _read_images 有2个可能路径:
        路径一: 第8步 分类的处理 -> load_categories_nolock -> attachCategories
        路径二: 第9步 实现非懒加载类 -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories
      1. load_images -> loadAllCategories -> load_categories_nolock -> attachCategories

    至此,文初的2个问题,rwe何时加载?分类如何加载? 相信大家都十分清楚了


    本节,我们已经熟悉了分类加载方式

    • 但是我们一切研究都是在本类分类都实现+Load方法的前提,那其他组合的情况是怎样呢?
    • attachCategories这些调用路径在什么情况下进入哪条路径呢?

    下一节OC底层原理十九:类的加载(下) 本类与分类load区别 & 关联属性,我们将所有情况都一一分析。

    相关文章

      网友评论

        本文标题:OC底层原理十八:类的加载(中) SEL & 分类的加载

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