iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址

序文

在前面文章《iOS底层之类的加载》中探求了类的加载流程,本篇将对分类展开探求,从分类的结构到分类的加载流程,来探求分类的实质。

Runtime优化

在《WWDC 2020 关于Runtime的优化》中介绍了关于Runtime的优化内容,核心内容是对rw扩展出rwe,来优化整个运转时的功能。

咱们的应用程序装载到设备时,系统会为安装程序分配一段内存,这段内存是不可变的称为clean memory也便是ro,当程序运转启动时,系统会拓荒新的内存来运转程序,这段内存是可变化的称之为dirty memory也便是rw,由于系统内存有限,所以rw这段内存是比较宝贵的。

但是在rw中的数据很多是不会改变的,直接从ro读取即可,需求改变的数据经过Runtime运转时操作的数据,比如类的办法、特点、协议等,将这些数据存放在rwe上,这样就能够达到对dirty memory的优化。

分类的意义便是要动态的给类添加办法等,那么分类的探求就从rwe下手。

分类的结构

自界说类LGPerson和分类LGPerson (Cat),然后经过clang生成cpp文件检查

@interface LGPerson : NSObject
{
  NSString * name;
}
@property (nonatomic, copy) NSString * nickName;
- (void)instanceMethod;
+ (void)classMethod;
@end
@implementation LGPerson
- (void)instanceMethod {
  NSLog(@"%s", __func__ );
}
+ (void)classMethod {
  NSLog(@"%s", __func__ );
}
@interface LGPerson (Cat)
@property (nonatomic, copy) NSString * lg_nickName;
- (void)lg_categoryInstanceMethod;
+ (void)lg_categoryClassMethod;
@end
@implementation LGPerson (Cat)
- (void)lg_categoryInstanceMethod {
  NSLog(@"%s", **__func__** );
}
+ (void)lg_categoryClassMethod {
  NSLog(@"%s", **__func__** );
}
@end

主类的cpp代码

iOS底层之分类的加载

LGPerson的主类cpp中有实例办法类办法、以及特点的setget办法;

分类的cpp代码

iOS底层之分类的加载

到在分类中有实例办法类办法,并没有特点lg_nickNamesetget办法

LGPerson (Cat)分类遵守协议NSObject,从头生成cpp

iOS底层之分类的加载
分类的类型是_category_t,经过category_t在源码中能够找到分类的结构界说

iOS底层之分类的加载

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;
  }
};
  • name: 分类名称;
  • cls:主类;
  • instanceMethods:实例办法;
  • classMethods: 类办法;
  • protocols: 所遵守的协议;
  • instanceProperties:实例特点,并没有setget办法;
  • _classProperties:类特点

经过分类的结构能够看出,分类是没有元类的,主类的类办法是在元类中,分类的类办法是在classMethods中。

分类的加载

rwe是经过类中的extAllocIfNeeded办法创建,假如已有值直接返回rwe,假如没有则经过extAlloc创建。

iOS底层之分类的加载

在源码中大局查找extAllocIfNeeded,调用的办法有
attachCategories分类、class_setVersion设置版别、addMethods_finish动态添加办法、class_addProtocol添加协议、_class_addProperty添加特点、objc_duplicateClass类的复制、demangledName修正类名

这些办法中有关分类的只需attachCategories办法

attachCategories办法探求

// 将办法列表、特点和协议从类别附加到类。假设cats中的类别都是按加载次序加载和排序的,最旧的类别优先。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
        int flags)
{
  /*
  * Only a few classes have more than 64 categories during launch.
  * This uses a little stack, and avoids malloc.
  * 只需少数类在启动期间具有超过64个类别。这使用了一个小仓库,并避免了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.
     * 类别必须以正确的次序添加,即从后到前。
     * 为了完成分块,咱们早年到后迭代cats_list,向后构建本地缓冲区,
     * 并对块调用attachList。attachLists预先准备好列表,
     * 因而终究结果按预期次序排列。
  */
  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(); // 初始化rwe
  // cats_count分类数量,循环处理分类
  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) { // 第一次mcount = 0
        prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__ );
        rwe->methods.attachLists(mlists, mcount);
        mcount = 0;
      }
            //++mcount,将mlist放在mlists的最终
      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;
      }
            //++propcount,将proplist放在proplists的最终
      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中添加办法
    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中添加特点
  rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
  // rwe中添加协议
  rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

