iOS底层探索分类关联对象

1,632 阅读6分钟

调试代码准备

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

image.png

能够看到主类和类扩展的方法放在一起

image.png

分类的方法单独放在一起

image.png 分类有自己的数据结构_category_t,并没找到类扩展单独的数据结构。 根据前面的篇章,我们知道分类实现load方法会影响类的加载和编译,类扩展是否也会影响呢?

下面来验证一下:

首先要删除main.m文件中的分类部分代码,然后运行lldb调试:

image.png

此时主类方法和类扩展的方法都已经加载进来了,也就是说类扩展方法和主类方法都伴随着类的加载一起加载,并不会影响类的加载和编译。

2.类的扩展与分类区别与联系

1.category:类别,分类

  • 专门用来给类添加新的方法
  • 不能该类添加成员属性,添加了成员变量,也无法取到。
  • 注意:其实可以通过runtime给分类添加属性
  • 分类中用@properity定义变量,只会生成变量的gettersetter方法的声明,不会生成方法的实现和带下划线的成员变量。 2.extension:类扩展
  • 可以说成是特殊的分类,也称匿名分类
  • 可以给类添加成员属性,但也是似有变量
  • 可以给类添加方法,也是私有方法

补充

1.methodList数据结构

image.png 通过lldb调试,可以通过get方法拿到method_t。那么 method_list_t数据结构是什么样的呢?

image.png

method_list_t继承entsize_list_tt

image.png 能够使用get是因为entsize_list_tt结构体内部实现了get方法,通过: get方法传入偏移量,然后调用getOrEnd,最终返回一个偏移的指针。所以method_list_t是个数组Array_t,需要偏移量取值,里面存储的是指针,指向method_t

method_t的数据结构 image.png method_t能够使用调用big方法打印nameimptypes

2.主类没有实现load方法,分类实现了,数据加载情况

上篇文章分析到:主类没有实现`load方法,分类有调用顺序:

_read_images非懒加载类 >  realizeClassWithoutSwift > methodizeClass > attachToClass

注意:没有调用attachCategories

这次我添加了多个分类,且都实现了load方法。

通过调用测试得出结论:超过一个分类实现load方法,会调用attachCategories方法。

为什么会出现这种情况呢?

image.png 因为主类没有实现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 {

image.png

Read(Process *process, lldb::addr_t addr)实现关键部分:

image.png

左边红框内是结构体class_ro_t内部成员,右边红框是根据地址偏移计算的值。然后进行结构体赋值。

比如类的指针,通过强转,也能够进行相关赋值

image.png

关联对象

我们给分类添加属性,是通过关联对象实现的,通过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中是这么定义的: image.png

接着看_object_set_associative_reference的内部实现:

image.pngobject统一保存成DisguisedPtr类型

image.png

image.pngpolicyvalue进行面向对象方式的存储,当执行到

person.cate_name  = @"KC";

打印association如下:

图片.png

image.png AssociationsManager:定义

image.png AssociationsManager初始化会调用AssociationsManagerLock.lock(),过了函数作用域就会析构调用AssociationsManagerLock.unlock()

image.png image.png 下面AssociationsHashMap是一个全局的mapkeyDisguisedPtr<objc_object>valueObjectAssociationMap

回到_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)函数中 接着来到

image.png image.png 图片.png 第一次进来,调用InsertIntoBucket函数,插入一个空的bucket,并返回make_pairmakeIterator的第三个参数为true image.png refs_result.second赋值为true image.png

第二次进入try_emplace image.png 图片.png

第二次进入try_emplace,并再次调用InsertIntoBucket函数,这次传过来的是association

image.png 查找到bucket,把association放到bucket里面,替换其中的空

图片.png 图片.png

关联对象设值流程:

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为空

image.png image.png

关联对象插入空流程:

1.根据DisgursedPtr找到AssociationsHashMap中的iterator迭代查询器

2.清理迭代器

3.其实如果插入空值相当去清除

objc_getAssociatedObject

objc818中是这么定义的: 图片.png 图片.png 关联对象取值流程

1:创建一个AssociationsManager关联类

2.获取唯一的全局静态哈希Map

3.根据DisgursedPtr找到AssociationsHashMap中的iterator迭代查询器

4.如果这个迭代查询器不是最后一个获取:OBjectAssociationMap(策略和value)

5.找到AssociationsHashMap中的迭代查询器获取一个经过属性修饰符修饰的value

6.返回value

objc_removeAssociatedObjects

objc818中是这么定义的:

图片.png 图片.png 关联对象释放流程

1:创建一个AssociationsManager关联类

2.获取唯一的全局静态哈希Map

3.根据DisgursedPtr找到AssociationsHashMap中的iterator迭代查询器

4.如果这个迭代查询器不是最后一个获取:OBjectAssociationMap(策略和value)

5.擦除OBjectAssociationMap

总结:其实就是两层哈希map,存取的时候两层处理(类似二维数组)

associationManager.png