iOS底层学习:类的加载(一)

前篇内容回顾应用程序的加载(二)

  • dyld在初始化主程序initializeMainExecutable()的时候会初始化所有的镜像文件doInitialization()
  • 在初始化动态库的时候会优先保证libSystem库的初始化。
  • libSystem库初始化的时候又会来到libobjc.A.dylib中的_objc_init()中。
  • 这样就从dyld中来到了runtime的重要程序_objc_init()
  • _objc_init()中完成了向dyld的几个重要函数的注册:_dyld_objc_notify_register(&map_images, load_images, unmap_image);
  • dyld完成初始化doInitialization()之后就会发送通知notifySingle(),进行下一步处理。

类的加载过程

现在看看objcdyld注册的几个函数的作用

  • map_images:主要是读取镜像文件,把类的相关信息加载到内存中。
  • load_imagesload方法的初始化调用。

map_images

map_images的调用时机就是在_dyld_objc_notify_register内部完成函数注册后。

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
    // record functions to call
    sNotifyObjCMapped   = mapped;
    sNotifyObjCInit     = init;
    sNotifyObjCUnmapped = unmapped;

    // call 'mapped' function with all images mapped so far
    try {
        notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
    }
}

map_images函数内部最重要的一步就是读取镜像文件:

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

_read_images

这一步,主要是读取加载类的信息,按照官方文档注释主要有一下几个步骤:
1、初始化参数配置。
2、@selector方法读取
3、类的读取
4、修复重映射一些没有被镜像文件加载进来的类
5、一些特定消息函数指针修改
6、类协议读取
7、修复没有被加载的协议
8、分类处理
9、类的加载处理
10、没有被处理的类,优化那些被破坏的类

1.初始化参数配置

这里有targetpointer对象的初始化。关于targetpointer参考WWDC2020 Class数据结构变化
然后就是创建NXMapTable,这个表是用来存放类的。方便将来的快速查找用。

if (!doneOnce) {
  doneOnce = YES;
  launchTime = YES;
  initializeTaggedPointerObfuscator();
  int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
}
2.@selector方法读取

实际上是把这些方法用namedSelectors这个表存放起来。

static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 
{
    SEL result = 0;

    if (shouldLock) selLock.assertUnlocked();
    else selLock.assertLocked();

    if (!name) return (SEL)0;

    result = search_builtins(name);
    if (result) return result;
    
    conditional_mutex_locker_t lock(selLock, shouldLock);
    auto it = namedSelectors.get().insert(name);
    if (it.second) {
        // No match. Insert.
        *it.first = (const char *)sel_alloc(name, copy);
    }
    return (SEL)*it.first;
}
3.类的读取

这里主要的一点就是readClass(),在这之前 cls只是一个指针地址,还没有和类进行关联。经过了readClass()之后,才能正式关联到类。

    // Discover classes. Fix up unresolved future classes. Mark bundle classes.
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();

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

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized){
  if (missingWeakSuperclass(cls)) {...}
  if (Class newCls = popFutureNamedClass(mangledName)) {...}
   if (headerIsPreoptimized  &&  !replacing) {…}
   else {
        addNamedClass(cls, mangledName, replacing);
        addClassTableEntry(cls);
    }
}

readclass基本思路就是:

  • 找不到该类的父类,可能weak-linked,直接返回nil;
  • 找到类了,类是否是一个future class(简单理解为将来可能改变的类),如果有变化则创建新类,并把旧类的数据拷贝一份然后赋值给新类newCls,然后调用addRemappedClass进行重映射,用新的类替换掉旧的类,并返回新类newCls的地址
  • 找到类了,如果类没有任何变化,则不进行任何操作,直接返回class
  • addNamedClass()已经读取完成的类,会被存放到了这个表gdb_objc_realized_classes里面
  • addClassTableEntry ()里面会把这个读取完成的类添加到allocatedClasses表里面,然后再判断addMeta是否为YES,然后会把这个类的元类也添加到allocatedClasses这个表里面。这是一个递归调用过程,只有这个类第一次进来的时候addMeta为真,之后的递归调用addMeta为假,保证了元类只被添加一次。
