内容简介:有了之前和基于之前的源码分析,我们来分析一下
有了之前 Runtime 的基础,一些内部实现就很好理解了。在OC中可以通过 Category 添加属性、方法、协议,在 Runtime 中 Class 和 Category 都是通过结构体实现的。
和 Category 语法很相似的还有 Extension ,二者的区别在于, Extension 在编译期就直接和原类编译在一起,而 Category 是在运行时动态添加到原类中的。
基于之前的源码分析,我们来分析一下 Category 的实现原理。
在 _read_images 函数中会执行一个循环嵌套,外部循环遍历所有类,并取出当前类对应 Category 数组。内部循环会遍历取出的 Category 数组,将每个 category_t 对象取出,最终执行 addUnattachedCategoryForClass 函数添加到 Category 哈希表中。
// 将category_t添加到list中,并通过NXMapInsert函数,更新所属类的Category列表
static void addUnattachedCategoryForClass(category_t *cat, Class cls,
header_info *catHeader)
{
// 获取到未添加的Category哈希表
NXMapTable *cats = unattachedCategories();
category_list *list;
// 获取到buckets中的value,并向value对应的数组中添加category_t
list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
// 替换之前的list字段
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}
复制代码
Category 维护了一个名为 category_map 的哈希表,哈希表存储所有 category_t 对象。
// 获取未添加到Class中的category哈希表
static NXMapTable *unattachedCategories(void)
{
// 未添加到Class中的category哈希表
static NXMapTable *category_map = nil;
if (category_map) return category_map;
// fixme initial map size
category_map = NXCreateMapTable(NXPtrValueMapPrototype, 16);
return category_map;
}
复制代码
上面只是完成了向 Category 哈希表中添加的操作,这时候哈希表中存储了所有 category_t 对象。然后需要调用 remethodizeClass 函数,向对应的 Class 中添加 Category 的信息。
在 remethodizeClass 函数中会查找传入的 Class 参数对应的 Category 数组,然后将数组传给 attachCategories 函数,执行具体的添加操作。
// 将Category的信息添加到Class,包含method、property、protocol
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
isMeta = cls->isMetaClass();
// 从Category哈希表中查找category_t对象,并将已找到的对象从哈希表中删除
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
复制代码
在 attachCategories 函数中,查找到 Category 的方法列表、属性列表、协议列表,然后通过对应的 attachLists 函数,添加到 Class 对应的 class_rw_t 结构体中。
// 获取到Category的Protocol list、Property list、Method list,然后通过attachLists函数添加到所属的类中
static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// 按照Category个数,分配对应的内存空间
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
// 循环查找出Protocol list、Property list、Method list
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
// 执行添加操作
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
复制代码
这个过程就是将 Category 中的信息,添加到对应的 Class 中,一个类的 Category 可能不只有一个,在这个过程中会将所有 Category 的信息都合并到 Class 中。
方法覆盖
在有多个 Category 和原类的方法重复定义的时候,原类和所有 Category 的方法都会存在,并不会被后面的覆盖。假设有一个方法叫做 method , Category 和原类的方法都会被添加到方法列表中,只是存在的顺序不同。
在进行方法调用的时候,会优先遍历 Category 的方法,并且后面被添加到项目里的 Category ,会被优先调用。上面的例子调用顺序就是 Category3 -> Category2 -> Category1 -> TestObject 。如果从方法列表中找到方法后,就不会继续向后查找,这就是类方法被 Category ”覆盖”的原因。
问题
在有多个 Category 和原类方法重名的情况下,怎样在一个 Category 的方法被调用后,调用所有 Category 和原类的方法?
可以在一个 Category 方法被调用后,遍历方法列表并调用其他同名方法。但是需要注意一点是,遍历过程中不能再调用自己的方法,否则会导致递归调用。为了避免这个问题,可以在调用前判断被调动的方法 IMP 是否当前方法的 IMP 。
那怎样在任何一个 Category 的方法被调用后,只调用原类方法呢?
根据上面对方法调用的分析, Runtime 在调用方法时会优先所有 Category 调用,所以可以倒叙遍历方法列表,只遍历第一个方法即可,这个方法就是原类的方法。
Category Associate
在项目中经常会用到 Category ,有时候会遇到给 Category 添加属性的需求,这时候就需要用到 associated 的 Runtime API 了。例如下面的例子中,需要在属性的 set 、 get 方法中动态添加实现。
// 声明文件
@interface TestObject (Category)
@property (nonatomic, strong) NSObject *object;
@end
// 实现文件
#import <objc/runtime.h>
#import <objc/message.h>
static void *const kAssociatedObjectKey = (void *)&kAssociatedObjectKey;
@implementation TestObject (Category)
- (NSObject *)object {
return objc_getAssociatedObject(self, kAssociatedObjectKey);
}
- (void)setObject:(NSObject *)object {
objc_setAssociatedObject(self, kAssociatedObjectKey, object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
复制代码
在 Category 中添加属性后,默认是没有实现方法的,如果调用属性则会崩溃,而且还会提示下面两个警告信息。
Property 'object' requires method 'object' to be defined - use @dynamic or provide a method implementation in this category Property 'object' requires method 'setObject:' to be defined - use @dynamic or provide a method implementation in this category 复制代码
下面让我们看一下 associated 的源码,看 Runtime 是怎么通过 Runtime 动态添加 set 、 get 的。下面是 objc_getAssociatedObject 函数的实现代码, objc_setAssociatedObject 实现也是类似,这里节省地方就不贴出来了。
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
复制代码
从源码可以看出,所有通过 associated 添加的属性,都被存在一个单独的哈希表 AssociationsHashMap 中。 objc_setAssociatedObject 和 objc_getAssociatedObject 函数本质上都是在操作这个哈希表,通过对哈希表进行映射来存取对象。
在 associated 的 API 中会设置一些内存管理的关键字,例如 OBJC_ASSOCIATION_ASSIGN ,这是用来指定对象的内存管理的,这些关键字在 Runtime 源码中也有对应的处理。
简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github 上,下载 Runtime PDF 合集。把所有 Runtime 文章总计九篇,都写在这个 PDF 中,而且左侧有目录,方便阅读。
下载地址: Runtime PDF 麻烦各位大佬点个赞,谢谢!:grin:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 探秘Runtime - 剖析Runtime结构体
- 探秘 AFNetworking
- Java随机数探秘
- Java 随机数探秘
- 探秘Runtime - Runtime介绍
- Elastic 探秘之遗落的珍珠
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Automate This
Christopher Steiner / Portfolio / 2013-8-9 / USD 25.95
"The rousing story of the last gasp of human agency and how today's best and brightest minds are endeavoring to put an end to it." It used to be that to diagnose an illness, interpret legal docume......一起来看看 《Automate This》 这本书的介绍吧!