iOS底层原理14:类的加载原理(上)| 8月更文挑战

这是我参与8月更文挑战的第3天,活动详情查看: 8月更文挑战

在之前应用程序加载的文章中我们梳理了dyld的加载流程,接下来我们以_objc_init为入口,详细分析代码逻辑;

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?
    // 环境变量的初始化 终端打开环境变量帮助 export OBJC_HELP=1
    environ_init();
    // 关于线程key的绑定,比如线程数据的析构函数
    tls_init();
    // 系统级C++全局静态函数调用,在dyld调用我们的静态构造函数之前,lldb会调用_objc_init,因此我们必须自己做
    static_init();
    // 运行时环境初始化
    runtime_init();
    // 异常处理系统初始化
    exception_init();
#if __OBJC2__
    // 缓存条件初始化 只在OBjective-C++ 2.0版本中生效
    cache_t::init();
#endif
    // 启动回调机制,通常不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待的加载tramplienes dylib
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}
复制代码
  • environ_init():环境变量的初始化 终端打开环境变量帮助指令为: export OBJC_HELP=1;
  • tls_init():关于线程key的绑定,比如线程数据的析构函数;
  • static_init():系统级C++全局静态函数调用,在dyld调用我们的静态构造函数之前,lldb会调用_objc_init,因此我们必须自己做;
  • runtime_init():运行时环境初始化;
  • exception_init():异常处理系统初始化;
  • cache_t::init():缓存条件初始化 只在OBjective-C++ 2.0版本中生效;
  • _imp_implementationWithBlock_init:启动回调机制,通常不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待的加载tramplienes dylib;

environ_init

主要是运行时环境变量的一些初始化操作,这些环境变量可以在调试的时候给我们提供帮助,源码如下:

其中核心代码是最后的for循环,将其抽出,我们看一下它的打印结果:

该方法会打印出我们可能会用到的一些环境变量,这些环境变量会在我们开发进行调试时提供帮助;

除了循环打印外,我们可以在终端通过export OBJC_HELP=1来打印环境变量:

这些环境变量,均可以通过target -- Edit Scheme -- Run --Arguments -- Environment Variables中进行配置;

我们以OBJC_DISABLE_NONPOINTER_ISA为例:

环境变量设置之前,Person对象的isa

设置环境变量:

查看Person对象的isa

OBJC_DISABLE_NONPOINTER_ISA可以控制是否是nonpointer_isa

我们再来尝试一下OBJC_PRINT_LOAD_METHODS这个环境变量:

运行程序:

OBJC_PRINT_LOAD_METHODS环境变了可以打印出项目中所有的load方法,为我们的优化提供思路(load方法会影响程序启动)

tls_init

线程key的绑定,比如线程数据的析构函数,源码如下:

static_init

系统级C++全局静态函数调用,在dyld调用我们的静态构造函数之前,llbc会调用_objc_init,因此我们必须自己做,源码如下:

在之前的分析中,我们分析出C++构造函数会在doInitialization->doModInitFunctions的流程中被调用,那么为什么此处还会自己在调用呢?根据注释我们知道,_objc_init会比dyld更早的调用objc自己的静态构造函数;因为dyld执行的过程中,需要以来一些底层库,为了能够让dyld及时执行,所以提前调用了C++构造函数

我们用代码验证一下,我们在_objc_init方法上边添加一个C++构造函数

断点执行源码程序:

继续执行:

可以看到C++构造函数确实是在此方法中被调用的;

runtime_init

运行时环境初始化,源码如下:

通过init方法,我们知道unattachedCategoriesallocatedClasses是两张表,后续会详细讲解;

exception_init

异常处理系统的初始化,源码如下:

_objc_terminate对应的实现为:

crash是指系统发生的一些异常的指令之后,发出的信号;当有crash发生时,会来到_objc_terminate方法,然后在uncaught_handler中抛出异常;

App层我们可以定义一个函数,将函数赋值给fn也就是uncaught_handler,这样,在App中我们就能通过这个函数接收跑出的异常,并进行处理;

crash处理的流程:

cache_t::init

缓存条件的初始化,源码如下:

_imp_implementationWithBlock_init

启动回调机制,通常不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待的加载tramplienes dylib,源码如下:

