IOS底层原理之类的加载过程

1,378 阅读23分钟

一、 前言

在上一篇文章《IOS底层原理之main函数之前-dyld的加载流程》中,小编分析了dyld的加载流程,已然知道了在程序运行之初dyld对动态库进行加载、链接等系列动作之后程序会进入到libobjc.A.dylib库中的_objc_init方法调用_dyld_objc_notify_register方法进行类的加载,但是我们仍未知类的加载流程,这篇文章将分析_dyld_objc_notify_register所做的工作内容。

二、_objc_init方法

_objc_init方法是定义在libobjc.A.dylib库中objc-os.mm中的一个方法,libobjc.A.dylib库的代码苹果是开源的,你可以这里下载到。下载下来的源码直接编译是通不过的,需要自己修改下配置。本文是基于objc4-750.1版本的源码进行分析的。 先来看下_objc_init方法的代码实现。

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();
    // _objc_init->map_images->map_images_nolock->_read_images->realizeClass
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

在这段代码中我们可以看到_objc_init方法的内部实际上是调用了多个函数,下面我们来逐一分析下。

1、environ_init方法

environ_init方法是对一系列的环境变量的初始化,在该方法的内部有这样的一段打印环境变量的代码。

if (PrintHelp  ||  PrintOptions) {
        if (PrintHelp) {
            _objc_inform("Objective-C runtime debugging. Set variable=YES to enable.");
            _objc_inform("OBJC_HELP: describe available environment variables");
            if (PrintOptions) {
                _objc_inform("OBJC_HELP is set");
            }
            _objc_inform("OBJC_PRINT_OPTIONS: list which options are set");
        }
        if (PrintOptions) {
            _objc_inform("OBJC_PRINT_OPTIONS is set");
        }

        for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
            const option_t *opt = &Settings[i];            
            if (PrintHelp) _objc_inform("%s: %s", opt->env, opt->help);
            if (PrintOptions && *opt->var) _objc_inform("%s is set", opt->env);
        }
    }

如果我们尝试修改上面代码的判断条件,那么在得到一些环境变量的描述信息

我们可以在Xcode中通过修改环境变量的值来达到我们调试的一些目的。

2、tls_init方法

tls_init方法非常简单,主要是对线程key的绑定。

void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
    _objc_pthread_key = TLS_DIRECT_KEY;
    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}

3、static_init方法

static_init方法的代码如下:

/***********************************************************************
* static_init
* Run C++ static constructor functions.
* libc calls _objc_init() before dyld would call our static constructors, 
* so we have to do it ourselves.
**********************************************************************/
static void static_init()
{
    size_t count;
    auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        inits[i]();
    }
}

我们看到代码上面关于static_init方法的注释,意思是说运行C++静态构造函数,在dyld 调用我们的静态构造函数之前libc会调用_objc_init方法,所以我们必须自己去实现。

4、lock_init方法

lock_init方法其实是一个空的实现方法,猜测其方法的实现是在汇编完成的。

5、exception_init方法

exception_init方法是初始化了libobjc的异常处理系统,注册异常处理的回调,从而监控异常的处理。

6、_dyld_objc_notify_register方法

//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded.  During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images.  During any later dlopen() call,
// dyld will also call the "mapped" function.  Dyld will call the "init" function when dyld would be called
// initializers in that image.  This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);

上面这段是关于_dyld_objc_notify_register方法的定义,从该方法的注释内容中我们得出几点信息:

  1. 该方法只在objc运行期间中被使用;
  2. 当objc镜像文件被映射、初始化、取消映射关系时注册处理程序;
  3. dyld将会通过一个包含objc-image-info的镜像文件数组来回调mapped方法;
  4. 该函数方法被调用的时候,会调用mapped函数对已经加载的镜像文件处理;
  5. 当dyld调用initializers的时候回调用init方法;

我们看到的_dyld_objc_notify_register里面的三个参数,map_images加载image,load_image初始化image,unmapped移除image,这三个参数都是指针函数,对应dyld中的_dyld_objc_notify_register方法,调用registerObjCNotifiers方法将这三个函数的地址作为参数传入。

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
	sNotifyObjCMapped	= mapped;
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;

	// call 'mapped' function with all images mapped so far
	try {
		notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
	}
	catch (const char* msg) {
		// ignore request to abort during registration
	}

	// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
	for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
		ImageLoader* image = *it;
		if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
			dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
			(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		}
	}
}

