OC -- 类的加载(上)

284 阅读8分钟

APP加载流程探索之dyld 中,我们探讨了dyld动态链接的流程,当时使用的源码是dyld-852.2,里面是dyld3版本。

我们现在用dyld-941.5再来看一个小地方。运行项目,在ViewController中断点,bt一下,看栈信息:

图片.png

看到这里用的是dyld4,其中有个notifyObjCInit方法,到源码查找一下,发现是在RuntimeState::setObjCNotifiers方法里赋的值,查找setObjCNotifiers,发现它也是在_dyld_objc_notify_register方法中调用的,跟dyld3中是一样的。所以,从源码上看,dyld3和dyld4虽然有一点区别,但是整体的逻辑还是一样的。

_objc_init

APP加载流程探索之dyld 中说到,_dyld_objc_notify_register有三个参数:

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

那这三个参数具体做了什么呢?在探讨这个问题之前,我们先看看_objc_init方法里都有啥。

我们在_objc_init方法里看到,在调用_dyld_objc_notify_register之前,还有一堆带有“init”的方法,我们看看它们是干什么的。

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
}

environ_init

这个方法主要是做环境变量的初始化。

我们首先在Xcode中打开项目的Edit Scheme:

图片.png

在跳出来的弹框中,添加两个参数,OBJC_PRINT_OPTIONS和OBJC_HELP,value都设置为YES:

图片.png

之后再运行项目时,environ_init方法就能拿到这两个值,并赋值给PrintHelp和PrintOptions变量,项目就会打印这些环境变量。

图片.png

看看打印的结果:

图片.png

这里打印出了很多的环境变量,以及对应的说明。我们拿其中的一个变量OBJC_PRINT_LOAD_METHODS举例子,它后面的说明是”log calls to class and category +load methods“,就是说如果设置了OBJC_PRINT_LOAD_METHODS变量为YES,那么就会把项目中所有实现了+(void)load方法的类打印出来。我们来试一下,创建一个XXPerson类,实现load方法,结果打印如下:

图片.png

确实把XXPerson类打印出来了。

由于环境变量数量较多,就不一一解释了,这里有张环境变量列表:

0000000111.png

tls_init

这个方法是为每一个线程创建一个析构方法。 其实系统是为每一个线程都开辟了独立的空间,来存储线程的数据。这一部分的内容以后再介绍,本文先一笔带过。

static_init

图片.png

static_init()方法主要是去运行c++的静态构造函数,走到init()的时候会走到上面的构造函数function()。

_objc_init是在dyld调用静态构造方法之前,如果我们在自己写的类里面也添加这个function()方法,那么代码就会先走static_init()这里的function(),然后走我们自己的类中的load()方法,再走我们的类中的function()方法。

runtime_init

图片.png

runtime_init方法主要是创建两张表:一张是分类表,一张是类表。后面分类的加载和类的加载,就会分别往这两张表中添加数据

exception_init

这个方法主要是初始化libobjc的异常处理系统,比如数组越界、找不到方法实现等问题的报错,都是在这里初始化的。

图片.png

cache_t::init

初始化相关缓存。

_dyld_objc_notify_register

以上几个方法走完,就到了我们今天的重点了。因为第三个参数unmap_image是在库被删除时调用的,所以这里我们暂时不讲它。

load_images

点击进入load_images方法:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }

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

找到prepare_load_methods((const headerType *)mh),看注释Discover load methods,是要找load方法,点进去看看:

图片.png

这个方法主要是做两步工作:
1、找到非懒加载的类,记录下来
2、找到非懒加载的分类,记录下来

_getObjc2NonlazyClassList就是找到所有非懒加载类。

什么叫非懒加载?就是实现了+(void)load方法的类。找到所有非懒加载类,然后遍历,对每个类执行schedule_class_load(remapClass(classlist[i])); schedule_class_load方法内部会进行递归的调用,一直对参数中的类的父类进行递归调用。而且是先找到父类,再执行add_class_to_loadable_list方法。