4.修复重映射一些没有被镜像文件加载进来的类
  • fix up重映射的类
  • 类列表和nolazy类列表保持未映射
  • 类引用和super引用将重新映射用于进行消息分发
    if (!noClassesRemapped()) {
        for (EACH_HEADER) {
            Class *classrefs = _getObjc2ClassRefs(hi, &count);
            for (i = 0; i < count; i++) {
                remapClassRef(&classrefs[i]);
            }
            // fixme why doesn't test future1 catch the absence of this?
            classrefs = _getObjc2SuperRefs(hi, &count);
            for (i = 0; i < count; i++) {
                remapClassRef(&classrefs[i]);
            }
        }
    }
5.一些特定消息函数指针修改
static void 
fixupMessageRef(message_ref_t *msg)
{    
    msg->sel = sel_registerName((const char *)msg->sel);

    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == @selector(alloc)) {
            msg->imp = (IMP)&objc_alloc;
        } else if (msg->sel == @selector(allocWithZone:)) {
            msg->imp = (IMP)&objc_allocWithZone;
        } else if (msg->sel == @selector(retain)) {
            msg->imp = (IMP)&objc_retain;
        } else if (msg->sel == @selector(release)) {
            msg->imp = (IMP)&objc_release;
        } else if (msg->sel == @selector(autorelease)) {
            msg->imp = (IMP)&objc_autorelease;
        } else {
            msg->imp = &objc_msgSend_fixedup;
        }
    } 
    else if (msg->imp == &objc_msgSendSuper2_fixup) { 
        msg->imp = &objc_msgSendSuper2_fixedup;
    } 
    else if (msg->imp == &objc_msgSend_stret_fixup) { 
        msg->imp = &objc_msgSend_stret_fixedup;
    } 
    else if (msg->imp == &objc_msgSendSuper2_stret_fixup) { 
        msg->imp = &objc_msgSendSuper2_stret_fixedup;
    } 
#if defined(__i386__)  ||  defined(__x86_64__)
    else if (msg->imp == &objc_msgSend_fpret_fixup) { 
        msg->imp = &objc_msgSend_fpret_fixedup;
    } 
#endif
#if defined(__x86_64__)
    else if (msg->imp == &objc_msgSend_fp2ret_fixup) { 
        msg->imp = &objc_msgSend_fp2ret_fixedup;
    } 
#endif
}

这里对一些特殊的消息的函数地址进行了修改,比如调用alloc方法的时候,虽然源码流程没有objc_alloc但是实际上alloc的实现地址被指向了objc_alloc
这里需要修改的方法多是系统自己的一些方法,从mac-o的静态段__objc_msgrefs中获取的。

6.类协议读取

主要还是看readProtocol()

    for (EACH_HEADER) {
        extern objc_class OBJC_CLASS_$_Protocol;
        Class cls = (Class)&OBJC_CLASS_$_Protocol;
        ASSERT(cls);
        NXMapTable *protocol_map = protocols();
        bool isPreoptimized = hi->hasPreoptimizedProtocols();
        if (launchTime && isPreoptimized && cacheSupportsProtocolRoots) {
            if (PrintProtocols) {
                _objc_inform("PROTOCOLS: Skipping reading protocols in image: %s",
                             hi->fname());
            }
            continue;
        }
        bool isBundle = hi->isBundle();
        protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
        for (i = 0; i < count; i++) {
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
        }
    }

读取类的协议,基本流程就是把读取的协议加入到protocol_map表中。

7. remapProtocolRef
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++;
    }
}
8.读取分类
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }
9.nolayz类的加载

虽然前面已经读取过类,但是正常情况下并没完全加载类的信息,这里才是正常加载类的信息。

