「OC」源码学习——类的加载

「OC」源码学习——类和分类的加载

前言

+load+initialize 是两个常用的类方法,它们在不同的时间点被调用,并且有不同的用途。在消息转发的流程之中,有一个很重要的方法就是lookUpImpOrForward,其中使用了realizeAndInitializeIfNeeded_locked对懒加载类进行初始化,而经过一系列的调用其实就会调用realizeClassWithoutSwift的方法

realizeClassWithoutSwift的测试

探究懒加载和非懒加载进入实现的时机

测试一

JCClass没有实现 + load 方法,是懒加载类,主程序调用[JCClass alloc]的初始化方法。走lookUpImpOrForward去实现类的实现
image-20250513112219801

结论:懒加载类会在第一次调用的时候进行加载,加载的时机是在消息查找流程中的lookUpImpOrForward方法中。

测试二

父类JCClass实现 + load 方法,子类JCSubClass不实现 + load 方法。清理缓存,主程序子类调用初始化方法。

结论:父类实现+ load, 子类不实现+ load。父类是非懒加载类,子类是懒加载类。

测试三

父类JCClass不实现 + load 方法,子类JCSubClass实现 + load 方法。清理缓存,主程序子类先调用的初始化方法,父类再调用的初始化方法。

发现父类没有进入!cls->isRealized(), 父类是懒加载类。因为递归调用realizeClassWithoutSwift完善继承链并处理当前类的父类、元类;如果有父类,就通过addSubclass把当前类放到父类的子类列表中去

结论:如果子类实现+ load,那么父类也会在子类被加载的时候,一起被加载。原因是子类在加载的时候会对父类和元类进行处理。

+load

方法设计

+load方法在类或分类加载到内存之中进行一次性的初始化操作,例如:在load方法之中实现方法交换。因为这个应用程序的生命周期早期完成,这种操作不需要重复进行,只需调用一次,这个方法运行时间过早,可能有些类还没加载,我这个我们无法保证

调用时机

当OC在运行,类和分类被加载的时候,就会调用,_objc_init对函数进行初始化,在map_images调用之后,使用load_images函数进行加载,而在load_images内部,调用流程如下

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }

    // 如果这里没有+load方法,则不带锁返回。
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // 发现 load 方法
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);  // 准备load方法 - 加载load到内存
    }

    // 调用 +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}
  • +load被加载到内存:prepare_load_methods
  • +load被调用:call_load_methods

往下步入函数

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    lockdebug::assert_locked(&loadMethodLock);

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}


static void call_class_loads(void)
{
    int i;
    
    /* 1. 分离当前可加载的类列表 */
    struct loadable_class *classes = loadable_classes; // 获取全局待加载类列表
    int used = loadable_classes_used;                  // 获取已使用的类数量
    loadable_classes = nil;                            // 清空全局类列表指针
    loadable_classes_allocated = 0;                    // 重置已分配内存大小
    loadable_classes_used = 0;                         // 重置已使用计数器
    
    /* 2. 遍历执行所有+load方法 */
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;                     // 获取类对象
        load_method_t load_method = (load_method_t)classes[i].method; // 类型转换为函数指针
        if (!cls) continue; 

        // 调试模式下打印加载日志
        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, @selector(load));          // 直接调用类方法
    }
    
    /* 3. 释放分离出来的类列表内存 */
    if (classes) free(classes);
}

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // 分离当前可加载分类列表
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;          // 清空全局分类列表指针
    loadable_categories_allocated = 0;  // 重置已分配内存大小
    loadable_categories_used = 0;       // 重置已使用计数器

    // 遍历调用所有分离出的分类的 +load 方法
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);  // 获取分类所属的类对象
        if (cls && cls->isLoadable()) {  // 检查类是否可加载
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, @selector(load));  // 直接调用分类的 +load 方法
            cats[i].cat = nil;  // 标记该分类已处理
        }
    }

    // 压缩列表(保留顺序,移除已处理的分类)
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i - shift] = cats[i];  // 前移未处理的分类
        } else {
            shift++;  // 记录已处理分类的空位数量
        }
    }
    used -= shift;  // 更新有效分类数量

    // 合并新增的可加载分类到分离的列表中
    new_categories_added = (loadable_categories_used > 0);
    for (i = 0; i < loadable_categories_used; i++) {
        if (used == allocated) {  // 动态扩容策略
            allocated = allocated * 2 + 16;
            cats = (struct loadable_category *)realloc(cats, allocated * sizeof(struct loadable_category));
        }
        cats[used++] = loadable_categories[i];  // 追加新增分类
    }

    // 释放旧全局列表内存
    if (loadable_categories) free(loadable_categories);

    // 重新附加更新后的列表(若仍有未处理分类)
    if (used) {
        loadable_categories = cats;
        loadable_categories_used = used;
        loadable_categories_allocated = allocated;
    } else {  // 无剩余分类则释放内存
        if (cats) free(cats);
        loadable_categories = nil;
        loadable_categories_used = 0;
        loadable_categories_allocated = 0;
    }

    return new_categories_added;  // 返回是否有新增分类
}