_dyld_objc_notify_register

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
复制代码

这里有个问题,为什么map_images前边加了&,而load_images方法前边你没有呢?

是因为load_images方法中的操作比较简单,只是调用了load方法,而map_images中的操作就相对复杂;&表示取地址,&map_images表示指针传递;这样做有什么好处呢?由于map_images需要映射镜像文件,操作比较耗时,指针传递能确保map_images调用保持同步;

map_images中调用了map_images_nolock,该方法主要实现如下:

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;

    // Perform first-time initialization if necessary.
    // This function is called before ordinary library initializers. 
    // fixme defer initialization until an objc-using image is found?
    if (firstTime) {
        // 相关初始化
        preopt_init();
    }

    if (PrintImages) {
        _objc_inform("IMAGES: processing %u newly-mapped images...\n", mhCount);
    }


    // Find all images with Objective-C metadata.
    // 查找所有带有Objective-C元数据的映像
    hCount = 0;

    // Count classes. Size various table based on the total.
    // 计算类的个数,取决于表的总大小
    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) {
                // no objc data in this entry
                continue;
            }
            
            if (mhdr->filetype == MH_EXECUTE) {
                // Size some data structures based on main executable's size
#if __OBJC2__
                // If dyld3 optimized the main executable, then there shouldn't
                // be any selrefs needed in the dynamic map so we can just init
                // to a 0 sized map
                if ( !hi->hasPreoptimizedSelectors() ) {
                  size_t count;
                  _getObjc2SelectorRefs(hi, &count);
                  selrefCount += count;
                  _getObjc2MessageRefs(hi, &count);
                  selrefCount += count;
                }
#else
                _getObjcSelectorRefs(hi, &selrefCount);
#endif
                
#if SUPPORT_GC_COMPAT
                // Halt if this is a GC app.
                if (shouldRejectGCApp(hi)) {
                    _objc_fatal_with_reason
                        (OBJC_EXIT_REASON_GC_NOT_SUPPORTED, 
                         OS_REASON_FLAG_CONSISTENT_FAILURE, 
                         "Objective-C garbage collection " 
                         "is no longer supported.");
                }
#endif
            }
            
            hList[hCount++] = hi;
            
            if (PrintImages) {
                _objc_inform("IMAGES: loading image for %s%s%s%s%s\n", 
                             hi->fname(),
                             mhdr->filetype == MH_BUNDLE ? " (bundle)" : "",
                             hi->info()->isReplacement() ? " (replacement)" : "",
                             hi->info()->hasCategoryClassProperties() ? " (has class properties)" : "",
                             hi->info()->optimizedByDyld()?" (preoptimized)":"");
            }
        }
    }

    // Perform one-time runtime initialization that must be deferred until 
    // the executable itself is found. This needs to be done before 
    // further initialization.
    // (The executable may not be present in this infoList if the 
    // executable does not contain Objective-C code but Objective-C 
    // is dynamically loaded later.
    if (firstTime) {
        sel_init(selrefCount);
        arr_init();

#if SUPPORT_GC_COMPAT
        // Reject any GC images linked to the main executable.
        // We already rejected the app itself above.
        // Images loaded after launch will be rejected by dyld.

        for (uint32_t i = 0; i < hCount; i++) {
            auto hi = hList[i];
            auto mh = hi->mhdr();
            if (mh->filetype != MH_EXECUTE  &&  shouldRejectGCImage(mh)) {
                _objc_fatal_with_reason
                    (OBJC_EXIT_REASON_GC_NOT_SUPPORTED, 
                     OS_REASON_FLAG_CONSISTENT_FAILURE, 
                     "%s requires Objective-C garbage collection "
                     "which is no longer supported.", hi->fname());
            }
        }
#endif

#if TARGET_OS_OSX
        // Disable +initialize fork safety if the app is too old (< 10.13).
        // Disable +initialize fork safety if the app has a
        //   __DATA,__objc_fork_ok section.

//        if (!dyld_program_sdk_at_least(dyld_platform_version_macOS_10_13)) {
//            DisableInitializeForkSafety = true;
//            if (PrintInitializing) {
//                _objc_inform("INITIALIZE: disabling +initialize fork "
//                             "safety enforcement because the app is "
//                             "too old.)");
//            }
//        }

        for (uint32_t i = 0; i < hCount; i++) {
            auto hi = hList[i];
            auto mh = hi->mhdr();
            if (mh->filetype != MH_EXECUTE) continue;
            unsigned long size;
            if (getsectiondata(hi->mhdr(), "__DATA", "__objc_fork_ok", &size)) {
                DisableInitializeForkSafety = true;
                if (PrintInitializing) {
                    _objc_inform("INITIALIZE: disabling +initialize fork "
                                 "safety enforcement because the app has "
                                 "a __DATA,__objc_fork_ok section");
                }
            }
            break;  // assume only one MH_EXECUTE image
        }
#endif

    }

    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]);
        }
    }
}
复制代码
  • preopt_init():初始化相关环境;
  • hCount:存放所有带有Objective-C元数据的镜像的个数,其在while循环时的hList[hCount++] = hi进行hCount++赋值操作;
  • totalClasses:存放的个数,的个数取决于的总大小;
  • _read_images:加载镜像文件;
  • loadImageFuncs:所有工作准备就绪之后,调用镜像文件加载功能;

