美文网首页
【iOS重学】Category的底层原理

【iOS重学】Category的底层原理

作者: 重庆妹子在霾都 | 来源:发表于2022-11-24 14:00 被阅读0次

    写在前面

    本文博主将从Category的基本使用和底层原理来窥探一下Runtime下的Category 是如何实现的。博主这里参考的苹果源码版本是:objc4_838版本。

    Category的基本使用

    // Person 类
    @interface Person : NSObject
    
    - (void)run;
    
    @end
    
    @implementation Person
    
    - (void)run {
      NSLog(@"%s",__func__);
    }
    
    @end
    
    // Person + Test 分类
    @interface Person (Test)
    
    - (void)test;
    
    @end
    
    @implementation Person (Test)
    
    - (void)test {
        NSLog(@"%s",__func__);
    }
    
    @end
    

    使用命令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m -o Person+test.cppPerson+Test.m文件转化为c++底层代码,分析该c++文件,我们可以看到分类的底层结构为:

    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; // 属性列表
    };
    

    Person+Test.m底层结构为:

    1.png
    对应上面_category_t结构可以看到:

    1.本类名为Person
    2.因为Person+Test.m我们只写了一个test的实例方法,所以我这里也很明显看到这里传了一个方法列表。
    :这里的_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test其实就是一个结构体名称。

    Category底层原理窥探

    运行时Runtime入口:objc-os.mm文件。
    Category源码阅读顺序:

    // objc-os.mm 文件
    1. _objc_init 
    3. map_images_nolock
    
    // objc_runtime_new.mm 文件
    2. map_images
    4. loadAllCategories();
    5. load_categories_nolock();
    6. attachCategories();
    7. attachLists();
    

    其中 4 - 7 是我们接下来需要重点分析的。

    Category_t 结构体

    Runtime下Category_t结构体如下:

    struct category_t {
      const char *name;
      classref_t cls;
      WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;
      WrappedPtr<method_list_t, method_list_t::Ptrauth> 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);
    
      protocol_list_t *protocolsForMeta(bool isMeta) {
          if (isMeta) return nullptr;
          else return protocols;
      }
    };
    

    map_images_nolock

    map_images_nolock可以理解为是运行时的开始,内部实现如下:

    2.png
    从上图可以看到:这里因为是倒序遍历也就影响了分类方法之间的优先级顺序,所以后编译的分类方法会放在先编译的前面。

    loadAllCategories

    该方法指的是:加载项目中所有分类。

    static void loadAllCategories() {
      mutex_locker_t lock(runtimeLock);
      for (auto *hi = FirstHeader; hi != NULL; hi = hi->getNext()) {
        // 加载每个类所有的分类模块
        load_categories_nolock(hi);
      }
    }
    

    load_categories_nolock

    该方法指的是:加载一个类所有的分类模块。

    static void load_categories_nolock(header_info *hi) {
      // 是否有类属性
      bool hasClassProperties = hi->info()->hasCategoryClassProperties();
    
      size_t count;
      auto processCatlist = [&](category_t * const *catlist) {
          // 遍历需要处理的分类列表
          for (unsigned i = 0; i < count; i++) {
              category_t *cat = catlist[i];
              // 获取分类的主类
              Class cls = remapClass(cat->cls);
              locstamped_category_t lc{cat, hi};
    
              if (!cls) {
                  // 获取不到本类 可能是弱链接
                  if (PrintConnecting) {
                      _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                   "missing weak-linked target class",
                                   cat->name, cat);
                  }
                  continue;
              }
    
              // Process this category.
              if (cls->isStubClass()) {
                  // 无法确定元类对象是哪个 所以先附着在stubClass身上
                  // Stub classes are never realized. Stub classes
                  // don't know their metaclass until they're
                  // initialized, so we have to add categories with
                  // class methods or properties to the stub itself.
                  // methodizeClass() will find them and add them to
                  // the metaclass as appropriate.
                  if (cat->instanceMethods ||
                      cat->protocols ||
                      cat->instanceProperties ||
                      cat->classMethods ||
                      cat->protocols ||
                      (hasClassProperties && cat->_classProperties))
                  {
                      objc::unattachedCategories.addForClass(lc, cls);
                  }
              } else {
                  // First, register the category with its target class.
                  // Then, rebuild the class's method lists (etc) if
                  // the class is realized.
                  if (cat->instanceMethods ||  cat->protocols
                      ||  cat->instanceProperties)
                  {
                      if (cls->isRealized()) { // 类对象已经初始化完毕 进行合并
                          attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                      } else {
                          objc::unattachedCategories.addForClass(lc, cls);
                      }
                  }
    
                  if (cat->classMethods  ||  cat->protocols
                      ||  (hasClassProperties && cat->_classProperties))
                  {
                      if (cls->ISA()->isRealized()) { // 元类对象已经初始化完毕 进行合并
                          attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                      } else {
                          objc::unattachedCategories.addForClass(lc, cls->ISA());
                      }
                  }
              }
          }
      };
    
      processCatlist(hi->catlist(&count));
      processCatlist(hi->catlist2(&count));
    }
    

    attachCategories

    该方法指的是:合并分类的方法列表、属性列表、协议列表等到本类里面。

    static void
    attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                     int flags) {
      if (slowpath(PrintReplacedMethods)) {
          printReplacements(cls, cats_list, cats_count);
      }
      if (slowpath(PrintConnecting)) {
          _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                       cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                       cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
      }
    
      /*
       * Only a few classes have more than 64 categories during launch.
       * This uses a little stack, and avoids malloc.
       *
       * Categories must be added in the proper order, which is back
       * to front. To do that with the chunking, we iterate cats_list
       * from front to back, build up the local buffers backwards,
       * and call attachLists on the chunks. attachLists prepends the
       * lists, so the final result is in the expected order.
       */
      constexpr uint32_t ATTACH_BUFSIZ = 64;
      method_list_t   *mlists[ATTACH_BUFSIZ];
      property_list_t *proplists[ATTACH_BUFSIZ];
      protocol_list_t *protolists[ATTACH_BUFSIZ];
    
      uint32_t mcount = 0;
      uint32_t propcount = 0;
      uint32_t protocount = 0;
      bool fromBundle = NO;
      bool isMeta = (flags & ATTACH_METACLASS);
      auto rwe = cls->data()->extAllocIfNeeded();
    
      // 遍历某个类的分类列表
      for (uint32_t i = 0; i < cats_count; i++) {
          auto& entry = cats_list[i];
          // 取出分类里面的方法列表
          method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
          if (mlist) {
              if (mcount == ATTACH_BUFSIZ) {
                  prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                  rwe->methods.attachLists(mlists, mcount);
                  mcount = 0;
              }
              // 将分类的方法列表放在创建好的容器里面
              mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
              fromBundle |= entry.hi->isBundle();
          }
    
          // 取出分类里面的属性列表
          property_list_t *proplist =
              entry.cat->propertiesForMeta(isMeta, entry.hi);
          if (proplist) {
              if (propcount == ATTACH_BUFSIZ) {
                  rwe->properties.attachLists(proplists, propcount);
                  propcount = 0;
              }
              proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
          }
    
          // 取出分类里面的协议列表
          protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
          if (protolist) {
              if (protocount == ATTACH_BUFSIZ) {
                  rwe->protocols.attachLists(protolists, protocount);
                  protocount = 0;
              }
              protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
          }
      }
    
      if (mcount > 0) {
          // 如果有剩下的方法列表
          prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                             NO, fromBundle, __func__);
          // 将剩下的方法列表附着在本类的方法列表
          rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
          if (flags & ATTACH_EXISTING) {
              flushCaches(cls, __func__, [](Class c){
                  // constant caches have been dealt with in prepareMethodLists
                  // if the class still is constant here, it's fine to keep
                  return !c->cache.isConstantOptimizedCache();
              });
          }
      }
    
      rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    
      rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
    }
    

    attachLists

    该方法指的是:把分类方法真正合并在主类的方法列表里面。

    void attachLists(List* const * addedLists, uint32_t addedCount) {
            // 如果添加的方法列表count为0 直接返回
          if (addedCount == 0) return;
    
          if (hasArray()) {
              // many lists -> many lists
                // 本类里面有多个方法列表
              uint32_t oldCount = array()->count;
              uint32_t newCount = oldCount + addedCount;
              array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
              newArray->count = newCount;
              array()->count = newCount;
    
              for (int i = oldCount - 1; i >= 0; i--)
                  newArray->lists[i + addedCount] = array()->lists[i]; // 这个其实是在把之前的方法列表挪到新数组后面。
              for (unsigned i = 0; i < addedCount; i++)
                  newArray->lists[i] = addedLists[i]; // 把分类的方法列表添加到新数组里面。
              free(array());
              setArray(newArray);
              validate();
          }
          else if (!list  &&  addedCount == 1) {
              // 本类原本没有方法列表 分类方法列表Count == 1
              list = addedLists[0];
              validate();
          } 
          else {
              // 本类原本只有一个方法列表 
              Ptr<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;
              for (unsigned i = 0; i < addedCount; i++)
                  array()->lists[i] = addedLists[i];
              validate();
          }
      }
    
      void tryFree() {
          if (hasArray()) {
              for (uint32_t i = 0; i < array()->count; i++) {
                  try_free(array()->lists[i]);
              }
              try_free(array());
          }
          else if (list) {
              try_free(list);
          }
      }
    
      template<typename Other>
      void duplicateInto(Other &other) {
          if (hasArray()) {
              array_t *a = array();
              other.setArray((array_t *)memdup(a, a->byteSize()));
              for (uint32_t i = 0; i < a->count; i++) {
                  other.array()->lists[i] = a->lists[i]->duplicate();
              }
          } else if (list) {
              other.list = list->duplicate();
          } else {
              other.list = nil;
          }
      }
    };
    

    attachLists方法是分类原理实现最核心的方法,我这里用一张图来模拟分类的底层原理如下:

    3.png

    模拟场景

    模拟场景:Person类有两个分类:Person+Eat.hPerson+Run.h,如下:

    // Person 类
    @interface Person : NSObject
    
    - (void)test;
    - (void)test1;
    
    @end
      
    // Person + Eat 类
    @interface Person (Eat)
    
    - (void)eat;
    - (void)test;
    
    @end
      
    // Person + Run 类
    @interface Person (Run)
    
    - (void)run;
    - (void)test;
    
    @end
    

    按照上图的分析结果,Person类中class_rw_ext_tmethods结构如下:

    解释
    1.把Person的方法列表挪动到数组最后
    2.把Person的分类方法列表添加到前面

    4.png

    提示
    1、如果主类和分类都会有-(void)test方法,会优先调用分类的方法,原因是分类的方法列表在前面,注意这里不是覆盖了原来的方法。
    2、Person两个分类都有-(void)test方法,调用哪个方法是根据编译顺序来决定的,后参与编译的优先级更高,比如上例中调用的是Person+Eat中的test方法。

    写在最后

    啦啦啦,关于Category的底层原理窥探就到这里结束了,如有错误的地方还望各位大佬多多指教,最后欢迎到我的个人博客逛逛。

    相关文章

      网友评论

          本文标题:【iOS重学】Category的底层原理

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