iOS底层-Category的本质、+load、+initialize

Category

分类单独编译的产物

  1. 创建一个Person类,然后给Person类创建一个分类Person+Test

     #import "Person.h"
     @interface Person (Test)
     -(void)test;
     @end
        
     #import "Person+Test.h"
    
     @implementation Person (Test)
     -(void)test{
         NSLog(@"--test--");
     }
     +(void)test2{
           
     }
     @end
    
  2. 通过命令行编译成cpp,抽取核心代码如下:

     // @implementation Person (Test)
        
     //对象方法-test : _I_Person_Test_test
     static void _I_Person_Test_test(Person * self, SEL _cmd) {
         NSLog((NSString *)&__NSConstantStringImpl__var_folders_t2_d94848jx1fq4vd6k8qcsfdq00000gn_T_Person_Test_584f82_mi_0);
     }
        
     //类方法: _C_Person_Test_test2
     static void _C_Person_Test_test2(Class self, SEL _cmd) {
        
     }
     // @end
        
     /*********结构体对象的定义**********/
     //属性对象
     struct _prop_t {
     	const char *name;
     	const char *attributes;
     };
        
     //协议对象
     struct _protocol_t {
         void * isa;  // NULL
         const char *protocol_name;
         const struct _protocol_list_t * protocol_list; // super protocols
         const struct method_list_t *instance_methods;
         const struct method_list_t *class_methods;
         const struct method_list_t *optionalInstanceMethods;
         const struct method_list_t *optionalClassMethods;
         const struct _prop_list_t * properties;
         const unsigned int size;  // sizeof(struct _protocol_t)
         const unsigned int flags;  // = 0
         const char ** extendedMethodTypes;
     };
        
        
     //方法对象
     struct _objc_method {
     	struct objc_selector * _cmd;
     	const char *method_type;
     	void  *_imp;
     };
        
     //成员变量对象
     struct _ivar_t {
     	unsigned long int *offset;  // pointer to ivar offset location
     	const char *name;
     	const char *type;
     	unsigned int alignment;
     	unsigned int  size;
     };
        
     /*********这两个是类对象的定义**********/
        
     //类对象中 struct _class_ro_t *ro; 成员变量的定义
     struct _class_ro_t {
     	unsigned int flags;
     	unsigned int instanceStart;
     	unsigned int instanceSize;
     	const unsigned char *ivarLayout;
     	const char *name;
     	const struct _method_list_t *baseMethods;
     	const struct _objc_protocol_list *baseProtocols;
     	const struct _ivar_list_t *ivars;
     	const unsigned char *weakIvarLayout;
     	const struct _prop_list_t *properties;
     };
     //类对象的定义
     struct _class_t {
     	struct _class_t *isa;
     	struct _class_t *superclass;
     	void *cache;
     	void *vtable;
     	struct _class_ro_t *ro;
     };
        
     //那么这个是什么呢? 分类对象的定义
        
     struct _category_t {
     	const char *name; //类名
     	struct _class_t *cls; //相当于isa指针
     	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_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
     	sizeof(_objc_method),
     	1,
        	
     };
     //创建类方法结构体对象
     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_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
     	sizeof(_objc_method),
     	1,
        	
     };
        
        
     extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_Person;
        
     /*
      创建一个_category_t类型的分类对象_OBJC_$_CATEGORY_Person_$_Test
      */
     static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
     {
     	"Person", //类名为Person
     	0, // &OBJC_CLASS_$_Person,
         //实例方法列表
     	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
         //类方法列表
     	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,
     	0, //协议列表为0
     	0, //属性列表为0
     };
        
     //创建一个 OBJC_CATEGORY_SETUP_$_Person_$_Test静态函数  就是一个setup函数
     //只要调用 这个setup函数,就会将分类对象_OBJC_$_CATEGORY_Person_$_Test的isa指针指向Person
     static void OBJC_CATEGORY_SETUP_$_Person_$_Test(void ) {
     	_OBJC_$_CATEGORY_Person_$_Test.cls = &OBJC_CLASS_$_Person;
     }
     #pragma section(".objc_inithooks$B", long, read, write)
     //创建一个数组指针OBJC_CATEGORY_SETUP,数组内部成员为OBJC_CATEGORY_SETUP_$_Person_$_Test函数指针
     __declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
     	(void *)&OBJC_CATEGORY_SETUP_$_Person_$_Test,
     };
        
     //创建一个L_OBJC_LABEL_CATEGORY_数组,数组内部元素为_category_t(分类对象)类型
     static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
     	&_OBJC_$_CATEGORY_Person_$_Test,
     };
     static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
    
    
    1. 分析一下下面这个东西:

       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_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
       	sizeof(_objc_method),
       	1,
              	
       };
      
      1. 定义一个_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test 类型的结构体,并且赋值,这么写就看懂了

         //1. 定义结构体
         static struct{
             unsigned int entsize;  // sizeof(struct _objc_method)
             unsigned int method_count;
             struct _objc_method method_list[1];
         } _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ;
                     
         //2. 简化
         typedef struct _CATEGORY_INSTANCE_METHODS_Person_  _method_list_t1 ;
                     
         //3. 将test对象方法生成一个_objc_method元素
         struct _objc_method  _objc_method_t = {
             (struct objc_selector *)"test", //_cmd;
             "v16@0:8", //method_type
             (void *)_I_Person_Test_test //_imp
         };
                     
         //4. 还原
         _method_list_t1  = {
             sizeof(_objc_method),   //一个方法对象的大小
             1,  //方法个数为1
             {_objc_method_t} //只有一个_objc_method类型元素的数组
         };
        
    2. 最终产物4个:

      1. OBJC_CATEGORY_SETUP数组指针
        1. 内部存放setup函数
      2. L_OBJC_LABEL_CATEGORY_$ 数组指针
        1. 内部存放category_t类型的对象_OBJC_$_CATEGORY_Person_$_Test
      3. category_t结构体类型的对象_OBJC_$_CATEGORY_Person_$_Test
        1. 对象内部存放着当前分类的类名、isa、实例方法列表、类方法列表、协议列表、属性列表
      4. setup函数OBJC_CATEGORY_SETUP_$_Person_$_Test
        1. 一调用改函数,就会给_OBJC_$_CATEGORY_Person_$_Test的cls成员赋值,赋值为&OBJC_CLASS_$_Person

