iOS大师养成之路 — 类的加载

4,882 阅读10分钟

在介绍这个流程前,我们先来想一下以下几个问题:

  • 1、我们写的各种类在App包中是以什么形式存在,在程序运行起来的时候又是怎么被加载到内存中的呢?
  • 2、类到底包含哪些东西?这些内容都是什么时候拼装进去的?
  • 3、类里面的东西能修改吗?哪些能修改?为什么? 带着这几个问题,我们再来聊一聊类的加载的哪些事。

开篇:void _objc_init(void) 始祖诞生

在这里将初始化一系列的角色来为object登场做好准备 1、环境变量初始化 void environ_init(void) 看一眼源代码内容


* environ_init

* Read environment variables that affect the runtime.

* Also print environment variable help, if requested.

**********************************************************************/

void environ_init(void) 

{
    if (issetugid()) {
        // All environment variables are silently ignored when setuid or setgid
        // This includes OBJC_HELP and OBJC_PRINT_OPTIONS themselves.
        return;
    } 

    // Turn off autorelease LRU coalescing by default for apps linked against
    // older SDKs. LRU coalescing can reorder releases and certain older apps
    // are accidentally relying on the ordering.
    // rdar://problem/63886091
//    if (!dyld_program_sdk_at_least(dyld_fall_2020_os_versions))
//        DisableAutoreleaseCoalescingLRU = true;

    bool PrintHelp = false;
    bool PrintOptions = false;
    bool maybeMallocDebugging = false;

    // Scan environ[] directly instead of calling getenv() a lot.
    // This optimizes the case where none are set.
    for (char **p = *_NSGetEnviron(); *p != nil; p++) {
        if (0 == strncmp(*p, "Malloc", 6)  ||  0 == strncmp(*p, "DYLD", 4)  ||  
            0 == strncmp(*p, "NSZombiesEnabled", 16))
        {
            maybeMallocDebugging = true;
        }

        if (0 != strncmp(*p, "OBJC_", 5)) continue;

        if (0 == strncmp(*p, "OBJC_HELP=", 10)) {
            PrintHelp = true;
            continue;
        }

        if (0 == strncmp(*p, "OBJC_PRINT_OPTIONS=", 19)) {
            PrintOptions = true;
            continue;
        }
        

        if (0 == strncmp(*p, "OBJC_DEBUG_POOL_DEPTH=", 22)) {
            SetPageCountWarning(*p + 22);
            continue;
        }

        const char *value = strchr(*p, '=');
        if (!*value) continue;
        value++;

        for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
            const option_t *opt = &Settings[i];
            if ((size_t)(value - *p) == 1+opt->envlen  &&  
                0 == strncmp(*p, opt->env, opt->envlen))
            {
                *opt->var = (0 == strcmp(value, "YES"));
                break;
            }
        }
    }

    // Special case: enable some autorelease pool debugging
    // when some malloc debugging is enabled 
    // and OBJC_DEBUG_POOL_ALLOCATION is not set to something other than NO.

    if (maybeMallocDebugging) {
        const char *insert = getenv("DYLD_INSERT_LIBRARIES");
        const char *zombie = getenv("NSZombiesEnabled");
        const char *pooldebug = getenv("OBJC_DEBUG_POOL_ALLOCATION");

        if ((getenv("MallocStackLogging")
             || getenv("MallocStackLoggingNoCompact")
             || (zombie && (*zombie == 'Y' || *zombie == 'y'))
             || (insert && strstr(insert, "libgmalloc")))
            &&
            (!pooldebug || 0 == strcmp(pooldebug, "YES")))
        {
            DebugPoolAllocation = true;
        }
    }
//    if (!os_feature_enabled_simple(objc4, preoptimizedCaches, true)) {
//        DisablePreoptCaches = true;
//    }
    // Print OBJC_HELP and OBJC_PRINT_OPTIONS output.

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

