iOS 应用的加载objc篇

iOS 应用的加载objc

前言

在上一篇文章应用加载分析中我们经过对dlyd的探索最后引入到了objc中的_objc_init函数,那么_objc_init到底做了什么呢?下面我们将对_objc_init进行进一步探索,并通过探索来了解一下应用加载在obcj中都做了什么。

注: 本文使用的是objc4-779.1

1. _objc_init探索

_objc_init源码:

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

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();
    cache_init();
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
  • 首先判断是否已经初始化,如果已经初始化了则直接返回。

2.1 environ_init

environ_init 源码:

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

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

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

我们可以看到environ_init主要是读取一些环境变量以及打印环境变量的一些帮助信息。

2.2 tls_init

tls_init源码:

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
}

此处做的是一些关于线程key的绑定,比如每个线程数据的析构函数。

2.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]();
    }
}

这里会运行c++静态构造函数。在dyld调用我们的静态构造函数之前,libc会调用_objc_init(),所以我们必须自己做。并且这里只会初始化系统内置的C++静态构造函数,我么自己代码里面写的并不会在这里初始化。

2.4 runtime_init

runtime_init源码:


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

运行时的初始化。分类哈希表和allocated的类的哈希表初始化。

2.5 exception_init

exception_init源码:

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

这里做的是初始化一个异常处理系统,我们诚信触发的异常都会来到这里:

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

old_terminate是处理异常的回调其流程如下:

  1. 判断是否是一个活跃的异常
  2. 如果是活跃的,检查是否是objc抛出的异常
  3. 如果是objc抛出的异常,调用uncaught_handler回调函数指针
  4. 如果不是objc抛出的异常,就继续C++的异常终止操作

2.6 cache_init

cache_init 源码:

void cache_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
}

这里做的主要是缓存的一些初始化。

2.7 _imp_implementationWithBlock_init

_imp_implementationWithBlock_init 源码:

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

这里是懒加载的方式初始化trampoline machinery

2.8 _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);

译:

  1. 注意:仅供objc运行时使用。
  2. objc镜像被映射(mapped)取消映射(unmapped)初始化(initialized)注册的回调函数就会被调用。
  3. dyld将为镜像中objc-image-info节中的数组回调映射(mapped)函数。
  4. 这些些dylibs的镜像会自动增加引用计数,所以objc不再需要处理。
  5. 在调用_dyld_objc_notify_register函数的过程中,会对它们调用dlopen函数,以防止它们被卸载。
  6. 在以后任何调用dlopen的期间,dyld将优先使用已加载的objc镜像调用映射(mapped)函数
  7. dyld也会调用映射(mapped)函数,当调用dyld时,dyld将调用init函数。
  8. objc调用任何+load方法时,该镜像中的初始值将被设定

_dyld_objc_notify_register函数的三个参数mappedinitunmapped其实都是函数指针,我们点击参数类型一看便知。其实就在函数上面定义的。

函数指针定义

这三个函数指针都是在dyld中回调的,我们点击_dyld_objc_notify_register跳转到它的实现源码一看便知,这里调用了registerObjCNotifiers函数。

_dyld_objc_notify_register

接着来到dyldregisterObjCNotifiers函数中进行查看

registerObjCNotifiers

registerObjCNotifiers函数中首先就是赋值这三个函数的指针。定义如下,跟我们在objc中看到的一模一样。

函数指针.jpg

通过这几张截图内容可以说明在registerObjCNotifiers的内部,libObjc传过来的三个函数指针被dyld保存在了本地静态变量中,最终函数是否被调用,取决于这三个静态变量,在registerObjCNotifiers中的try中我们通过如下注释:

// call 'mapped' function with all images mapped so far

再此调用mapped函数来映射所有镜像

那么也就是说notifyBatchPartial里面会进行真正的函数指针调用,下面我们来看看,由于又是个大的方法代码众多我们只看重点:

notifyBatchPartial.jpg

在这里我们可以看到tell objc about new images告诉objc镜像已经映射完成了。在箭头的地方就是真正调用指针的地方。那么这三个函数的的调用关系就基本理清楚了,但是这三个函数具体做了什么呢?下面我们一一探索。

3. 探索 map_images

首先我们查看一下map_images的源码实现:

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

根据注释我们可以知道,这个函数是处理dyld映射给定的镜像,跟我们上面分析的一致,然后它还可以在获取到特定ABI的锁后调用与ABI无关的代码。

由于代码很简单,肯定不是核心的实现,我们继续向下探索来到map_images_nolock函数:

乍一看150多行代码,也不少了,但是仔细一看,大部分代码都是进行镜像文件信息的提取,以及各种环境的判断,只有_read_images才是该函数最核心的。在这里hCount就是header count的意思,表示Mach-Oheader的数量。

_read_images 调用.jpg

3.1 _read_images

那么我们看看_read_images的定义吧:

_read_images定义.jpg

这里通过_read_images函数对连接的header进行初始化,从头开始进一步处理。这里我们发现_read_images有400行左右的代码,我们不妨折叠一下代码,看看有多少个分支,先总体预览一下:

_read_images折叠代码.jpg

通过折叠代码,我们可以大致将_read_images分为如下的几个流程:

