(一)Category

作者: dandelionYD | 来源:发表于2019-03-12 16:44 被阅读54次

    面试题:

    1.Category的实现原理
    析:
    Category编译之后的底层结构是struct_category_t,
    里面存储着分类的对象方法、类方法、属性 、协议信息,
    在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象,元类对象中)
    
    2.Category能否添加成员变量?如果可以,如何给Category添加成员变量?
    析:
    不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
    
    3.Category和Class Extension的区别是什么?
    析:
    Class Extension在编译的时候,它的数据就已经包含在类信息中
    Category是在运行时,才会将数据合并到类信息中
    
    4.Category中有load方法?load方法什么时候调用?load方法能继承?
    析:
    有load方法
    load方法在runtime加载类、分类的时候调用
    load方法可以继承,但是在一般的情况下不会主动去调用load方法,都是让系统自动调用
    
    5.load、initialize方法的区别是啥?它们在Category中的调用顺序?以及出现继承时他们之间的调用过程?
    

    接下来我们逐步分析:
    准备:首先搭建好可以跑runtime源码的过程,参考配置运行objc4-750和使用

    gitHub_Demo

    1.Category简单的使用

    //Person类
    @interface Person : NSObject{
        double   _height;
    }
    -(void)run;
    @end
    
    @implementation Person
    -(void)run{
        NSLog(@"%s",__FUNCTION__);
    }
    @end
    
    //Person+addCategory  --->Person的分类
    @interface Person (addCategory)
    -(void)speak;
    @property (nonatomic,strong)NSString  *name;
    @end
    
    @implementation Person (addCategory)
    -(void)speak{
        NSLog(@"%s--->%f",__FUNCTION__,_height);
    }
    @end
    
    //myPerson --->Person的子类
    @interface myPerson : Person
    @end
    
    @implementation myPerson
    -(void)speak{
        NSLog(@"%s",__FUNCTION__);
    }
    @end
    
    //Person+addCategory2   ----->Person的分类
    @interface Person (addCategory2)
    -(void)speak;
    @end
    
    @implementation Person (addCategory2)
    -(void)speak{
        NSLog(@"%s",__FUNCTION__);
    }
    -(void)run{
        NSLog(@"%s",__FUNCTION__);
    }
    @end
    
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            myPerson *p  = [[myPerson alloc]init];
            [p run];
            [p speak];
    //        NSLog(@"name-->%@",p.name);//-[Person name]: unrecognized selector sent to instance 0x600001c9cf50
        }
        return 0;
    }
    
    发现:---------------------
     1.在不修改原有的类的基础上,为这个类添加一些方法
     2.分类是用于给原有类添加方法的, 它只能添加方法, 不能添加属性(成员变量)
     3.可以在分类中访问原有类中公开的属性
     4.方法的调用顺序:分类->本类->父类
     5.如果分类中有和原有类同名的方法, 会调用分类中的方法(说会忽略原有类的方法)
     6.分类中的@property, 只会生成setter/getter方法的声明, 不会生成实现以及私有的成员变量
     7.如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定
        会执行最后一个参与编译的分类中的方法(即编译的时候加的.m文件)
    
    

    2.Category的本质

    • 首先:打开打印信息的配置 如下:
    category_01.png

    OBJC_PRINT_CLASS_SETUP:YES

    • 我们看下下面的代码:
    //创建一个Student类
    #import <Foundation/Foundation.h>
    #import "myProtocol.h"
    @interface Student : NSObject
    @property (assign,nonatomic) id<myProtocol> delegate;
    @end
    
    #import "Student.h"
    @implementation Student
    @end
    
    ========创建一个protocol======
    #import <Foundation/Foundation.h>
    @protocol myProtocol <NSObject>
    @required
    -(void)test;
    @optional
    -(void)test2;
    @end
    
    
    =======创建分类=======
    #import "Student.h"
    @interface Student (addCategory)
    -(void)run;
    +(void)play;
    @property (strong,nonatomic) NSString *testPro;
    @end
    
    #import "Student+addCategory.h"
    @implementation Student (addCategory)
    -(void)run{
        NSLog(@"%s",__FUNCTION__);
        [self.delegate test];
        [self.delegate test2];
    }
    +(void)play{
        NSLog(@"%s",__FUNCTION__);
    }
    -(void)setTestPro:(NSString *)testPro{
        NSLog(@"setTestPro");
    }
    -(NSString*)testPro{
        NSLog(@"getTestPro");
        return  @"111";
    }
    @end
    
    ======创建一个Preson对象=====
    #import <Foundation/Foundation.h>
    #import "myProtocol.h"
    @interface Person : NSObject<myProtocol>
    @end
    
    #import "Person.h"
    @implementation Person
    -(void)test{
        NSLog(@"执行protocol的方法---test");
    }
    -(void)test2{
        NSLog(@"执行protocol的方法---test2");
    }
    @end
    
    
    =====在main里面=====
    #import <Foundation/Foundation.h>
    #import "Student.h"
    #import "Student+addCategory.h"
    #import <objc/runtime.h>
    #import "Person.h"
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Student *stu = [[Student alloc]init];
            Person *p = [[Person alloc]init];
            stu.delegate = p;
            [stu run];
            [Student play];
    
            unsigned int count;
            //实例对象方法
            Method *methodList = class_copyMethodList(object_getClass(stu), &count);
            for (unsigned int i = 0; i < count; i++) {
                Method method = methodList[I];
                NSString *methodName = NSStringFromSelector(method_getName(method));
                NSLog(@"实例_方法名:%d----%@",i,methodName);
            }
            free(methodList);
            
            //类对象方法
            Method *methodList2 = class_copyMethodList(object_getClass([Student class]), &count);
            for (unsigned int i = 0; i < count; i++) {
                Method method = methodList2[I];
                NSString *methodName = NSStringFromSelector(method_getName(method));
                NSLog(@"类对象_方法名:%d----%@",i,methodName);
            }
            free(methodList2);
            
            stu.testPro = @"111";
            NSLog(@"---%@",stu.testPro);
        }
        return 0;
    }
    
    =====================部分控制台log=======================
    objc[20404]: CLASS: realizing class 'Student' (meta) 0x100002818 0x100002268 #0
    objc[20404]: CLASS: methodizing class 'Student' (meta)
    objc[20404]: METHOD +[Student play]
    objc[20404]: CLASS: realizing class 'Student' 0x100002840 0x1000022d8 #0
    objc[20404]: CLASS: methodizing class 'Student' 
    objc[20404]: METHOD -[Student setTestPro:]
    objc[20404]: METHOD -[Student testPro]
    objc[20404]: METHOD -[Student delegate]
    objc[20404]: METHOD -[Student setDelegate:]
    objc[20404]: METHOD -[Student run]
    objc[20404]: CLASS: realizing class 'Person' (meta) 0x100002868 0x100002678 #0
    objc[20404]: CLASS: methodizing class 'Person' (meta)
    objc[20404]: CLASS: realizing class 'Person' 0x100002890 0x100002740 #0
    objc[20404]: CLASS: methodizing class 'Person' 
    objc[20404]: METHOD -[Person test2]
    objc[20404]: METHOD -[Person test]
    02.Category的底层分析[20404:2857276] -[Student(addCategory) run]
    02.Category的底层分析[20404:2857276] 执行protocol的方法---test
    02.Category的底层分析[20404:2857276] 执行protocol的方法---test2
    02.Category的底层分析[20404:2857276] +[Student(addCategory) play]
    02.Category的底层分析[20404:2857276] 实例_方法名:0----setTestPro:
    02.Category的底层分析[20404:2857276] 实例_方法名:1----testPro
    02.Category的底层分析[20404:2857276] 实例_方法名:2----delegate
    02.Category的底层分析[20404:2857276] 实例_方法名:3----setDelegate:
    02.Category的底层分析[20404:2857276] 实例_方法名:4----run
    02.Category的底层分析[20404:2857276] 类对象_方法名:0----play
    02.Category的底层分析[20404:2857276] setTestPro
    02.Category的底层分析[20404:2857276] getTestPro
    02.Category的底层分析[20404:2857276] ---111
    

    现象—>在runtime进行加载【镜像】的时候,底层做了处理

    • 接下来我们看看runtime底层到底做了什么?见下

      1.
      void _objc_init(void){
          static bool initialized = false;
          if (initialized) return;
          initialized = true;
          
          environ_init();
          tls_init();
          static_init();
          lock_init();
          exception_init();
      
          // 动态的加载【镜像】并且载入到内存中去
          _dyld_objc_notify_register(&map_images, load_images, unmap_image);
      }
      
      2.
      void  map_images(unsigned count, const char * const paths[],
                 const struct mach_header * const mhdrs[]){
          mutex_locker_t lock(runtimeLock);//加锁
          return map_images_nolock(count, paths, mhdrs);
      }
      
      3.
      void  map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                        const struct mach_header * const mhdrs[]){
       。。。。。。
       if (hCount > 0) {
              //开始装载所有的类
              _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
          }
          firstTime = NO;
      }
      
      4.
      void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){
           。。。。。。。
          // Discover categories. //开始去 【寻找】分类啦
          for (EACH_HEADER) {
              category_t **catlist = 
                  _getObjc2CategoryList(hi, &count);//_getObjc2CategoryList-->获取到分类列表
              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);
                      }
                  }
              }
          }
      。。。
      }
      
    5.
    /***********************************************
    * 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
        //获取该类未h挂载的分类列表
        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);
        }
    }
    
    //说明:
    (1)将 Category 的内容添加到已经存在的 Class 中,最后刷新下 method caches
    (2)调用 attachCategories 函数之前,会先使用 unattachedCategoriesForClass 函数获取类中还未添加的类别列表。这个列表类型为 locstamped_category_list_t,它封装了 category_t 以及对应的 header_info。header_info 存储了实体在镜像中的加载和初始化状态,以及一些偏移量.
    
    6.
    // Attach method lists and properties and protocols from categories to a class.
    // Assumes the categories in cats are all loaded and sorted by load order, 
    // oldest categories first.
    static void  attachCategories(Class cls, category_list *cats, bool flush_caches)
    {
        if (!cats) return;//如果没有分类列表 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--) {//循环分类列表
            //取出第 i 个分类
            auto& entry = cats->list[I];
            
            //从分类里取出对应的实例/类方法表-->存入到mlist数组里面
            method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
            if (mlist) {
                mlists[mcount++] = mlist;
                fromBundle |= entry.hi->isBundle();
            }
            //从分类里取出对应的实例/类属性列表,并加到对应的二维链表中-->存入到proplist数组里面
            property_list_t *proplist = 
                entry.cat->propertiesForMeta(isMeta, entry.hi);
            if (proplist) {
                proplists[propcount++] = proplist;
            }
            //从分类里取出遵守的协议列表,并加到对应的二维链表中-->存入到protolist数组里面
            protocol_list_t *protolist = entry.cat->protocols;
            if (protolist) {
                protolists[protocount++] = protolist;
            }
        }
       
        //遍历完分类后,取出类/元类加载到内存(堆区)的 class_rw_t 结构体
        //class_rw_t中存放着类对象的方法,属性和协议等数据
        auto rw = cls->data();
        
        //准备方法列表:加锁扫描方法列表,将新方法放在每一个分类的方法前面(对每个分类方法进行排序)
        prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
        
        //attachList方法内部将分类和本类相应的对象方法,属性,和协议进行了合并
       // 添加方法到类/元类中
        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);  
    }
    
    7.
      // attachList方法内部将分类和本类相应的对象方法,属性,和协议进行了合并
        //addedLists:需要添加的列表
        //addedCount:列表个数
        void attachLists(List* const * addedLists, uint32_t addedCount) {
            if (addedCount == 0) return;
    
            //realloc ->memmove -> memcpy
            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;
                
                
                /*
                 void  *memmove(void *__dst, const void *__src, size_t __len);
                 __dst:移动内存的目的地
                 __src:被移动的内存首地址
                 __len : 被移动的内存长度
                 作用:将__src的内存移动__len块内存 到 __dst中
                 */
                //内存移动
                memmove(
                        array()->lists + addedCount,// array()->lists->类对象原来的方法列表,属性列表,协议列表 + 分类数组长度
                        array()->lists,
                        oldCount * sizeof(array()->lists[0])//原来数组占据的空间
                        );
                
                /*内存拷贝
                  void    *memcpy(void *__dst, const void *__src, size_t __n);
                  __dst : 拷贝内存的拷贝目的地
                  __src : 被拷贝的内存首地址
                  __n : 被移动的内存长度
                  将__src的内存拷贝__n块内存到__dst中
                 */
                
                //内存复制
                memcpy(
                       array()->lists,//原来方法、属性、协议列表数组
                       addedLists,//分类方法、属性、协议列表数组
                       addedCount * sizeof(array()->lists[0]) //增加的数组占据的空间
                       );
                /*
                 发现原来指针并没有改变,至始至终指向开头的位置。并且经过memmove和memcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。
                 那么为什么要将分类方法的列表追加到本来的对象方法前面呢,这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。
                 其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用
                 */
            }
            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]));
            }
        }
    

    对于最后的内存移动和复制 —>具体看下图

    category_02.jpeg

    大概的流程如下:

    objc-os.mm

    • _objc_init
    • map_images
    • map_images_nolock

    objc_runtime-new.mm

    • _read_images
    • remethodizeClass
    • attachCategories
    • methods.attachLists、properties.attachLists、protocols.attachLists
    • realloc、memmove、memcpy

    3.我们从另外一个角度看看本质

    在控制台输入:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Student+addCategory.m -o category.cpp

    发现.cpp文件里面有:

    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;
    };
    
    static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[3];
    } _OBJC_$_CATEGORY_INSTANCE_METHODS_Student_$_addCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        3,
        {{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Student_addCategory_run},
        {(struct objc_selector *)"setTestPro:", "v24@0:8@16", (void *)_I_Student_addCategory_setTestPro_},
        {(struct objc_selector *)"testPro", "@16@0:8", (void *)_I_Student_addCategory_testPro}}
    };
    
    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_Student_$_addCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"play", "v16@0:8", (void *)_C_Student_addCategory_play}}
    };
    

    总结:

    category的底层原理、Category 不能直接添加成员变量的原因

    • 将category中的方法,属性,协议数据放在category_t结构体中,
    • 然后将结构体内的方法列表拷贝到类对象的方法列表中。
      • 在 App 启动加载镜像文件时,会在 _read_images函数间接调用到 attachCategories函数,完成向类中添加 Category的工作。原理就是向 class_rw_t中的 method_array_t, property_array_t, protocol_array_t数组中分别添加 method_list_t, property_list_t, protocol_list_t
    • Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量

    友情链接:

    相关文章

      网友评论

        本文标题:(一)Category

        本文链接:https://www.haomeiwen.com/subject/mcpnpqtx.html