稍微干点坏事,打印的 _objc_inform 两行代码的限制条件解开跑一下。 不看不知道,一看吓一跳。原来有这么多的东西都是可以打印出来的

  • OBJC_PRINT_IMAGES: log image and library names as they are loaded
  • OBJC_PRINT_IMAGE_TIMES: measure duration of image loading steps
  • OBJC_PRINT_LOAD_METHODS: log calls to class and category +load methods
  • OBJC_PRINT_INITIALIZE_METHODS: log calls to class +initialize methods
  • OBJC_PRINT_RESOLVED_METHODS: log methods created by +resolveClassMethod: and +resolveInstanceMethod:
  • OBJC_PRINT_CLASS_SETUP: log progress of class and category setup
  • OBJC_PRINT_PROTOCOL_SETUP: log progress of protocol setup
  • OBJC_PRINT_IVAR_SETUP: log processing of non-fragile ivars
  • OBJC_PRINT_VTABLE_SETUP: log processing of class vtables
  • OBJC_PRINT_VTABLE_IMAGES: print vtable images showing overridden methods
  • OBJC_PRINT_CACHE_SETUP: log processing of method caches
  • OBJC_PRINT_FUTURE_CLASSES: log use of future classes for toll-free bridging
  • OBJC_PRINT_PREOPTIMIZATION: log preoptimization courtesy of dyld shared cache
  • OBJC_PRINT_CXX_CTORS: log calls to C++ ctors and dtors for instance variables
  • OBJC_PRINT_EXCEPTIONS: log exception handling
  • OBJC_PRINT_EXCEPTION_THROW: log backtrace of every objc_exception_throw()
  • OBJC_PRINT_ALT_HANDLERS: log processing of exception alt handlers
  • OBJC_PRINT_REPLACED_METHODS: log methods replaced by category implementations
  • OBJC_PRINT_DEPRECATION_WARNINGS: warn about calls to deprecated runtime functions
  • OBJC_PRINT_POOL_HIGHWATER: log high-water marks for autorelease pools
  • OBJC_PRINT_CUSTOM_CORE: log classes with custom core methods
  • OBJC_PRINT_CUSTOM_RR: log classes with custom retain/release methods
  • OBJC_PRINT_CUSTOM_AWZ: log classes with custom allocWithZone methods
  • OBJC_PRINT_RAW_ISA: log classes that require raw pointer isa fields
  • OBJC_DEBUG_UNLOAD: warn about poorly-behaving bundles when unloaded
  • OBJC_DEBUG_FRAGILE_SUPERCLASSES: warn about subclasses that may have been broken by subsequent changes to superclasses
  • OBJC_DEBUG_NIL_SYNC: warn about @synchronized(nil), which does no synchronization
  • OBJC_DEBUG_NONFRAGILE_IVARS: capriciously rearrange non-fragile ivars
  • OBJC_DEBUG_ALT_HANDLERS: record more info about bad alt handler use
  • OBJC_DEBUG_MISSING_POOLS: warn about autorelease with no pool in place, which may be a leak
  • OBJC_DEBUG_POOL_ALLOCATION: halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools
  • OBJC_DEBUG_DUPLICATE_CLASSES: halt when multiple classes with the same name are present
  • OBJC_DEBUG_DONT_CRASH: halt the process by exiting instead of crashing
  • OBJC_DEBUG_POOL_DEPTH: log fault when at least a set number of autorelease pages has been allocated
  • OBJC_DISABLE_VTABLES: disable vtable dispatch
  • OBJC_DISABLE_PREOPTIMIZATION: disable preoptimization courtesy of dyld shared cache
  • OBJC_DISABLE_TAGGED_POINTERS: disable tagged pointer optimization of NSNumber et al.
  • OBJC_DISABLE_TAG_OBFUSCATION: disable obfuscation of tagged pointers
  • OBJC_DISABLE_NONPOINTER_ISA: disable non-pointer isa fields
  • OBJC_DISABLE_INITIALIZE_FORK_SAFETY: disable safety checks for +initialize after fork
  • OBJC_DISABLE_FAULTS: disable os faults
  • OBJC_DISABLE_PREOPTIMIZED_CACHES: disable preoptimized caches
  • OBJC_DISABLE_AUTORELEASE_COALESCING: disable coalescing of autorelease pool pointers
  • OBJC_DISABLE_AUTORELEASE_COALESCING_LRU: disable coalescing of autorelease pool pointers using look back N strategy 2、线程相关的初始化 void tls_init(void) 内容很简单,就是多线程相关的准备初始化