for (EACH_HEADER) {
    …
        addClassTableEntry(cls);
        realizeClassWithoutSwift(cls, nil);
    …
    }
10.newly-resolved future classes处理

如果是一些被改动过的类,会被重新创建并加载到内存中并实现。

    if (resolvedFutureClasses) {
        for (i = 0; i < resolvedFutureClassCount; i++) {
            Class cls = resolvedFutureClasses[i];
            if (cls->isSwiftStable()) {
                _objc_fatal("Swift class is not allowed to be future");
            }
            realizeClassWithoutSwift(cls, nil);
            cls->setInstancesRequireRawIsaRecursively(false/*inherited*/);
        }
        free(resolvedFutureClasses);
    }

realizeClassWithoutSwift 类信息的加载

类能够使用的前提是类的信息已经正确加载到内存中,在发送消息的时候都会判断这个类是否已经实现了。
realizeClassWithoutSwift()的作用就是把类的信息加载到内存中。

  1. 读取类的data
    编译时类在内存中的位置就确定了,ro存放的类名称、方法、协议和实例变量的信息,是只读的。rw则是由于类的信息可能发生改变,比如添加属性、方法、添加协议,实际上绝大多数的类都不会改变,因此为了优化,出现了ro_or_rw_ext用于存放改变的信息。
    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro();
        ASSERT(!isMeta);
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = objc::zalloc<class_rw_t>();
        rw->set_ro(ro);
        rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
        cls->setData(rw);
    }
  …
    methodizeClass(cls, previously);
  1. 完成继承链的实现
    依照isa的继承关系递归调用realizeClassWithoutSwift()完成父类、元类的实现。
  2. 加载分类methodizeClass()
  • methodizeClass()ro中读取方法列表、属性列表、协议列表并添加到rwe中。
  • 添加分类的方法。
static void methodizeClass(Class cls, Class previously)
{
    runtimeLock.assertLocked();

    bool isMeta = cls->isMetaClass();
    auto rw = cls->data(); 
    auto ro = rw->ro();
    auto rwe = rw->ext();
    ...
    method_list_t *list = ro->baseMethods();//获取ro的baseMethods
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        if (rwe) rwe->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (rwe && proplist) {
        rwe->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (rwe && protolist) {
        rwe->protocols.attachLists(&protolist, 1);
    }
    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, @selector(initialize), (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
         objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
    ....
}

attachToClass 添加分类方法

methodlist内部添加分类的方法单独列出来。
分类的添加相对比较麻烦,因为分类中包含了属性、方法、协议,但没有成员变量。
同时也会初始化rwe

void attachToClass(Class cls, Class previously, int flags)
{
    runtimeLock.assertLocked();
    ASSERT((flags & ATTACH_CLASS) ||
           (flags & ATTACH_METACLASS) ||
           (flags & ATTACH_CLASS_AND_METACLASS));
    const char *mangledName  = cls->mangledName();
    const char *LGPersonName = "LGPerson";   
    
    auto &map = get();
    auto it = map.find(previously);

    if (it != map.end()) {
        category_list &list = it->second;
        if (flags & ATTACH_CLASS_AND_METACLASS) {
            int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
            attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
            attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
        } else {
            attachCategories(cls, list.array(), list.count(), flags);
        }
        map.erase(it);
    }
}

关于attachLists()

以添加分类的方法为例,方法在类的存储结构中是以二维数组的形式存在,本类的方法列表和分类的方法列表都是作为二维数组中的一维数组元素。
每当有新的分类的list要添加到类的信息中时,会直接先重新开辟新的二维数组,大小为之前二维数组的容量加新分类list,然后把新的list放在扩容后的二维数组前面,旧的list顺位向后内存平移。
这样一来,后添加的分类的方法总是排在类的方法列表的前面,那么类在发送消息的时候如果出现分类方法和本类方法重名的情况,总会调用位置靠前的分类方法,造成了一种分类方法覆盖了本类方法的错觉,实际上本类方法依然还在,但是没机会调用而已。

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