乐趣区

探秘Runtime-Runtime加载过程

该文章属于 < 简书 — 刘小壮 > 原创,转载请注明:

< 简书 — 刘小壮 > https://www.jianshu.com/p/4fb2d7014e9e



程序加载过程

在 iOS 程序中会用到很多系统的动态库,这些动态库都是动态加载的。所有 iOS 程序共用一套系统动态库,在程序开始运行时才会开始链接动态库。

除了在项目设置里显式出现的动态库外,还会有一些隐式存在的动态库。例如 objcRuntime所属的 libobjc.dyldlibSystem.dyld,在 libSystem 中包含常用的 libdispatch(GCD)libsystem_c(C 语言基础库)、libsystem_blocks(Block) 等。

使用动态库的优点:

  1. 防止重复。iOS 系统中所有 App 公用一套系统动态库,防止重复的内存占用。
  2. 减少包体积。因为系统动态库被内置到 iOS 系统中,所以打包时不需要把这部分代码打进去,可以减小包体积。
  3. 动态性。因为系统动态库是动态加载的,所以可以在更新系统后,将动态库换成新的动态库。

加载过程

在应用程序启动后,由 dyld(the dynamic link editor) 进行程序的初始化操作。大概流程就像下面列出的步骤,其中第 3、4、5 步会执行多次,在 ImageLoader 加载新的 image 进内存后就会执行一次。

  1. 在引用程序启动后,由 dyld 将应用程序加载到二进制中,并完成一些文件的初始化操作。
  2. Runtimedyld 中注册回调函数。
  3. 通过 ImageLoader 将所有 image 加载到内存中。
  4. dyldimage 发生改变时,主动调用回调函数。
  5. Runtime接收到 dyld 的函数回调,开始执行 map_imagesload_images 等操作,并回调 +load 方法。
  6. 调用 main() 函数,开始执行业务代码。

ImageLoaderimage 的加载器,image可以理解为编译后的二进制。

下面是在 Runtimemap_images函数打断点,观察回调情况的汇编代码。可以看出,调用是由 dyld 发起的,由 ImageLoader 通知 dyld 进行调用。

关于 dyld 我并没有深入研究,有兴趣的同学可以到 Github 上下载源码研究一下。

动态加载

一个 OC 程序可以在运行过程中动态加载和链接新类或 Category,新类或Category 会加载到程序中,其处理方式和其他类是相同的。动态加载还可以做许多不同的事,动态加载允许应用程序进行自定义处理。

OC 提供了 objc_loadModules 运行时函数,执行 Mach-O 中模块的动态加载,在上层 NSBundle 对象提供了更简单的访问API

map images

Runtime 加载时,会调用 _objc_init 函数,并在内部注册三个函数指针。其中 map_images 函数是初始化的关键,内部完成了大量 Runtime 环境的初始化操作。

map_images 函数中,内部也是做了一个调用中转。然后调用到 map_images_nolock 函数,内部核心就是 _read_images 函数。