void tls_init(void)

{

#if SUPPORT_DIRECT_THREAD_KEYS

    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);

#else

    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);

#endif

}

\

3、静态构造函数的初始化 static void static_init() C++静态构造方法的调用--都是系统的构造方法

/***********************************************************************

* 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]();
    }
    auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        UnsignedInitializer init(offsets[i]);
        init();
    }
}

4、运行时初始化 runtime_init() 名字很特别,看内容代码就知道是关于objc未添加的分类的初始化和已开辟内存的分类的初始化准备

void runtime_init(void)
{
    objc::unattachedCategories.init(32);
    objc::allocatedClasses.init();
}

5、异常捕获初始化 void exception_init(void) 看代码结合注释发现就是注册一个异常回调,如果是OC 类型的异常,则能正常抛出异常,只不过是用了C 语言的函数指针赋值

/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
void exception_init(void)
{
    old_terminate = std::set_terminate(&_objc_terminate);
}


/***********************************************************************
* _objc_terminate
* Custom std::terminate handler.
*

* The uncaught exception callback is implemented as a std::terminate handler. 

* 1. Check if there's an active exception

* 2. If so, check if it's an Objective-C exception

* 3. If so, call our registered callback with the object.

* 4. Finally, call the previous terminate handler.

**********************************************************************/

static void (*old_terminate)(void) = nil;
static void _objc_terminate(void)
{
    if (PrintExceptions) {
        _objc_inform("EXCEPTIONS: terminating");
    }

    if (! __cxa_current_exception_type()) {
        // No current exception.
        (*old_terminate)();
    }

    else {
        // There is a current exception. Check if it's an objc exception.
        @try {
            __cxa_rethrow();
        } @catch (id e) {
            // It's an objc object. Call Foundation's handler, if any.
            (*uncaught_handler)((id)e);
            (*old_terminate)();
        } @catch (...) {
            // It's not an objc object. Continue to C++ terminate.
            (*old_terminate)();
        }
    }
}

6、缓存初始化 void cache_t::init() 这里处理的是缓存的问题,如果该应用是有缓存的话,需要恢复部分缓存,如果这些缓存正在被列为将要清理的垃圾缓存的话,这个时候会被禁止清除因为启动的时候可能会用的着。

void cache_t::init()
{
#if HAVE_TASK_RESTARTABLE_RANGES
    mach_msg_type_number_t count = 0;
    kern_return_t kr;
    while (objc_restartableRanges[count].location) {
        count++;
    }
    kr = task_restartable_ranges_register(mach_task_self(),
                                          objc_restartableRanges, count);
    if (kr == KERN_SUCCESS) return;
    _objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
                kr, mach_error_string(kr));
#endif // HAVE_TASK_RESTARTABLE_RANGES

}

7、紧急初始化一些库 _imp_implementationWithBlock_init(); 一般情况下都是懒加载的话不需要这么处理,在有些进程中需要更早地加载就在这里处理。看注释就是为了修复某些bug,看样子苹果的开发人员也需要各种插入打补丁。果然是程序都难逃此劫。

/// Initialize the trampoline machinery. Normally this does nothing, as
/// everything is initialized lazily, but for certain processes we eagerly load
/// the trampolines dylib.
void _imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
    // Eagerly load libobjc-trampolines.dylib in certain processes. Some
    // programs (most notably QtWebEngineProcess used by older versions of
    // embedded Chromium) enable a highly restrictive sandbox profile which
    // blocks access to that dylib. If anything calls
    // imp_implementationWithBlock (as AppKit has started doing) then we'll
    // crash trying to load it. Loading it here sets it up before the sandbox
    // profile is enabled and blocks it.
    //
    // This fixes EA Origin (rdar://problem/50813789)
    // and Steam (rdar://problem/55286131)
    if (__progname &&
        (strcmp(__progname, "QtWebEngineProcess") == 0 ||
         strcmp(__progname, "Steam Helper") == 0)) {
        Trampolines.Initialize();
    }
