OC底层原理探索—类的加载(1)

分析之前先引入一张图

image.png

dyld在进行类加载时是由map_imagesload——images这两大方法来进行加载的,而这两大方法是由objc_init->_dyld_objc_notify_register这两个流程方法进行加载的

objc_init()

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
  //读取影响运⾏时的环境变量。如果需要,还可以打印环境变量帮助
    environ_init();
// 关于线程key的绑定 - ⽐如每线程数据的析构函数
    tls_init();
//运⾏C ++静态构造函数。在dyld调⽤我们的静态构造函数之前,`libc` 会调⽤ _objc_init()
    static_init();
//runtime运⾏时环境初始化,⾥⾯主要是:unattachedCategories,allocatedClasses
    runtime_init();
// 初始化libobjc的异常处理系统
    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
}

由上述代码可知,在_objc_init方法中进行了一系列的初始化,而我们类的加载时存在_dyld_objc_notify_register(&map_images, load_images, unmap_image);这个方法中

_dyld_objc_notify_register dyld注册

应用程序加载时会调用objc_init(),当执行_dyld_objc_notify_register注册函数时,会将三个方法注册到dyld中。

void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped);
  • map_images:这里传入的是方法引用,也就是方法的实现地址。管理文件中和动态库中所有文件,如类class协议protocol方法selector分类category的实现。
  • load_images:该方法传入的是值,即方法实现。加载执行+load方法
  • unmap_image:dyld将image移除时,会触发该函数

【问题】由_dyld_objc_notify_register这个方法我们看到传入了两个方法_dyld_objc_notify_registerload_images,但是为什么map_images传入方法时为什么添加&这个符号呢?

1.是因为这个属于指针拷贝,这个地方需要内部调用的这个函数,与原函数同步发生变化。map_images此函数需要映射整个镜像,是一个比较耗时的过程,如果没有发生同步变化,会发生错乱
2.而load_images没有使用取地址符是因为,此方法只是简单的load方法的调用,没必要进行同步变化

read_images流程分析

map_images方法的主要作用是将Mach-O中的类信息加载到内存中,在进入到map_images通过一步步探索最终来到_read_images方法
map_images -> map_images_nolock -> _read_images

read_images主要进行了以下的操作
  • 1: 条件控制进⾏⼀次的加载
  • 2: 修复预编译阶段的 @selector 的混乱问题
  • 3: 错误混乱的类处理
  • 4:修复重映射⼀些没有被镜像⽂件加载进来的类
  • 5: 修复⼀些消息!
  • 6: 当我们类⾥⾯有协议的时候 :readProtocol
  • 7: 修复没有被加载的协议
  • 8: 分类处理
  • 9: 类的加载处理
  • 10 : 没有被处理的类 优化那些被侵犯的类

接下来我们着重的分析几个重点流程

1.主要流程分析

1: 条件控制进⾏⼀次的加载

doneOnceNo时,进入if语句,紧接着将doneOnce变为yes,所以这个判断只进入一次

if (!doneOnce) {

        doneOnce = YES;
        launchTime = YES;
        ....省略部分代码
        int namedClassesSize = 

            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;

        gdb_objc_realized_classes =

            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
 }

这里会创建一个哈希表gdb_objc_realized_classes,所有的类将放入这个表中,目的是方便快捷查找类。gdb_objc_realized_classes是命名类并且不在dyld共享缓存中,无论是否实现。

2.修复预编译阶段的 @ selector的混乱问题
    // sel 名字 + 地址
    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名字可能相同,但是地址会出现不同,这个时候需要统一进行修复


image.png

【问题】为什么相同的类,方法名相同,但是方法的地址不同,按理说,方法名与方法的地址应该都相同。

假设我们在系统中按照Foundation-> CoreFoundation->AVFoundation这个顺序加载

【答案】是因为我么整个系统中会存在多个库,例如:Foundation 、CoreFoundation等,每个框架中的每个类基本都会存在retain方法,当执行该方法时,需要将方法平移到程序出口的位置执行,Foundation框架中的ratain方法,位置为0,CoreFoundation位置则为CoreFoundation + 0的大小,因此方法地址的不同,方法需要平移调整

3、错误混乱的类处理

这里是从Mach_O中取出所有的类进行遍历

    for (EACH_HEADER) {
        if (! mustReadClasses(hi, hasDyldRoots)) {
            // Image is sufficiently optimized that we need not call readClass()
            continue;
        }
        //编译后从类列表中取出所有类,即从Mach-O中的__objc_classlist静态段中取出
        classref_t const *classlist = _getObjc2ClassList(hi, &count);

        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->hasPreoptimizedClasses();

        for (i = 0; i < count; i++) {
            //此时cls只是一个地址
            Class cls = (Class)classlist[I];
//            此时cls 地址+名字
            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;
            }
        }
    }