void _objc_init(void)
{
    // .... 各种 init
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

void map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{rwlock_writer_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}

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

_read_images 函数中完成了大量的初始化操作,函数内部代码量比较大,下面是精简版带注释的源代码。

先整体梳理一遍 _read_images 函数内部的逻辑:

  1. 加载所有类到类的 gdb_objc_realized_classes 表中。
  2. 对所有类做重映射。
  3. 将所有 SEL 都注册到 namedSelectors 表中。
  4. 修复函数指针遗留。
  5. 将所有 Protocol 都添加到 protocol_map 表中。
  6. 对所有 Protocol 做重映射。
  7. 初始化所有非懒加载的类,进行 rwro 等操作。
  8. 遍历已标记的懒加载的类,并做初始化操作。
  9. 处理所有 Category,包括ClassMeta Class
  10. 初始化所有未初始化的类。
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    header_info *hi;
    uint32_t hIndex;
    size_t count;
    size_t i;
    Class *resolvedFutureClasses = nil;
    size_t resolvedFutureClassCount = 0;
    static bool doneOnce;
    TimeLogger ts(PrintImageTimes);

#define EACH_HEADER \
    hIndex = 0;         \
    hIndex < hCount && (hi = hList[hIndex]); \
    hIndex++

    if (!doneOnce) {
        doneOnce = YES;
        // 实例化存储类的哈希表,并且根据当前类数量做动态扩容
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
    }

    // 由编译器读取类列表,并将所有类添加到类的哈希表中,并且标记懒加载的类并初始化内存空间
    for (EACH_HEADER) {if (! mustReadClasses(hi)) {continue;}

        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->isPreoptimized();

        /** 将新类添加到哈希表中 */
        
        // 从编译后的类列表中取出所有类,获取到的是一个 classref_t 类型的指针
        classref_t *classlist = _getObjc2ClassList(hi, &count);
        for (i = 0; i < count; i++) {
            // 数组中会取出 OS_dispatch_queue_concurrent、OS_xpc_object、NSRunloop 等系统类,例如 CF、Fundation、libdispatch 中的类。以及自己创建的类
            Class cls = (Class)classlist[i];
            // 通过 readClass 函数获取处理后的新类,内部主要操作 ro 和 rw 结构体
            Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);

            // 初始化所有懒加载的类需要的内存空间
            if (newCls != cls  &&  newCls) {
                // 将懒加载的类添加到数组中
                resolvedFutureClasses = (Class *)
                    realloc(resolvedFutureClasses, 
                            (resolvedFutureClassCount+1) * sizeof(Class));
                resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
            }
        }
    }
    
    // 将未映射 Class 和 Super Class 重映射,被 remap 的类都是非懒加载的类
    if (!noClassesRemapped()) {for (EACH_HEADER) {
            // 重映射 Class,注意是从_getObjc2ClassRefs 函数中取出类的引用
            Class *classrefs = _getObjc2ClassRefs(hi, &count);
            for (i = 0; i < count; i++) {remapClassRef(&classrefs[i]);
            }
            // 重映射父类
            classrefs = _getObjc2SuperRefs(hi, &count);
            for (i = 0; i < count; i++) {remapClassRef(&classrefs[i]);
            }
        }
    }

    // 将所有 SEL 都注册到哈希表中,是另外一张哈希表
    static size_t UnfixedSelectors;
    sel_lock();
    for (EACH_HEADER) {if (hi->isPreoptimized()) continue;

        bool isBundle = hi->isBundle();
        SEL *sels = _getObjc2SelectorRefs(hi, &count);
        UnfixedSelectors += count;
        for (i = 0; i < count; i++) {const char *name = sel_cname(sels[i]);
            // 注册 SEL 的操作
            sels[i] = sel_registerNameNoLock(name, isBundle);
        }
    }

    // 修复旧的函数指针调用遗留
    for (EACH_HEADER) {message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
        if (count == 0) continue;
        for (i = 0; i < count; i++) {
            // 内部将常用的 alloc、objc_msgSend 等函数指针进行注册,并 fix 为新的函数指针
            fixupMessageRef(refs+i);
        }
    }

    // 遍历所有协议列表,并且将协议列表加载到 Protocol 的哈希表中
    for (EACH_HEADER) {
        extern objc_class OBJC_CLASS_$_Protocol;
        // cls = Protocol 类,所有协议和对象的结构体都类似,isa 都对应 Protocol 类
        Class cls = (Class)&OBJC_CLASS_$_Protocol;
        assert(cls);
        // 获取 protocol 哈希表
        NXMapTable *protocol_map = protocols();
        bool isPreoptimized = hi->isPreoptimized();
        bool isBundle = hi->isBundle();

        // 从编译器中读取并初始化 Protocol
        protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
        for (i = 0; i < count; i++) {readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
        }
    }
    
    // 修复协议列表引用,优化后的 images 可能是正确的,但是并不确定
    for (EACH_HEADER) {
        // 需要注意到是,下面的函数是_getObjc2ProtocolRefs,和上面的_getObjc2ProtocolList 不一样
        protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
        for (i = 0; i < count; i++) {remapProtocolRef(&protolist[i]);
        }
    }

    // 实现非懒加载的类,对于 load 方法和静态实例变量
    for (EACH_HEADER) {
        classref_t *classlist = 
            _getObjc2NonlazyClassList(hi, &count);
        for (i = 0; i < count; i++) {Class cls = remapClass(classlist[i]);
            if (!cls) continue;
            // 实现所有非懒加载的类(实例化类对象的一些信息,例如 rw)
            realizeClass(cls);
        }
    }

    // 遍历 resolvedFutureClasses 数组,实现所有懒加载的类
    if (resolvedFutureClasses) {for (i = 0; i < resolvedFutureClassCount; i++) {
            // 实现懒加载的类
            realizeClass(resolvedFutureClasses[i]);
            resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/);
        }
        free(resolvedFutureClasses);
    }

    // 发现和处理所有 Category
    for (EACH_HEADER) {
        // 外部循环遍历找到当前类,查找类对应的 Category 数组
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        // 内部循环遍历当前类的所有 Category
        for (i = 0; i < count; i++) {category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            // 首先,通过其所属的类注册 Category。如果这个类已经被实现,则重新构造类的方法列表。bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                // 将 Category 添加到对应 Class 的 value 中,value 是 Class 对应的所有 category 数组
                addUnattachedCategoryForClass(cat, cls, hi);
                // 将 Category 的 method、protocol、property 添加到 Class
                if (cls->isRealized()) {remethodizeClass(cls);
                    classExists = YES;
                }
            }

            // 这块和上面逻辑一样,区别在于这块是对 Meta Class 做操作,而上面则是对 Class 做操作
            // 根据下面的逻辑,从代码的角度来说,是可以对原类添加 Category 的
            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {remethodizeClass(cls->ISA());
                }
            }
        }
    }

    // 初始化从磁盘中加载的所有类,发现 Category 必须是最后执行的
    // 从 runtime objc4-532 版本源码来看,DebugNonFragileIvars 字段一直是 -1,所以不会进入这个方法中
    if (DebugNonFragileIvars) {realizeAllClasses();
    }