在这段代码里面我们有看到notifyBatchPartial方法的调用,在notifyBatchPartial方法的内部有这样的一段代码

(*sNotifyObjCMapped)(objcImageCount, paths, mhs);

这就意味着程序走到这一步会回调 'mapped'方法,这里的'mapped'方法正是我们在_dyld_objc_notify_register方法中所传入的map_images函数的地址。

三、map_images方法

map_images方法在image加载到内存的时候会触发该方法的调用。在map_images方法的内部会调用map_images_nolock方法,map_images_nolock方法会调用_read_images方法,该方法是map_images的核心内容。 _read_images方法代码较长,下面将逐一分析该方法的工作内容。

1、创建存储类的容器

_read_images方法首先创建了两张表用来存储类,一是gdb_objc_realized_classes 存储所有的类,包括已实现和未实现并且在dyld共享缓存中不存在的类,其容量是类数量的4/3。二是allocatedClasses存储已经初始化的类。

// Preoptimized classes don't go in this table.
// 4/3 is NXMapTable's load factor
int namedClassesSize = 
    (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =
    NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);

2、类的重映射

从编译后的类列表中取出所有类,获取到一个classref_t类型的指针。遍历所有的类,通过调用readClass方法对类进行处理得到一个新的类,先来看下readClass方法做了些什么?

2.1、readClass方法

readClass方法的内部首先是有一个判断,判断当前的类的祖宗类中是否有丢失的weak-linked类,如果有则当前类的所有信息不可信,会将其添加到重映射表里面,映射为nil。

if (missingWeakSuperclass(cls)) {
        // No superclass (probably weak-linked). 
        // Disavow any knowledge of this subclass.
        if (PrintConnecting) {
            _objc_inform("CLASS: IGNORING class '%s' with "
                         "missing weak-linked superclass", 
                         cls->nameForLogging());
        }
        addRemappedClass(cls, nil);
        cls->superclass = nil;
        return nil;
    }

static bool missingWeakSuperclass(Class cls)
{
    assert(!cls->isRealized());

    if (!cls->superclass) {
        // superclass nil. This is normal for root classes only.
        return (!(cls->data()->flags & RO_ROOT));
    } else {
        // superclass not nil. Check if a higher superclass is missing.
        Class supercls = remapClass(cls->superclass);
        assert(cls != cls->superclass);
        assert(cls != supercls);
        if (!supercls) return YES;
        if (supercls->isRealized()) return NO;
        return missingWeakSuperclass(supercls);
    }
}

接下来程序会popFutureNamedClass方法重新返回一个新的类newCls,如果newCls不为nil,就会对newCls的ro中的数据设置到rw中,并且将当前类添加到重映射表里面,映射为newCls。在测试调试时发现程序不会进入到这个判断内部,也就是说这个if判断条件在我们创建的类以及系统的类并不会成立,所以类的rw赋值初始化并不是在这里完成的。 popFutureNamedClass方法其实判断未来需要处理的类集合future_named_class_map是否为空。

 if (Class newCls = popFutureNamedClass(mangledName)) {
        // This name was previously allocated as a future class.
        // Copy objc_class to future class's struct.
        // Preserve future's rw data block.
        
        if (newCls->isAnySwift()) {
            _objc_fatal("Can't complete future class request for '%s' "
                        "because the real class is too big.", 
                        cls->nameForLogging());
        }
        
        class_rw_t *rw = newCls->data();
        const class_ro_t *old_ro = rw->ro;
        memcpy(newCls, cls, sizeof(objc_class));
        rw->ro = (class_ro_t *)newCls->data();
        newCls->setData(rw);
        freeIfMutable((char *)old_ro->name);
        free((void *)old_ro);
        
        addRemappedClass(cls, newCls);
        
        replacing = cls;
        cls = newCls;
    }
static Class popFutureNamedClass(const char *name)
{
    runtimeLock.assertLocked();

    Class cls = nil;

    if (future_named_class_map) {
        cls = (Class)NXMapKeyFreeingRemove(future_named_class_map, name);
        if (cls && NXCountMapTable(future_named_class_map) == 0) {
            NXFreeMapTable(future_named_class_map);
            future_named_class_map = nil;
        }
    }

    return cls;
}

接下来程序会调用addNamedClassaddClassTableEntry两个方法,将当前的类分别插入到gdb_objc_realized_classesallocatedClasses这两张前面已经创建好的表中。 我们先来看下addNamedClass方法的实现。