3.2 doneOnce

顾名思义,这里只会执行一次,那么这一次都做了些什么事情呢?

doneOnce1.jpg
  • 首先将doneOncelaunchTime置为YES
  • 然后通过SUPPORT_NONPOINTER_ISA这个宏判断当前是否支持开启内存优化的isa,如果支持则在某些条件下需要禁用这个优化
  • 然后通过SUPPORT_INDEXED_ISA判断当前是否将类存储在isa作为类表的索引,如果是的话,在遍历所有的Mach-O的头部,对Swift3.0之前的代码禁用isa的内存优化
doneOnce2.jpg
  • 通过宏TARGET_OS_OSX判断是否是macOS执行环境,不是则不处理
  • macOS执行环境,如果版本小于10.11则需要禁用掉nonPointerIsa,原因是APP过于老旧
  • 然后在遍历Mach-O的头部,判断如果有__DATA,__objc_rawisa段的存在,则禁用nonPointerIsa,原因是新APP加载老的扩展的时候会需要这样的判断操作
doneOnce3.jpg
  • 如果关闭了Tagged Pointers则进行一些处理
  • 用随机性的方式初始化objc_debug_taggedpointer_obfuscator
  • 计算一个合适大小的size作为表的容量,这里使用3/4这个装载因子来作为扩容的临界值
  • 初始化gdb_objc_realized_classes哈希表,用来加载所有的类

表的定义如下:

// This is a misnomer: gdb_objc_realized_classes is actually a list of 
// named classes not in the dyld shared cache, whether realized or not.
NXMapTable *gdb_objc_realized_classes;  // exported for debuggers in objc-gdb.h

译:这是一个误称,gdb_objc_realized_classes实际上存的是不在dyld共享缓存里面的类,无论这些类是否实现。

3.3 Fix up @selector references

Fix up @selector references .jpg

这里还会修复SEL引用,主要就是注册SEL。通过调用sel_registerNameNoLock函数最终调用__sel_registerName函数进行真正的注册。

  • 先加锁,然后遍历EACH_HEADER
  • 如果开启了预优化,就跳转到下一个
  • 如果没有则获取当前镜像是否是Bundle类型
  • 然后通过_getObjc2SelectorRefs函数拿到所有SEL的引用
  • 然后对所有的SEL引用调用sel_registerNameNoLock函数进行注册

__sel_registerName 源码:

static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 
{
    SEL result = 0;

    if (shouldLock) selLock.assertUnlocked();
    else selLock.assertLocked();

    if (!name) return (SEL)0;

    result = search_builtins(name);
    if (result) return result;
    
    conditional_mutex_locker_t lock(selLock, shouldLock);
    auto it = namedSelectors.get().insert(name);
    if (it.second) {
        // No match. Insert.
        *it.first = (const char *)sel_alloc(name, copy);
    }
    return (SEL)*it.first;
}

__sel_registerName流程:

  • 判断是否要加锁,如果需要就加锁
  • 如果传入的name为空就返回SEL0,也就是空SEL,因为在底层SEL就是一个数值类型
  • 通过search_builtins函数获取一个result,如果已经注册就直接返回
  • 加锁,然后获取namedSelectors哈希表,获取的过程中没有就会创建
  • 获取到后则插入,判断second的值,这里应该是secondtrue则表示没有匹配上,没匹配上就通过sel_alloc来创建一个SEL赋值到哈希表的缓存中(PS:大概意思明白,但是it的数据结构没太弄明白,为什么这样用?)
  • 最后返回刚刚allocSEL

3.4 Discover classes. Fix up unresolved future classes. Mark bundle classes.

Discover classes.jpg

这里是发现那些未解析的future类,标记bundle类,如果没有则通过调用readClass函数将类

  • 首先获取一个boolhasDyldRoots用来在进入到循环后通过mustReadClasses函数判断那些Image被充分优化,如果被优化就可以跳过这个步骤,一般不会来到这里
  • 然通过_getObjc2ClassList函数来获取到Mach-O中类的列表
  • 读取header是否是**Bundle**,读取header是否开启了优化
  • 然后遍历获取到的类,通过readClass处理是否是future类的修复,然后返回一个新类,判断返回的新类和列表类是否一致,如果不一致并且新类不为空,则说明被修复过了,则重新为类开辟内存,添加到数组中(初始化所有懒加载的类需要的内存空间 - 现在数据没有加载到的 - 连类都没有初始化的)

readClass 源码:

/***********************************************************************
* readClass
* Read a class and metaclass as written by a compiler.
* Returns the new class pointer. This could be: 
* - cls
* - nil  (cls has a missing weak-linked superclass)
* - something else (space for this class was reserved by a future class)
*
* Note that all work performed by this function is preflighted by 
* mustReadClasses(). Do not change this function without updating that one.
*
* Locking: runtimeLock acquired by map_images or objc_readClassPair
**********************************************************************/
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    const char *mangledName = cls->mangledName();
    
    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;
    }
    
    cls->fixupBackwardDeployingStableSwift();

    Class replacing = nil;
    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;
    }
    
    if (headerIsPreoptimized  &&  !replacing) {
        // class list built in shared cache
        // fixme strict assert doesn't work because of duplicates
        // ASSERT(cls == getClass(name));
        ASSERT(getClassExceptSomeSwift(mangledName));
    } else {
        addNamedClass(cls, mangledName, replacing);
        addClassTableEntry(cls);
    }

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

