iOS 底层原理 iOS 进阶之路

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

2020-10-24  本文已影响0人  markhetao

OC底层原理 学习大纲

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

上节回顾:

上节回顾

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


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

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

准备工作:

1. sel注册

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

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

image.png
  • 其中_getObjc2SelectorRefsmacho__objc_selrefs,存储的内容是SEL:
    image.png

进入sel_registerNameNoLock:

image.png image.png

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

image.png
// 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);
image.png image.png image.png

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

拓展:

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

2.分类的本质

// 本类
@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. 官方帮助文档

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

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

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

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

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

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

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

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

image.png image.png

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

3. 分类的加载

如何研究呢?

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

// >>>> 测试代码
    const char *mangledName = cls->mangledName();
    const char * HTPersonName = "HTPerson";
    if (strcmp(HTPersonName, mangledName) == 0 ) {
        if (!isMeta) {
            printf("%s - 精准定位: %s\n", __func__, mangledName);
        }
    }
    // <<<< 测试代码
image.png

ro的读取:

image.png
image.png

methodizeClass的内容是:

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

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

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

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

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

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

4. attachCategories详解

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

image.png

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

image.png

attachCategories流程:

此处分为3小部分讲解:

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

4.1 rwe的初始化

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

image.png image.png

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

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

image.png

只有extAllocIfNeededdeepCopy调用了。

还记得上面ro的读取吗?

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

4.2 数据读取和prepareMethodLists函数排序

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

image.png image.png

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

  • 我们在上面定位测试代码打印处加上断点,运行代码,到达断点后,往下进入循环内:
    image.png
  • 发现此时name已从编译时的HTPerson变成了CatA,而我们的cls仍旧是HTPerson
    (类地址在内存中是唯一的,地址相同表示是一个类)
    image.png

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

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

image.png

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

image.png

4.3 prepareMethodLists函数排序

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

image.png image.png image.png

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

image.png

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

4.4 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中的整个流程和细节。

image.png

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

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);
    }
}

第一处被调用:loadAllCategories

image.png

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

image.png

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

image.png

总结:
分类的加载,总得来说有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何时加载?分类如何加载? 相信大家都十分清楚了


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

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

上一篇 下一篇

猜你喜欢

热点阅读