那么在这里,镜像文件究竟是如何被加载的呢?其核心是方法

_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
复制代码

read_image流程(重点)

此方法看上去较为复杂,那么我们将所有的判断分支关闭,来整体看一下这个函数:

如此一看,就比较清晰了,里边有很多log信息,根据log信息大致分析出_read_images函数的主要流程:

  • 1.条件控制进行一次的加载
  • 2.修复预编译阶段的@selector的混乱问题
  • 3.错误混乱的类处理
  • 4.修复重映射一些没有被镜像文件加载进来的类
  • 5.修复一些消息
  • 6.当类中有协议的时候的处理:readProtocol
  • 7.修复没有被加载的协议
  • 8.分类的处理
  • 9.类的加载处理
  • 10.没有被处理的类,优化那些被删除没有回收的类(未来类)

下边我们逐步分析其流程:

1.条件控制进行一次的加载

  • initializeTaggedPointerObfuscator:小对象类型,里边主要做了一些混淆的操作,不作为重点;
  • namedClassesSize:计算创建表格需要的大小,此处是之前cache中3/4的反向计算,计算容量
  • NXCreateMapTable:创建一张哈希表,用来存放类信息,表的大小为namedClassesSize;

通过NXCreateMapTable创建了一张哈希表,用来存放类信息

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.
// This list excludes lazily named classes, which have to be looked up
// using a getClass hook.
NXMapTable *gdb_objc_realized_classes;  // exported for debuggers in objc-gdb.h
复制代码

意思是,这个哈希表用来存储不在共享缓存且已命名的类,无论类是否实现,总容量是类的数量的4/3;

2.修复预编译阶段的@selector的混乱问题

修复@selector引用,sel是一串带地址的字符串

  • _getObjc2SelectorRefs: 通过_getObjc2SelectorRefs拿到MachO中的静态段__objc_selrefs;
  • sel_registerNameNoLock: 注册sel,将其添加到哈希表namedSelectors中;
  • sels[i] = sel:从MachO中读取的sel的地址不是真实的地址,需要重新赋值,以从dyld中读取的sel地址为准;

此代码块主要是通过_getObjc2SelectorRefs拿到MachO中的静态段__objc_selrefs,遍历列表,通过sel_registerNameNoLocksel添加到哈希表中,而sel_registerNameNoLock是从dyld中读取的数据;

_getObjc2SelectorRefs的源码如下:

GETSECT(_getObjc2SelectorRefs,        SEL,             "__objc_selrefs"); 
复制代码

通过它可以获取到MachO中的静态段__objc_selrefs;

发现同样的retain方法,两个地址却不一样,所以需要将地址重新赋值;

3.错误混乱的类处理(类有了名字和地址)

  • Class cls = (Class)classlist[i]:获取cls,此时cls只是一个地址;
  • readClass:读取类,在此之后cls才会真正的具备名字;

断点打印cls:

执行过readClass之后:

通过断点调试发现,经过readClass步骤之后,类有了地址名字;

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;
}
复制代码

