OC底层探究 - Category

507 阅读13分钟

Category

Category简介

category 是 Objective-C 2.0之后添加的语言特性,category 的主要作用是能够在不改变原来类内容的基础上,为已经存在的类添加方法。除此之外,apple还推荐了 category 的另外两个使用场景:

  • 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处,
    • 可以减少单个文件的体积
    • 可以把不同的功能组织到不同的 category 里
    • 可以由多个开发者共同完成一个类
    • 可以按需加载想要的category
  • 声明私有方法

不过除了apple推荐的使用场景,广大开发者脑洞大开,还衍生出了 category 的其他几个使用场景:

  • 模拟多继承
  • 把 framework 的私有方法公开

Category简单实现

@interface MJPerson : NSObject
- (void)run;
@end

@implementation MJPerson
- (void)run
{
    NSLog(@"MJPerson - run");
}
@end

@interface MJPerson (Test)
- (void)test;
@end

@implementation MJPerson (Test)
- (void)test
{
    NSLog(@"test");
}
+ (void)test2
{
}
@end

Category底层结构

我们通过如下命令,将 .m 文件转换为 .cpp 文件:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MJPerson+Test.m -o MJPerson+Test.cpp

MJPerson+Test.cpp 文件中,Category 的底层结构实际上是这样的:

struct _category_t {
    const char *name;  //类的名字
    struct _class_t *cls;  //类
    const struct _method_list_t *instanceMethods;  //category中所有给类添加的实例方法的列表
    const struct _method_list_t *classMethods;  //category中所有添加的类方法的列表
    const struct _protocol_list_t *protocols;  //category实现的所有协议的列表
    const struct _prop_list_t *instanceProperties;  //category中添加的所有属性
};

下面是 Category MJPerson(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_INSTANCE_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test", "v16@0: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", "v16@0:8", (void *)_C_MJPerson_Test_test2}}
};
// Category实现
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,
};

可以看到,在编译后,Category 中添加的对象方法并没有被整合到类对象中,只是保存在 _category_t 结构体中,那么他们是在什么时候被整合到类对象中的呢?接下来我们通过探究objc源码来分析 runtime 是如何加载 Category 并将其整合进类对象和元类对象中的。

打开 objc 源码,搜索 category_t { ,发现在源码中 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的加载处理过程

category加载过程

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unopt imizedTotalClasses)
{
		... // 省略
    
		// 发现和处理所有Category
		for (EACH_HEADER) {
         // 外部循环遍历找到当前类,查找类对应的Category数组
         category_t **catlist = _getObjc2CategoryList(hi, &count);
         bool hasClassProperties = hi->info()->hasCategoryClassProperties();

         // 内部循环遍历当前类的所有的Category 
         for (i = 0; i < count; i++) {
             category_t *cat = catlist[i];
             Class cls = remapClass(cat->cls);
             // 首先,通过所属的类注册Category。如果这个类已经被实现,则重新构造类的方法列表 
             bool classExists = NO;
             if (cat->instanceMethods || cat->protocols || cat->instanceProperties) {
                 // 将Category添加到对应的value中,value是Class对应的所有的Category数组
                 addUnattachedCategoryForClass(cat, cls, hi);
                 // 将Category的method,protocol,property添加到Class中
                 if (cls->isRealized()) {
                   	 // 重新组织Class
                     remethodizeClass(cls);
                     classExists = YES;
                 }
             }
             // 与上面逻辑相同,不过是在Meta Class中进行操作
             if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) {
                 addUnattachedCategoryForClass(cat, cls->ISA(), hi); 
                 if (cls->ISA()->isRealized()) {
                     remethodizeClass(cls->ISA());
                 }

             }
         }
     }
     
     ... // 省略
}

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    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)" : "");
        }
        // 将Category附加在Class中
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

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_t, method_t],
  			[method_t, method_t]
  	]
  	*/
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
  	// 属性数组
  	/*
  	[
  			[property_t, property_t],
  			[property_t, property_t]
  	]
  	*/
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
  	// 协议数组
  	/*
  	[
  			[protocol_t, protocol_t],
  			[protocol_t, protocol_t]
  	]
  	*/
    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_t, method_t]
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
      	// 由这里可以看出,最后参与编译的Category,会被放在mlists的最前面
      
				// 取出分类中的属性列表
        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;
        }
    }
		// 得到类对象里面的数据  struct class_rw_t {}
    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);
}

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;
      	// 内存挪移,将原来类的方法列表挪动到最后,空出来addedCount个内存空间
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
      	// 内存复制,将Category中添加的方法列表复制到空出来的addedCount个内存空间中
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
      	// 从这里可以看出,当类和Category中有同名方法时,为什么会执行Category中的方法了
    }
    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]));
    }
}
  1. Category 是在 runtime 时候加载,而不是在编译的时候。
  2. 分类可以重新实现原来类中的方法,但是会覆盖掉原来的方法,会导致原来的方法没法再使用(实际上并没有真的替换,而是 Category 的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的 Category 的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法)。
  3. 当分类、原来类、原来类的父类中有相同方法时,方法调用的优先级:分类(当有多个分类时,最后参与编译的分类优先) –> 原来类 –> 父类,即先去调用分类中的方法,分类中没这个方法再去原来类中找,原来类中没有再去父类中找。

