【类的加载】-(2)懒加载类与分类


本页所使用的objc runtime 756.2,来自 Apple 开源文档

类的加载探寻系列:
1、【类的加载】-(1)类的启动

2、【类的加载】-(2)懒加载类与分类

3、【类的加载】-(3)loading_images

一、懒加载类

1.1 概念

懒加载(lazy loading)又称为延迟加载,它是指系统不会在初始化是就加载某个对象,而是在第一次调用(使用 get 方法)时才加载这个对象到内存。它的实现方法实质上就是覆写该对象的 get 方法,并将该对象在初始化时需要实现的代码在 get 方法中实现。

而对于数据结构而言,惰性加载是指从一个数据对象通过方法获得里面的一个属性对象时,这个对应对象实际并没有随其父数据对象创建时一起保存在运行空间中,而是在其读取方法第一次被调用时才从其他数据源中加载到运行空间中,这样可以避免过早地导入过大的数据对象但并没有使用的空间占用浪费。

1.2 优点

  • 不需要在 viewDidLoad 中实例化对象,简化代码,使结构清晰
  • 提升初始化加载速度
  • 减少内存占用

1.2 区分

通过 load() 方法,在编译器就已经处理好类

1.2.1 非载类的加载步骤:

  • 找到类的指针:

    classref_t *classlist = 
    _getObjc2NonlazyClassList(hi, &count);
  • 强转为Class类实例

    for (i = 0; i < count; i++) {
    Class cls = remapClass(classlist[i])
  • 添加到内存:

    addClassTableEntry(cls);
  • 实现非懒加载类——实例化类的信息,如rw:

    realizeClassWithoutSwift(cls);

1.2.2 懒加载类的加载步骤

  • 查找懒加载类的方法

    向类发送消息,方法查找该类 lookUpImpOrForward

    系统向一个没有实现的类的方法发送消息,通过isa查找方法缓存失败后,会进入到慢速查找 方法lookUpImpOrForward 里面,类是否实现过的判断:

    IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
    bool initialize, bool cache, bool resolver)
    {
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
    imp = cache_getImp(cls, sel);
    if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
    cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    // runtimeLock may have been dropped but is now locked again
    }
  • 对方法进行实现

    接下来,realizeClassMaybeSwiftAndLeaveLocked 方法的实现会落实到realizeClassMaybeSwiftMaybeRelock 这个方法,会返回真实的类的结构:

    static Class
    realizeClassMaybeSwiftAndLeaveLocked(Class cls, mutex_t& lock)
    {
    return realizeClassMaybeSwiftMaybeRelock(cls, lock, true);
    }
    static Class
    realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
    {
    lock.assertLocked();

    if (!cls->isSwiftStable_ButAllowLegacyForNow()) {
    // Non-Swift class. Realize it now with the lock still held.
    // fixme wrong in the future for objc subclasses of swift classes
    realizeClassWithoutSwift(cls);
    if (!leaveLocked) lock.unlock();
    } else {
    // Swift class. We need to drop locks and call the Swift
    // runtime to initialize it.
    lock.unlock();
    cls = realizeSwiftClass(cls);
    assert(cls->isRealized()); // callback must have provoked realization
    if (leaveLocked) lock.lock();
    }

    return cls;
    }

    可以看到,在这个类里面,进到!cls->isSwiftStable_ButAllowLegacyForNow 这个选择,也就是非swift 类的情况下,对类cls进行了方法属性的实现,对的,没错,实现放方法,就放在realizeClassWithoutSwift里,这也是之前讨论过的非懒加载来初次实现的方法。

二、分类及其加载

2.1 分类的结构

为了搞清楚分类在底层的特点,先用代码一步一步来摸索

生成一个分类:

@interface NSObject (Eat)

- (void)eat;
- (void)swallow;
@end

输入编译代码,得到相应的.cpp 文件

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc NSObject+Eat.m

得到对应的cpp文件