这段代码经过断点发现,并不会执行;此处是处理一些未来类,什么是未来类呢?根据注释可以知道已经被移动过,应该删除却没有删除的类称为未来类,这部分类需要在此处进行处理;

4.修复重映射一些没有被镜像文件加载进来的类

主要是将未映射的ClassSuper Class进行重新映射:

  • _getObjc2ClassRefs用来获取MachO中静态段__objc_classrefs,即获取类的引用;
  • _getObjc2SuperRefs用来获取MachO中静态段__objc_superrefs,即获取父类的引用;

通过注释可知,被remapClassRef操作的类都是懒加载的类;

5.修复一些消息

  • _getObjc2MessageRefs:获取MachO的静态段__objc_msgrefs;
  • fixupMessageRef:将函数指针进行注册,并修复为新指针;

通过_getObjc2MessageRefs获取MachO中的__objc_msgrefs,遍历并通过fixupMessageRef将函数指针进行注册,并修复为新指针

6.当类中有协议的时候的处理:readProtocol

遍历所有协议列表,并且将协议列表加载到Protocol的哈希表中

  • Class cls = (Class)&OBJC_CLASS_$_Protocol;:cls = Protocol类,所有协议和对象的结构体都类似,isa都对应Protocol类
  • NXMapTable *protocol_map = protocols(); 创建protocol哈希表,表名为protocol_map
  • _getObjc2ProtocolList:通过_getObjc2ProtocolList获取到MachO中的静态段__objc_protolist协议列表,即从编译器中读取并初始化protocol
  • readProtocol:循环遍历,通过readProtocol方法将协议添加到protocol_map哈希表中

7.修复没有被加载的协议

  • _getObjc2ProtocolRefs:获取到MachO的静态段 __objc_protorefs
  • 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++;
    }
}
复制代码

主要通过_getObjc2ProtocolRefs获取MachO中的__objc_protorefs,然后遍历需要修复的协议,通过remapProtocolRef方法比较当前协议和协议列表中的同一个内存地址的协议是否相同,不同则替换;

8.分类的处理

主要用来处理分类,需要在分类初始化并将数据加载到类之后才能执行,对于运行时的分类,会在_dyld_objc_notify_register的执行完成之后的第一个load_images调用之后发现

9.类的加载处理(返回一个类的真实数据结构)

实现非懒加载的类,对于load方法和静态实例变量

nlclslist方法源码:

const classref_t *header_info::nlclslist(size_t *outCount) const
{
#if __OBJC2__
    // This field is new, so temporarily be resilient to the shared cache
    // not generating it
    if (isPreoptimized() && hasPreoptimizedSectionLookups()) {
          *outCount = nlclslist_count;
          const classref_t *list = (const classref_t *)(((intptr_t)&nlclslist_offset) + nlclslist_offset);
      #if DEBUG
          size_t debugCount;
          assert((list == _getObjc2NonlazyClassList(mhdr(), &debugCount)) && (*outCount == debugCount));
      #endif
          return list;
    }
    return _getObjc2NonlazyClassList(mhdr(), outCount);
#else
    return NULL;
#endif
}
复制代码

addClassTableEntry方法源码:

/***********************************************************************
* addClassTableEntry
* Add a class to the table of all classes. If addMeta is true,
* automatically adds the metaclass of the class as well.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
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.
    auto &set = objc::allocatedClasses.get();

    ASSERT(set.find(cls) == set.end());

    if (!isKnownClass(cls))
        set.insert(cls);
    if (addMeta)
        addClassTableEntry(cls->ISA(), false);
}
复制代码
  • 通过_getObjc2NonlazyClassList获取MachO的静态段__objc_nlclslist,即非懒加载类的表
  • addClassTableEntry(cls);将类及其元类插入表
  • realizeClassWithoutSwift对类进行初始化(第三步中只有名字和地址,data数据并没有被加载),分配读写数据(比如rw),返回一个类的真实结构

注意,此处操作的为非懒加载类

10.没有被处理的类,优化那些被删除没有回收的类(未来类)

  • realizeClassWithoutSwift:实现类
  • realizeAllClasses:实现所有类

通过对流程的梳理,发现其核心内容在第3步的readClass和第9步realizeClassWithoutSwift两个方法

猜你喜欢

转载自juejin.im/post/6992031760783507493