图片.png

add_class_to_loadable_list是把类存到一个“可加载的类”的表中,所以如果一个类实现了+(void)load方法,那么它的继承链向上的那些父类都会被记录到这个表中,每个类存储的信息是load方法IMP

图片.png

在记录非懒加载的分类时,存到“可加载的类”的表中的是:分类load方法IMP

当prepare_load_methods执行完毕后,就走到call_load_methods方法了。这个方法做的事情很简单,就是:去上面说到的存储非懒加载类和分类的表中,
1、找到类,执行其load方法
2、找到分类,执行其load方法

至此,我们知道了,load_images方法主要是针对非懒加载类及其父类,和非懒加载分类的load进行了调用,它们调用的顺序是:父类子类分类

map_images

map_images方法里执行了map_images_nolock方法,点击进去,这个方法在第一次调用时会执行preopt_init方法,是关于共享缓存优化的处理。

图片.png

继续走,有个while方法,这个while主要做的是找到mach-o文件里面的所有的类。mach-o文件的头部有个配置文件,这个配置文件描述了mach-o文件里哪一部分是代码哪一部分是数据,dyld就是根据它来读取mach-o文件的。这段代码没必要细看,知道它的作用就行。

{
        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)":"");
            }
        }
    }

读取完所有的类以后,要执行两个方法:

图片.png

sel_init:初始化c++的构造函数和析构方法;
arr_init:自动释放池的初始化、散列表的初始化、关联对象的相关初始化

之后就会走_read_images方法,这个方法要重点看下。

_read_images方法的代码很长,我就挑着说了,首先看到这个方法里有很多的fix up的log:

ts.log("IMAGE TIMES: fix up selector references");
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");

这是因为现在使用的是虚拟内存,每次app启动时,可执行文件或者动态库的地址都是随机生成的,这个时候就需要dyld通过rebasebinding两步操作,来修复镜像中的资源指针,让其指向正确的地址。

继续看代码,第一次进入这个方法会默认打开指针优化:

DisableNonpointerIsa = true;

接着,根据环境变量,设置是否使用存储小对象的taggedPointer,taggedPointer会在以后专门详细讲解。

if (DisableTaggedPointers) {
    disableTaggedPointers();
}

之后调用NXCreateMapTable方法,这个方法会创建一个表,用来存放不在dyld共享缓存中的一些类。这个我们暂时不管。

再往下,这段代码是对所有方法的修复:

图片.png

看打印的信息:

图片.png

sels里的地址之所以是错的,原因也是我们上面提到的虚拟内存的问题。

接着是获取所有的要加载的类,逐个进行加载:

图片.png

readClass方法里,为了让代码断在我自己写的类XXTeacher中,我添加了一段代码:

图片.png

断点断在XXTeacher类时,一步一步往下调试,发现并没有对这个类的rw、ro等进行操作,而是停在了这一行:

图片.png

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

    if (!isKnownClass(cls))
        set.insert(cls);
    if (addMeta)
        addClassTableEntry(cls->ISA(), false);
}

我们看到allocatedClasses,它就是上面runtime_init方法创建的类表,addClassTableEntry方法就是将类存入这个类表中。这个方法是个递归方法,也会把类的元类存到这个表中。

再回到_read_images方法中,在将类存到表中以后,我们看到后面还有实现非懒加载类的方法,代码会走到这里:

图片.png

这个方法是真正的去给类创建rw空间,给类的data赋值,在之前的文章里也说过,rw是运行时创建的。

图片.png

上图中,我也让代码断在我自己写的XXTeacher类,所以我们可以查看一下这个类的数据是不是都存在了。

图片.png

图片.png

我们看到,确实拿到了XXTeacher类的属性和方法。

至此,我们今天的讲解先告一段落。这篇文章简单地介绍了load_images和map_images两个方法做了哪些事情。它们都是对非懒加载类和分类进行操作,至于懒加载类是怎么加载的,我们下一篇再介绍。