iOS 类的加载原理上

802 阅读4分钟

类的加载原理:
iOS 类的加载原理上
iOS 类的加载原理中
iOS 类的加载原理下
分类的加载原理补充及类扩展 , 关联对象介绍

我们在 iOS 应用程序加载流程分析 中介绍了 dyld,其中 dyld 在加载的过程中会做一件重要的事情就是链接镜像文件 images,但是这里只是映射过来,还只是一个库,还没有变为我们的内存。例如创建的一个 LGPerson 的类,里面有方法跟协议,但是只要加载到内存的时候,我们才能初始化它,并调用对应的方法跟协议,才能使用它。那么这里我们就要讲一个很重要的过程,就是类如何加载到内存中。镜像文件是一种 MachO 格式,也就是读取 MachO 文件并获取到类及相关方法对应的地址信息,并加载的内存中的过程。

_objc_init 分析

void _objc_init(void)
{
    // 相关变量的一些赋值
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // 环境变量的初始化
    environ_init();
    // 线程暂存缓存池的创建
    tls_init();
    // 全局静态 c++ 函数调用
    static_init();
    // unattachedCategories.init 跟 allocatedClasses 这两张表的初始化
    runtime_init();
    // 异常的监听
    exception_init();
#if __OBJC2__
    // 缓存条件初始化
    cache_t::init();
#endif
    // 启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    // map_images()
    // load_images()
#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

讲类的加载我们要从 map_images 开始讲起,但是 map_images 是被 _dyld_objc_notify_register 函数调起的,所以讲 map_images 之前先看下 在 _dyld_objc_notify_register 之前 _objc_init 方法中又做了哪些事情。下面我们来介绍一下。

environ_init 环境变量的初始化

image.png

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

static_init 全局静态 c++ 函数调用

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

这里需要注意的一点是,在我们前面也讲了,在 dyld 源码的 doModInitFunctions 方法中会自动调用全局的 c++ 静态函数。但是在 objc 源码里面这里比较特殊,objc 中的 c++ 静态函数不需要由 dyld 来调用,都是由 objc 自己负责调用。这主要是方便 objc 中所以的环境能及时的准备充分得当。这里已经开启了 runtime 的下层,所以写在 objc 中的 c++ 静态函要保证立即调用。

runtime_init

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

unattachedCategories.initallocatedClasses 这两张表的初始化。

exception_init 异常的监听

void exception_init(void)
{
    old_terminate = std::set_terminate(&_objc_terminate);
}

cache_t::init()

缓存条件初始化。

_imp_implementationWithBlock_init

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

read_images 主体流程

在以上事件完成以后会执行 _dyld_objc_notify_register(&map_images, load_images, unmap_image),这里我们先来看一下 map_images 函数。

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 函数中这里就两行代码,紧接着会执行 map_images_nolock 函数。

map_images_nolock

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
}

map_images_nolock 函数中我们只看跟镜像文件加载相关的代码。这里就来到了我们要探究的比较重要的一个函数 _read_images

_read_images

image.png image.png image.png

在这里我们可以看到,_read_images 函数中有三百多行代码,但是当我们把每段代码都折行收缩的后,可以看到每段代码折行都对应一个 ts.log,每个 ts.log 都对应不同的功能。总结了一下 _read_images 主要做了以下事情。

  • 1: 条件控制进行一次的加载
  • 2: 修复预编译阶段的 @selector 的混乱问题
  • 3: 错误混乱的类处理
  • 4:修复重映射一些没有被镜像文件加载进来的 类
  • 5: 修复一些消息!
  • 6: 当我们类里面有协议的时候 : readProtocol
  • 7: 修复没有被加载的协议
  • 8: 分类处理
  • 9: 类的加载处理
  • 10 : 没有被处理的类 优化那些被侵犯的类

这里我们对着代码看下其中几条。

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

 int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        // 表的创建,gdb_objc_realized_classes 是一张总表,不管类是否实现都在这张表中,前面讲的 runtime_init 中讲的 allocatedClasses 表中存放的是已经开辟过的类
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);

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

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

这里因为 SEL sel = sel_registerNameNoLock(name, isBundle) 是从当前文件中读取的,是通过 dyld 读取的。而 SEL *sels = _getObjc2SelectorRefs(hi, &count); 是从 MachO 文件中读取的,是虚拟地址,是变化的。所以要以 sel 为准。因为 SEL 除了函数名称以外还有函数地址。所以这里判断相同函数名的 selsels[i] 是否相等。如果不相等就进行修改 sels[i] = sel

3: 错误混乱的类处理

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

image.png

这里我们分别在 readClass(cls, headerIsBundle, headerIsPreoptimized) 前后 po cls,可以发现在 readClass(cls, headerIsBundle, headerIsPreoptimized)cls 只是从 MachO 文件中读取的地址,但是执行完 readClass 之后,cls 就被关联上了类名。那么 readClass 具体做了什么呢?这里我们来具体看一下。

readClass 分析

image.pngreadClass 函数的实现中我们可以看到,cls 我们刚传进来的时候是地址,但是返回出去之后会被关联类名,那么是哪一步做的关联呢,这里我们来断点看一下。

image.png 首先我们打印 mangledName 可以看到,会打印所有类的名称。在 LGPerson 之前都是系统的类,这里做一下判断,然后断点看一下。

步骤 1 步骤 2 步骤 3

通过断点我们可以看到,readClass 会在步骤 1 跟步骤 2 执行 addNamedClassaddClassTableEntry,下面会有这两个方法的介绍。函数会在步骤 3 返回 cls

addNamedClass

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

        // getMaybeUnrealizedNonMetaClass uses name lookups.
        // Classes not found by name lookup must be in the
        // secondary meta->nonmeta table.
        addNonMetaClass(cls);
    } else {
        // 这里会把 name 添加到 NXMapInsert 表中
        NXMapInsert(gdb_objc_realized_classes, name, cls);
    }
    ASSERT(!(cls->data()->flags & RO_META));

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

addClassTableEntry

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

    // This class is allowed to be a known class via the shared cache or via
    // data segments, but it is not allowed to be in the dynamic table already.
    auto &set = objc::allocatedClasses.get();

    ASSERT(set.find(cls) == set.end());
    // 判断当前类是否被知道,如果不被知道就把 cls 添加到要加载的表中
    if (!isKnownClass(cls))
        set.insert(cls);
    // 判断是否需要添加元类,如果需要的话递归调用 addClassTableEntry 方法,类的加载要同步把元类也要加载进来
    if (addMeta)
        addClassTableEntry(cls->ISA(), false);
}