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

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

准备阶段

  • 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

// 分类 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加入测试代码断点
readClass加入测试代码和断点
  • attachCategories加入测试代码断点
attachCategories加入测试代码和断点
  • HTPerosn首次调用处加上断点:
image.png

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

1.1 本类+load,分类无

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

  • 运行代码,进入了readClass处:
image.png

提取信息如下:

  1. 路径: 是map_images调用的
  2. ro函数列表:此时ro读取的是macho中的,ro中已包含本类和所有函数信息(14个)。
  3. 函数排序: 分类的函数不会覆盖本类的同名函数,而是后加载的分类函数排序在先加载的分类和本类前面
  • 放开断点,继续运行,发现没有进入attachCategories内部。

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

1.2. 本类+load,分类+load

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

  • 运行代码,进入了readClass处:
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方法

  • 运行代码,进入了readClass处:
image.png

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

  • 继续运行代码,没有进入attachCategories中。

1.4. 本类无,分类+load

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

  • 运行代码,进入了readClass处:
image.png
  • 继续运行代码,进入了attachCategories处,在attachLists加入三个断点,继续运行,发现attachLists0->1加载了HTPerosn本类函数
image.png
  • 继续运行代码,发现attachLists0->1加载了HTPerosn本类属性
image.png
  • 继续运行代码,发现进入了attachLists中`1->多:
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方法

  • 运行代码,进入了readClass处:
image.png
  • 发现ro加载好本类和2个分类所有数据(14个函数),没有再进入attachCategories了。

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


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

image.png


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

2.1 Category:类别,分类

  • 专门用来给类添加新的方法
  • 不能给类添加成员属性,添加了也取不到。
  • 分类中用@property定义的变量,只会生成变量的 gettersetter方法,不能生成方法实现带下划线成员变量
  • 成员属性不可添加:
@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实现?

  • A: 在属性的getset方法实现内,动态添加关联对象
// 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. 关联对象

  • 点击进入objc_setAssociatedObject:
image.png
  • 点击进入get():
image.png
  • 看不懂是什么..., get()看不懂,那我们往看看它的调用者:SetAssocHook
image.png
  • 我们查看结构,发现它就是嵌套了一层objc_hook_setAssociatedObject的方法。调用get(),就是读取内容。所以:
SetAssocHook.get()(object, key, value, policy);

可以直接写成

_base_objc_setAssociatedObject(object, key, value, policy);
  • 我们进入_base_objc_setAssociatedObject
image.png

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

image.png
  • 进入_object_set_associative_reference

    image.png

  • 在分析关联对象写入操作前,我们先回顾一下本类的正常属性的写入操作:

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

  • cdmain.m文件夹,clang -rewrite-objc main.mm -o main.cpp编译一份cpp文件,打开main.cpp文件,搜索HTPerson,找到属性nameset方法:

    image.png

  • 发现常规是调用objc_setProperty完成set方法,我们在源码中检查objc_setProperty的实现:

image.png
  • 进入reallySetProperty
image.png
  • 主要流程:1. 通过地址读取属性 -> 2.新值retain -> 3.属性赋值 -> 4.旧值release

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

3.2 关联对象写入操作:

我们回到_object_set_associative_reference流程:

3.2.1 记录数据
  • DisguisedPtrObjcAssociation分别对入参objectpolicyvalue进行了包装。
  • 查看DisguisedPtr结构,只有一个value。 所以实际是将入参object对象给到DisguisedPtr对象的value,包装记录一下。
image.png
  • 查看ObjcAssociation结构,只有_policy_value。 所以实际是将入参policy策略和value新值给到ObjcAssociation对象,包装记录一下。
image.png
3.2.2 新值retain
  • 接下来查看acquireValue(),发现是完成了新值retain:
image.png
3.2.3 赋值或释放
  • 接下来到了核心执行环节
1. 创建管理对象 & hashMap
   AssociationsManager manager;
image.png

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

  • 我们创建HTObjc进行测试,打印结果显示,确实是构造析构函数:
    image.png
  • AssociationsManager结构中,manager只是对外代言人,并不是唯一的,AssociationsHashMap才是唯一的。

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存在(赋值)

  • 返回结构如下:


    image.png
  • try_emplace创建空ObjectAssociationMap查询的键值对

  • 进入try_emplace查看源码:(不管是否存在,都会返回true)

    image.png

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

  • 进入LookupBucketFor,发现有两个同名方法,是重载方法,唯一区别是第二个入参的是否有const

    image.png

  • 我们观察外部try_emplace源码,入参TheBucket是没有const声明的,所以进入的是第二个LookupBucketFor:

    image.png

  • 回到第一个LookupBucketFor,循环查找key对应的buckets:

    image.png

  • 通过setHasAssociatedObjects标记对象存在关联对象

image.png
  • 查看setHasAssociatedObjects:
image.png

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

image.png
  • 查看hasAssociatedObjects
    image.png
  • 继续往下执行,我们在第二次try_emplace前后检查refs:

  • 第二次try_emplace前:插入的Bucktes是空桶,所以还没值:

    image.png

  • 第二次try_emplace后:插入的Bucktes已经有值了:

    image.png

  • 往下走,到达association.swap(result.first->second)时,我们用当前policy策略value值组成了一个ObjcAssociation替换原来BucketT中的空:

image.png
  • 观察内容,此时赋值操作已完成。
2.2 value不存在(移除):
  • 首先,寻找类对

    image.png

  • 查看find内部:找到了返回buckets没找到返回end()

image.png
  • 先找到类对,再找到当前类的关联属性对,将当前关联属性对质空,buckets计数更新
    image.png
3.2.4 旧值release
  • 接下来查看releaseHeldValue(),发现是完成了旧值retain:
image.png

小总结:

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

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


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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342