iOS 底层探索篇 ——类的加载原理补充,类拓展和关联对象
1.分类加载是否需要排序
由上一篇文章知道,在方法列表中,2个分类的方法的数组指针是在主类之前的,并且数组指针里面是经过排序的。
首先添加2个分类,并且类和2个分类都有一个方法叫做testfunc1
。
运行,看到getMethodNoSuper_nolock
里面。看到这里是遍历查找
。
输出看到beginLists
是分类LS
。
array_t
里面存的是3个2维指针
,这个指针没有必要进行排序,他们各自有各自的排序。取methodList的时候进行了遍历查找,也说明了分类加载不需要进行排序。
那么2分查找的时候需要进行probe--
呢?因为当类和分类都在data数据段
被读取的时候,方法整合到一块
了,所以需要probe–去找到最前面
的那个方法。
2.methodList的数据结构
methodList的数据结构是method_list_t
,继承自entsize_list_tt
。
从上篇文章中可以知道,methodList
是以指针方式存储的。
而当需要获取其中的元素时候,需要调用get
方法来获取,这也说明了method_list_t里面存的也是指针而不是值
,否则的话是可以以数组的方式取出来的。而指针则指向了method_t
。
在看到get方法,可以看出get方法是通过地址平移
来获取到指针地址的。
3.主类没有实现load方法,分类实现load方法时的数据加载
当主类没有实现load方法,那么分类时候load方法时候情况会是什么样的呢?
首先创建3个分类,其中2个分类里面有方法和属性,剩下一个没有
。
在有方法和属性的分类中,选择一个实现load方法
。
运行后发现主类非懒加载
,但是分类都没有加载
。
两个分类都实现load方法
,运行后可以发现分类加载进去了
。
这里可以得出,当2个或以上的分类实现了load方法,那么分类就会进行非懒加载。
在看到attachCategories
的时候,cats_count为2
,那么是哪个分类没有加载进去呢?
输出来看一下哪个分类没有加载。这里看到LS
和LSS
加载了,LSSS
也就是没有方法和属性的分类没有加载,说明了系统做了优化,当分类什么都没有的时候,就不会被加载浪费性能。
在看到load_images
里面,发现走的是prepare_load_methods
而不是正常的loadAllCategories
方法。prepare_load_methods
也就是迫使主类实现的地方,
看到里面的prepare_load_methods
的实现,发现确实让主类进行了非懒加载,也就是让主类被迫营业了。
4.class_ro_t
在llvm看一下class_ro_t。
看到这里有个read
,查找一下read。看到read里面进行了地址的处理,然后获得了extractor
,然后进行了各个属性进行了赋值
。
5.类扩展
分类:categoty
- 专门用来给类添加新方法。
- 不能给类添加成员属性,添加了成员变量也无法取到,但是可以通过runtime给分类添加属性,即属性关联,重写setter、getter方法。
- 分类中用@property定义变量,只会生成变量的getter,setter方法的声明,不能生成方法实现和带下划线的成员变量。
扩展:extension
- 特殊的分类,也叫做匿名分类。
- 可以给类添加成员属性,但是是私有变量。
- 可以给类添加方法,也是私有方法。
这就是扩展,其必须定义在@interface 和 @implementation 之间
。
clang
一下 main.m 后打开。发现拓展的方法以及属性已经存在了
那么类扩展是否会和分类那样有一个自己的结构体呢?找了一圈发现没有。分类会影响到类的加载
,那么拓展会吗?
为LGPerson
添加一个类扩展并添加方法,然后在LGPerson中添加方法的实现。LGPerson实现load
方法,在realizeClassWithoutSwift
打下断点然后运行。
运行后在realizeClassWithoutSwift输出ro的方法列表然后观察,发现类扩展已经被加载进去了。也就是说,在类加载的时候,类扩展作为类的一部分已经加载进去了。
6.关联对象
关联对象的底层原理的实现,主要分为两部分:
- 通过
objc_setAssociatedObject
设值流程 - 通过
objc_getAssociatedObject
取值流程
在分类LGA
中重写属性cate_namet
、cate_age
的set
、get
方法,通过runtime
的属性关联方法也就是objc_setAssociatedObject
和objc_getAssociatedObject
实现。
关联对象设值流程
看objc_setAssociatedObject
的具体实现。这种设计模式属于是接口模式
,这种模式下对外的接口不变,也就是外部调用永远是调用objc_setAssociatedObject,内部的逻辑变化不影响外部的调用
。
然后来到_object_set_associative_reference
里面。看到这里有四个参数。
- objc:要关联的对象,即给谁添加关联属性
- key:标识符,方便下次查找
- value:要存的值
- policy: 关联策略
往下看,看到了对object
进行了处理。
点进去查看disguise
方法,对ptr
进行了处理,也就是参数
的处理,所以这里就是对object
包装了一下,包装成统一的数据结构
。
在往下是ObjcAssociation association{policy, value}
。
这里的const void *
也就是属性的名字,比如说cate_name,cate_age,这里是键值匹配。而在AssociationsHashMap
中,不同对象也对应不同的AssociationsMap,所以就是双层HashMap结构
。
运行一下程序,来到_object_set_associative_reference
里面。
输出一下确实是我们设的值。
其中,AssociationsManager manager
是一个构造函数。模仿着写一个以便更好的理解。
运行后发现输出了 KC 来了 和大师班 NB 。
所以AssociationsManager manager
相当于调用了 Ass ociationsManager()
和~AssociationsManager()
。
在看到AssociationsHashMap
其实是获取_mapStorage
,而_mapStorage是静态变量
,所以AssociationsHashMap这张表是单例,是唯一的
。
回到_object_set_associative_reference
往下走,输出associations
,发现里面还没有值。
再输出refs_result
,看到second
为true
,那么就会把 isFirstAssociation 设为 true,看到注释也写着 it’s the first association we make,也就是当第一次关联的时候会把 isFirstAssociation设为true
。
refs_result是从associations.try_emplace(disguised, ObjectAssociationMap{})
来的,看一下try_emplace
方法。看到这里会创建新的桶子
,这里的key是LGPerson 的地址
。
首先会进入LookupBucketFor
去查找是有已经有了桶子,看到两个实现,因为在try_emplace中是BucketT没有 const
,所以走的是下面的实现。下面的实现会调用上面的实现。
看上面的实现这里是得到了哈希的下标
。
往下就开始死循环找bucket,没找到就在哈希。
当第一次进来的时候,LookupBucketFor(Key, TheBucket)
是找不到的,所以走到了下面。这里就插入一个空的桶子进去,并且还进行了3/4扩容2倍
的操作。
然后就得到了 TheBucket
_object_set_associative_reference
往下走,又进入了一次try_emplace
。
进去后发现LookupBucketFor(Key, TheBucket)
还是没有进去,这是因为
这时候bucket里面还没有东西插进去,如果是空的桶子,LookupBucketFor
也会返回false
。
这里的又会走到InsertIntoBucket
,然后在InsertIntoBucket
里面将值插入到first
里面
在_object_set_associative_reference
中输出result
,发现first中有了值。
而first里面的ptr
则是我们存进去的bucket
。
而当传一个空
的值的话,那么就会把这个桶子清除
。
关联对象: 设值流程总结
1: 创建一个 AssociationsManager 管理类
2: 获取唯一的全局静态哈希Map
3: 判断是否插入的关联值是否存在:
3.1: 存在走第4步
3.2: 不存在就走 : 关联对象插入空流程
4: 创建一个空的 ObjectAssociationMap 去取查询的键值对
5: 如果发现没有这个 key 就插入一个 空的 BucketT进去 返回
6: 标记对象存在关联对象
7: 用当前 修饰策略 和 值 组成了一个 ObjcAssociation 替换原来 BucketT 中的空
8: 标记一下 ObjectAssociationMap 的第一次为 false
关联对象插入空流程总结
1: 根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
2: 清理迭代器
3: 其实如果插入空置 相当于清除
关联对象流程图。
图片来自链接: Style_月月.