load方法

+load

如果一个类有多个分类,并且多个分类中都重写了 +load() 方法,那么具体会执行哪个 +load() 方法呢?

按照我们之前对 Category 的学习,如果分类中有原类的重名方法,那么会调用分类中的方法,若有多个分类,会按照编译顺序,调用最后参与编译的分类中的方法,也就是说 +load() 方法只应该被调用一次

但是当我们实际尝试之后发现,类和各个分类的 +load() 方法都被调用了,为什么会这样呢?

我们通过分析 objc 源码来分析,首先找到 _objc_init 方法,在其中发现了 load_images 方法:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
      	// 从这里可以看到,+load方法是从类中直接拿到方法实现的地址,从而直接调用,没有通过objc_msgSend函数进行调用
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

struct loadable_class {
    Class cls;  // may be nil
    IMP method; // 方法实现
};

那么如果存在父类,子类以及子类的分类,+load() 方法的调用顺序是什么样的呢?

通过上面的源码我们可以看到,首先是调用类的 +load() 方法,其次再调用分类的 +load() 方法。

我们一点一点来分析,类的 +load() 方法是从 loadable_classes 中取出来直接调用的,那么 loadable_classes 是怎么来的呢?我们往前看一点,在 load_images 方法中有一个 prepare_load_methods 方法,查看源码:

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertWriting();

    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}
  • prepare_load_methods,从编译后的类列表中依次取出类,执行 schedule_class_load 方法
    • schedule_class_load
      • schedule_class_load(cls->superclass),递归调用自己,去添加自己父类的 +load 方法
      • add_class_to_loadable_list(cls),发现我们寻找的 loadable_classes
    • add_category_to_loadable_list(cat)

由此我们可以得出结论,load方法的调用顺序为:

  • 先调用类的 +load 方法
    • 按照编译先后顺序调用(先编译先调用)
    • 调用子类的 +load 方法之前会先调用父类的 +load 方法
  • 后调用分类的 +load方法
    • 按照编译先后顺序调用(先编译先调用)

initialize方法

initialize

关联对象

我们可以通过Category给类添加属性,但是这时候与类中的属性是有区别的:

  • 在类中添加的属性,OC会自动添加成员变量,生成getter、setter方法的声明和实现
  • 在Category中添加的属性,只会自动生成getter、setter方法的声明,而没有实现,也没有添加成员变量

默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中,但可以通过关联对象来间接实现。

关联对象提供了以下API:

// 添加关联对象
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)