源码分析

  1. 下载objc4源码,打开,搜索category_,定位到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;
         // 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);
     };
    
    1. 编译分析的是实际结果,这个是源码结果
  2. 结论:程序运行过程中会通过运行时将每一个分类中的对象方法列表合并到类对象中去,类方法列表合并到元类对象中去,下面通过源码分析证明
    1. 定位到objc-os.mm文件,这个文件也是运行时的入口
      1. 定位到_objc_init方法
      2. 内部调用_dyld_objc_notify_register,而且传参有个map_images函数名,点击进入map_images
      3. map_images内部调用map_images_nolock,进入
      4. map_images_nolock内部最后会调用一个_read_images(读取模块的意思),进入
      5. 方法内部有个注释叫 // Discover categories. ,下面是个for循环,内部有个remethodizeClass,调用了2次

         //重新组织对象方法
         remethodizeClass(cls);
         //重新组织类方法
         remethodizeClass(cls->ISA());
        
      6. remethodizeClass方法实现:

         /*
         * remethodizeClass
         * //附加标标准的分类到一个已经存在的类中
         * Attach outstanding categories to an existing class. 
         * //修改类的方法、协议、属性列表
         * Fixes up cls's method list, protocol list, and property list.
         * //更新类、子类的方法缓存
         * Updates method caches for cls and its subclasses.
         * Locking: runtimeLock must be held by the caller
         */
         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);
             }
         }
        
      7. 进入attachCategories方法

         /**
          将分类中的方法列表和属性列表、协议列表添加到一个类中
                    
          @param cls 要添加的类
          @param cats 这个类的分类列表
          @param flush_caches 是否是缓存
          */
         static void
         attachCategories(Class cls, category_list *cats, bool flush_caches)
         {
             if (!cats) return;
             if (PrintReplacedMethods) printReplacements(cls, cats);
                       
             //1. 是否是元类
             bool isMeta = cls->isMetaClass();
                    
             //2. 分配3个数组内存,将所有当前类的分类的方法、属性、协议列表分别整合到3个数组中mlists、proplists、protolists
            //分配方法数组内存mlists
             method_list_t **mlists = (method_list_t **)
                 malloc(cats->count * sizeof(*mlists));
             //分配属性数组内存proplists
             property_list_t **proplists = (property_list_t **)
                 malloc(cats->count * sizeof(*proplists));
             //分配协议数组内存protolists
             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;
             //注意:这里使用i--所以说,如过有n个分类,有相同的方法,最后编译的那个分类,会覆盖掉之前编译的分类
             while (i--) {
                 auto& entry = cats->list[i];
                 //取出分类的类或者对象方法
                 method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
                 if (mlist) {
                     //放入到mlists数组
                     mlists[mcount++] = mlist;
                     fromBundle |= entry.hi->isBundle();
                 }
                 //取出分类的类或者对象属性
                 property_list_t *proplist = 
                     entry.cat->propertiesForMeta(isMeta, entry.hi);
                 if (proplist) {
                      //放入到proplists数组
                     proplists[propcount++] = proplist;
                 }
                 //取出类的协议
                 protocol_list_t *protolist = entry.cat->protocols;
                 if (protolist) {
                     //放入到protolists数组
                     protolists[protocount++] = protolist;
                 }
             }
                        
             //3. 获取到当前类/元类的class_rw_t属性,这个属性包含了当前类原有的方法、属性、协议列表
             //class_rw_t类型
             auto rw = cls->data();
                        
             //4. 将分类集成的方法、属性、协议列表插入到当前类中rw->methods.attachLists
             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);
         }
        
      8. 进入attachLists方法如下:

          /**
          数组的合并
          @param addedLists 要合并的数组
          @param addedCount 数组的元素个数
          */
         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;
                 //将原来数组的数据移动到新的内存中,放到数组后边
                 memmove(array()->lists + addedCount, array()->lists, 
                         oldCount * sizeof(array()->lists[0]));
                 //将分类的数组拷贝到新呢内存中,放到数组前面
                 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]));
             }
         }
        
    2. 从上面的分析可以额外得到下面2个结论
      1. 多个分类有相同的方法实现时,最后编译的那个方法会覆盖(假覆盖)掉之前编译的方法
      2. 原来的类与分类有相同的方法实现时,会先调用分类的方法
  3. 结论:
    1. 分类并不是在编译期整合到原来的类中的,分类在编译期独自编译,在程序运行时进行整合
    2. Category的加载处理过程
      1. 通过Runtime加载某个类的所有Category数据
      2. 把所有Category的方法、属性、协议数据,合并到一个大数组中
        1. 后面参与编译的Category数据,会在数组的前面
      3. 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

