浅谈 Category 的本质以及加载过程

语言: CN / TW / HK

CategoryObjective-C 2.0 之后添加的特性,一般我们使用 Category 的场景主要可以动态地为已经存在的类扩展新的属性和方法。这样做的好处就是:

  • 可以减少臃肿的代码。
  • 可以把不同的功能拆开,方便以后的维护。

以下源码来自于 opensource.apple.comobjc4-750.tar.gz

Presen类 
// Presen.h
#import <Foundation/Foundation.h>
@interface Preson : NSObject
{
    int _age;
}
- (void)run;
@end

// Presen.m
#import "Preson.h"
@implementation Preson
- (void)run
{
    NSLog(@"Person - run");
}
@end

Presen扩展1
// Presen+Test.h
#import "Preson.h"
@interface Preson (Test) <NSCopying>
- (void)test;
+ (void)abc;
@property (assign, nonatomic) int age;
- (void)setAge:(int)age;
- (int)age;
@end

// Presen+Test.m
#import "Preson+Test.h"
@implementation Preson (Test)
- (void)test
{
}

+ (void)abc
{
}
- (void)setAge:(int)age
{
}
- (int)age
{
    return 10;
}
@end

Presen分类2
// Preson+Test2.h
#import "Preson.h"
@interface Preson (Test2)
@end

// Preson+Test2.m
#import "Preson+Test2.h"
@implementation Preson (Test2)
- (void)run
{
    NSLog(@"Person (Test2) - run");
}
@end

实例对象的 isa 指针指向类对象,类对象的 isa 指针指向元类对象,当 Preson 对象调用 run 方法时,通过实例对象的 isa 指针找到类对象,然后在类对象中查找对象方法,如果没有找到,就通过类对象的 superclass 指针找到父类对象,接着去寻找 run 方法。

猜想结论:

通过 runtime 机制动态将分类中的对象方法存储在类对象中的;将分类中类方法的话,存储在元类对象中。

数据结构

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);
};

注意:

  • name :是指 class_name 而不是 category_name
  • cls :要扩展的类对象,编译期间是不会定义的,而是在运行时通过 * name 对应到对应的类对象。
  • instanceMethods :category 中所有给类添加的实例方法的列表。
  • classMethods :category 中所有添加的类方法的列表。
  • protocols :category 实现的所有协议的列表。
  • instanceProperties :category 中添加的所有属性。

发现分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成 getset 方法的声明,需要自己去实现。

从 category 的定义也可以看出 category 可以添加实例方法,类方法,甚至可以实现协议,添加属性,无法添加实例变量。

For example

使用 clang 的命令去看看 category 到底会变成什么:

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;
};
extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache;
#pragma warning(disable:4273)

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test", "[email protected]:8", (void *)_I_MJPerson_Test_test}}
};

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test2", "[email protected]:8", (void *)_C_MJPerson_Test_test2}}
};

extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_MJPerson;

static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
	"MJPerson",
	0, // &OBJC_CLASS_$_MJPerson,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test,
	0,
	0,
};


static void OBJC_CATEGORY_SETUP_$_MJPerson_$_Test(void ) {
	_OBJC_$_CATEGORY_MJPerson_$_Test.cls = &OBJC_CLASS_$_MJPerson;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
	(void *)&OBJC_CATEGORY_SETUP_$_MJPerson_$_Test,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_MJPerson_$_Test,
};

从代码中可以发现:

编译器生成了 _OBJC_$_CATEGORY_MJPerson_$_Test 的结构体, _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test 对象方法,

_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test 类方法,并且一一对应为上面结构体内赋值。

并且两者的命名格式都遵循了 公共前缀+扩展列表+类名+category 名字的命名方式。

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test", "[email protected]:8", (void *)_I_MJPerson_Test_test}}
};

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test2", "[email protected]:8", (void *)_C_MJPerson_Test_test2}}
};

_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test 结构体中可以找到分类中对应的对象方法和类方法。

_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test 结构体中名称用 static 来修饰,目的是用来区分可以在不同分类中扩展各种列表以及后面的 category 结构体本身命名,所以在同一个编译单元里我们的 category 名不能重复,否则会出现编译错误。

Category 的加载处理过程

  • 通过 Runtime 加载某个类的所有 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);
    // 根据 isMeta 判断是类对象还是元类对象
    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];
        // 将分类中的方法存入 mlist 数组中
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        // 将分类中的属性方法存入 mlist 数组中
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
        // 将分类中的协议存入 mlist 数组中
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
    // class_rw 结构体,用来存储类对象的对象方法,属性方法和协议
    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 ,得到它的方法列表 mlistproplistprotolist 并存入 mlistsproplistsprotolists 中。换句话说,它的主要作用就是将 Category 中的方法、属性和协议拼接到类(主类或元类)中,更新类的数据字段 data()mlistproplistprotolist 的值。

之后通过类对象的 data() 方法,拿到类对象的 class_rw_t 结构体 rw ,调用方法列表、属性列表、协议列表的 attachList 函数,将所有的分类的方法、属性、协议列表数组传进去,将分类和本类相应的对象方法,属性,和协议进行了合并。

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;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            // array()->lists 原来的方法列表
            memmove(array()->lists + addedCount, array()->lists,
                    oldCount * sizeof(array()->lists[0]));
            // addedLists 所有分类的方法列表,属性列表,协议列表
            memcpy(array()->lists, addedLists,
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        }
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists,
                   addedCount * sizeof(array()->lists[0]));
        }
}

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);

经过 memmovememcpy 方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用,本类的方法依然在内存中的。

通过以上可以看出:

  • Category 的方法没有“完全替换掉”原来类已经有的方法,也就是说如果 Category 和原来类都有 methodA ,那么 category 附加完成之后,类的方法列表里会有两个 methodA
  • Category 的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的 Category 的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。

分类的加载顺序取决于编译的顺序,最后面参与编译的分类优先调用。