readClass 分析:

  • 首先获取mangled name
  • 然后判断是否是弱引用的父类,如果是则调用addRemappedClass函数将其移除,并将其父类置空,返回一个nil
  • 对于Swift向后兼容的修复
  • 如果是future类就会进行rw、ro的处理,基本不会来到这里,我们并不会有太多的future类,所以cls也基本不会被赋值为newCls
  • 接下来进行断言处理,也基本不会来,都是出错了才会到这里,正常流程是不会走的
  • 下面就是将类信息通过调用addNamedClassaddClassTableEntry分别放入到我们doneOnceruntime_init初始化的初始化的哈希表中
  • 最后做一个共享缓存从不包含MH_BUNDLEs的处理
addNamedClass.jpg
addClassTableEntry.jpg

小结:
所以readClass主要就是处理future类,设置future类的ro、rw,然后将类插入到gdb_objc_realized_classes类总表allocatedClasses已开辟内存的类的表。但是我们用到future类的时候很少,所以这里面主要代码使用也就很少。这些都可以通过lldb断点调试进行验证,这里就不上验证的截图了,只说一下原理。

3.5 Fix up remapped classes

Fix up remapped classes.jpg

这里是修复重新映射的类,类表和非懒加载的类表没有被重映射的(也就是 _objc_classlist) 由于消息转发,类引用和父类引用会被重映射 (也就是 _objc_classrefs)(注:一般不会来到这里

  • 通过noClassesRemapped判断是否有类引用(_objc_classrefs)需要重映射,如果需要,则遍历EACH_HEADER
  • 通过_getObjc2ClassRefs_getObjc2SuperRefs分别取出当前镜像的类引用和父类引用,然后调用remapClassRef函数进行重新映射。

3.6 Fix up old objc_msgSend_fixup call sites

Fix up old objc_msgSend_fixup call sites.jpg

这里是修复旧的objc_msgSend_fixup调用

  • 首先还是通过一个宏SUPPORT_FIXUP来判断是否需要修复
  • 然后循环遍历EACH_HEADER,通过_getObjc2MessageRefs函数获取当前镜像中的消息引用
  • 然后通过遍历这些消息调用fixupMessageRef函数进行修复

fixupMessageRef 源码如下

/***********************************************************************
* fixupMessageRef
* Repairs an old vtable dispatch call site. 
* vtable dispatch itself is not supported.
**********************************************************************/
static void 
fixupMessageRef(message_ref_t *msg)
{    
    msg->sel = sel_registerName((const char *)msg->sel);

    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == @selector(alloc)) {
            msg->imp = (IMP)&objc_alloc;
        } else if (msg->sel == @selector(allocWithZone:)) {
            msg->imp = (IMP)&objc_allocWithZone;
        } else if (msg->sel == @selector(retain)) {
            msg->imp = (IMP)&objc_retain;
        } else if (msg->sel == @selector(release)) {
            msg->imp = (IMP)&objc_release;
        } else if (msg->sel == @selector(autorelease)) {
            msg->imp = (IMP)&objc_autorelease;
        } else {
            msg->imp = &objc_msgSend_fixedup;
        }
    } 
    else if (msg->imp == &objc_msgSendSuper2_fixup) { 
        msg->imp = &objc_msgSendSuper2_fixedup;
    } 
    else if (msg->imp == &objc_msgSend_stret_fixup) { 
        msg->imp = &objc_msgSend_stret_fixedup;
    } 
    else if (msg->imp == &objc_msgSendSuper2_stret_fixup) { 
        msg->imp = &objc_msgSendSuper2_stret_fixedup;
    } 
#if defined(__i386__)  ||  defined(__x86_64__)
    else if (msg->imp == &objc_msgSend_fpret_fixup) { 
        msg->imp = &objc_msgSend_fpret_fixedup;
    } 
#endif
#if defined(__x86_64__)
    else if (msg->imp == &objc_msgSend_fp2ret_fixup) { 
        msg->imp = &objc_msgSend_fp2ret_fixedup;
    } 
#endif
}

fixupMessageRef函数中有一个重要的地方就是把alloc指向了objc_alloc,这个在我们探索alloc的时候有关系到。
iOS Objective-C alloc 调用前会调用id objc_alloc(Class cls)的原因

3.7 Discover protocols. Fix up protocol refs.

Discover protocols. Fix up protocol refs.jpg

这里是遍历所有协议,将协议列表加载到protocol的哈希表中

  • 首先还是初始化一些类信息,断言判断
  • 通过protocols函数初始化protocol_map哈希表
  • 判断是否开启了预优化,或者是共享缓存中中的协议,这里不会处理
  • 获取是否是Bundle
  • 通过_getObjc2ProtocolList函数获取到当前镜像文件的所有协议
  • 循环遍历这些协议,通过readProtocol函数进一步处理

3.7.1 readProtocol

那么readProtocol函数到底做了些什么处理呢?我们来到该函数一探究竟。