剖析一下办法

  • 初始化rwe
  • 经过分类数量cats_count,循环处理分类中的办法特点协议
  • 循环中按倒序刺进法,将一切的办法存在mlists,协议存放在protolists,特点存放在proplists
  • 假如mcount大于0,说明有分类办法,经过prepareMethodLists向类中添加办法并排序,然后rwe中的methods调用attachLists,添加分类办法到rwe
  • rwe中的properties调用attachLists,添加分类特点到rwe
  • rwe中的protocols调用attachLists,添加分类协议到rwe

rwe中的办法特点协议都是调用的attachLists刺进数据,是由于他们的界说都继承list_array_tt,咱们看一下list_array_tt中的attachLists办法

attachLists办法探求

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; // 总数据数量
      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) { // 没有数据且刺进数据只需一条
      // 0 lists -> 1 list
      list = addedLists[0];// 直接刺进
      validate();
    }
    else { // 没有数据且刺进多条数据
      // 1 list -> many lists
      Ptr<List> oldList = list; // 取出旧值
      uint32_t oldCount = oldList ? 1 : 0; // 有旧值oldCount=1不然oldCount=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();
    }
  }

算法剖析:依据已有数据做不同状况处理

  1. 0 -> 1:无数据且新刺进数据为1条
  • 直接刺进到list
  1. 1 list -> many lists: 只需1条数据或刺进多条数据
  • 取出旧值;
  • oldCount赋值:有旧值oldCount=1不然oldCount=0
  • 核算刺进后数据总量newCount = oldCount + addedCount
  • 拓荒新空间;
  • 将旧值存放在新空间最终;
  • 依据addedCount早年往后刺进新值;
  1. many lists -> many lists: 已有多条数据
  • 取出旧值;
  • 核算刺进后数据总量newCount = oldCount + addedCount
  • 拓荒新空间;
  • 依据oldCount循环从后往前取出旧值,存放在新空间的后边方位;
  • 依据addedCount早年往后刺进新值;

依据hasArraysetArray判断,只需调用setArrayarray()->count中的count值大于0,hasArray即为YES

iOS底层之分类的加载

一切分类加载后,rwemethods结构应该为

iOS底层之分类的加载

分类加载实例探求

在类的加载探求中,咱们知道了类的加载机遇区分为懒加载类非懒加载类,即是否完成+load办法,分类的加载咱们相同按照懒加载非懒加载的形式探求。

  1. 主类和分类都为懒加载;
  2. 主类非懒加载,分类懒加载;
  3. 主类懒加载,分类非懒加载;
  4. 主类和分类都非懒加载;

实例类为LGPerson,就在attachCategories办法中经过类名mangledName比较LGPerson来准确加断点、打印输出调试

iOS底层之分类的加载

1.主类和分类都为懒加载

主类LGPerson声明实例办法sayHellosayByeBye

iOS底层之分类的加载
分类CatA中声明实例办法sayHello_A,以及重写主类办法sayHello
iOS底层之分类的加载
main函数中调用sayHello

iOS底层之分类的加载
并没有进入attachCategories函数中,同时能够看到sayHello是调用的分类的办法,由此能够知道假如主类分类都未完成+load办法,分类的办法等信息是在编译时就和主类编译在一起了,在类的加载流程中验证

iOS底层之分类的加载
main函数中调用LGPersonalloc办法开始加载类,这儿的method_list_t是从ro中读取的数据,输出检查

iOS底层之分类的加载