#endif
}

8、准备工作已经完毕,开始真正的加载MachO文件_dyld_objc_notify_register(&map_images, load_images, unmap_image) 。查看这个方法的注释,事实上在这个位置只是向 dyld 注册了一个三个回调函数,为什么这么说,原因有三 在objc4的源码中通篇都没有搜到这个方法的实现位置,只有声明位置。 在dyld的源码中找到了这个方法的实现位置。 加载images是dyld做的事情,objc库不可能做这个事情,但是objc库需要知道每个image加载之后的时机。所以在这里以C的方式注册了一个回调函数,希望每加载完一个image来调一下我这个map_images函数,以及在合适的时机调用load_images、 unmap_image方法

//
// 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源码中找到了实现位置
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    log_apis("_dyld_objc_notify_register(%p, %p, %p)\n", mapped, init, unmapped);
    gAllImages.setObjCNotifiers(mapped, init, unmapped);

}

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();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

map_images 映射通过dyld链接的所有的镜像文件

看源代码和注释了解到是通过ABI的接口和运行时加锁处理镜像文件映射逻辑,通过中间层接口map_images_nolock 处理所有的MachO 文件

/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
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);
}

我们来看一下 map_images_nolock 的主要流程代码 主要就是把所有的MachO 文件映射并读取主要的方法有:

  • preopt_init
  • _getObjcSelectorRefs
  • _read_images 其中跟类加载有直接关系的是 _read_images
/***********************************************************************
* map_images_nolock
* Process the given images which are being mapped in by dyld.
* All class registration and fixups are performed (or deferred pending
* discovery of missing superclasses etc), and +load methods are called.
*
* info[] is in bottom-up order i.e. libobjc will be earlier in the 
* array than any library that links to libobjc.
*
* Locking: loadMethodLock(old) or runtimeLock(new) acquired by map_images.
**********************************************************************/
#if __OBJC2__
#include "objc-file.h"
#else
#include "objc-file-old.h"
#endif

void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    static bool firstTime = YES;
    header_info *hList[mhCount];
    uint32_t hCount;
    size_t selrefCount = 0;
    if (firstTime) {
        preopt_init();
    }
    hCount = 0;
    int totalClasses = 0;
    int unoptimizedTotalClasses = 0;

    {
        uint32_t i = mhCount;
        while (i--) {
            const headerType *mhdr = (const headerType *)mhdrs[i];
            auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
            if (!hi) {
                continue;
            }
            if (mhdr->filetype == MH_EXECUTE) {
#if __OBJC2__
                if ( !hi->hasPreoptimizedSelectors() ) {
                  size_t count;
                  _getObjc2SelectorRefs(hi, &count);
                  selrefCount += count;
                  _getObjc2MessageRefs(hi, &count);
                  selrefCount += count;
                }
#else
                _getObjcSelectorRefs(hi, &selrefCount);

#endif
            }
            hList[hCount++] = hi;
        }
    }

    if (firstTime) {
        sel_init(selrefCount);
        arr_init();
    }

    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

    firstTime = NO;
    // Call image load funcs after everything is set up.
    for (auto func : loadImageFuncs) {
        for (uint32_t i = 0; i < mhCount; i++) {
            func(mhdrs[i]);
        }
    }
}

_read_images详解

doneOnce 只做一次的事情

我把这个代码块的其他打印和异常判断的逻辑去掉之后就是下面这些代码,这样看着清晰多了。主要是做了两个事情

  • 初始化小对象的混淆器initializeTaggedPointerObfuscator,看注释是加了随机码为了防止被黑客用类似的小对象入侵。
  • 初始化一张初始化类的总表 gdb_objc_realized_classes,便于后续查找插入。这里和早先公布的源代码还不一样,以前的已经初始化的类的表也是在这里初始化的,现在挪到更早的位置上在runtimeinit方法中就初始化了