#undef EACH_HEADER
}

其内部还调用了很多其他函数,后面会详细介绍函数内部实现。

load images

在项目中经常用到 load 类方法,load类方法的调用时机比 main 函数还要靠前。load方法是由系统来调用的,并且在整个程序运行期间,只会调用一次,所以可以在 load 方法中执行一些只执行一次的操作。

一般 Method Swizzling 都会放在 load 方法中执行,这样在执行 main 函数前,就可以对类方法进行交换。可以确保正式执行代码时,方法肯定是被交换过的。

如果对一个类添加 Category 后,并且重写其原有方法,这样会导致 Category 中的方法覆盖原类的方法。但是 load 方法却是例外,所有 Category 和原类的 load 方法都会被执行。

源码分析

load方法由 Runtime 进行调用,下面我们分析一下 load 方法的实现,load的实现源码都在 objc-loadmethod.mm 中。

在一个新的工程中,我们创建一个 TestObject 类,并在其 load 方法中打一个断点,看一下系统的调用堆栈。

从调用栈可以看出,是通过系统的动态链接器 dyld 开始的调用,然后调到 Runtimeload_images函数中。load_images函数是通过 _dyld_objc_notify_register 函数,将自己的函数指针注册给 dyld 的。

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

load_images 函数中主要做了两件事,首先通过 prepare_load_methods 函数准备 Class load listCategory load list,然后通过 call_load_methods 函数调用已经准备好的两个方法列表。

void
load_images(const char *path __unused, const struct mach_header *mh)
{if (!hasLoadMethods((const headerType *)mh)) return;
    prepare_load_methods((const headerType *)mh);
    call_load_methods();}

首先我们看一下 prepare_load_methods 函数的实现,看一下其内部是怎么查找 load 方法的。可以看到,其内部主要分为两部分,查找 Classload方法列表和查找 Category 方法列表。

准备类的方法列表时,首先通过 _getObjc2NonlazyClassList 函数获取所有非懒加载类的列表,这时候获取到的是一个 classref_t 类型的数组,然后遍历数组添加 load 方法列表。

Category过程也是类似,通过 _getObjc2NonlazyCategoryList 函数获取所有非懒加载 Category 的列表,得到一个 category_t 类型的数组,需要注意的是这是一个指向指针的指针。然后对其进行遍历并添加到 load 方法列表,ClassCategoryload方法列表是两个列表。

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    // 获取到非懒加载的类的列表
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 设置 Class 的调用列表
        schedule_class_load(remapClass(classlist[i]));
    }
    
    // 获取到非懒加载的 Category 列表
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        // 忽略弱链接的类别
        if (!cls) continue;
        // 实例化所属的类
        realizeClass(cls);
        // 设置 Category 的调用列表
        add_category_to_loadable_list(cat);
    }
}

在添加类的 load 方法列表时,内部会递归遍历把所有父类的 load 方法都添加进去,顺着继承者链的顺序添加,会先把父类添加在前面。然后会调用 add_class_to_loadable_list 函数,将自己的 load 方法添加到对应的数组中。

