iOS 底层原理 IOS开发知识点iOS 进阶之路

OC底层原理十九:类的加载(下) 本类与分类load区别 & 关

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

OC底层原理 学习大纲

上一节 ,我们已完整的分析分类的加载过程,知识量较大,需要慢慢消化。

本节进行拓展和补充以下内容:

  1. 本类分类+load区别
  2. Category分类与Extension拓展的区别
  3. 关联对象

准备工作:

1. 本类分类+load区别

上一节我们的研究都是在本类分类实现+Load方法的前提下完成的。 而且attachCategories有多种被调用的路径,具体什么情况走哪条路径,我们不清楚。

现在,我们开始覆盖性测试和探究:(ps: 下面以+load区分是否实现+load方法)

  1. 本类+load,分类
  2. 本类+load,分类+load
  3. 本类,分类
  4. 本类,分类+load
  5. 本类,分类A ,分类B+load

准备阶段

// 本类
@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

// 分类 CatB
@interface HTPerson (CatB)

@property (nonatomic, copy) NSString *catB_name;
@property (nonatomic, assign) int catB_age;

- (void)func1;
- (void)func3;
- (void)func2;

+ (void)classFunc;

@end

@implementation HTPerson (CatB)

+ (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[]) {
    @autoreleasepool {
        HTPerson * person = [HTPerson alloc];
        [person func1];
    }
    return 0;
}

我们在readClassattachCategories两个函数内部加入定位的测试代码,并在printf一行加入断点(确保当前观察的使我们的HTPerson类):

  // >>>> 测试代码
    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);
        }
    }
    // <<<< 测试代码
readClass加入测试代码和断点 attachCategories加入测试代码和断点 image.png

准备工作完成后,我们可以开始探索了:

1.1 本类+load,分类无

测试配置: 保留HPPerson类+load,注释掉CatACatB分类的+load方法

image.png

提取信息如下:

  1. 路径: 是map_images调用的
  2. ro函数列表:此时ro读取的是macho中的,ro中已包含本类和所有函数信息(14个)。
  3. 函数排序: 分类的函数不会覆盖本类的同名函数,而是后加载的分类函数排序在先加载的分类和本类前面

结论:【本类+load,分类无】的情况:数据在编译层已经加入data中。

1.2. 本类+load,分类+load

测试配置: 保留HPPerson类CatACatB分类的+load方法

image.png

提取信息如下:

  1. 路径: 是map_images调用的
  2. ro函数列表:此时ro读取的是macho中的,ro中仅有HTPerosn本类函数信息(8个)。

继续运行代码,进入attachCategories处:

image.png

attachLists拓展:

此处可观察到attachLists加载顺序,验证上一节attachLists的分析

  • 我们在attachLists加入三个断点,检查排序。

  • 运行代码,发现第一次是从extAllocIfNeeded初始化rwe时进入,从macho中只存储了本类信息,由于当前是首次创建,所以attachLists走的是0->1的流程,是直接将addLists[0]赋值给了list

    image.png
  • 继续运行代码,发现是本类的属性进入attachLists0->1

    image.png
  • 继续运行代码,发现CatA函数进入attachLists1->多
    (可以看到oldListHTPerson本类8个函数,addedListsCatA分类3个函数)

    image.png
  • 继续运行代码,发现CatA属性进入attachLists1->多

image.png
  • 继续运行代码,发现本类的元类函数(类方法)进入attachLists0->1
image.png
  • 继续运行代码,发现CatA的元类函数(类方法)进入attachLists1->多
image.png
  • 继续运行代码,又回到了attachCategories处,我们继续运行代码,进入CatB函数进入attachLists->更多:
image.png
  • 继续运行代码,发现CatB属性进入attachLists多->更多

    image.png
  • 继续运行代码,发现CatB的元类函数(类方法)进入attachLists多->更多

    image.png

总结:


image.png

1.3. 本类无,分类无

测试配置: 注释HPPerson类CatACatB分类的+load方法

image.png

此时在map_images阶段,macho中记录了本类所有分类数据

1.4. 本类无,分类+load

测试配置: 注释HPPerson类+load方法、保留CatACatB分类的+load方法

image.png image.png image.png image.png

💣 注意: 此时addedCount2,表示当前需要添加的列表有2个元素。并不是只有CatB分类。我们打印 addedLists[0]addedLists[1],就找到了CatACatB两个分类

Q: 为什么本类没有+load方法,只实现分类+load方法,也在app启动前加载出来了呢?

A: 我们查看左边堆栈,load_images调用了prepare_load_methods