readProtocol 1.jpg

根据注释我们可以知道,该函数的作用是读取由编译器编写的协议。

readProtocol 2.jpg
  • 首先根据镜像文件的类型选择NXMapKeyCopyingInsert还是NXMapInsert
  • 根据mangledName取出是否存在oldProtocol
  • 如果存在,判断新旧是否相等,相等就不处理了,说明处理过,不相等则则说明我们选择了另一个协议,这里应该是对共享缓存的一些处理,如果是共享缓存里面的需要将新协议清除,这样才不会被别人使用,共享缓存中的不能修改,另外也基本不会来到这个分支进行处理,因为headerIsPreoptimizedtrue的时候在_read_image函数中已经continue了。
readProtocol 3.jpg

这个分支也不会到来,还是上面一样的原因headerIsPreoptimizedtrue的时候在_read_image函数中已经continue了,通过全局搜索也没有其他调用的地方。

根据注释我们可以知道共享缓存初始化了协议对象本身,但是为了允许缓存外替换,我们现在需要将它添加到协议表中。

所以这个分支的主要内容就是对协议的一个替换,但是也不会被执行,应该是苹果对未来的处理,现在还没用到。

readProtocol 4.jpg

从未预先优化的镜像中读取出新的协议像分配了足够的存储空间,我们将它修复好(修复isa)然后存储到Protocol哈希表中。

readProtocol 5.jpg

从未预先优化的镜像中读取出新的协议,但是没有分配足够的存储空间。我们就重新分配好新协议的存储空间,然后修复好(修复isa)后将其存储到Protocol哈希表中。

3.8 Fix up @protocol references

Fix up @protocol references.jpg

这里是修复@protocol的列表引用,对所有的协议进行重映射,优化后的镜像可能是正确的,但也不确定,所以修复一下。

  • 首先还是做个判断,共享缓存且启动时且被预先优化的,可以跳过
  • 通过_getObjc2ProtocolRefs函数取出协议列表
  • 循环遍历通过remapProtocolRef进行重映射

remapProtocolRef 源码

