调试代码准备
1.下载objc818可调试源码
2.main.m
文件添加调试代码:
/**************本类声明start**************/
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)instanceMethod;
+ (void)classMethod;
@end
/**************本类声明end**************/
/**************类扩展start**************/
@interface LGPerson ()
@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, assign) int ext_age;
- (void)ext_instanceMethod;
+ (void)ext_classMethod;
@end
/**************类扩展end**************/
/**************本类实现start**************/
@implementation LGPerson
- (void)instanceMethod{
NSLog(@"%s",__func__);
}
+ (void)classMethod{
NSLog(@"%s",__func__);
}
- (void)ext_instanceMethod{
NSLog(@"%s",__func__);
}
+ (void)ext_classMethod{
NSLog(@"%s",__func__);
}
@end
/**************本类实现end**************/
/**************分类start**************/
@interface LGPerson (LGA)
@property (nonatomic, copy) NSString *cateA_name;
@property (nonatomic, assign) int *cateA_age;
- (void)cateA_instanceMethod1;
- (void)cateA_instanceMethod2;
+ (void)cateA_classMethod1;
+ (void)cateA_classMethod2;
@end
@implementation LGPerson (LGA)
+ (void)load{}
- (void)cateA_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod2{
NSLog(@"%s",__func__);
}
+ (void)cateA_classMethod1{
NSLog(@"%s",__func__);
}
+ (void)cateA_classMethod2{
NSLog(@"%s",__func__);
}
@end
/**************分类end**************/
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson * person = [LGPerson alloc];
}
return 0;
}
3.将main.m
编译成c++
文件
clang -rewrite-objc main.m -o main.cpp
类的扩展 VS 分类
1.验证类扩展是否影响类的加载
打开main.cpp
文件,全局搜索_method_list_t
能够看到主类和类扩展的方法放在一起
分类的方法单独放在一起
分类有自己的数据结构
_category_t
,并没找到类扩展单独的数据结构。
根据前面的篇章,我们知道分类实现load
方法会影响类的加载和编译,类扩展是否也会影响呢?
下面来验证一下:
首先要删除main.m
文件中的分类部分代码,然后运行lldb
调试:
此时主类方法和类扩展的方法都已经加载进来了,也就是说类扩展方法和主类方法都伴随着类的加载一起加载,并不会影响类的加载和编译。
2.类的扩展与分类区别与联系
1.category
:类别,分类
- 专门用来给类添加新的方法
- 不能该类添加成员属性,添加了成员变量,也无法取到。
- 注意:其实可以通过
runtime
给分类添加属性 - 分类中用
@properity
定义变量,只会生成变量的getter
、setter
方法的声明,不会生成方法的实现和带下划线的成员变量。 2.extension
:类扩展 - 可以说成是特殊的分类,也称匿名分类
- 可以给类添加成员属性,但也是似有变量
- 可以给类添加方法,也是私有方法
补充
1.methodList数据结构
通过
lldb
调试,可以通过get
方法拿到method_t
。那么
method_list_t
数据结构是什么样的呢?
method_list_t
继承entsize_list_tt
能够使用
get
是因为entsize_list_tt
结构体内部实现了get
方法,通过:
get
方法传入偏移量,然后调用getOrEnd
,最终返回一个偏移的指针。所以method_list_t
是个数组Array_t
,需要偏移量取值,里面存储的是指针,指向method_t
。
method_t
的数据结构
method_t
能够使用调用big
方法打印name
、imp
、types
。
2.主类没有实现load方法,分类实现了,数据加载情况
上篇文章分析到:主类没有实现`load方法,分类有调用顺序:
_read_images
非懒加载类 > realizeClassWithoutSwift
> methodizeClass
> attachToClass
注意:没有调用
attachCategories
这次我添加了多个分类,且都实现了load
方法。
通过调用测试得出结论:超过一个分类实现load
方法,会调用attachCategories
方法。
为什么会出现这种情况呢?
因为主类没有实现
load
方法,它调用顺序是:
load_images
>prepare_load_methods
>realizeClassWithoutSwift
。
调用了realizeClassWithoutSwift
,最终会执行attachCategories
方法。
注意:如果只创建了分类文件,没有实现任何,这个分类不会被加载
3.分类加载是否需要排序
有上面的结论得出method_list_t
是个数组里面存储的是指针,如果有多个分类,就是二维数组,method_list_t
存的就是各个分类方法数组的的指针,及主类方法数组的指针。
分类方法数组指针会排在主类方法数组的指针前面,不需要将各数组内部指针取出进行排序。
如果不需要排序,那么消息查找过程中,为什么要二分查找:
// 如果分类中也有这个方法,调用分类方法
while (probe > first && keyValue == (uintptr_t)getName((probe - 1)))
{
probe--;
}
因为方法加载除了二维指针的形式(调用attachCategories
),还有就是存在data
数据段里的,将分类主类的方法都放到了一起,这时通过while
循环减减,找到最前面的方法调用。
4.class_ro_t:通过指针地址获得数据结构原因
我们lldb
调试的时候,通过一个class_ro_t
的一个指针就能得到class_ro_t
的数据结构,并能赋值,为什么可以这么操作。
read_images
> readClass
的调用,就使用了class_ro_t
,就有了Macho
的数据,这还是在dyld
阶段,所以class_ro_t
相关处理因该在LLVM
阶段。
进入LLVM源码工程,全局搜索class_ro_t {
Read(Process *process, lldb::addr_t addr)
实现关键部分:
左边红框内是结构体class_ro_t
内部成员,右边红框是根据地址偏移计算的值。然后进行结构体赋值。
比如类的指针,通过强转,也能够进行相关赋值
关联对象
我们给分类添加属性,是通过关联对象实现的,通过objc_setAssociatedObject
进行设值,通过objc_getAssociatedObject
进行取值,通过objc_removeAssociatedObjects
进行释放。那么接下来通过源码调试来探究关联对象流程。
修改main.m
文件
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson * person = [LGPerson alloc];
person.cate_name = @"KC";
person.cate_age = @"18";
[person saySomething];
LGTeacher *teacher = [LGTeacher alloc];
teacher.cate_name = @"Cooci";
}
return 0;
}
objc_setAssociatedObject
在objc818
中是这么定义的:
接着看_object_set_associative_reference
的内部实现:
将
object
统一保存成DisguisedPtr
类型
对
policy
,value
进行面向对象方式的存储,当执行到
person.cate_name = @"KC";
打印association
如下:
AssociationsManager
:定义
AssociationsManager
初始化会调用AssociationsManagerLock.lock()
,过了函数作用域就会析构调用AssociationsManagerLock.unlock()
下面
AssociationsHashMap
是一个全局的map
,key
是DisguisedPtr<objc_object>
,value
是ObjectAssociationMap
回到_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
函数中
接着来到
第一次进来,调用
InsertIntoBucket
函数,插入一个空的bucket
,并返回make_pair
,makeIterator
的第三个参数为true
refs_result.second
赋值为true
第二次进入try_emplace
第二次进入try_emplace
,并再次调用InsertIntoBucket
函数,这次传过来的是association
查找到
bucket
,把association
放到bucket
里面,替换其中的空
关联对象设值流程:
1.创建一个AssociationsManager
管理类
2.获取唯一的全局静态哈希Map
3.判断是否插入的关联值是否存在:
3.1存在走第4步
3.2不存在走:关联对象插入空流程
4.创建一个空的ObjectAssociationMap
去取查询的键值对
5.如果发现没有这个key
就插入一个空的BucketT
进去并返回
6.标记对象存在关联对象
7.用当前修饰策略和值组成一个ObjcAssociation
替换原来BucketT
中的空
8.标记一下ObjcetAssociationMap
的第一次为false
如果_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
参数value
为空
关联对象插入空流程:
1.根据DisgursedPtr
找到AssociationsHashMap
中的iterator
迭代查询器
2.清理迭代器
3.其实如果插入空值相当去清除
objc_getAssociatedObject
在objc818
中是这么定义的:
关联对象取值流程
1:创建一个AssociationsManager
关联类
2.获取唯一的全局静态哈希Map
3.根据DisgursedPtr
找到AssociationsHashMap
中的iterator
迭代查询器
4.如果这个迭代查询器不是最后一个获取:OBjectAssociationMap
(策略和value)
5.找到AssociationsHashMap
中的迭代查询器获取一个经过属性修饰符修饰的value
6.返回value
objc_removeAssociatedObjects
在objc818
中是这么定义的:
关联对象释放流程
1:创建一个AssociationsManager
关联类
2.获取唯一的全局静态哈希Map
3.根据DisgursedPtr
找到AssociationsHashMap
中的iterator
迭代查询器
4.如果这个迭代查询器不是最后一个获取:OBjectAssociationMap
(策略和value)
5.擦除OBjectAssociationMap
总结:其实就是两层哈希map
,存取的时候两层处理(类似二维数组)