iOS 类的加载原理上

作者: 晨曦的简书 | 来源:发表于2021-07-17 18:28 被阅读0次

    类的加载原理:
    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 环境变量的初始化

    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



    在这里我们可以看到,_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;
                }
            }
        }
    

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

    readClass 分析


    readClass 函数的实现中我们可以看到,cls 我们刚传进来的时候是地址,但是返回出去之后会被关联类名,那么是哪一步做的关联呢,这里我们来断点看一下。
    首先我们打印 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);
    }
    

    相关文章

      网友评论

        本文标题:iOS 类的加载原理上

        本文链接:https://www.haomeiwen.com/subject/ajlspltx.html