/***********************************************************************
* remapProtocolRef
* Fix up a protocol ref, in case the protocol referenced has been reallocated.
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
static size_t UnfixedProtocolReferences;
static void remapProtocolRef(protocol_t **protoref)
{
    runtimeLock.assertLocked();

    protocol_t *newproto = remapProtocol((protocol_ref_t)*protoref);
    if (*protoref != newproto) {
        *protoref = newproto;
        UnfixedProtocolReferences++;
    }
}

remapProtocol 源码:

/***********************************************************************
* remapProtocol
* Returns the live protocol pointer for proto, which may be pointing to 
* a protocol struct that has been reallocated.
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
static ALWAYS_INLINE protocol_t *remapProtocol(protocol_ref_t proto)
{
    runtimeLock.assertLocked();

    // Protocols in shared cache images have a canonical bit to mark that they
    // are the definition we should use
    if (((protocol_t *)proto)->isCanonical())
        return (protocol_t *)proto;

    protocol_t *newproto = (protocol_t *)
        getProtocol(((protocol_t *)proto)->mangledName);
    return newproto ? newproto : (protocol_t *)proto;
}

通过remapProtocolRefremapProtocol两个函数实现协议的重映射,以防被引用的协议被重新分配。

3.9 Discover categories.

Discover categories

这里是处理所有的分类

  • 首先获取一个是否包含类属性的boolhasClassProperties
  • 用了一种我看不懂的C++语法从镜像文件获取分类列表catlist
  • 然后遍历这个列表,首先拿到分类还有一个locstamped_category_t类型的结构体存储分类信息和header info
  • 判断cls是否存为空,为空则continue,很好理解,类都不存在就不可能存在分类
  • 对于存根类Stub classes)的处理,存根类在初始化之前并不知道它们的元类,因此我们必须向存根本身添加带有类方法或属性的类别。在调用methodizeClass函数的时候将找到它们,并将它们适当地添加到元类中
  • 接下来就是正常分类的存储了,首先判断是否有对象方法协议实例属性,然后判断类是否实现了(isRealized),如果实现了将这些附加到类中,否则将调用addForClass进行处理。
  • 然后判断是否有类方法协议类属性&&hasClassProperties,将这些附加到元类中,然后判断元类是否实现了(isRealized),如果实现了将这些附加到类中,否则将调用addForClass进行处理。

我的另一篇文章iOS 分类的加载会详细的介绍分类的加载过程,以及上述方法的调用时机。

3.10 Realize non-lazy classes

Realize non-lazy classes.jpg

这里是实现非懒加载类的加载,一般实现了+load方法和一些静态实例会在此处处理

  • 首先还是循环遍历EACH_HEADER,并对每一个镜像文件通过_getObjc2NonlazyClassList函数获取类列表。
  • 遍历这个类列表
    • 调用remapClass获取到类信息
    • 非空判断
    • 调用addClassTableEntry函数将类添加到allocatedClasses表中
    • 如果是Swift类并且是Swift metadata initializer(元数据的初始化),则打印报错信息,因为这样的类不允许是非懒加载的
    • 最后调用realizeClassWithoutSwift函数

3.10.1 realizeClassWithoutSwift

那么realizeClassWithoutSwift函数具体做了什么呢?点进去一探究竟。又是将近130多行的代码,我们一步一步的分析:

realizeClassWithoutSwift1.jpg

根据注释的意思,我们可以知道这里是对类的首次初始化操作,包括分配ro、rw,不包含任何的Swift类的初始化,最后返回类的真实类结构。

realizeClassWithoutSwift2.jpg

这里就很简单了,加锁、初始化一些变量,对类进行非空和已经初始化的判断。

realizeClassWithoutSwift3.jpg
  • 这里是通过判断是否是future的类来进行处理,一般都会进入到else进行处理。
  • 如果是future类则直接给rw赋值,给ro赋值,调用changeInfo函数,修改类信息(一般不会进入该分支)
  • 如果不是则给rw初始化存储空间,给rwro赋值,设置rwflag,调用setData函数初始化一些类信息。
realizeClassWithoutSwift4.jpg
  • 这里就是处理了一些是元类时的信息
  • 选择一个类索引,如果没有就调用chooseClassArrayIndex函数进行设置
  • 打印一些信息
realizeClassWithoutSwift5.jpg

拿到supercls父类)和metacls元类

realizeClassWithoutSwift6.jpg

这里就是对nonPointerIsa的一些处理

  • 首先判断是否是元类,如果是则调用setInstancesRequireRawIsa处理
  • 如果不是元类则进一步处理
realizeClassWithoutSwift7.jpg
  • 设置该类的父类和元类,元类其实就是类的isa指向的地方
  • 如果是父类,并且不是元类则调用reconcileInstanceVariables函数对ro进行更新
  • 如果还没有设置fastInstanceSize则设置它
  • 拷贝一些flagsrorw
  • 对一些flags进行传递,从ro或者superclass父类
  • 给父类添加子类列表,如果没有父类则标记该类为root
  • 最后调用methodizeClass,这里面注释是Attach categories,那么是不是附加分类的东西呢?我们需要进一步探索。

小结:
realizeClassWithoutSwift就是初始化Objective-C的类信息,

  1. 初始化ro,以及rw的部分信息,
  2. 赋值类的父类和元类
  3. 赋值类的子类
  4. 调用methodizeClass函数

3.10.2 methodizeClass

methodizeClass函数大约 有70多行代码,我们同样进行一步一步的分析:

methodizeClass1.jpg

根据注释我们可以知道methodizeClass函数的功能是完善cls的方法列表,协议列表和属性列表以及对分类的附加。

methodizeClass2.jpg

首先还是加锁,初始化isMetalrwro以及打印一些信息。

methodizeClass3.jpg

ro中的methodpropertyprotocol取出来,然后attachListsrw中的methodspropertiesprotocols中。

methodizeClass4.jpg
  • 将分类附加到类上,对于元类和类需要传入不同的附加参数分别是ATTACH_METACLASSATTACH_CLASS_AND_METACLASS,如果是元类只附加类方法,如果是类则需要将对象方法附加到类上,将对象方法附加的类的isa即元类上。
  • 从类中取出未附加的类进行附加
  • debug环境打印信息

PS: 其实按照这个步骤不会走到分类附加这个if分支,只会调用分支下面那行代码,因为在realizeClassWithoutSwift(cls, nil);的时候previously就已经是nil了,所以该流程不会进行。那么为什么还要写这些代码呢?肯定还有其他调用会进行处理。我们通过全局搜索methodizeClass(发现该函数的调用处只有realizeClassWithoutSwift函数中,我们再次全局搜索realizeClassWithoutSwift(发现使previously不为空的地方只有_objc_realizeClassFromSwift函数,如下图:

_objc_realizeClassFromSwift.jpg

看来这个函数是处理Swift相关的,通过注释我们可以知道当该类是Swift类但是需要执行Objc的初始化。一个有四种情况:

  1. cls != nil; previously == cls,cls已经被实现
  2. cls != nil; previously == nil,cls是运行时创建的
  3. cls != nil; previously != cls,cls被重新alloc
  4. cls == nil, previously != nil,cls被取消

但是目前只支持前两种情况

我们尝试搜索_objc_realizeClassFromSwift的调用,发现并不在objc中,但是发现如下内容:

/** 
 * Perform Objective-C initialization of a Swift class.
 * Do not call this function. It is provided for the Swift runtime's use only 
 * and will change without notice or mercy.
 */
#if !(TARGET_OS_OSX && __i386__)
#define OBJC_REALIZECLASSFROMSWIFT_DEFINED 1
OBJC_EXPORT Class _Nullable
_objc_realizeClassFromSwift(Class _Nullable cls, void * _Nullable previously)
    OBJC_AVAILABLE(10.14.4, 12.2, 12.2, 5.2, 3.2);
#endif

译: 在Objc中不要调用此函数,它仅供Swift运行时调用,并将在不通知或允许的情况下进行更改。说明这是Swift运行时和OC交互的一个函数,在这里我们就不进一步探索了

既然分支调用不到,那么我们看看其下面那行代码调用的函数究竟实现了什么。

attachToClass 源码分析