static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
    runtimeLock.assertLocked();
    Class old;
    if ((old = getClassExceptSomeSwift(name))  &&  old != replacing) {
        inform_duplicate(name, old, cls);

        // getMaybeUnrealizedNonMetaClass uses name lookups.
        // Classes not found by name lookup must be in the
        // secondary meta->nonmeta table.
        addNonMetaClass(cls);
    } else {
        NXMapInsert(gdb_objc_realized_classes, name, cls);
    }
    assert(!(cls->data()->flags & RO_META));

    // wrong: constructed classes are already realized when they get here
    // assert(!cls->isRealized());
}

我们看到在addNamedClass方法里面纯粹的将类插入到gdb_objc_realized_classes表中,如果有存在相同名称的类,将保持旧的映射。

接下我看下addClassTableEntry方法。

static void addClassTableEntry(Class cls, bool addMeta = true) {
    runtimeLock.assertLocked();

    // This class is allowed to be a known class via the shared cache or via
    // data segments, but it is not allowed to be in the dynamic table already.
    assert(!NXHashMember(allocatedClasses, cls));

    if (!isKnownClass(cls))
        NXHashInsert(allocatedClasses, cls);
    if (addMeta)
        addClassTableEntry(cls->ISA(), false);
}

addClassTableEntry方法的代码非常的简单,就是将已经初始化的类包括元类插入到allocatedClasses表中。 这样我们就有了两张完整的表,一张大表存储着在编译期确定的所有的类,一张小表存储着已经初始化的类。 让我们回到_read_images方法,在readClass方法调用之后会对比当前的类cls和新形成的类newCls。实际上在readClass方法里面的如果当前类是在未来需要处理的类,便会走入到popFutureNamedClass流程中去,程序会对当前的类进行处理后形成新的类返回,那么在回到_read_images方法之后的if判断条件自然是成立的,这里会初始化需要的内存空间,并将所有的未来需要处理的类添加到一个数组中。实际上测试调试的时候程序不会进入到这里来。

Class cls = (Class)classlist[I];
// 通过readClass函数获取处理后的新类,
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
if (newCls != cls  &&  newCls) {
    // Class was moved but not deleted. Currently this occurs 
    // only when the new class resolved a future class.
    // Non-lazily realize the class below.
    resolvedFutureClasses = (Class *)
        realloc(resolvedFutureClasses, 
                (resolvedFutureClassCount+1) * sizeof(Class));
    resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
2.2、修复重映射

接下来程序会对未映射Class和Super Class重映射。调用_getObjc2ClassRefs函数获取类的引用,调用_getObjc2SuperRefs函数获取父类的引用,然后通过remapClassRef方法进行类的重映射。

 if (!noClassesRemapped()) {
        for (EACH_HEADER) {
            // 重映射Class,注意是从_getObjc2ClassRefs函数中取出类的引用
            Class *classrefs = _getObjc2ClassRefs(hi, &count);
            for (i = 0; i < count; i++) {
                remapClassRef(&classrefs[I]);
            }
            // fixme why doesn't test future1 catch the absence of this?
            classrefs = _getObjc2SuperRefs(hi, &count);
            for (i = 0; i < count; i++) {
                remapClassRef(&classrefs[I]);
            }
        }
    }

3、注册SEL到namedSelectors哈希表

接下来程序会将所有的SEL都注册到一张哈希表中。首先通过_getObjc2SelectorRefs方法拿到方法引用列表,遍历列表调用sel_registerNameNoLock方法将方法插入到namedSelectors 哈希表中。

static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        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);
            }
        }
    }

sel_registerNameNoLock方法的内部调用__sel_registerName方法。__sel_registerName方法会先在namedSelectors哈希表中查找是否已经存在该方法,如果存在则表明该方法已经在列表中了,否则就会执行插入操作。

4、修复旧的函数指针调用遗留

接下来一步程序会对一些旧的函数指针进行修复。调用_getObjc2MessageRefs方法获取到消息的引用,遍历调用fixupMessageRef方法对一些常用的alloc、objc_msgSend等函数指针进行注册,并fix为新的函数指针

    // Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) {
        message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
        if (count == 0) continue;

        if (PrintVtables) {
            _objc_inform("VTABLES: repairing %zu unsupported vtable dispatch "
                         "call sites in %s", count, hi->fname());
        }
        for (i = 0; i < count; i++) {
             // 内部将常用的alloc、objc_msgSend等函数指针进行注册,并fix为新的函数指针
            fixupMessageRef(refs+i);
        }
    }

