事由:今年去面试,然后面试官问了我一些关于
runtime
的用法,我有说到Method Swizzling
。通过在category
的load
中去修改我们调用的方法,来达到全局修改的目的。随后面试官问到关于category
的实现,哇! 尴尬,我好像从来没有想过这个问题。现在有时间就给整理一下,水平有限,肯定会有很多不足。希望大家多多指点!多谢 zzz
</br>
category
是Objective-C 2.0
之后添加的语言特性. 一般我们使用它有以下两种场景
- 给系统类添加方法和属性
- 通过
组合
的设计模式把类的实现分开成多个category
在几个不同的文件里面- 可以减少单个文件的体积
- 可以把不同的功能组织到不同的
category
里 - 可以由多个开发者共同完成一个类
- 可以只加载自己想要的
category
,达到业务分离
</br>
关于问题(因为确实不知道该从什么地方开始看起,所以强迫试的给自己定了几个问题。让自己去弄明白)
-
category
是什么东西 -
category
是怎样加载的 -
category
方法为什么可以覆盖宿主类的方法 -
category
的属性跟方法是怎么添加到宿主类的 -
category
为什么可以添加属性,方法,协议。缺不能添加成员变量
</br>
category
是什么东西
objc
所有类和对象都是c
结构体,category
也一样。我们可以通过clang
去看一下
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
_category_t
里面有名字、宿主类的对象、实例方法列表、类方法列表、协议方法列表、属列表性。可以看到是没有成员变量列表的,因为category
是依赖runtime
的,而在运行时对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。这就是为什么我们没有在_category_t
里面找到成员变量列表和category
不可以添加成员变量的原因
</br>
category
是怎样加载的
上面我们提到过category
是依赖runtime
的。那我们来看一下runtime
的加载过程。下面用到的runtime
源码都来自于点这! 我下载的是723
最新的版本
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
</br>
开始是一些初始化方法
map_images
方法表示将文件中的二进制文件映射到内存,category被添加到宿主类就发生在这个方法里面。我们来看一下这个方法实现
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
// 加锁操作 保证在映射到dyld过程调用ABI是安全的
rwlock_writer_t lock(runtimeLock);
//函数在加锁后就转向了 map_images_nolock 函数
return map_images_nolock(count, paths, mhdrs);
}
map_images_nolock
方法代码太长,我就不粘过来了。它主要做的操作是在函数中,它检查传入的每个image
,如果image
有需要的信息,就将它记录在hList
中,并将hCount
加一,最终判断hCount>0
来调用_read_images
读取 image 中的数据 。
</br>
我们再来看_read_images
,方法有点长。我给跟category
相关代码截取出来了
for (EACH_HEADER) {
// 获取 镜像中的所有分类
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
// 从这开始,正式对category开始处理
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
//为类添加未依附的分类,把Category和类关联起来
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
//为类添加未依附的分类,把Category和metaClass关联起来。因为类方法是存在元类中的
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
</br>
哇! 终于 终于 到了最关键的方法了</br>
</br> 首先。我们调用remethodizeClass
来调用category
的幕后大佬----attachCategories
方法,从名字就可以看出来它的作用,添加category
</br>
plz show me the code !!!
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
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));
// Count backwards through cats to get newest categories first
/*
#warning attachCategories
category的加载顺序是通过编译顺序决定的
这样倒序遍历,保证先将数组内元素(category)的方法从后往前添加到新数组
这样编译在后面的category方法会在数组的前面
*/
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
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);
}
</br>
得到重新组合的数组在调用
attachLists
方法
</br>
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
//C 库函数 void *realloc(void *ptr, size_t size) 尝试重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
/*
1.memmove
函数原型:void *memmove(void *dest, const void *source, size_t count)
返回值说明:返回指向dest的void *指针
参数说明:dest,source分别为目标串和源串的首地址。count为要移动的字符的个数
函数说明:memmove用于从source拷贝count个字符到dest,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中。
2.memcpy
函数原型:void *memcpy(void *dest, const void *source, size_t count);
返回值说明:返回指向dest的void *指针
函数说明:memcpy功能和memmove相同,但是memcpy中dest和source中的区域不能重叠,否则会出现未知结果。
3.两者区别
函数memcpy() 从source 指向的区域向dest指向的区域复制count个字符,如果两数组重叠,不定义该函数的行为。
而memmove(),如果两函数重叠,赋值仍正确进行。
memcpy函数假设要复制的内存区域不存在重叠,如果你能确保你进行复制操作的的内存区域没有任何重叠,可以直接用memcpy;
如果你不能保证是否有重叠,为了确保复制的正确性,你必须用memmove。
*/
/*
这样就完成将category方法列表里面的方法 加到 class的方法列表里面而且是前面。等到我们再去调用class的方法时候,我们通过去遍历class的方法列表去查到SEL,找到就会调用相应方法。由于category的方法在前面---导致所以会覆盖宿主类本来的方法(这就是为什么category方法的优先级高于宿主类方法)
属性和协议同理!!!!!!!!!!!
*/
//相当于给array()->lists 重新放在起始地址 = array()->lists的起始地址 + addedCount
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
//相当于给addedLists 这个category新数组加到array()->的起始地址
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
</br>
以上应该可以回答,之前提的所有问题。如有疑问,可以联系我。
笔者是一个刚入门iOSer
这次关于category
的管中窥豹,一定有很多的不足,希望大家不吝赐教!
有任何问题可以留言,或者直接联系QQ:346658618
希望可以相互学习,一起进步!
网友评论