load的本质

  1. 从上面知道,对象、类对象调用一个方法时,会优先选择分类中的调用,那么+load方法也一样吗?

     #import <Foundation/Foundation.h>
     @interface Person : NSObject
     + (void)test;
     @end
        
     #import "Person.h"
     @implementation Person
     +(void)load{
         NSLog(@"Person---+load");
     }
     + (void)test {
         NSLog(@"Person---+test");
     }
     @end
        
     #import "Person+Test1.h"
     @implementation Person (Test1)
     +(void)load{
         NSLog(@"Person---Test1--+load");
     }
     + (void)test {
         NSLog(@"Person---Test1--+test");
     }
     @end
        
        
     #import "Person+Test2.h"
     @implementation Person (Test2)
     +(void)load{
         NSLog(@"Person---Test2--+load");
     }
     + (void)test {
         NSLog(@"Person---Test2--+test");
     }
     @end
        
     //main函数调用
     [Person test];
    
    1. 打印如下

       Person---+load
       Person---Test1--+load
       Person---Test2--+load
       Person---Test2--+test
      
    2. 发现test类方法调用时,只调用了最后编译的分类的test方法,但是load却不是这样,各自调用各自的,那这是为什么呢?
    3. 使用runtime打印Person的类方法列表如下:

       类名===Person== 类中的方法列表=== load, test, load, test, load, test, 
      
      1. 说明分类中的方法只是加的覆盖,其实每个方法都存在
  2. 经过上面发现,分类中的load方法,不会被覆盖,什么原因呢?分析运行时源码
    1. 找到运行时入口定位到objc-os.mm文件
    2. 定位到_objc_init方法
    3. 内部调用_dyld_objc_notify_register,而且传参有个load_images函数名,点击进入load_images(加载镜像、模块)
    4. 内部有个方法调用 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.调用所有类的+load方法
               // 1. Repeatedly call class +loads until there aren't any more
               while (loadable_classes_used > 0) {
                   call_class_loads();
               }
               //2. 调用分类的方法
               // 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;
       }
      
    5. call_load_methods通过这个方法发现:
      1. 直接遍历所有的类调用+load方法
      2. 遍历所有的分类,调用分类的+load方法
      3. 并不是通过objc_msgSend发消息,通过isa指针找到元类,然后遍历寻找的,因此,runtime加载类调用+load,跟手动调用test方法机制不一样.
        1. 但是如果我们手动调用+load,比如[Person load],那么他就跟test调用一样了。
      4. 跟test的调用有本质的不同
  3. 集成体系的情况下,load方法的调用顺序
    1. 给Person创建一个分类Student 那么load的顺序如何呢?

       Person---+load
       Student---+load
       Person---Test1--+load
       Person---Test2--+load
       Student---Test1--+load
       Student---Test2--+load
      
    2. 发现: 先从类开始,父类到子类;然后从分类开始,父类到子类

  4. 总结:
    1. +load方法会在runtime加载类、分类时调用
    2. 每个类、分类的+load,在程序运行过程中只调用一次
    3. 调用顺序
      1. 先调用类的+load
        1. 按照编译先后顺序调用(先编译,先调用)
        2. 调用子类的+load之前会先调用父类的+load
      2. 再调用分类的+load
        1. 按照编译先后顺序调用(先编译,先调用)