if (!doneOnce) {
        doneOnce = YES;
        launchTime = YES;
        initializeTaggedPointerObfuscator();
        int namedClassesSize =  (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
    }

修复方法指针,源码如下,主要是修复一些方法对不上的异常情况

// Fix up @selector references

// Fix up @selector references
    static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        for (EACH_HEADER) {
            if (hi->hasPreoptimizedSelectors()) 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 sel = sel_registerNameNoLock(name, isBundle);
                if (sels[i] != sel) {
                    sels[i] = sel;
                }
            }
        }
    }

发现类,修复没有解决的未来的类。

  • 首先,判断是否是dyld进行优化处理了不需要立即读取的类,如果是的,就不处理
  • 然后,读取MachO文件中的所有的段,通过_getObjc2ClassList获取所有的类,用classlist进行收集。
  • 遍历所有的类,通过readClass将这些读取到的类加入到类的总表中。稍后会详细分解这个函数处理的逻辑
  • 处理未来的类被处理掉的异常情况,对未来的类的总量进行重新开辟内存,并进行赋值保存
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
    for (EACH_HEADER) {
        if (! mustReadClasses(hi, hasDyldRoots)) {
            // Image is sufficiently optimized that we need not call readClass()
            continue;
        }
        classref_t const *classlist = _getObjc2ClassList(hi, &count);
        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->hasPreoptimizedClasses();
        for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[i];
            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;
            }
        }
    }

将已经绑定了的类进行标记. 这里的 headerIsBundle 一度让我很疑惑,一时不理解,直到我找到了下面的几个地方 在Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)的方法里找到了这个变量的使用位置。对于这段代码的注释的意思是:出于对未来的考虑,共享缓存中不能包含 MachO 头文件中的捆绑文件(也是就标记为绑定的类)。

// for future reference: shared cache never contains MH_BUNDLEs
    if (headerIsBundle) {
        cls->data()->flags |= RO_FROM_BUNDLE;
        cls->ISA()->data()->flags |= RO_FROM_BUNDLE;
    }

查看 RO_FROM_BUNDLE 的宏定义时给的解释是 对于这种可卸载的绑定好的类一定不能被链接器赋值。什么意思?就是说我这个被捆绑好的类你连接器不要在链接的时候处理,我这个是在未来的某个时候处理的。

// class is in an unloadable bundle - must never be set by compiler
#define RO_FROM_BUNDLE        (1<<29)

如果支持修复的话,那么修复旧的msg_send方法

这一段没有什么好聊的,就是为了处理一些异常或者是为了适配之前版本的事情

#if SUPPORT_FIXUP
    // 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++) {
            fixupMessageRef(refs+i);
        }
    }
#endif

发现协议,修复协议指针

这一段处理的是针对协议引用的修复

  • 首先准备好协议的散列表,如果是在启动阶段 & 是被优化了的协议那么跳过
  • 然后遍历插入协议的散列表 这里其实有个疑问,难道启动的时候不需要用到协议吗? 苹果给的注释解释是,跳过协议读取仅限于共享缓存中的协议,同时是支持roots操作的。但是启动的时候确实也会使用到协议,但是仅限于是共享缓存中协议。那怎么区分呢?通过isCanonical()标记的共享缓存中的协议才是被认可的,如果选择其他非共享缓存的二级制文件作为规范定义的话情况可能就截然不同。
// Discover protocols. Fix up protocol refs.
    for (EACH_HEADER) {
        extern objc_class OBJC_CLASS_$_Protocol;
        Class cls = (Class)&OBJC_CLASS_$_Protocol;
        ASSERT(cls);
        NXMapTable *protocol_map = protocols();
        bool isPreoptimized = hi->hasPreoptimizedProtocols();

        if (launchTime && isPreoptimized) {
            if (PrintProtocols) {
                _objc_inform("PROTOCOLS: Skipping reading protocols in image: %s",
                             hi->fname());
            }
            continue;
        }
        
        bool isBundle = hi->isBundle();
        protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
        for (i = 0; i < count; i++) {
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
        }
    }

修复协议引用