// 获得关联对象
id objc_getAssociatedObject(id object, const void * key)

// 移除所有的关联对象
void objc_removeAssociatedObjects(id object)

Key的常见用法:

Key的常见用法

objc_AssociationPolicy

objc_AssociationPolicy对应的修饰符
OBJC_ASSOCIATION_ASSIGNassign
OBJC_ASSOCIATION_RETAIN_NONATOMICstrong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMICcopy, nonatomic
OBJC_ASSOCIATION_RETAINstrong, atomic
OBJC_ASSOCIATION_COPYcopy, atomic

关联对象的原理

实现关联对象技术的核心对象有

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjcAssociation

关联对象原理

我们去翻一下 runtime 的源码,在 objc-references.mm 文件中有个方法 _object_set_associative_reference

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                _class_setInstancesHaveAssociatedObjects(_object_getClass(object));
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

我们可以看到所有的关联对象都由AssociationsManager管理,而AssociationsManager定义如下:

class AssociationsManager {
    static OSSpinLock _lock;
    static AssociationsHashMap *_map;               // associative references:  object pointer -> PtrPtrHashMap.
public:
    AssociationsManager()   { OSSpinLockLock(&_lock); }
    ~AssociationsManager()  { OSSpinLockUnlock(&_lock); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

AssociationsManager 里面是由一个静态 AssociationsHashMap 来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局 map 里面。而 map 的 key 是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个 map 的 value 又是另外一个 AssociationsHashMap,里面保存了关联对象的kv对。

而在对象的销毁逻辑里面,见 objc-runtime-new.mm:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        Class isa_gen = _object_getClass(obj);
        class_t *isa = newcls(isa_gen);

        // Read all of the flags at once for performance.
        bool cxx = hasCxxStructors(isa);
        bool assoc = !UseGC && _class_instancesHaveAssociatedObjects(isa_gen);

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        
        if (!UseGC) objc_clear_deallocating(obj);
    }

    return obj;
}

嗯,runtime 的销毁对象函数 objc_destructInstance 里面会判断这个对象有没有关联对象,如果有,会调用 _object_remove_assocations 做关联对象的清理工作。

面试题

1.Category的实现原理

  • Category 编译之后的底层结构是 struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
  • 在程序运行的时候,runtime 会将 Category 的数据,合并到类信息中(类对象、元类对象中)

2.Category和Extension的区别是什么?

  • Class Extension 在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。
  • Category是在运行时决议,在运行时才会将数据合并到类信息中。

就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行时,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

3.Category中有load方法吗?load方法是什么时候调用的?load方法能继承吗?

  • 有load方法
  • load方法在runtime加载类、分类的时候调用
  • load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

4.load、initialize方法的区别是什么?它们在Category中的调用顺序,以及出现继承时它们之间的调用过程?

load、initialize方法的区别

  • 调用时机:load方法会在runtim加载类、分类时调用,每个类、分类的load方法在程序运行过程中只调用一次;+initialize方法会在类第一次接收到消息时调用
  • 调用方式:load 方法调用时是通过 Runtime 直接找到 load 方法的函数地址进行调用;+initialize是通过objc_msgSend进行调用的
  • 分类实现后:如果分类中实现了load方法,那么类和分类的load方法都会被调用;如果分类实现了+initialize,就覆盖类本身的+initialize调用

load、initialize的调用顺序?

  • load

    • 先调用类的load
      • 先编译的类,优先调用load
      • 调用子类的load之前,会先调用父类的load
    • 再调用分类的load
      • 先编译的分类,优先调用load
  • initialize

    • 先初始化父类
    • 再初始化子类(可能最终调用的是父类的initialize方法)
    • 如果分类实现了+initialize,就覆盖类本身的+initialize调用

5.Category能否添加成员变量?如果可以,如何给Category添加成员变量?

不能直接给Category添加成员变量,但是可以使用关联对象间接实现Category有成员变量的效果