initialize 的本质

  1. +initialize方法会在第一次接收到消息时调用,并且只会调用那一次
  2. 调用顺序
    1. 先调用父类的+initialize,再调用子类的+initialize
      1. 先初始化父类,再初始化子类,每个类只会初始化1次
  3. +initialize和+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点
    1. 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次
      1. 注意: 也就是说initialize这个方法不一定只会调用一次!!!
      2. 下面分析的伪代码可以证明这个原因
    2. 如果分类实现了+initialize,就覆盖类本身的+initialize调用
    3. 代码证明:

       //Person类
       #import "Person.h"
       @implementation Person
       +(void)initialize{
           NSLog(@"Person----initialize");
       }
       @end
              
       //Person的子类1
       #import "Student.h"
       @implementation Student
       //+(void)initialize{
       //    NSLog(@"Student----initialize");
       //}
       @end
              
       //Person的子类2
       #import "Teacher.h"
       @implementation Teacher
       //+(void)initialize{
       //    NSLog(@"Teacher----initialize");
       //}
       @end
              
       //main函数的调用
        [Student alloc];
       [Teacher alloc];
      
      1. Student、Teacher内部有实现+(void)initialize时

         Person----initialize
         Student----initialize
         Teacher----initialize
        
      2. Student、Teacher内部没有实现+(void)initialize时

         Person----initialize
         Person----initialize
         Person----initialize
        
  4. 源码分析流程
    1. objc4 的源码中搜索initialize(,定位到void _class_initialize(Class cls)函数,
    2. 进入_class_initialize,就是+initialize方法的本质!!!

       //类的+initialize方法
       void _class_initialize(Class cls)
       {
           //1. 变量
           Class supercls; //父类
           bool reallyInitialize = NO; //是否是第一次调用Initialized
              
           //2. 拿到父类
           supercls = cls->superclass;
           //3. 目的:确保父类先被初始化
           //判断父类是否存在,是否已经初始化,没有则递归
           if (supercls  &&  !supercls->isInitialized()) {
               _class_initialize(supercls);
           }
                  
           //4. 判断当前类是否是第一次Initialized
           {
               monitor_locker_t lock(classInitLock);
               if (!cls->isInitialized() && !cls->isInitializing()) {
                   cls->setInitializing();
                   //满足条件则是第一次YES
                   reallyInitialize = YES;
               }
           }
                  
           //5. 初始化当前类
           if (reallyInitialize) { 
               //当前类确实没有被初始化过,即从来没有调用过+initialize
               callInitialize(cls);
           }else{
               //已经调用过,则不在调用+initialize
               ...
           }
       }
      
    3. 进入callInitialize

       //本质就是objc_msgSend(cls,SEL_initialize);
       void callInitialize(Class cls)
       {
           ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
           asm("");
       }
      
    4. 总结上面的源码,整合称一下伪代码,+initialize的本质如下:

       if (当前类没有initialize) {
              
           if (父类没有initialize) { 
               //父类调用initialize
               objc_msgSend([Person class],@selector(initialize));
           }
           //自己调用initialize
           objc_msgSend([Student class],@selector(initialize));
       }
      

相关面试题

  1. Category的实现原理
    1. Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
    2. 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
  2. Category和Class Extension的区别是什么?
    1. Class Extension在编译的时候,它的数据就已经包含在类信息中
    2. Category是在运行时,才会将数据合并到类信息中
  3. Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
    1. 有load方法
    2. load方法在runtime加载类、分类的时候调用
    3. load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
  4. load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
    1. 区别:
      1. 调用方式区别:
        1. load是根据函数地址直接调用
        2. initialize是通过objc_msgSend调用
      2. 调用时刻区别:
        1. load是runtime加载类、分类的时候,只会调用一次
        2. initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会调用多次)
    2. 调用顺序:
      1. load
        1. 先调用类的load,
          1. 先编译的类,优先调用load
          2. 调用子类的load之前,先调用父类的load
        2. 再调用分类的load
          1. 先编译的类,优先调用load
      2. initialize
        1. 先初始化父类
        2. 在初始化子类(可能最终调用的是父类的initialize方法)
  5. Category能否添加成员变量?如果可以,如何给Category添加成员变量?
    1. 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
Table of Contents