5、添加协议到protocol_map哈希表

接下来程序会将所有的协议(protocol)添加到协议的哈希表中。调用_getObjc2ProtocolList方法获取到协议列表,然后通过调用readProtocol方法来对协议进行初始化并添加到protocol_map这个哈希表中。

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可能是正确的,但是并完全不确定是否正确。程序调用_getObjc2ProtocolRefs获取协议的引用列表,遍历调用remapProtocolRef方法,remapProtocolRef方法其实就是从协议列表中获取同一内存地址的协议,和当前的协议进行比较,如果不同则进行替换。

for (EACH_HEADER) {
    // 需要注意到是,下面的函数是_getObjc2ProtocolRefs,和上面的_getObjc2ProtocolList不一样
    protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
    for (i = 0; i < count; i++) {
        remapProtocolRef(&protolist[I]);
    }
}

6、初始化所有的非懒加载类

首先我们需要明白一个概念,懒加载类和非懒加载类的区别。苹果官方的解释是:

NonlazyClass is all about a class implementing or not a +load method.

这句话的意思是说要实现一个非懒加载的类,需要在类内部实现+load方法。那么我们就明白了实现了+load方法的类是非懒加载类,否则是懒加载类。 知道了懒加载类和非懒加载类的区别,下面我们来看下非懒加载类的加载流程。程序通过调用_getObjc2NonlazyClassList方法拿到了所有的非懒加载类的集合,遍历调用realizeClassWithoutSwift方法来实现所有的非懒加载类。

6.1 读取类的data()

首先程序会读取类的data信息获取到roro是在编译器就已经赋值的,主要存储类的实例变量、方法列表、协议列表等信息,是一个只读的结构。与ro相对应的是rw,rw是一个可读可写的结构,存储的是类的属性列表、方法列表、协议列表等信息,在这一步尚未有rw的赋值,还只是对rw进行初始化。

ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
    // This was a future class. rw data is already allocated.
    rw = cls->data();
    ro = cls->data()->ro;
    cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
    // Normal class. Allocate writeable class data.
    rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
    rw->ro = ro;
    rw->flags = RW_REALIZED|RW_REALIZING;
    cls->setData(rw);
}
6.2 递归处理父类和元类

递归调用realizeClassWithoutSwift方法对superclass和元类进行处理,以确保类的继承链的完整性。我们都知道NSObject是所有类的基类,而NSObject的父类是nil,所以递归到NSObject这一次就会终止,而元类不同,向上找到根元类,根元类的isa指向的是自己本身,这样递归将会进入死循环,但是remapClass方法实际上做的是类的查找操作,如果在表内已经存在该类就会返回nil,所以元类的递归处理也不会出现问题。

supercls = realizeClassWithoutSwift(remapClass(cls->superclass));
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()));

static Class remapClass(Class cls)
{
    runtimeLock.assertLocked();

    Class c2;

    if (!cls) return nil;

    NXMapTable *map = remappedClasses(NO);
    if (!map  ||  NXMapMember(map, cls, (void**)&c2) == NX_MAPNOTAKEY) {
        return cls;
    } else {
        return c2;
    }
}

6.3 rw赋值

在对类进行一些列的初始化工作之后,会调用methodizeClass方法,这个方法其实就是对类的rw进行赋值,将类的方法列表(包括分类中的方法)、属性列表、协议列表从ro中读取到rw中存储。

method_list_t *list = ro->baseMethods();
if (list) {
    prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
    rw->methods.attachLists(&list, 1);
}

property_list_t *proplist = ro->baseProperties;
if (proplist) {
    rw->properties.attachLists(&proplist, 1);
}

protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
    rw->protocols.attachLists(&protolist, 1);
}

// Root classes get bonus method implementations if they don't have 
// them already. These apply before category replacements.
if (cls->isRootMetaclass()) {
    // root metaclass
    addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
}

// Attach categories.
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);

我们看到程序中不管是方法列表、协议列表还是属性列表,装载到rw中都是通过attachLists方法。attachLists方法其实是在原有的list基础上做的扩容操作,并且将新的数据装载在list的前面。所谓的分类中的方法覆盖类中的同名方法不过是一种假象,其实是两个方法同时存在,只是分类的方法在列表的前面,在消息发送查找方法的时候是顺序查找的,所有调用的是分类的方法。分类请查阅IOS底层原理之Category窥探这一篇文章。

 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;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
   
            memmove(array()->lists + addedCount, array()->lists,
                    oldCount * sizeof(array()->lists[0]));
            
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