我们可以看到,+load方法的调用不是通过消息转发的流程来进行的,我们直接获取他的IMP来进行调用。同样也不难看出,我们在分类重写+load的时候,会先加载原本重写+load,再加载分类之中重写的+load,我们用例子展示一下。

image-20250514113854126

  1. load方法在程序启动的时候自动调用——即在read_images进行类的实现之后
  2. 先调用类的+load方法,再调用分类的方法
  3. 如果有多个分类重写+load方法,分类的load方法是按照编译顺序执行
  4. 每个类的 load 方法和每个分类的 load 方法只会调用一次。

+initialize

顾名思义,+initialize就是类的初始化方法,会在类的初次调用的时候自动调用,在这个时候系统的初始化完成,可以执行一些关联其他类的操作。+initialize方法是通过objc_msgSend()进行调用的。那我们看看我们源码,通过一系列方法的溯源,我们可以看到

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
}

initialize方法的调用是线程安全的,即在多线程环境中,initialize方法只会被调用一次,不会产生竞争条件。

调用顺序

我们分别在子类和父类之中实现initialize方法,以及在父类实现子类不实现,分类实现initialize方法,查看一下调用流程

子类不实现

image-20250514125613842

父类和子类都实现

image-20250514193300409

实现分类(父类分类和类之中都实现,子类不实现)

image-20250514193551373

归纳

  1. initialize走普通的消息发送机制。所以分类覆盖主类,当有多个分类都实现了initialize方法,执行最后被加载到内存中的分类的方法。

  2. initialize在类或者其子类的第一个方法被调用前(发送消息前)调用

  3. 如果父类和子类都实现了initialize方法,在调用子类时,

    • 如果父类的initialize方法调用过,则只调用子类的initialize方法;

    • 如果父类的initialize没用过,则先调用父类的initialize方法,在调用子类的initialize方法。(此时,再初始化父类的时候,不会再调用initialize方法)

  4. 父类实现,子类不实现,调用子类时,会调用两次父类的initialize方法

源码实现

void initializeNonMetaClass(Class cls)
{
    // 确保当前类是非元类(普通类)
    ASSERT(!cls->isMetaClass());

    // 强制父类先完成初始化(避免死锁)
    Class supercls = cls->getSuperclass();
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls); // 递归初始化父类
    }

    // 获取当前类的初始化锁(线程安全)
    lockClass(cls);

    // 三种可能的初始化状态:
    // 1. 已初始化:直接返回
    // 2. 正在初始化:
    //    A. 当前线程已持有锁(可重入)
    //    B. 在fork子进程中,父进程线程已终止
    // 3. 首次初始化:当前线程赢得竞争
    if (cls->isInitialized()) {
        unlockClass(cls);
        return;
    }
    if (cls->isInitializing()) {
        if (!MultithreadedForkChild || _thisThreadIsInitializingClass(cls)) {
            unlockClass(cls);
            return;
        } else {
            // 处理fork子进程中的初始化竞争
            lockClass(cls);
            _setThisThreadIsInitializingClass(cls);
            performForkChildInitialize(cls, supercls);
        }
    }

    // 设置类为初始化中状态,并收集回调函数
    cls->setInitializing();
    SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
    {
        mutex_locker_t lock(classInitLock);
        localWillInitializeFuncs.initFrom(willInitializeFuncs);
    }
    _setThisThreadIsInitializingClass(cls);

    if (MultithreadedForkChild) {
        // 在fork子进程中跳过+initialize调用
        performForkChildInitialize(cls, supercls);
        return;
    }

    // 触发所有注册的"will initialize"回调
    for (auto callback : localWillInitializeFuncs)
        callback.f(callback.context, cls);

    // 调用类的+initialize方法(可能递归触发父类)
    @try {
        callInitialize(cls); // 实际调用+initialize方法
    } @catch (...) {
        // 异常处理:标记初始化完成
        @throw;
    } @finally {
        // 完成初始化并释放锁
        lockAndFinishInitializing(cls, supercls);
    }
}