static void schedule_class_load(Class cls)
{if (!cls) return;
    // 已经添加 Class 的 load 方法到调用列表中
    if (cls->data()->flags & RW_LOADED) return;

    // 确保 super 已经被添加到 load 列表中,默认是整个继承者链的顺序
    schedule_class_load(cls->superclass);
    
    // 将 IMP 和 Class 添加到调用列表
    add_class_to_loadable_list(cls);
    // 设置 Class 的 flags,表示已经添加 Class 到调用列表中
    cls->setInfo(RW_LOADED); 
}

Category 则不需要考虑父类的问题,所以直接在 prepare_load_methods 函数中遍历 Category 数组,然后调用 add_category_to_loadable_list 函数即可。

add_category_to_loadable_list 函数中,会判断当前 Category 有没有实现 load 方法,如果没有则直接 return,如果实现了则添加到loadable_categories 数组中。

类的 add_class_to_loadable_list 函数内部实现也是类似,区别在于类的数组叫做loadable_classes

void add_category_to_loadable_list(Category cat)
{
    IMP method;

    // 获取 Category 的 load 方法的 IMP
    method = _category_getLoadMethod(cat);

    // 如果 Category 没有 load 方法则 return
    if (!method) return;
    // 如果已使用大小等于数组大小,对数组进行动态扩容
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

到此为止,loadable_classesloadable_categories 两个数组已经准备好了,load_images会调用 call_load_methods 函数执行这些 load 方法。在这个方法中,call_class_loads函数是负责调用类方法列表的,call_category_loads负责调用 Category 的方法列表。

void call_load_methods(void)
{
    bool more_categories;
    void *pool = objc_autoreleasePoolPush();

    do {
        // 反复执行 call_class_loads 函数,直到数组中没有可执行的 load 方法
        while (loadable_classes_used > 0) {call_class_loads();
        }
        more_categories = call_category_loads();} while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);
    loading = NO;
}

下面是调用类方法列表的代码,内部主要是通过对 loadable_classes 数组进行遍历,并获取到 loadable_class 的结构体,结构体中存在 ClassIMP,然后直接调用即可。

Category的调用方式和类的一样,就不在下面贴代码了。需要注意的是,load方法都是直接调用的,并没有走运行时的 objc_msgSend 函数。

static void call_class_loads(void)
{
    int i;
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    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; 
        (*load_method)(cls, SEL_load);
    }
    
    if (classes) free(classes);
}

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

根据上面的源码分析,我们可以看出 load 方法的调用顺序应该是 “父类 -> 子类 -> 分类” 的顺序。因为执行加载 Class 的时机是在 Category 之前的,而且 load 子类之前会先 load 父类,所以是这种顺序。

initialize

load 方法类似的也有 initialize 方法,initialize方法也是由 Runtime 进行调用的,自己不可以直接调用。与 load 方法不同的是,initialize方法是在第一次调用类所属的方法时,才会调用 initialize 方法,而 load 方法是在 main 函数之前就全部调用了。所以理论上来说 initialize 可能永远都不会执行,如果当前类的方法永远不被调用的话。

下面我们研究一下 initializeRuntime中的源码。

在向对象发送消息时,lookUpImpOrForward函数中会判断当前类是否被初始化,如果没有被初始化,则先进行初始化再调用类的方法。

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver);

// .... 省略好多代码

// 第一次调用当前类的话,执行 initialize 的代码
if (initialize  &&  !cls->isInitialized()) {_class_initialize (_class_getNonMetaClass(cls, inst));
}

// .... 省略好多代码

在进行初始化的时候,和 load 方法的调用顺序一样,会按照继承者链先初始化父类。_class_initialize函数中关键的两行代码是 callInitializelockAndFinishInitializing的调用。

// 第一次调用类的方法,初始化类对象
void _class_initialize(Class cls)
{
    Class supercls;
    bool reallyInitialize = NO;

    // 递归初始化父类。initizlize 不用显式的调用 super,因为 runtime 已经在内部调用了
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {_class_initialize(supercls);
    }
    
    {monitor_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {cls->setInitializing();
            reallyInitialize = YES;
        }
    }
    
    if (reallyInitialize) {_setThisThreadIsInitializingClass(cls);

        if (MultithreadedForkChild) {performForkChildInitialize(cls, supercls);
            return;
        }
        @try {// 通过 objc_msgSend()函数调用 initialize 方法
            callInitialize(cls);
        }
        @catch (...) {@throw;}
        @finally {
            // 执行 initialize 方法后,进行系统的 initialize 过程
            lockAndFinishInitializing(cls, supercls);
        }
        return;
    }
    
    else if (cls->isInitializing()) {if (_thisThreadIsInitializingClass(cls)) {return;} else if (!MultithreadedForkChild) {waitForInitializeToComplete(cls);
            return;
        } else {_setThisThreadIsInitializingClass(cls);
            performForkChildInitialize(cls, supercls);
        }
    }
}