void attachToClass(Class cls, Class previously, int flags)
    {
        runtimeLock.assertLocked();
        ASSERT((flags & ATTACH_CLASS) ||
               (flags & ATTACH_METACLASS) ||
               (flags & ATTACH_CLASS_AND_METACLASS));

        auto &map = get();
        auto it = map.find(previously);

        if (it != map.end()) {
            category_list &list = it->second;
            if (flags & ATTACH_CLASS_AND_METACLASS) {
                int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
                attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
                attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
            } else {
                attachCategories(cls, list.array(), list.count(), flags);
            }
            map.erase(it);
        }
    }

attachToClass函数主要是将分类列表通过调用attachCategories附加到类上,attachCategories的具体分析将会放到我的另一篇文章iOS Objective-C 分类的加载

小结:

methodizeClass的主要功能就是完善类信息:

  1. ro中的methodattach到rwmethods
  2. ro中的propertyattach到rwproperties
  3. ro中的protocolattach到rwprotocols
  4. 对于分类相关的信息通过attachToClass进行进一步处理

attachLists 初步探索:
我们看到在上面的函数中最后都是调用attachListsro中的方法、属性以及协议赋值到rw中,那么为什么都会用到这个方法呢?
我们点击查看method_list_tproperty_list_tprotocol_list_t结果如下:

attachLists.jpg

我们看到method_list_tivar_list_tproperty_list_t都是继承自entsize_list_ttprotocol_list_t虽然没有继承但也一样是个二维数组的结构,我们来到attachLists函数看看。

attachLists源码:

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]));
        }
    }
  • 判断要添加的数量是否为 0,如果为 0,直接返回

  • 判断当前调用 attachLists 的 list_array_tt 二维数组有多个一维数组

    • 如果是,说明是多对多的关系
    • 这里会通过 realloc 对容器进行重新分配,大小为原来的大小加上新增的大小
    • 然后通过 memmove 把原来的数据移动到容器的末尾
    • 最后把新的数据拷贝到容器的起始位置
  • 如果调用 attachLists 的 list_array_tt 二维数组为空且新增大小数目为 1,则直接取 addedList 的第一个 list 返回

  • 如果当前调用 attachLists 的 list_array_tt 二维数组只有一个一维数组

    • 如果是,说明是一对多的关系
    • 这里会通过 realloc 对容器进行重新分配,大小为原来的大小加上新增的大小
    • 因为原来只有一个一维数组,所以直接赋值到新 Array 的最后一个位置
    • 然后把新数据拷贝到容器的起始位置

3.11 Realize newly-resolved future classes

Realize newly-resolved future classes.jpg

这里是初始化一些新解析出来的future类,应该就是我们在运行时添加的。这也是上面3.4 Discover classes我们处理future时,如果没有这样的类,这里也是不会来到的。如果有的话就会调用realizeClassWithoutSwift函数做进一步的处理,然后还会调用setInstancesRequireRawIsaRecursively函数,将这个类及其所有子类标记为需要原始isa指针。

3.12 realizeAllClasses

realizeAllClasses.jpg

这里是实现所有类,但是有个判断,当调试Non Fragile的时候才会这样做。

3.13 打印日志

打印日志.jpg

最后就是打印一些日志了,根据该函数加载完成的一些信息进行打印。

3.14 小结

至此_read_images的流程我们就大概缕清了,下面我们来总结一些核心流程。

  1. 首先doneOnce第一次进来的时候,处理了isa是否开启内存优化,初始化了gdb_objc_realized_classes存储所有类的哈希表。
  2. 方法编号SEL的处理,都注册到nameSelectors表中
  3. 类的处理,加载所有类到gdb_objc_realized_classes表中
  4. 对类进行重映射
  5. 修复旧的objc_msgSend_fixup调用
  6. 将所有的协议都添加到protocol_map表中
  7. 重映射所有协议
  8. 分类处理包括类和元类
  9. 非懒加载类的初始化,进行rw、ro等操作
  10. future类处理
  11. 当调试Non Fragile的时候,实现所有的类
  12. 打印日志

4. 探索 load_image

接下来我们探索_dyld_objc_notify_register的第二个参数load_images,我们在dyld源码中搜索load_images对应的sNotifyObjCInit

sNotifyObjCInit.jpg

我们可以看到在dyldnotifySingle方法内部sNotifyObjCInit函数指针被调用了。

下面我们来到objc源码中进一步探索load_images

load_images 源码:

/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

这里是由dyld映射给定镜像中的+load方法。

  • 首先判断是否有+load方法,没有就直接返回,有的话就加锁进行处理
  • 调用prepare_load_methods函数加载包含+load方法的类和分类
  • 调用call_load_methods函数去实现+load方法的调用

4.1 prepare_load_methods 分析

prepare_load_methods 、schedule_class_load、add_class_to_loadable_list、add_category_to_loadable_list 源码:

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

    runtimeLock.assertLocked();

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

    category_t * const *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, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed 
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
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); 
}

// List of classes that need +load called (pending superclass +load)
// This list always has superclasses first because of the way it is constructed
static struct loadable_class *loadable_classes = nil;
static int loadable_classes_used = 0;
static int loadable_classes_allocated = 0;