打开这份.cpp 文件,熟悉的内容扑面而来

类的结构

下面看到的是分类的结构:

分类的结构

其中包含了

  • name - 主类的名字
  • cls - 分类的名字
  • instanceMethods - 实例方法列表
  • classMethods - 类方法列表
  • protocols - 分类遵循的协议列表
  • properties - 属性列表

接下来看其中方法method_list的结构:

分类方法的结构

得到了两个结构体,原结构体如下:

static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
}

下面实现的结构体为:

_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_NSObject_Eat_eat},
{(struct objc_selector *)"swallow", "v16@0:8", (void *)_I_NSObject_Eat_swallow}}
}

两个结构体,其实是一一对应关系

  • 占用内存:entsize —— sizeof(_objc_method)

  • 方法数量:method_count - 2

  • 方法列表:method_list -

    {(struct objc_selector *)"eat", "v16@0:8", (void *)_I_NSObject_Eat_eat},
    {(struct objc_selector *)"swallow", "v16@0:8", (void *)_I_NSObject_Eat_swallow}

    即 eat 和 swallow 两个方法

继续往下看,可以看到分类 _OBJC_$_CATEGORY_NSObject_$_Eat.cls 这个类指针指向了 主类OBJC_CLASS_$_NSObject 的地址

分类方法的结构

查询源码762,分类的结构如下:

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);
};

2.2 分类的加载流程

2.3.1 初始化

void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;

// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

2.3.2 处理dyld镜像

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);
}

2.3.4 解锁处理镜像

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;
}

可以看到读取镜像后,核心业务代码为_read_images 这个方法, 处理了所有的类。

2.3.5 实现分类方法

进到_read_images 方法,看到关于分类的在这里

这里逐一解释做了什么操作:

  1. 将分类与类表中存储。使用的是addUnattachedCategoryForClass 方法,看看具体如何实现的:

    如图所示,分别是:获取存放分类的表、分类扩容、分类插入表里。

    而分类插入到表里,是 分类——类进行key-value 对应存入的,具体的函数为:

    void *NXMapInsert(NXMapTable *table,
    const void *key,
    const void *value)
  2. 对方法的重新实现:remethodizeClass,来看看这个方法的实现

    这是啥?脚趾头都知道,核心业务方法是 attachCategories(cls, cats, true /*flush caches*/);,点开继续看。

  3. 往类里添加分类 - attachCategories

    所要做的,都写在上面了,就是做了3件事情:

    • 开辟内存空间
    • 遍历分类列表,把方法、属性、协议写到3组数组里
    • 通过attatchList 方法, 依次写入到类的rw 的methods、properties、protocols 里
  4. attachLists方法 - 粘贴方法/属性/协议的具体实现:

具体的代码如下:

这里可以看到,往类里添加分类的方法、属性、协议,一般有3种情况,处理如下:

  • 多对多。处理方式是为新内容创建空间,将旧内容平移到后面,将新内容拷贝放置最前面。
  • 0对1。即之前没有方法、属性或协议。这种比较简单,直接插入即可。
  • 一对多。这种和多对多类似,将新的内容拷贝放入内存最前面
  1. 内存移动和拷贝

    这里着重分析一下,平移,与拷贝。在C函数里,他们是这样的

    	/* dst 	: destination :目标地址
    src : source :被移动的内存首地址
    n : 被移动的内存长度
    memmove 将 src内存中移动 len长度部分到 dest
    memcpy 将 src内存中拷贝 n 长度部分到 dest
    */
    void *memcpy(void *__dst, const void *__src, size_t __n);
    void *memmove(void *__dst, const void *__src, size_t __len);
    • 第一步平移操作:

      memmove(array()->lists + addedCount, array()->lists, 
      oldCount * sizeof(array()->lists[0]));
      • dst:array()->lists + addedCount,即总数组的长度
      • src:被移动的数组为原内容(方法列表、属性、协议)
      • n:移动了的部分:array()->lists,即偏移原来的部分
    • 第二步操作

      memcpy(array()->lists, addedLists, 
      addedCount * sizeof(array()->lists[0]));
      • dst:array()->lists + addedCount,即总数组的长度
      • src:被拷贝的数组为原内容(方法列表、属性、协议)
      • len:拷贝了的部分:array()->lists,即偏移原来的部分

    009

    从这张图,可以清晰的看出,分类中的方法、属性和协议的添加次序,都是讲旧的移走,新的方法排最前面。

    这也解释了为何分类会覆盖类原有的方法,原理是因为相应的方法、属性和协议,在内存段中,排列靠前。