7、处理所有Category

首先调用_getObjc2CategoryList方法查找类对应的Category数组,然后遍历该数组调用addUnattachedCategoryForClass方法将分类注册到对应的目标类中,然后通过remethodizeClass方法将将Category的method、protocol、property添加到Class。

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

        for (i = 0; i < count; i++) {
            // 内部循环遍历当前类的所有Category
            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());
                }
            }
        }
    }

上面代码是精简过的代码,从这段代码中我们看到两个if判断内部的处理的逻辑是一样的,唯一不同的是针对的对象一个是类一个是元类。从上面的代码逻辑来分析是可以针对元类添加Category的。

8、map_images总结

以上便是map_images方法所做的全部工作内容,主要是对非懒加载类进行初始化加载,将类的方法、属性、协议,分类的方法、属性、协议进行了初始化。下面我们通过一张思维导图来看下map_images方法的主要工作。

map_images主要功能导图

四、懒加载类的加载流程

上面我们分析了非懒加载类的初始化加载过程,知道了非懒加载类是在运行期间就已经加载到了内存中的,那么懒加载的类是在什么时候初始化的呢?按照我们一般的理解,懒加载只有在使用的时候才会去初始化的,那么必然是在创建类的对象的时候将类加载到内存的,我们都知道在OC中对象的创建是通过alloc方法来为对象开辟内存空间并且初始化对象的,而alloc方法会走消息发送的流程,这就涉及到了消息发送中的一个重要的方法lookUpImpOrForward。在该方法的源码内部发现有这样的一个判断:

if (!cls->isRealized()) {
    cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}

这里的条件判断是在判断当前类是否已经初始化过,这正好符合懒加载类在使用的时候才去初始化的特性。如果条件判断成立,程序会调用realizeClassMaybeSwiftAndLeaveLocked方法,而在realizeClassMaybeSwiftAndLeaveLocked方法内部又会调用realizeClassMaybeSwiftMaybeRelock方法。

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

从上面的这段代码中我们看出去程序最终会调用realizeClassWithoutSwift方法完成类的初始化,该方法在前面的内容我们已经做了分析,该方法主要内容如下:

  1. 读取类的data并初始化rw;
  2. 递归处理类的父类和元类以保持类的继承链的完整性;
  3. 初始化类的常规属性值;
  4. 将类的方法列表(包括分类中的方法)、属性列表、协议列表从ro中读取到rw中存储。

由上面的分析我们知道了懒加载类是在类第一次消息发送的时候初始化加载到内存的

五、load_images分析

通过前面的分析我们知道了非懒加载类是因为实现了+load方法,那么+load方法有什么神奇之处呢?如果我们在+load方法上面下断点调试,我们就可以得出load方法的函数调用栈如下:

我们看到程序会调用load_images方法,这个方法就是_dyld_objc_notify_register(&map_images, load_images, unmap_image);中的load_images。

load_images方法的内部有两个重要的方法,一是发现load方法的prepare_load_methods方法,二是调用load方法的call_load_methods方法,下面我们来逐一分析这两个方法。

1、prepare_load_methods方法

prepare_load_methods方法主要是对非懒加载类和非懒加载分类进行的处理。

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

    runtimeLock.assertLocked();

    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    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;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}
1.1、 非懒加载类的load方法

首先会调用_getObjc2NonlazyClassList方法获取到所有的非懒加载类的集合,会通过 remapClass 获取类对应的指针,然后调用schedule_class_load递归地安排当前类和没有调用 + load 父类进入列表。在执行add_class_to_loadable_list(cls)将当前类加入加载列表之前,会先把父类加入待加载的列表,保证父类在子类前调用 load 方法。

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
1.2、 非懒加载分类的load方法

首先会调用_getObjc2NonlazyCategoryList方法获取到所有的非懒加载分类的集合,会通过 remapClass 获取分类的主类对应的指针,然后调用realizeClassWithoutSwift方法初始化类,这是因为如果主类是一个懒加载的类,而前面的分析我们得知这个时候的主类尚未加载到内存中来,分类却已经加载到内存中了,这样便会让分类失去了所属性,所以这里会将分类的主类初始化加载到内存中。最后会调用add_category_to_loadable_list方法将分类中的load方法加入到list中。

 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;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }

2、call_load_methods方法

call_load_methods方法实现所有类的+load方法的调用。其实就是一个do-while的循环,不断的遍历调用类和分类的load方法。

 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);
2.1、类的load方法

通过前面的分析我们知道了在prepare_load_methods方法调用阶段,程序已经将所有的类的load方法加入到了一个list中,这个list就是loadable_classes。程序循环遍历调用类的 + load 方法,直到 loadable_classes 为空。下面是call_class_loads方法的源码。

static void call_class_loads(void)
{
    int I;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    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, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

(*load_method)(cls, SEL_load);这一行代码就会调用类中的+load方法

2.2、分类的load方法

分类的load方法的调用相对类的load方法的调用会复杂一些,是在call_category_loads方法中实现的。

static bool call_category_loads(void)
{
  ...
    // 1. 获取当前可以加载的分类列表
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    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()) {
            // 2. 如果当前类是可加载的 `cls  &&  cls->isLoadable()` 就会调用分类的 load 方法
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

    // 3. 将所有加载过的分类移除 `loadable_categories` 列表
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[I];
        } else {
            shift++;
        }
    }
    used -= shift;

    // 4. 为 `loadable_categories` 重新分配内存,并重新设置它的值
    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];
    }
...
}

call_category_loads方法代码比较长,这里只是贴出了关键部位的代码,我们来分步解释方法的作用:

  1. 获取当前可以加载的分类列表
  2. 如果当前类是可加载的 cls && cls->isLoadable() 就会调用分类的 load 方法
  3. 将所有加载过的分类移除 loadable_categories 列表
  4. 为 loadable_categories 重新分配内存,并重新设置它的值

3、load_images总结

如上所述是load_images方法的工作内容,prepare_load_methods方法负责装载+load方法,call_load_methods实现各个类以及对应分类的+load方法调用,其调用规则是父类先于子类调用,主类先于分类调用。

load_images工作图

六、分类的加载

在前面我们分析了懒加载类和非懒加载类的初始化加载流程,知道了**懒加载类的初始化是在其第一次消息发送的时候初始化加载到内存的,非懒加载类是在runtime启动的时候,map_images方法执行期间就已经初始化加载到内存了。**那么类的分类是在什么时候加载的呢?分类和类一样也是有懒加载和非懒加载的,那么就会存在着以下四种情况:

1、非懒加载类+非懒加载分类

这种情况是类和分类都实现了+load方法,类是在runtime启动时map_images方法执行期间就已经初始化完成,并且我们在前面分析_read_images方法的时候发现已经对分类进行了处理,所以这种情况下分类的加载时机和类的加载时机其实是一样的。 来看下下面这段代码:

@implementation Person (Test)
+ (void)load{
    NSLog(@"%s",__func__);
}

- (void)testCategory{
    NSLog(@"testCategory");
}
@end

@implementation Person
+ (void)load{
   NSLog(@"%s",__func__);
}
@end

Person类以及分类都实现了+load方法,根据之前的分析在main函数之前通过lldb指令必然可以在类的rw里面查询到定义的testCategory方法。

2、非懒加载类+懒加载分类

这种情况是类中实现了+load方法,而分类中未实现+load方法。这个时候因为类是在runtime启动时map_images方法执行期间就已经初始化完成,这种情况实际上和第一种情况是一样的。

3、懒加载类+非懒加载分类

这种情况是在类中未实现+load方法,而在分类中实现了+load方法。前面分析我们知道了懒加载类是在其第一次发送消息的时候才会初始化加载到内存的,那么类在该类未做任何使用的时候是不会在内存中存在的,而前面我们在分析load_images的时候prepare_load_methods方法在处理非懒加载类的时候有对其对应的类进行初始化,使得类和分类关系完整性,所以这中情况下类的加载是在load_images的时候初始化加载到内存的。

3、懒加载类+懒加载分类

这种情况是类和分类都未实现+load方法,懒加载类在其第一次消息发送的时候才初始化加载到内存,调用的realizeClassWithoutSwift方法,在该方法内部调用的methodizeClass方法便会将分类中的方法插入到类的方法列表中。

七、总结

  1. 非懒加载类是在runtime启动时map_images方法执行期间就已经初始化完成;
  2. 懒加载类是在其第一次发送消息时初始化完成加载到内存的;
  3. +load方法调用规则是父类先于子类调用,主类先于分类调用;