前言:
iOS的分类(Category)是做iOS开发常用的一种模式,它可以让我们在不知道原有类内部结构的情况下添加属性,方法。合理的运用分类可以将代码很好的进行解耦,让代码更加清晰明了。
那么分类是如何做到的动态添加属性和方法的呢?要是在分类里面添加了和原类同名方法会发生什么呢?
1.Category简介
category是Object-C 2.0之后添加的语言特性,category的主要作用是为已经存在的类添加方法。使用场景:
- 可以将类的实现分开放到不同的文件里面。好处:
- 可以减少单个文件体积
- 可以将不同的功能组织到不同的category中
- 可以由多个开发者完成同一个类
- 可以按需要加载想要的category等等
- 生命私有方法
- 模拟多继承
- 把framework的私有方法公开
2.Category真面目
想要搞清楚category的真面目有一个最好的方法,那就是看源码。通过apple官方开放的runtiem源码,我们一起来看下category的内部实现和它的调用过程。
category 内部实现:
category实质上还是一个结构体category_t:
struct category_t {
const char *name; //分类名称
classref_t cls; //关联的类
struct method_list_t *instanceMethods; //实例方法列表
struct method_list_t *classMethods; //类方法
struct protocol_list_t *protocols; //协议列表
struct property_list_t *instanceProperties; //实例属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties; //类属性
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
可以看到category的内部有一个对class的引用,用于指向对应的类。在category中实现的实例方法,类方法等都是在category内的。那么category添加的方法是如何使用的呢?
category的调用
想要看出category怎么调用的,可以使用clang命令来查看对应的C++实现,这里就不赘述了。
调用栈
实际上category的加载是动态加载,在初始化加载的。所以,它实际上的调用栈的栈底是 void _objc_init(void)
方法。
那我们看下这个方法的内部实现:
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
//环境变量初始化
environ_init();
//线程独立键初始化
tls_init();
//静态构造函数初始化
static_init();
//线程锁初始化
lock_init();
//异常初始化
exception_init();
//通知注册
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
可以看到这个方法内部先做了一些初始化的操作,然后进行通知的注册。我们接着看下 map_images
函数。
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
//锁
rwlock_writer_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
这个函数内部又调用了 map_images_nolock
方法,我们再看下这个方法,因为这个方法内部的实现太多。所以,提取和category有关的内容:
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
而 _read_images
函数内部重要的category调用为 remethodizeClass
。
所以category的调用栈如下:
# _objc_init
## map_images
### map_images_nolock
#### _read_images
##### remethodizeClass
remethodizeClass实现
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
isMeta = cls->isMetaClass();
//获取cls中未完成拼接的分类列表
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
//将分类列表拼接到cls上
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
可以看出来在remethodizeClass
中先判断是否存在未拼接的分类列表,若是有的话,则进行分类的拼接过程attachCategories
。
下面看下attachCategories
的内部实现:
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
/*
初始化二维数组的临时变量,空间大小由分类列表的count决定
*/
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
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;//分类数量
bool fromBundle = NO;
while (i--) { //倒叙遍历,先访问最后编译的分类
//获取一个分类
auto& entry = cats->list[i];
//获取分类的方法列表,若ismeta是YES的话 则获取分类的元类的方法列表;
//若ismeta为No的话,则获取分类的实例方法列表
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;
}
}
//获取class的读写数据,其中包含主类的方法列表信息等
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);
}
从上面的代码中我们可以看出,系统会将编译的分类中的所有方法列表、属性列表、协议列表全都取出来。
并且将它们全都倒序添加到数组中,用于最后拼接到宿主类中。
注意,最关键的方法来了。attachLists
这个方法是最终实现将分类的列表拼接到宿主类中的工程
attachLists
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
//原有类的元素总数
uint32_t oldCount = array()->count;
//拼接后的类的元素总数
uint32_t newCount = oldCount + addedCount;
//根据新的重新分配内存
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
//设置原有类的元素count
array()->count = newCount;
//进行内存的移动,将原有类的列表后移到新分配内存的尾部
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
//将分类的列表数据拷贝到新分配内存的头部
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
这段代码可以看出,分类方法的拼接是通过从新开辟内存,将原有类的方法移到尾部,分类方法从头开始copy的方法来实现的。最后将这个list重写写入原有类的rw信息中。
总结
通过对源码的查看,我们应该可以理解几个点:
- 为什么在分类中实现原有类的同名方法会造成覆盖效果?那是因为内存移动的时候讲原有类的方法移到了尾部。
- 同名分类方法谁起作用取决于编译顺序。
至此关于分类的调用的主要代码差不多看完了,具体的一些分支想要更详细的了解的话,可以去查看runtime源码。