这里跟修复类的引用的思路差不多 在启动的时候,我们知道被优化的镜像文件引用指向了协议的共享缓存定义的位置。在启动的时候可以跳过,但是必须通过访问@protocol refs来获取稍后加载的共享缓存镜像中的协议引用。

for (EACH_HEADER) {
        // At launch time, we know preoptimized image refs are pointing at the
        // shared cache definition of a protocol.  We can skip the check on
        // launch, but have to visit @protocol refs for shared cache images
        // loaded later.
        if (launchTime && hi->isPreoptimized())
            continue;
        protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
        for (i = 0; i < count; i++) {
            remapProtocolRef(&protolist[i]);
        }
    }

发现分类

这里发现&装载分类。根据苹果的源码注释,给出如下解释 为了确保分类能在启动时能展示只有在初始化分类文件已经处理了,发现&装载的流程推迟在第一次执行load_images和注册回调函数已经完成之后进行。通过 didInitialAttachCategories 全局静态变量控制是否执行。 对于类别的发现时机:当其他线程在修复之前调用新类别的代码,类别的发现必须晚点以免造成潜在的资源竞争。

if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }

实现非懒加载类(实现了load方法和静态实例化方法的类)

这里就是循环遍历非懒加载类,并确保其已经加入类表中,然后实现所有的非懒加载类。对于swift有

        classref_t const *classlist = hi->nlclslist(&count);
        for (i = 0; i < count; i++) {
            Class cls = remapClass(classlist[i]);
            if (!cls) continue;

            addClassTableEntry(cls);
            if (cls->isSwiftStable()) {
                if (cls->swiftMetadataInitializer()) {
                    _objc_fatal("Swift class %s with a metadata initializer "
                                "is not allowed to be non-lazy",
                                cls->nameForLogging());
                }
                // fixme also disallow relocatable classes
                // We can't disallow all Swift classes because of
                // classes like Swift.__EmptyArrayStorage
            }
            realizeClassWithoutSwift(cls, **nil**);
        }
    }

遍历非懒加载类表确保将非懒加载的类加入到类表中

这里开始进行非懒加载类的变量实现

判断是否是swift类

如果是稳定的swift类同时拥有元类的初始化方法,那么报错

实现所有的非懒加载的类 realizeClassWithoutSwift

实现非swift的类,这个方法是类实现的重要方法,里面涉及到对rw、ro的处理、以及父类、元类的递归实现、父类子类关系建立和其他相关标志位的赋值,是类中最重要数据的处理环节。对应的还有给swift类实现的方法 _objc_realizeClassFromSwift

确认是否已经加入到类表中了

对于已经实现了的类,就直接返回处理,如果还没有实现那么继续

判断是否未来的类
  • 如果是未来的类,那么直接取出data()中的数据对rw、ro 赋值
  • 如果不是未来的类,开辟一个rw的内存空间进行ro数据拷贝。
  • 如果父类和元类还没有实现。那么递归进行实现。
  • 如果支持 SUPPORT_NONPOINTER_ISA 如果是元类,那么直接设置成FAST_CACHE_REQUIRES_RAW_ISA 如果不是元类 取出instancesRequireRawIsa()设置,再取 supercls->getSuperclass() 判断是否需要递归处理
及时更新父类和元类的数据

更新父类和元类,以免发生重映射导致数据改变

调节实例变量的内存偏移

调节实例变量的内存偏移值(此处会涉及到的情况是可能父类的ivars 有变化,所以必须对实例变量中的父类的内存偏移值做出调整)

设置快速初始化方式需要的内大小

如果还没有设置的话,设置快速初始化实例变量的内存大小值。这里主要是在初始化的时候提高效率

拷贝一些标志位 从ro -> rw

拷贝一些标志位值,cxx的构造和析构标志位,从ro 拷贝到rw

管理对象禁止标志位的设置

从父类或者ro中拷贝关联对象的禁止标志位

把当前类添加到父类的子类表中

将当前类添加到父类的子类表中addSubclass(supercls, cls);,如果没有父类的说明就是根类将其添加到根类中。

添加分类

methodizeClass(cls, previously);非懒加载类的分类就是在这个位置添加进去的

