ios底层 类和分类的懒加载

1,458 阅读7分钟

1.类的加载流程

我们先梳理下类的加载流程:

  • libObjcdyld 注册了回调 _dyld_objc_notify_register,当 dyldApp 以及 App 所依赖的一系列 Mach-O 镜像加载到当前 App 被分配的内存空间之后,dyld 会通过 _dyld_objc_notify_mapped 也就是 map_images 来通知 libObjc 来完成具体的加载工作,map_images 被调用之后会来到 _read_images

  • _read_images

    • 主要会进行类的加载工作,会插入所有的类到 gdb_objc_realized_classes 哈希表中(插入方式为 类名为 key,类对象为value, 不包括通过 共享缓存 里面的类),同时还会把类插入到 allocatedClasses 这个集合里面,注意,allocatedClasses 的类型为 NXHashTable,可以类比为 NSSet,而 gdb_objc_realized_classes 的类型为 NXMapTable,可以类比为 NSDictionary

    • 对所有的类进行重映射

    • 将所有的 SEL 插入到 namedSelectors 哈希表中(插入方式为:SEL 名称为 keySELvalue)

    • 修复函数指针遗留

    • 将所有的 Protocol 插入到 readProtocol 哈希表中(插入方式为:Protocol 名称为 keyProtocolvalue)

    • 对所有的 Protocol 做重映射

    • 初始化所有的非懒加载类,包括 rwro 的初始化操作

    • 处理所有的分类(包括类的分类和元类的分类)

2.懒加载类的加载流程

我们知道,非懒加载类是指实现了load方法的类,懒加载类是没有实现load方法的类,那么懒价值类是什么时候加载呢,我们推动是调用方法的时候。对此,我们在 lookUpImpOrForward方法里打个断点

此时的 inst 只是一个地址,说明还没有初始化。我们让程序接着下面走,会来到这样一行代码:
顺着 realizeClassMaybeSwiftAndLeaveLocked 方法往下面走走看

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

我们一路跟随断点来到了 realizeClassMaybeSwiftMaybeRelock方法,然后我们看到了我们熟悉的一个方法 realizeClassWithoutSwift,这个方法内部会进行 ro/rw的赋值操作以及 categoryattatch 也就是下面的流程

3.分类的底层实现

为了探究分类的底层实现,我们只需要用 clang 的重写命令

clang -rewrite-objc LGTeacher+test.m -o category.cpp

我们查看 category.cpp 这个文件,来到文件尾部可以看到:

static struct _category_t _OBJC_$_CATEGORY_LGTeacher_$_test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"LGTeacher",
	0, // &OBJC_CLASS_$_LGTeacher,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_LGTeacher_$_test,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_LGTeacher_$_test,
	0,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_LGTeacher_$_test,
};

我们找到category_t的源码:

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

可以看出:

  • name : 是分类所关联的类,也就是类的名字,而不是分类的名字
  • cls : 我们在前面可以看到 clang 重写后这个值为 0,但是后面有注释为 &OBJC_CLASS_$_LGTeacher ,也就是我们的类对象的定义,所以这里其实就是我们要扩展的类对象,只是在编译期这个值并不存在
  • instanceMethods : 分类上存储的实例方法
  • classMethods :分类上存储的类方法
  • protocols :分类所实现的协议
  • instanceProperties:分类所定义的实例属性,不过我们一般在分类中添加属性都是通过关联对象来实现的

4.分类的加载

4.1 分类没有实现load方法

4.1.1 与懒加载类配和加载

我们先分析第一种情况,也就是类和分类都不实现 load方法的情况。首先,非懒加载类的流程上面我们已经探索过了,在向类第一次发送消息的时候,非懒加载类才会开始加载,而根据我们上一章类的加载探索内容,在 realizeClassWithoutSwift方法的最后有一个 methodizeClass方法,在这个方法里面会有一个** Attach categories**的地方:

但是我们断点之后发现这个时候通过 unattachedCategoriesForClass方法并没有取到分类,我们此时不妨通过 LLDB打印一下当前类里面是否已经把分类的内容附加上了。前面的流程大家都很熟悉了,我们直接看 clsrw中的 methods是否有内容:

此时 LGTeacher类里面是没有方法的,这里读取 rw却有一个结果,我们不难看出这是位于 LGTeacher+test分类中的一个 initialize方法,这个方法是我手动加个这个分类的。这样进一步证明了,如果是懒加载类,并且分类也是懒加载,那么分类的加载并不会来到 unattachedCategoriesForClass,而是直接在编译时加载到了类的 ro里面,然后在运行时被拷贝到了类的rw里面。

4.1.2 与非懒加载类配合加载

当类为非懒加载类的时候,走的是 _read_images 里面的流程,这个时候我们的懒加载分类是在哪加载的呢?

我们直接在 methodizeClass 方法中打上断点,并做了一下简单的判断:

同时通过 LLDB 打印,发现分类的方法已经在类的 ro 里面了,所以说分类的加载其实跟类的懒加载与否并没有关系,也就是说懒加载的分类都是在编译时期被加载的

4.2 分类实现load方法

4.1.1 与懒加载类配和加载

其实懒加载和非懒加载的最大区别就是加载是否提前,而实现了 +load方法的分类,面对的是懒加载的类,而懒加载的类我们前面已经知道了,是在第一次发送消息的时候才会被加载的,那我们直接在lookupImpOrForward => realizeClassMaybeSwiftAndLeaveLocked=> realizeClassMaybeSwiftMaybeRelock=> realizeClassWithoutSwift=> methodizeClass流程中的 methodizeClass打上断点,看下在这里分类会不会被加载:

这一次通过 unattachedCategoriesForClass 取出来值了,并且在这之前 cls 的 ro 中并没有分类的 initialize 方法:
但是我们注意观察此时的调用堆栈:
为什么走的不是发送消息的流程,而走的是 load_images 里面的 prepare_load_methods 方法呢?我们来到 prepare_load_methods 方法处:
可以看到,其实是在这里调用了realizeClassWithoutSwift方法来加载类的。而上面的 _getObjc2NonlazyCategoryList方法显示就是获取的所有的非懒加载分类,然后遍历这些非懒加载分类,然后去加载这些分类所依赖的类。这个逻辑很好理解,非懒加载分类让我们的懒加载类实现提前了,所以说懒加载类并不一定只会在第一次消息发送的时候加载,还要取决于有没有非懒加载的分类,如果有非懒加载的分类,那么就走的是 load_images里面的 prepare_load_methodsrealizeClassWithoutSwift

4.1.2 与非懒加载类配合加载

非懒加载类的流程我们也十分熟悉了,在 _read_images 里面进行加载,而此时,分类也是非懒加载。我们还是在 methodizeClass 里面进行断点:

结果如上图所示,这次从 unattachedCategoriesForClass 方法取出来的是 NULL 值,显然分类不是在这个地方被加载的,我们回到 _read_images 方法,还记得那个 Discover categories 流程吗,我们打开里面的断点:
因为当前类已经在前面的非懒加载类加载流程中被加载完成,所以这里会来到 remethodizeClass 方法,我们进入其内部实现:

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

其实 attachCategories这个方法只会在实现了 load方法的分类下才会被调用,而来到 attachCategories之前又取决于类是否为懒加载,如果是懒加载,那么就在 load_images里面去处理,如果是非懒加载,那么就在 map_images里面去处理。

总结

分类的加载如果有load方法,那么运行时决定,如果没有实现load方面,那么编译时决定。

  • 懒加载分类 + 懒加载类

类的加载在第一次消息发送的时候,而分类的加载则在编译时

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

类的加载在 _read_images 处,分类的加载还是在编译时

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

类的加载在 load_images 内部,分类的加载在类加载之后的 methodizeClass

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

类的加载在 _read_images 处,分类的加载在类加载之后的 reMethodizeClass