image.png
  • prepare_load_methods中会检查有没有非懒加载的分类,如果有就执行下面的循环。
    循环中在add_category_to_loadable_list加载分类前,会执行realizeClassWithoutSwift先检查本类是否实现。
image.png

1.5 本类,分类A ,分类B+load

测试配置: 注释HPPerson类CatA分类的+load方法,保留CatB分类的+load方法

image.png

本类,分类A+load ,分类B 的结果与这个一样


总结:本类和分类的+load区别:

image.png

2. Category分类与Extension拓展的区别

2.1 Category:类别,分类

  • 成员属性不可添加:
@interface HTPerson(CatA) {
    NSString * catA_name; // 不可这样添加
}
  • @property属性可添加:
@interface HTPerson(CatA)
@property (nonatomic, copy) NSString *prop_name;
@end

编译器可读取名称。表示有gettersetter方法的声明。

  • 运行后会crash。是因为没有实现带下划线成员变量
    image.png

2.2 Extension:类拓展

拓展必须添加在@interface声明@implementation实现之间:

image.png
  • Extension拓展@interface声明是一样的作用,但是Extension拓展中的成员变量属性方法都是私有的。
  • 可以通过clang,查看编译结果进行验证Extension类拓展下划线成员变量函数等,都直接加入本类相关位置完成相应实现

Q: Category中的属性如何用runtime实现?

// CatA分类
#import <objc/runtime.h>
// 本类
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation HTPerson
@end

// CatA分类
@interface HTPerson (CatA)
@property (nonatomic, copy) NSString *catA_name; // 属性
@end

@implementation HTPerson(CatA)

- (void)setCatA_name:(NSString *)catA_name { // 给属性`catA_name`,动态添加set方法
    objc_setAssociatedObject(self, "catA_name", catA_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)catA_name { // 给属性`catA_name`,动态添加get方法
    return objc_getAssociatedObject(self, "catA_name");
}
@end

参数解读:

  • 动态设置关联属性: objc_setAssociatedObject(关联对象,关联属性key,关联属性value策略)
  • 动态读取关联属性:objc_getAssociatedObject(关联对象,关联属性key)

3. 关联对象

image.png image.png image.png
SetAssocHook.get()(object, key, value, policy);

可以直接写成

_base_objc_setAssociatedObject(object, key, value, policy);
image.png

加入断点,验证一下,确实是给HTPerson属性完成了赋值

image.png

3.1 回顾本类正常属性写入操作:

image.png image.png

熟悉了常规属性写入流程。 现在我们来对比关联对象写入操作

3.2 关联对象写入操作:

我们回到_object_set_associative_reference流程:

3.2.1 记录数据
  • 查看DisguisedPtr结构,只有一个value。 所以实际是将入参object对象给到DisguisedPtr对象的value,包装记录一下。
image.png
  • 查看ObjcAssociation结构,只有_policy_value。 所以实际是将入参policy策略和value新值给到ObjcAssociation对象,包装记录一下。
image.png
3.2.2 新值retain
image.png
3.2.3 赋值或释放
1. 创建管理对象 & hashMap
   AssociationsManager manager;
image.png

Q: 这样真的创建了对象吗?

  • 我们创建HTObjc进行测试,打印结果显示,确实是构造析构函数:
    image.png

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

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

2. 代码结构分析:

  • 进入get(),发现是调用的_storage

    image.png
  • 返回查看_storage,发现是static静态声明。所以AssociationsHashMap确实是内存中独一份

    image.png
2 关联值value是否存在

2.1 value存在(赋值)

运行代码。断点查询,发现没有这个key插入一个空的BucketT进去并返回true

image.png image.png

Q:请问关联对象是否需要手动释放
A:指针优化的isa中的has_assoc记录了是否有关联属性,在析构函数触发时,会检查是否有关联属性主动释放

image.png
  • 查看hasAssociatedObjects
    image.png
image.png
2.2 value不存在(移除):
image.png
3.2.4 旧值release
image.png

小总结:

  • AssociationsHashMap内有多个类对key-value结构,而每个类对应的value,又包含多个关联属性对key-value结构。
  • 所以我们不管插入还是移除,都是先通过信息找到相应的类对,再从类对value中,通过关联属性key找到对应的关联属性,进行相应操作。

其中复杂的DisguisedPtrObjcAssociation结构,都只是关联属性信息的一层包装,负责记录信息统计计数而已。


至此,我们对类的加载,分类和拓展、关联属性,都已经非常熟悉了。

上一篇下一篇

猜你喜欢

热点阅读