我们知道 Objective - C 中 Category 主要有以下作用:
- 不改变原有类的实现对类添加新的接口
- 将类的接口按功能模块分类,模块更清晰
- 声明私有方法
我们还知道,即使没有引入 Category 的头文件,Category 的方法也会被添加进主类的方法列表里,可以通过 performSelector
的方式使用,导入头文件只是为了通过编译器的静态检查。
那么 Category 是如何添加到主类里的呢?下面我们一起来学习记录下。
Category 的结构
首先了解下 Category 的结构,打开 runtime.h
,我们看到了 Category 的定义
/// An opaque type that represents a category.
typedef struct objc_category *Category;
是指向 objc_category
的 C 结构体,定义如下
struct objc_category {
char *category_name OBJC2_UNAVAILABLE;
char *class_name OBJC2_UNAVAILABLE;
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list *class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
}
通过上面的结构体,我们可以很清楚的了解存储的内容。
我们接着下载 objc 的源码,打开 objc-runtime-new.h
,有如下定义:
typedef struct category_t {
const char *name;
struct class_t *cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct objc_property_list *instanceProperties;
} category_t;
其中,
-
name
是指class_name
而不是category_name
-
cla
是指要扩展的类 -
objc_property_list
是指 Category 中所有的 properties,也就是我们通过objc_setAssociatedObject
动态添加的属性
Category 的加载过程
打开 objc
源码中,在 libobjc.order
中我们能看到如下的执行顺序
__objc_init
_map_images
_sel_registerName
___sel_registerName
__objc_search_builtins
_phash
_lookup
_exception_init
__getImageSlide
__getObjcImageInfo
_getsegbynamefromheader
__getObjcModules
__malloc_internal
__objc_internal_zone
_verify_gc_readiness
_gc_init
_rtp_init
__read_images
...
我们要关注的是 _read_images
,在这个方法里 load 了所有的类、协议和 Category,其中加载 Category 的代码段如下:
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
// Do NOT use cat->cls! It may have been remapped.
class_t *cls = remapClass(cat->cls);
// 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;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (isRealized(cls)) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
getName(cls), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
/* || cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->isa, hi);
if (isRealized(cls->isa)) {
remethodizeClass(cls->isa);
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
getName(cls), cat->name);
}
}
}
}
// Category discovery MUST BE LAST to avoid potential races
// when other threads call the new category code before
// this thread finishes its fixups.
通过上面的代码我们发现,实例方法被加入到当前的类对象中,类方法被加入到当前 Class 的 MetaClass 中,(Class 和 MetaClass 的概念可以查看我之前写的这篇文章)。方法的添加逻辑主要是在 remethodizeClass
的 attachCategoryMethods
里执行。
static void
attachCategoryMethods(class_t *cls, category_list *cats,
BOOL *outVtablesAffected)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
BOOL isMeta = isMetaClass(cls);
method_list_t **mlists = _malloc_internal(cats->count * sizeof(*mlists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int i = cats->count;
BOOL fromBundle = NO;
while (i--) {
method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= cats->list[i].fromBundle;
}
}
attachMethodLists(cls, mlists, mcount, fromBundle, outVtablesAffected);
_free_internal(mlists);
}
这里面的主要操作就是取出 category_list
的所有方法列表,然后倒序添加到 mlists
中,最后再将 mlists
正序添加到被扩展的类中。因此,新生成的 Category 的方法会优先添加到方法列表里。
如果原来类的方法列表是 A、B,Category 的方法列表是 C、D,那么添加后的方法列表是 C、D、A、B。
至此,Category 的方法便被添加到了类中。
由于 Category 的方法会插入到原始类之前,我们要注意不要用 Category 来覆盖原始类的方法
这是我写的 runtime 系列文章中的一篇,还有以下几篇从其他方面对 runtime 进行了介绍
- iOS runtime之消息转发
- iOS runtime 之 Class 和 MetaClass
- 深入理解 Objective-C 的方法调用流程
- Objective-C 深入理解 +load 和 +initialize
参考资料:
- http://blog.leichunfeng.com/blog/2015/05/18/objective-c-category-implementation-principle/
- http://chun.tips/blog/2014/11/06/bao-gen-wen-di-objective%5Bnil%5Dc-runtime(3)%5Bnil%5D-xiao-xi-he-category/
如果您觉得本文对您有所帮助,请点击「喜欢」来支持我。
转载请注明出处,有任何疑问都可联系我,欢迎探讨。
网友评论
这句话在别的文章中也看过相同的表述,但还有点疑问。
创建一个BaseViewController(继承UIViewController);再创建一个UIViewController的分类Example
1.Example中增加一个ViewDidLoad方法。运行发现,Example和BaseViewController中的ViewDidLoad都能得到调用。
2.Example和BaseViewController中增加一个自定义方法(如:print)。运行,发现只会调用Example的print
是不是可以说明,并不是所有分类方法都会覆盖原类方法,对于系统已经定义的这些方法,如果使用分类进行覆盖,则都会得到调用。像MJRefresh的demo中,就写了一个UIViewController的分类并重写了dealloc来记录ViewController的销毁。如果这样的话,就可以将一些需要统一处理的东西做到分类里。
这确实也是对的,系统调用BaseViewController的ViewDidLoad方法,而BaseViewController调用了super的ViewDidLoad方法。而super的ViewDidLoad方法被分类方法覆盖。所以就形成了先执行super分类的方法,再执行BaseViewController的ViewDidLoad方法。
之前好像看到过有文章说,viewDidLoad 这个问题是因为,在原来的类里调用了 [super viewDidLoad] 的原因,还是不太清楚~