iOS底层之分类的加载
此时,list中的办法数主类和分类的办法集合,主类办法放在集合的最终方位,但这儿办法还没有经过排序处理,经过prepareMethodLists会对list进行排序修正

iOS底层之分类的加载
经过断点跟进,再输出一下经过fixupMethodList处理后的list

iOS底层之分类的加载
处理后,主类的sayHello办法排在了分类sayHello后边。在调用sayHello时,音讯查找流程在对排序好的list进行二分法查找,并且会经过while循环找到最前面的同名办法,这样分类办法就覆盖了主类办法

iOS底层之分类的加载

2.主类非懒加载、分类懒加载

LGPerson的主类完成+load办法,分类不完成

iOS底层之分类的加载
main函数中调用sayHello

iOS底层之分类的加载
也没有进入attachCategories函数中,sayHello是调用的分类的办法,由此能够知道假如主类非懒加载和分类懒加载,分类的办法等信息也是在编译时就和主类编译在一起了。
下面在类的加载流程中验证

iOS底层之分类的加载
这儿能够看出

  • LGPerson是在程序启动时_read_images完成加载的,
  • 分类的办法也是编译时就和主类办法编译在一起了,这儿是经过ro获取的method_list_t,分类办法也在ro中。

3.主类懒加载、分类非懒加载

主类LGPerson不完成+load办法,分类完成

iOS底层之分类的加载
运转检查

iOS底层之分类的加载
也没有进入attachCategories函数中,sayHello是调用的分类的办法,由此能够知道假如主类懒加载和分类非懒加载,分类的办法等信息也是在编译时就和主类编译在一起了。
在类的加载流程中验证

iOS底层之分类的加载

  • LGPerson是在程序启动时_read_images完成加载的,分类非懒加载会导致主类被迫成为非懒加载类
  • 分类的办法也是编译时就和主类办法编译在一起了,这儿是经过ro获取的method_list_t,分类办法也在ro中。

4.主类和分类都为非懒加载

主类LGPerson和分类CatA都完成+load办法,运转检查

iOS底层之分类的加载

经过输出打印能够看出,主类的加载分类的加载是在不同的流程中

主类加载

iOS底层之分类的加载

能够看出主类的加载是在_read_images流程,这儿从ro读取的method_list_t中只需主类的两个办法。

分类的加载

iOS底层之分类的加载

分类的加载流程是load_images->loadAllCategories->load_categories_nolock->attachCategories,在attachCategories中会创建rwe,并调用prepareMethodLists对分类办法进行排序处理,然后调用rwemethodsattachLists刺进分类的mlist

这儿的LGPerson的分类有3个,只在分类CatA中完成了+load办法,咱们调用分类CatC的办法

iOS底层之分类的加载

发现加载分类的时候,并没有输出分类CatC的名字,也便是没有分类CatC的加载,为什么能够调用sayHello_C成功了呢?

猜测:分类CatC没有完成+load,会不会是编译时和主类编译在一起了

再盯梢一下主类的加载流程

iOS底层之分类的加载

这儿看到,主类ro中只需主类自己完成的两个办法,所以猜测不成立。

再盯梢一下分类的加载流程

iOS底层之分类的加载

这儿看到,在加载分类CatB时,确有分类CatC的办法,他们的共同点是都没有完成+load,系统在编译时会把未完成+load办法的分类合并成一个分类来处理,从而简化分类的加载流程。

咱们再添加一个分类CatD做验证

iOS底层之分类的加载

iOS底层之分类的加载
能够看到,分类加载时把未完成+load办法的CatBCatCCatD一起加载的,验证了系统在编译时会把未完成+load办法的分类合并成一个分类来处理。

总结

分类的加载原理和类相同区分为是否为懒加载,两者组合可分为4种状况

iOS底层之分类的加载

以上是对分类的加载流程探求过程的总结,不免有不足和错误之处,如有疑问请在评论区留言吧