额外的思考

这里是非懒加载的类走进来的流程,那么懒加载的类呢,难道不处理吗?懒加载的类又是如何触发类的加载的呢? 首先我们来澄清一个思路,懒加载意味着用到才会加载,那么就只有再调用相关方法的时候才会用到,那么必然会走到这个方法里来 lookUpImpOrForward 然后,我们来到这个方法中发现了 realizeAndInitializeIfNeeded_locked 再往下找,找到了

if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

看这个写法就知道下一步就是要加载类了经过一层中间接口 realizeClassMaybeSwiftMaybeRelock 再来到了这里还是这个熟悉的方法 realizeClassWithoutSwift

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, nil);
        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();
    }

实现新解析的未来的类,以防止CF来操作他们

遍历所有的解析的未来的类

  • 先判断是否是稳定的swif类,如果是就直接报错。swift中不允许这种操作
  • 实现该类
  • 标记该类已经所有的子类都要求要原生的isa指针

调试打印

这里主要有两步

  • 如果是调试可靠的ivars,那么实现所有的类
  • 如果需要打印预优化的信息,那么打印相关的预优化信息 这些优化包括 被优化的方法、dyld的优化、被预优化的类、还没有预优化的方法、已经被预分类的方法比例、被预注册的类占总量的占比、多少协议还没有被预优化。

总结

我们来回顾开篇提到的几个问题?

我们的代码是怎么被加载进去的?

1、通过这一系列的步骤跟过来,我们发现其实我们写的代码在编译期间被包装成了一个个的MachO文件,在启动的时候这些个类信息直接从MachO文件中读取出来的,读者如果对这个过程感兴趣建议去阅读一下《程序员的自我修养》专门将装载编译的书。这些数据被读取出来之后都是被映射到对应的数据表中方面增删改查,有个专业术语叫散列。

类到底包含哪些东西,是什么时候拼装进去的?

这里我只说一下最主要的东西,class_data_bits_t bits;这里包含了类中主要的数据,包括属性、方法、协议、成员变量等等。这里的组装时机是在 realizeClassWithoutSwift方法中。如需要了解其他的请移步笔者的另外一篇关于类的介绍的文章juejin.cn/post/684490…

类里面的东西能修改吗?哪些能修改?为什么?

我们在realizeClassWithoutSwift的流程中发现rw中的ro其实是从MachO文件中直接读取的只是可读的,而rw则不一样,是从ro中拷贝过来的、是可读可写的。也就意味着rw中的东西是可以修改的,而ro中则不能。但是我们不是有动态注入类的runtime的接口么?难道不能随便加吗?为了说明这个问题我们来看看动态注册类时做了什么?添加ivar的时候又做了什么?

void objc_registerClassPair(Class cls)

{
    mutex_locker_t lock(runtimeLock);
    checkIsKnownClass(cls);
    if ((cls->data()->flags & RW_CONSTRUCTED)  ||
        (cls->ISA()->data()->flags & RW_CONSTRUCTED)) 
    {
        _objc_inform("objc_registerClassPair: class '%s' was already "
                     "registered!", cls->data()->ro->name);
        return;
    }
    if (!(cls->data()->flags & RW_CONSTRUCTING)  ||  
        !(cls->ISA()->data()->flags & RW_CONSTRUCTING))
    {
        _objc_inform("objc_registerClassPair: class '%s' was not "
                     "allocated with objc_allocateClassPair!", 
                     cls->data()->ro->name);
        return;
    }
    // Clear "under construction" bit, set "done constructing" bit

    cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
    cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
    // Add to named class table.

    addNamedClass(cls, cls->data()->ro->name);

}

我们在上面这段代码中看到了注册的时候修改了一个标志位 RW_CONSTRUCTED 的状态,而class_addIvar中有下面的一句判断,而就是说如果已经是 RW_CONSTRUCTING状态的时候就不能再修改了,这就是为什么注册之后就不能添加ivar的原因。

// Can only add ivars to in-construction classes.
    if (!(cls->data()->flags & RW_CONSTRUCTING)) {
        return NO;
    }