2.3 分类与懒加载的搭配

2.2.1 分类实现load 方法(非懒分类)

  • 懒加载的类 + 非懒加载的分类

    1. 先实现非懒加载类的分类

      具体是读取镜像 read_images

      以及: addUnattachedCategoryForClass 将读到的分类插入到分类的表里,备用

      请见代码实现:

      static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
      header_info *catHeader)
      {
      runtimeLock.assertLocked();

      // DO NOT use cat->cls! cls may be cat->cls->isa instead
      NXMapTable *cats = unattachedCategories();
      category_list *list;

      list = (category_list *)NXMapGet(cats, cls);
      if (!list) {
      list = (category_list *)
      calloc(sizeof(*list) + sizeof(list->list[0]), 1);
      } else {
      list = (category_list *)
      realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
      }
      list->list[list->count++] = (locstamped_category_t){cat, catHeader};
      NXMapInsert(cats, cls, list);
      }
    2. 分类督促懒加载类实现,不然分类无处依附。

      这里使用的是prepare_load_methods方法

    3. 实现主类后,将分类粘贴到主类上

      先进行 realizeClassWithoutSwift,然后 unattachedCategoriesForClass 贴到主类上

      具体的实现步骤如下:

      1. read_images
      2. prepare_load_methods
      3. realizeClassWithoutSwift
      4. unattachedCategoriesForClass
  • 非懒加载的类 + 非懒加载的分类

    直接编译获取到

    这种情况是最普遍的,就是直接取 class 里的data()->ro,在ro 里找到对应的方法

    获取到过程是:

    • read_images - 读取镜像
    • realizeClassWithoutSwift(实现非懒加载类)
    • methodizedClass - 实现方法
    • unattachedCategoriesForClass - 把内存里的类插入到表里
    • attachCategories - 往类后面粘贴分类结构

2.2 分类未实现load 方法(懒分类)

如果分类并不主动实现 +load() 方法,就由编译时实现类与分类的查找实现

  • 非懒加载类 + 懒加载的分类

    通过镜像来查找实现

    1. 读取镜像 read_images
    2. realizeClassWithoutSwift(实现非懒加载类)
    3. methodizedClass
    4. 读取类里的 data->ro 信息
  • 懒加载的类 + 懒加载的分类

    因为懒加载的类在编译时并不会主动实现,所以通过方法查找一步一步找到

    1. 方法查找的消息传递 - lookupimporforward
    2. realizeClassWithoutSwift
    3. methodizedClass

2.3 总结

  • 懒加载的分类: 编译时就已经决定处理好。

  • 非懒加载分类:运行时再来处理。

以上和懒加载的类是刚刚相反。


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
【数据结构与算法】-(1)基础篇 【数据结构与算法】-(1)基础篇
算法是解决特定问题对求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
2020-03-31 李佳
下一篇 
【底层探索】- runtime面试题集 【底层探索】- runtime面试题集
〇、引言前面一步步学习了对象、类、方法、类的加载等,这些其实都是Runtime 的基础,而Runtime 又是iOS开发语言 Objective C 的精髓,因此关于Runtime 的面试题举不胜举。 下面就简单的介绍几个,并提供解题思路,
2020-03-30 李佳
  目录