// List of categories that need +load called (pending parent class +load)
static struct loadable_category *loadable_categories = nil;
static int loadable_categories_used = 0;
static int loadable_categories_allocated = 0;


/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

/***********************************************************************
* add_category_to_loadable_list
* Category cat's parent class exists and the category has been attached
* to its class. Schedule this category for +load after its parent class
* becomes connected and has its own +load method called.
**********************************************************************/
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    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++;
}

根据函数名字我们可以大概知道这里是关于实现+load方法类的预处理,其实就是对于懒加载类和懒加载分类的实现。

  • 首先初始化一些变量并加锁
  • 通过_getObjc2NonlazyClassList函数取出实现+load方法的类(非懒加载类)
  • 遍历这些类进行,通过schedule_class_load函数递归类的父类,保证父类的+load方法排在子类签名,在schedule_class_load函数中,通过调用add_class_to_loadable_list函数将类的+load方法存入loadable_classes
  • 接下来通过_getObjc2NonlazyCategoryList函数读取出实现了+load方法的分类(非懒加载分类)
  • 遍历这些分类,通过realizeClassWithoutSwift函数实现类,因为分类依托于类,类不存在分类也就不存在
  • 然后调用add_category_to_loadable_list函数将分类中的+load方法存入loadable_categories

4.2 call_load_methods 分析

call_load_methods 源码:

/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.
* Class +load methods are called superclass-first. 
* Category +load methods are not called until after the parent class's +load.
* 
* This method must be RE-ENTRANT, because a +load could trigger 
* more image mapping. In addition, the superclass-first ordering 
* must be preserved in the face of re-entrant calls. Therefore, 
* only the OUTERMOST call of this function will do anything, and 
* that call will handle all loadable classes, even those generated 
* while it was running.
*
* The sequence below preserves +load ordering in the face of 
* image loading during a +load, and make sure that no 
* +load method is forgotten because it was added during 
* a +load call.
* Sequence:
* 1. Repeatedly call class +loads until there aren't any more
* 2. Call category +loads ONCE.
* 3. Run more +loads if:
*    (a) there are more classes to load, OR
*    (b) there are some potential category +loads that have 
*        still never been attempted.
* Category +loads are only run once to ensure "parent class first" 
* ordering, even if a category +load triggers a new loadable class 
* and a new loadable category attached to that class. 
*
* Locking: loadMethodLock must be held by the caller 
*   All other locks must not be held.
**********************************************************************/
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

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

call_load_methods调用类和分类中的+load方法,分别通过call_class_loads函数和call_category_loads函数,根据prepare_load_methods函数添加的顺序,会优先调用父类的+load方法。

  • 通过objc_autoreleasePoolPush压栈一个自动释放池
  • 开始一个do while循环,条件是loadable_classes_used > 0 || more_categories
  • 遍历loadable_classes中的+load方法进行调用,直到找不到位置
  • 通过call_category_loads函数调用一次分类中的+load方法
  • 通过objc_autoreleasePoolPop出栈一个自动释放池

小结:

load_image主要是加载非懒加载的类和分类。

5. initialize

在开发过程中与+load作用差不多的就是+initialize,都是我们用来提前初始化一些数据方法。在上面我们知道了+load方法的调用时机,那么+initialize在哪里被调用的呢?

我们实现一个LGPerson的类,代码如下:


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject

@end

NS_ASSUME_NONNULL_END

#import "LGPerson.h"

@implementation LGPerson

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

+ (void)initialize
{
    NSLog(@"%s",__func__);
}
@end

我们在main函数中分别调用LGPersonalloc方法和class方法:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LGPerson *p = [LGPerson alloc];
//        Class cls = [LGPerson class];
    }
    return 0;
}

无论调用alloc还是class打印结果都是如下:

打印结果.jpg

所以说我们的initialize的调用并不在alloc或者class中,这个时候我们想initialize的调用很大概率是在发送消息的时候,因为allocclass都是方法,方法的本质就是消息的发送,iOS Objective-C 消息的查找所以我们来到lookUpImpOrForward,翻阅后找到如下代码,并设置如下断点。

lookUpImpOrForward.jpg

当先来到mian中我们调用LGPerson的断点时,在开启此断点,以便确定确实我们研究的是LGPerson类。断点会来两次,第一次是查找alloc方法,第二次是查找initialize方法,调用堆栈如下:

调用堆栈.jpg

当我们过掉第二次断点后就打印了initialize方法内的内容,并且我们可以通过调用堆栈验证我们的initialize是由initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass调起的。我们点击调用堆栈中的initializeNonMetaClass就可以直接找到调用initialize方法的地方,callInitialize如此的完美简单明了。

initializeNonMetaClass.jpg

callInitialize 源码:

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

callInitialize源码中我们也可以清晰的看到直接通过消息的发送调起了initialize

小结:

至此我们的initialize的调用就很清楚了,在我们第一次发送消息的时候就会调用initialize方法,这个就比+load方法好多了,类似于懒加载,用到类的时候在调用,而不像+load启动就会被调用。这也是Apple推荐的使用方式。尽量不用+load,使用initialize替代它。

6.相关知识点延伸

6.1 懒加载类的加载

首先我们来到lookUpImpOrForward函数,我们会看到这样一行代码:

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