此时cls只是一个地址

此时cls是 地址+名称

由上图可知cls被赋予名称时通过readClass方法,接下来我们看下readClass方法

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{

    const char *mangledName = cls->nonlazyMangledName();
    //调试专用    
    if (strcmp(mangledName, "LGPerson") == 0)
    {
        printf("%s LGPerson....\n",__func__);
    }
    
    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->setSuperclass(nil);
        return nil;
    }
    
    cls->fixupBackwardDeployingStableSwift();

    Class replacing = nil;
    if (mangledName != nullptr) {
        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));

            // Manually set address-discriminated ptrauthed fields
            // so that newCls gets the correct signatures.
            newCls->setSuperclass(cls->getSuperclass());
            newCls->initIsa(cls->getIsa());

            rw->set_ro((class_ro_t *)newCls->data());
            newCls->setData(rw);
            freeIfMutable((char *)old_ro->getName());
            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(mangledName == nullptr || getClassExceptSomeSwift(mangledName));
    } else {
        if (mangledName) { //some Swift generic classes can lazily generate their names
            addNamedClass(cls, mangledName, replacing);
        } else {
            Class meta = cls->ISA();
            const class_ro_t *metaRO = meta->bits.safe_ro();
            ASSERT(metaRO->getNonMetaclass() && "Metaclass with lazy name must have a pointer to the corresponding nonmetaclass.");
            ASSERT(metaRO->getNonMetaclass() == cls && "Metaclass nonmetaclass pointer must equal the original class.");
        }
        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;
}

  • 通过nonlazyMangledName获取类名
    const char *nonlazyMangledName() const {
        return bits.safe_ro()->getName();
    }

    const class_ro_t *safe_ro() const {
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) {
            // maybe_rw 是 rw
            return maybe_rw->ro();
        } else {
            // maybe_rw 实际上时ro
            return (class_ro_t *)maybe_rw;
        }
    }

这里获取非懒加载的类名如果rw中存在这从rw中取,反之从ro中获取

  • 通过addNamedClass方法,将当前类添加到已经创建好的gdb_objc_realized_classes哈希表,也就是read_Images第一步,doneOnce只加载一次时创建的哈希表,该表用于存放所有类
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 {
        //添加到gdb_objc_realized_classes哈希表
        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方法中的objc::allocatedClassesobjc_init()中runtime_init()方法中出现过,allocatedClasses.init进行内存中类的表创建。
static void
addClassTableEntry(Class cls, bool addMeta = true)
{
    runtimeLock.assertLocked();
    auto &set = objc::allocatedClasses.get();

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

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

所以综上所述,readClass的主要作用就是将Mach-O中的类读取到内存,即插入表中,但是目前的类仅有两个信息:地址以及名称,而mach-O的其中的data数据还未读取出来,所以这个时候的cls可以认为只是一个空房子,房子里面的家具什么的还没有搬入进入(方法、属性、协议等)

4. 类的加载处理

    // +load handled by prepare_load_methods()

    // Realize non-lazy classes (for +load methods and static instances)
    for (EACH_HEADER) {
        classref_t const *classlist = hi->nlclslist(&count);
        for (i = 0; i < count; i++) {
            Class cls = remapClass(classlist[i]);
            if (!cls) continue;

            addClassTableEntry(cls);

            if (cls->isSwiftStable()) {
                if (cls->swiftMetadataInitializer()) {
                    _objc_fatal("Swift class %s with a metadata initializer "
                                "is not allowed to be non-lazy",
                                cls->nameForLogging());
                }
                // fixme also disallow relocatable classes
                // We can't disallow all Swift classes because of
                // classes like Swift.__EmptyArrayStorage
            }
            
            const char *mangledName = cls->nonlazyMangledName();
            if (strcmp(mangledName, "LGPerson") == 0)
            {
                printf("%s LGPerson....\n",__func__);
            }
            
            realizeClassWithoutSwift(cls, nil);
        }
    }

    ts.log("IMAGE TIMES: realize non-lazy classes");

在这个方法中,当我们类实现了load方法是,会进入以上方法,即 非懒加载,在这个方法中最重要的方法当属realizeClassWithoutSwift这个方法,此方法我们下一篇在讲解

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

推荐阅读更多精彩内容