分类与类

分类的加载通过查看源码我们知道,实现加载分类全部都在realizeClassWithoutSwift这个方法之中完成

懒加载类与非懒加载类的实现在于,他们是否对+load方法进行重写,如果重写的话非懒加载类就会在map_images的调用类的实现,将rw的内容提前加载。

懒加载类 + 懒加载分类

image-20250514201417252

我们可以看到我们的懒加载类是在main函数调用之后,通过消息转发到realizeClassWithoutSwift之中完成类的实现

非懒加载类+懒加载分类

image-20250514204557158

我们可以看到这个类的实现在main函数调用之前就调用了,我们现在需要看的是分类的方法是否被添加到其中

image-20250514211228631

通过ro打印出来的方法列表数量为2,我们此时可以断定了类和分类的方法都被加入到了ro之中

结论:不管是懒加载类或是非懒加载类,懒加载分类的方法在编译时就确定了。

懒加载类+非懒加载分类

根据我们之前学习的流程,我们可以知道懒加载类是在第一次发送消息的时候才被加载,我们可以看到流程应该与懒加载类 + 懒加载分类的实现一样

image-20250514213425805

即使主类未实现 +load 方法,只要其 任意一个分类实现了 +load,编译器就会将主类标记为 非延迟加载类(Non-Lazy Class),并写入 Mach-O 的 __objc_nlclslist 段。在_getObjc2NonlazyClassList 获取非懒加载类列表,read_image阶段就对类进行了实现。

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

不难看出调用栈和之前的非懒加载类没有什么区别,都是在read_images之中进行实现

image-20250515105414749

我们接下来要查看的是,我们的分类方法这时候有没有被加入到ro之中,很明显count = 1,分类的方法没有被加入到ro之中。我们知道,非懒加载的分类需要通过 attachCategories 函数动态附加到类的方法列表中,而这一过程会创建或更新 rwe。带这个这个合理的猜测我们在源码之中进行一下实验

image-20250515105348115

具体的源码流程我们可以在attachCategories,加入以下代码进行调试,加上断点看看是否能进入这个内容。

 const char *className = cls->mangledName();
   const char *targetName = "JCClass";
    
   bool isTargetClass = (className && strcmp(className, targetName) == 0);
    if (isTargetClass) {
        printf("===== Attaching categories to JCClass =====\n");
        printf("Class address: %p\n", cls);
        printf("Category count: %u\n", cats_count);
        printf("Is metaclass? %s\n", (flags & ATTACH_METACLASS) ? "YES" : "NO");
        
        // 触发断点(调试时启用)
        // asm("int3");
    }

经过我的测试,就是只有类和分类都为非懒加载时JCClass才会进入这个方法的断点,与我们之前的推测相符

image-20250515160805346

小结

  1. 只有在非懒加载类和非懒加载分类的情况下,ro保存了本类的,rwe保存本类和分类的; 其它情况下ro保存了本类和分类的,没有rwe
  2. Runtime API 对类进行更改也会产生rwe

懒加载类调用流程

这里给出两种不同的类的具体调用流程,有兴趣的读者可以通过源码自行进行调试

map_images -> map_images_nolock -> _read_images -> 类对象第一次接收到消息 -> cache里没有找到 _objc_msgSend_uncached -> lookUpImpOrForward -> realizeClassMaybeSwiftMaybeRelock -> 加载类 realizeClassWithoutSwift -> 分类添加到类 methodizeClass

非懒加载类调用流程

map_images -> map_images_nolock -> _read_images -> 发现类 readClass -> 加载类 realizeClassWithoutSwift -> 分类添加到类 methodizeClass-> attachCategories

参考文章

iOS进阶之路 (十一)分类的加载

iOS mapImage与loadImage底层探索

iOS 类的加载原理下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值