if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
    cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    // runtimeLock may have been dropped but is now locked again

    // If sel == initialize, class_initialize will send +initialize and 
    // then the messenger will send +initialize again after this 
    // procedure finishes. Of course, if this is not being called 
    // from the messenger then it won't happen. 2778172
}

当第一次发送消息的时候,就会实现该类及其原类:

realizeClassMaybeSwiftAndLeaveLocked源码:

static Class
realizeClassMaybeSwiftAndLeaveLocked(Class cls, mutex_t& lock)
{
    return realizeClassMaybeSwiftMaybeRelock(cls, lock, true);
}

realizeClassMaybeSwiftMaybeRelock源码:

/***********************************************************************
* realizeClassMaybeSwift (MaybeRelock / AndUnlock / AndLeaveLocked)
* Realize a class that might be a Swift class.
* Returns the real class structure for the class. 
* Locking: 
*   runtimeLock must be held on entry
*   runtimeLock may be dropped during execution
*   ...AndUnlock function leaves runtimeLock unlocked on exit
*   ...AndLeaveLocked re-acquires runtimeLock if it was dropped
* This complication avoids repeated lock transitions in some cases.
**********************************************************************/
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, 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();
    }

    return cls;
}

通过realizeClassMaybeSwiftMaybeRelock函数我们可以知道,当不是Swift的类会调用realizeClassWithoutSwift函数,这就跟我们在3.10的时候介绍的一模一样了。

initializeAndLeaveLocked 源码:

// Locking: caller must hold runtimeLock; this may drop and re-acquire it
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
    return initializeAndMaybeRelock(cls, obj, lock, true);
}

initializeAndMaybeRelock 源码:

/***********************************************************************
* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
* inst is an instance of cls, or nil. Non-nil is better for performance.
* Returns the class pointer. If the class was unrealized then 
* it may be reallocated.
* Locking: 
*   runtimeLock must be held by the caller
*   This function may drop the lock.
*   On exit the lock is re-acquired or dropped as requested by leaveLocked.
**********************************************************************/
static Class initializeAndMaybeRelock(Class cls, id inst,
                                      mutex_t& lock, bool leaveLocked)
{
    lock.assertLocked();
    ASSERT(cls->isRealized());

    if (cls->isInitialized()) {
        if (!leaveLocked) lock.unlock();
        return cls;
    }

    // Find the non-meta class for cls, if it is not already one.
    // The +initialize message is sent to the non-meta class object.
    Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);

    // Realize the non-meta class if necessary.
    if (nonmeta->isRealized()) {
        // nonmeta is cls, which was already realized
        // OR nonmeta is distinct, but is already realized
        // - nothing else to do
        lock.unlock();
    } else {
        nonmeta = realizeClassMaybeSwiftAndUnlock(nonmeta, lock);
        // runtimeLock is now unlocked
        // fixme Swift can't relocate the class today,
        // but someday it will:
        cls = object_getClass(nonmeta);
    }

    // runtimeLock is now unlocked, for +initialize dispatch
    ASSERT(nonmeta->isRealized());
    initializeNonMetaClass(nonmeta);

    if (leaveLocked) runtimeLock.lock();
    return cls;
}

initializeAndMaybeRelock首先通过getMaybeUnrealizedNonMetaClass函数在clsinst中找出一个非元类,通过判断非元类有没有被实现,如果实现了就返回了,如果没实现则调用realizeClassMaybeSwiftAndUnlock进行实现处理,这就跟上面一样了

结论:对于懒加载的类,在第一次发送消息的时候进行实现,会同时实现它的元类和其本身

验证以上结论:

我们创建一个LGPerson类,实现sayClassFunc类方法和saySomething对象方法,添加一个testName属性。调用代码如下图,并设置断点:

main.jpg

来到上面断点后在lookUpImpOrForward函数中添加如下断点:

lookUpImpOrForward断点.jpg

过掉main函数中的断点后,来到上图所示断点,我们通过lldb进行打印,查看我们的类信息。

lldb.jpg

此时我们可以看到我们的rwmethods的list地址是0x00,说明懒加载类并没有初始化,如果初始化了应该是个实际的地址,其实ro里面也没有值,我们过掉这行代码再次进行lldb查看:

lldb.jpg

此时我们看到了sayClassFunc,说明cls是元类,这里对元类进行了初始化。

下面我们来看看类:

lldb.jpg

如上图箭头所示,我们的rw中并没有方法和属性。并且根据内容可以看出,这些都是野指针,虽然不是000,但也不是真实数据,其实ro里面也没有值,这里就不上图了。我们过掉断点再次查看:

lldb ro 里面已经有值.jpg
lldb rw 里面也已经有值.jpg

经过上面两幅图我们可以看到,过掉断点后rorw都已经有值了,说明该处确实完成了懒加载类的初始化。

注!但是有一点需要注意,如果该类作为非懒加载类的父类,同样会在非懒加载类初始化的时候进行初始化操作。就是不会在发送消息的时候进行初始化了。即使该类是懒加载类。当该类有个非懒加载的分类也会提前加载,懒加载正所谓用到才加载,所以说对于懒加载类的加载并不止上面介绍的这一种加载方式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349