调试代码准备
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,存取的时候两层处理(类似二维数组)