简介
Category
是 Objective-C 2.0
之后添加的特性,一般我们使用 Category
的场景主要可以动态地为已经存在的类扩展新的属性和方法。这样做的好处就是:
- 可以减少臃肿的代码。
- 可以把不同的功能拆开,方便以后的维护。
以下源码来自于opensource.apple.com的objc4-750.tar.gz。
在runtime.h中查看定义中:
#if __OBJC2__
typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;
#else
typedef struct old_method *Method;
typedef struct old_ivar *Ivar;
typedef struct old_category *Category;
typedef struct old_property *objc_property_t;
#endif
打开 objc
源代码,在objc-runtime-new.h中我们可以发现:
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;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta) {
if (isMeta) return nil; // classProperties;
else return instanceProperties;
}
};
注意:
-
name
:是指class_name
而不是category_name
。 -
cls
:要扩展的类对象,编译期间是不会定义的,而是在运行时通过 *name
对应到对应的类对象。 -
instanceMethods
:category 中所有给类添加的实例方法的列表。 -
classMethods
:category 中所有添加的类方法的列表。 -
protocols
:category 实现的所有协议的列表。 -
instanceProperties
:category 中添加的所有属性。
从 category 的定义也可以看出 category 可以添加实例方法,类方法,甚至可以实现协议,添加属性,无法添加实例变量。
For example
使用 clang
的命令去看看 category
到底会变成什么:
clang -rewrite-objc Person+Student.m
注意:
解决方法
在终端中输入下面的命令:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
然后打开文件目录,会发现多了一个 10w
多行的 .cpp
文件。
上图中可以发现:
- 编译器生成了实例方法列表
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Student
和属性列表_OBJC_$_PROP_LIST_Person_$_Student
,并且两者的命名格式都遵循了公共前缀+扩展列表+类名+category名字的命名方式。 - 在实例方法列表里面找到了在
Student
这个category
里面扩展的study
方法,属性列表里面找到了在Student
里添加的age
属性。 -
category
的名称用static
来修饰,目的是用来区分可以在不同分类中扩展各种列表以及后面的category
结构体本身命名,所以在同一个编译单元里我们的category
名不能重复,否则会出现编译错误。
Category的原理
Category
是依赖于 dyld
动态加载:
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();
lock_init();
exception_init();
// Register for unmap first, in case some +load unmaps something
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
Category
被附加到类上面是在 map_images
的时候发生的,在 new-ABI
的标准下,_objc_init
里面的调用的 map_images
最终会调用 objc-runtime-new.mm
里面的 _read_images
方法,而在 _read_images
方法的结尾,有以下的代码片段:
/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked
* list beginning with headerList.
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
if (DisableTaggedPointers) {
disableTaggedPointers();
}
initializeTaggedPointerObfuscator();
if (PrintConnecting) {
_objc_inform("CLASS: found %d classes during launch", totalClasses);
}
// namedClasses
// Preoptimized classes don't go in this table.
// 4/3 is NXMapTable's load factor
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
ts.log("IMAGE TIMES: first time tasks");
}
// Discover categories.
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;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
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))
{
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);
}
}
}
}
}
从代码中看出,
通过 _getObjc2ClassList
方法获取所有的 Category
然后添加到 catlist
数组中;把 Category
的实例方法、协议以及属性添加到类上;把 Category
的类方法和协议添加到类的 metaclass
上。addUnattachedCategoryForClass
方法只是把类和 Category
做一个关联映射。
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
remethodizeClass
方法将 Category
合并到类中,然后更新类的方法列表、协议列表和属性列表,更新类以及子类的方法缓存。
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
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);
}
attachCategories
方法中,将方法列表、属性和协议列表从 Category
合并到类中。
首先根据方法列表,属性列表,协议列表,malloc
分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。
通过 while
循环,遍历所有的 Category
,得到它的方法列表 mlist
、proplist
和 protolist
并存入 mlists
、proplists
和 protolists
中。换句话说,它的主要作用就是将 Category
中的方法、属性和协议拼接到类(主类或元类)中,更新类的数据字段 data()
中 mlist
、proplist
和 protolist
的值。
之后通过类对象的 data()
方法,调用方法列表、属性列表、协议列表的 attachList
函数,将所有的分类的方法、属性、协议列表数组传进去,将分类和本类相应的对象方法,属性,和协议进行了合并。
attachList
函数中 memmove
内存移动和 memcpy
内存拷贝。
// memmove :内存移动。
/* __dst : 移动内存的目的地
* __src : 被移动的内存首地址
* __len : 被移动的内存长度
* 将__src的内存移动__len块内存到__dst中
*/
void *memmove(void *__dst, const void *__src, size_t __len);
// memcpy :内存拷贝。
/* __dst : 拷贝内存的拷贝目的地
* __src : 被拷贝的内存首地址
* __n : 被移动的内存长度
* 将__src的内存移动__n块内存到__dst中
*/
void *memcpy(void *__dst, const void *__src, size_t __n);
经过 memmove
和 memcpy
方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用,本类的方法依然在内存中的。
通过以上可以看出:
-
Category
的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category
和原来类都有methodA
,那么category
附加完成之后,类的方法列表里会有两个methodA
。 -
Category
的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的Category
的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。