通过 objc_msgSend 函数调用 initialize 方法。

void callInitialize(Class cls)
{((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

lockAndFinishInitializing函数中会完成一些初始化操作,其内部会调用 _finishInitializing 函数,在函数内部会调用 classsetInitialized函数,核心工作都由 setInitialized 函数完成。

static void lockAndFinishInitializing(Class cls, Class supercls)
{monitor_locker_t lock(classInitLock);
    if (!supercls  ||  supercls->isInitialized()) {_finishInitializing(cls, supercls);
    } else {_finishInitializingAfter(cls, supercls);
    }
}

负责初始化类和元类,函数内部主要是查找当前类和元类中是否定义了某些方法,然后根据查找结果设置类和元类的一些标志位。

void 
objc_class::setInitialized()
{
    Class metacls;
    Class cls;

    // 获取类和元类对象
    cls = (Class)this;
    metacls = cls->ISA();

    bool inherited;
    bool metaCustomAWZ = NO;
    if (MetaclassNSObjectAWZSwizzled) {
        metaCustomAWZ = YES;
        inherited = NO;
    }
    else if (metacls == classNSObject()->ISA()) {
        // 查找是否实现了 alloc 和 allocWithZone 方法
        auto& methods = metacls->data()->methods;
        for (auto mlists = methods.beginCategoryMethodLists(), 
                  end = methods.endCategoryMethodLists(metacls); 
             mlists != end;
             ++mlists)
        {if (methodListImplementsAWZ(*mlists)) {
                metaCustomAWZ = YES;
                inherited = NO;
                break;
            }
        }
    }
    else if (metacls->superclass->hasCustomAWZ()) {
        metaCustomAWZ = YES;
        inherited = YES;
    } 
    else {auto& methods = metacls->data()->methods;
        for (auto mlists = methods.beginLists(),
                  end = methods.endLists(); 
             mlists != end;
             ++mlists)
        {if (methodListImplementsAWZ(*mlists)) {
                metaCustomAWZ = YES;
                inherited = NO;
                break;
            }
        }
    }
    if (!metaCustomAWZ) metacls->setHasDefaultAWZ();
    if (PrintCustomAWZ  &&  metaCustomAWZ) metacls->printCustomAWZ(inherited);

    bool clsCustomRR = NO;
    if (ClassNSObjectRRSwizzled) {
        clsCustomRR = YES;
        inherited = NO;
    }
    // 查找元类是否实现 MRC 方法,如果是则进入 if 语句中
    if (cls == classNSObject()) {auto& methods = cls->data()->methods;
        for (auto mlists = methods.beginCategoryMethodLists(), 
                  end = methods.endCategoryMethodLists(cls); 
             mlists != end;
             ++mlists)
        {if (methodListImplementsRR(*mlists)) {
                clsCustomRR = YES;
                inherited = NO;
                break;
            }
        }
    }
    else if (!cls->superclass) {
        clsCustomRR = YES;
        inherited = NO;
    } 
    else if (cls->superclass->hasCustomRR()) {
        clsCustomRR = YES;
        inherited = YES;
    } 
    else {
        // 查找类是否实现 MRC 方法,如果是则进入 if 语句中
        auto& methods = cls->data()->methods;
        for (auto mlists = methods.beginLists(), 
                  end = methods.endLists(); 
             mlists != end;
             ++mlists)
        {if (methodListImplementsRR(*mlists)) {
                clsCustomRR = YES;
                inherited = NO;
                break;
            }
        }
    }
    if (!clsCustomRR) cls->setHasDefaultRR();
    if (PrintCustomRR  &&  clsCustomRR) cls->printCustomRR(inherited);
    metacls->changeInfo(RW_INITIALIZED, RW_INITIALIZING);
}

需要注意的是,initialize方法和 load 方法不太一样,Category中定义的 initialize 方法会覆盖原方法而不是像 load 方法一样都可以调用。


简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github 上,下载 Runtime PDF 合集。把所有 Runtime 文章总计九篇,都写在这个 PDF 中,而且左侧有目录,方便阅读。

下载地址:Runtime PDF
麻烦各位大佬点个赞,谢谢!????

退出移动版