OC類的加載流程

類的加載主要分如下幾個(gè)階段階段:1踩晶、類從鏡像文件映射到內(nèi)存中,并存儲(chǔ)到類表枕磁;2渡蜻、類結(jié)構(gòu)初始化,包括rw,ro等的setup茸苇;3排苍、加載類的properties、methedList学密、protocols淘衙、categorys。
那類是在程序運(yùn)行的哪個(gè)時(shí)機(jī)加載的腻暮?是如何加載的幔翰?類的相關(guān)方法、屬性西壮、協(xié)議、還有類別等都是什么時(shí)候加載的呢叫惊?

類的映射

類的映射時(shí)發(fā)生在程序啟動(dòng)過(guò)程中的款青。dyld啟動(dòng)工程中會(huì)調(diào)用libobjc庫(kù)的objc_init初始化這個(gè)庫(kù),此時(shí)函數(shù)objc_init除了做一些初始工作之外還會(huì)向dyld注冊(cè)監(jiān)聽(tīng)函數(shù)霍狰,以便在鏡像文件映射抡草、取消映射和初始化objc時(shí)調(diào)用。這其中就有map_images函數(shù)蔗坯,進(jìn)行鏡像文件映射時(shí)會(huì)調(diào)用map_images函數(shù)進(jìn)行OC類的映射康震。通過(guò)源碼分析類的映射函數(shù)調(diào)用流程大概是這樣的:
objc_init ->map_images -> map_images_nolock -> _read_images -> addNamedClass(cls, mangledName, replacing) -> addClassTableEntry(cls)

這其中我們逐個(gè)進(jìn)行講解宾濒。

objc_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();// static_init() Run C++ static constructor functions. libc calls _objc_init() before dyld would call our static constructors,  so we have to do it ourselves.
    runtime_init(); // 類表allocatedClasses.init()和unattachedCategories.init(32)看似跟category相關(guān)的表
    exception_init();//異常處理回調(diào)函數(shù)初始化
    cache_init();
    _imp_implementationWithBlock_init();
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

源碼解讀:

environ_init()讀取影響運(yùn)行時(shí)的環(huán)境變量腿短。如果需要,還可以打印環(huán)境變量幫助。
tls_init()關(guān)于線程key的綁定 - 比如每條線程數(shù)據(jù)的析構(gòu)函數(shù)。
static_init()有注釋可知運(yùn)行C++靜態(tài)析構(gòu)函數(shù)噩凹。在dyld調(diào)用我們的靜態(tài)構(gòu)造函數(shù)之前姊舵,libc會(huì)調(diào)用_objc_init(), 因此我們必須自己調(diào)用。
runtime_init() 類表allocatedClasses.init()和unattachedCategories.init(32)看似跟category相關(guān)的表隘截,這個(gè)在后面類的
imp_implementationWithBlock_init(void)啟動(dòng)回調(diào)機(jī)制。通常不會(huì)這么做。因?yàn)樗械某跏荚挾际嵌栊缘哪模菍?duì)于某些進(jìn)程,我們會(huì)迫不及待地加載trampolines dylib疹鳄。
_dyld_objc_notify_register向dyld注冊(cè)監(jiān)聽(tīng)函數(shù)拧略。以便在鏡像文件映射、取消映射和初始化objc時(shí)調(diào)用尚辑。Dyld將使用包含objc-image-info的鏡像文件的數(shù)組回調(diào)給map_images函數(shù)辑鲤。
runtime_init() 運(yùn)行時(shí)環(huán)境初始化,里面主要是類表allocatedClasses 和unattachedCategories
exception_init()初始化ibobjc的異常處理系統(tǒng)

map_images源碼解析

函數(shù)map_images是在dyld進(jìn)行鏡像文件映射時(shí)調(diào)用的回調(diào)函數(shù)杠茬,dyld將使用包含objc-image-info的鏡像文件的數(shù)組回調(diào)給map_images函數(shù)月褥,然后調(diào)用map_images_nolock完成接下來(lái)的事情:

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_nolock源碼解析

主要完成如下幾件事情:

  • 讀取鏡像文件
    通過(guò)函數(shù)addHeader讀取鏡像文件(mach-o)獲取文件頭部信息列表弛随,獲取含有Objective-C元數(shù)據(jù)(metadata)的鏡像文件列表hList和個(gè)數(shù)hCount,同時(shí)統(tǒng)計(jì)類的個(gè)數(shù)totalClasses宁赤。
const headerType *mhdr = (const headerType *)mhdrs[i];
            auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
            if (!hi) {
                // no objc data in this entry
                continue;
            }
            hList[hCount++] = hi;
  • 判斷是否第一次調(diào)用舀透,做一些初始化工作
  // Perform one-time runtime initialization that must be deferred until 
    // the executable itself is found. This needs to be done before 
    // further initialization.
    // (The executable may not be present in this infoList if the 
    // executable does not contain Objective-C code but Objective-C 
    // is dynamically loaded later.
    if (firstTime) {
        sel_init(selrefCount);
        arr_init();
        .......
    }
  • 將第一步獲取的hCount、hList[]和totalClasses傳遞給_read_images完成類的映射决左。
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
read_images源碼解析

read_images是加載類信息的主要函數(shù)愕够,看其函數(shù)聲明:

/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked 
* list beginning with headerList. 
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)

由于實(shí)現(xiàn)代碼比較多,這里根據(jù)其注釋整理了它的主要做的功能:

1佛猛、條件控制進(jìn)行一次的加載惑芭,如果是第一次加載會(huì)創(chuàng)建一個(gè)類名映射表gdb_objc_realized_classes
2继找、Fix up @selector references.修復(fù)預(yù)編譯階段的@selector的混亂問(wèn)題遂跟,比如地址偏移不一致等;
3婴渡、Discover classes. Fix up unresolved future classes. Mark bundle classes. 處理錯(cuò)誤混亂的類幻锁;
4、讀取類表边臼,調(diào)用readClass添加類名映射哄尔;
5、Fix up remapped classes.修復(fù)重映射一些沒(méi)有被鏡像文件加載進(jìn)來(lái)的類柠并;
6岭接、 Fix up old objc_msgSend_fixup call sites.修復(fù)一些消息;
7堂鲤、Discover protocols. Fix up protocol refs.當(dāng)我們類里面有的時(shí)候:readProtocol亿傅;
8、Fix up @protocol references.修復(fù)沒(méi)有被加載的協(xié)議瘟栖;
9葵擎、Discover categories. Only do this after the initial category attachment has been done. 分類處理;
10半哟、Realize non-lazy classes (for +load methods and static instances).懶加載類的加載處理酬滤;
11、Realize newly-resolved future classes, in case CF manipulates them.沒(méi)有被處理的類寓涨,優(yōu)化那些被非法操作的類盯串。
這其中這里要講的是1、3戒良、6体捏、8和9。其中6、8和9會(huì)在類的初始化過(guò)過(guò)程中分析几缭。

  • gdb_objc_realized_classes的創(chuàng)建
    在程序第一次進(jìn)來(lái)的時(shí)候會(huì)根據(jù)傳入的類的總數(shù)totalClasses創(chuàng)建一個(gè)表gdb_objc_realized_classes河泳,gdb_objc_realized_classes將存儲(chǔ)所有類和類名的映射,不管這個(gè)類是否實(shí)現(xiàn)年栓。
// namedClasses
        // Preoptimized classes don't go in this table.
        // 4/3 is NXMapTable's load factor
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
  • 讀取類表_getObjc2ClassList
    第4步中中拆挥,根據(jù)傳入的header_info **hList 通過(guò)_getObjc2ClassList遍歷讀取出OC類列表,然后把一個(gè)個(gè)Class交給readClass函數(shù)來(lái)處理:
    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源碼解析
    這一步主要讀取類名某抓,并通過(guò)函數(shù)addNamedClass將類名和類進(jìn)行映射纸兔,寫(xiě)入,然后調(diào)用addClassTableEntry把類添加到類表里頭否副。
      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);
  • addNamedClass源碼解析
    函數(shù)addNamedClass中實(shí)際上就是把類和類名通過(guò)NXMapInsert方法寫(xiě)入到我們之前提到的第一次進(jìn)來(lái)時(shí)創(chuàng)建的類名映射表gdb_objc_realized_classes里面:
/***********************************************************************
* addNamedClass
* Adds name => cls to the named non-meta class map.
* Warns about duplicate class names and keeps the old mapping.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
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 {
        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源碼解析
    函數(shù)addClassTableEntry實(shí)際上是將類注冊(cè)到所有類的類表allocatedClasses中汉矿。這個(gè)表不止有類,還包含元類备禀。這個(gè)表的初始化是在前面提到的_objc_init中調(diào)用runtime_init完成的负甸。
/***********************************************************************
* addClassTableEntry
* Add a class to the table of all classes. If addMeta is true,
* automatically adds the metaclass of the class as well.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
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);
}

至此加載結(jié)類就都緩存到了類表里面了。類存儲(chǔ)到類表之后就開(kāi)始進(jìn)行類的初始化痹届。

類的初始化

類的初始化是在函數(shù)realizeClassWithoutSwift中實(shí)現(xiàn)的。在_readImages完成類的映射之后打月,接著就調(diào)用了realizeClassWithoutSwift進(jìn)行初始化:

    // +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
            }
            realizeClassWithoutSwift(cls, nil);
        }
    }

在這段代碼中我們發(fā)現(xiàn)它是通過(guò)讀取nlclslist(&count)表來(lái)獲取classlist的

classref_t const *classlist = hi->nlclslist(&count);

這與我們之前讀取_getObjc2ClassList表不一樣队腐。這又是為什么呢?通過(guò)上面的注釋“Realize non-lazy classes (for +load methods and static instances)”我們發(fā)現(xiàn)原來(lái)這里面實(shí)現(xiàn)的是non-lazy classes奏篙,翻譯過(guò)來(lái)就是懶加載類柴淘。那什么是懶加載類?注釋里也說(shuō)了秘通,就是實(shí)現(xiàn)了+load方法或者靜態(tài)實(shí)例變量为严。這里我們就明白了,這里的nlclslist其實(shí)讀取的是懶加載類的表肺稀。有了懶加載類就有非懶加載類第股,那根據(jù)注釋的意思,以及我們對(duì)realizeClassWithoutSwift的調(diào)用流程的分析话原,我們對(duì)懶加載類非懶加載類做了如下總結(jié):

1夕吻、為什么要分為懶加載類非懶加載類
前面已經(jīng)完成了類的映射和注冊(cè)繁仁,接下來(lái)是類的初始化了涉馅。類的初始化實(shí)際上就是生成類對(duì)象,并設(shè)置類結(jié)構(gòu)的各個(gè)屬性黄虱。那這里就有一個(gè)問(wèn)題稚矿,初始化類對(duì)象勢(shì)必開(kāi)辟一定的內(nèi)存空間,在一個(gè)比較大的項(xiàng)目中,如果所有類都在啟動(dòng)時(shí)初始化晤揣,那一定會(huì)占用不少內(nèi)存空間桥爽,而且在程序運(yùn)行的某一個(gè)時(shí)間內(nèi)特別是啟動(dòng)階段,我們并不一定需要訪問(wèn)所有的類碉渡,因此如果在啟動(dòng)階段就初始話所有的類那必將造成性能損耗聚谁,同時(shí)也浪費(fèi)大量?jī)?nèi)存。因此滞诺,類的初始話時(shí)機(jī)非常重要形导。那類都在什么時(shí)候初始話呢?當(dāng)然是在需要的時(shí)候初始化比較好习霹。因此朵耕,根據(jù)類的初始化時(shí)機(jī),我們把類分成懶加載類非懶加載類淋叶。非懶加載類是在程序啟動(dòng)的時(shí)候初始化阎曹,也就是在map_images時(shí)期初始化;懶加載類是在程序運(yùn)行之后根據(jù)需要初始化的煞檩,一般來(lái)說(shuō)是在第一次訪問(wèn)類的時(shí)候會(huì)判斷類有沒(méi)有實(shí)現(xiàn)处嫌,沒(méi)有的話再進(jìn)行初始化。
2斟湃、懶加載類非懶加載類分別的初始化流程是什么樣的呢熏迹?
非懶加載類:前面我們知道,非懶加載類是在app啟動(dòng)時(shí)完成初始化的凝赛。而且前面頁(yè)說(shuō)到_dyld_objc_notify_register像dyld注冊(cè)了三個(gè)函數(shù)注暗,其中就有,map_images在鏡像文件映射時(shí)調(diào)用墓猎,_load_images則是初始化完成之后并調(diào)用類和category的+load方法捆昏,要調(diào)用類或者category的+load方法,那類或者category所屬的類就得在這之前被初始化毙沾,因此非懶加載類會(huì)在map_images中,在_load_images函數(shù)之前被初始化左胞。其函數(shù)流程如下:
map_images -> map_images_nolock -> _read_images -> readClass -> realizeClassWithoutSwift
懶加載類:未實(shí)現(xiàn)+load方法膨俐,在第一次向類發(fā)送消息時(shí)被動(dòng)調(diào)用realizeClassWithoutSwift以實(shí)現(xiàn)類的初始化。其調(diào)用流程如下:
lookUpImpOrForward -> realizeAndInitializeIfNeeded_locked -> realizeClassMaybeSwiftAndLeaveLocked -> realizeClassMaybeSwiftMaybeRelock -> realizeClassWithoutSwift
消息發(fā)送流程中罩句,只有相應(yīng)的類初始化才可能有值焚刺,才可能存在方法列表。所以消息發(fā)送時(shí)要先檢查類是否已經(jīng)被初始化了门烂。

realizeClassWithoutSwift源碼解析

雖然類的初始化時(shí)機(jī)不一樣乳愉,但是不管在哪個(gè)時(shí)機(jī)兄淫,初始化過(guò)程都是通過(guò)調(diào)用函數(shù)realizeClassWithoutSwift進(jìn)行實(shí)現(xiàn)的:

/***********************************************************************
* realizeClassWithoutSwift
* Performs first-time initialization on class cls, 
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class. 
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClassWithoutSwift(Class cls, Class previously)

關(guān)于它初始話的內(nèi)容,我們必須得先了解類的底層結(jié)構(gòu)蔓姚。根據(jù)這個(gè)函數(shù)的注釋捕虽,它是第一次對(duì)類執(zhí)行初始化,不包括swift相關(guān)的初始化坡脐。接下來(lái)通過(guò)源碼分析realizeClassWithoutSwift的主要功能:

1泄私、創(chuàng)建并設(shè)置class_rw_t;
2备闲、初始化緩存晌端;
3、類和元類的實(shí)現(xiàn)恬砂;
4咧纠、設(shè)置nonpointer標(biāo)記;
5泻骤、設(shè)置父類和元類漆羔;
6、設(shè)置C++構(gòu)造和析構(gòu)函數(shù).cxx_construct/destruct
7狱掂、如果有父類則添加到父類的子類表里演痒;如果沒(méi)有父類,則設(shè)置為根類rootClass;
8趋惨、調(diào)用methodizeClass完成方法嫡霞、屬性、協(xié)議等的加載希柿。

接下來(lái)主要介紹1、3和8养筒。其他源碼比較簡(jiǎn)單曾撤,也有相應(yīng)的注釋。

copy class_ro_t 到 class_rw_t
   class_rw_t *rw;
    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);
    }

因?yàn)榇蟛糠值念愒畏啵ㄎ覀冏远x的類都不是 future class挤悉,所以這里只分析else部分的代碼。當(dāng)我們把類加載到內(nèi)存中時(shí)巫湘,會(huì)創(chuàng)建一個(gè)Class装悲,同時(shí)會(huì)將編譯時(shí)確定的類相關(guān)的數(shù)據(jù)比如說(shuō)成員變量(ivars)、屬性(properties)尚氛、方法(methodList)诀诊、協(xié)議(protocols)等數(shù)據(jù)都存在class_ro_t結(jié)構(gòu)當(dāng)中,并在把class_ro_t的指針objc_class存儲(chǔ)到類的bits屬性中阅嘶。因此我們可以看到代碼中通過(guò)cls->data()把原始的class_ro_t(ro)讀取出來(lái)属瓣,然后創(chuàng)建一個(gè)class_rw_t (rw)载迄,然后把ro復(fù)制到rw中,最后把rw關(guān)聯(lián)到class里面抡蛙,同時(shí)設(shè)置初始化狀態(tài)flag护昧。這其中class_ro_t、class_rw_t和Class的關(guān)系都可以在類的底層結(jié)構(gòu)里面看到粗截。

父類惋耙、元類的初始化

函數(shù)realizeClassWithoutSwift中不止當(dāng)前類初始化,還對(duì)父類熊昌、元類遞歸初始化绽榛,確立好類的繼承鏈和isa指向:

    // Realize superclass and metaclass, if they aren't already.
    // This needs to be done after RW_REALIZED is set above, for root classes.
    // This needs to be done after class index is chosen, for root metaclasses.
    // This assumes that none of those classes have Swift contents,
    //   or that Swift's initializers have already been called.
    //   fixme that assumption will be wrong if we add support
    //   for ObjC subclasses of Swift classes.
    supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
methodizeClass源碼解析

最后realizeClassWithoutSwift會(huì)調(diào)用methodizeClass函數(shù)完成methods、properties浴捆、protocols和category等的加載蒜田。接下來(lái)源碼分析:

/***********************************************************************
* methodizeClass
* Fixes up cls's method list, protocol list, and property list.
* Attaches any outstanding categories.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
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();

    // Methodizing for the first time
    if (PrintConnecting) {
        _objc_inform("CLASS: methodizing class '%s' %s", 
                     cls->nameForLogging(), isMeta ? "(meta)" : "");
    }

    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);
        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);
    }

    // Root classes get bonus method implementations if they don't have 
    // them already. These apply before category replacements.
    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 {
            // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

#if DEBUG
    // Debug: sanity-check all SELs; log method list contents
    for (const auto& meth : rw->methods()) {
        if (PrintConnecting) {
            _objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(meth.name()));
        }
        ASSERT(sel_registerName(sel_getName(meth.name())) == meth.name());
    }
#endif
}
加載方法列表(methods)

在類的初始化過(guò)程中加載方法列表相對(duì)重要,也相對(duì)復(fù)雜选泻。首先它會(huì)從ro中讀取編譯時(shí)確定的方法列表baseMethodList:

// Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);
        if (rwe) rwe->methods.attachLists(&list, 1);
    }

讀取baseMethods之后會(huì)調(diào)用prepareMethodLists冲粤,然后baseMethods作為一個(gè)元素加入到數(shù)組addedLists中。這里也可以看出方法列表底層其實(shí)是個(gè)二維數(shù)組页眯。

    // Add method lists to array.
    // Reallocate un-fixed method lists.
    // The new methods are PREPENDED to the method list array.

    for (int i = 0; i < addedCount; i++) {
        method_list_t *mlist = addedLists[i];
        ASSERT(mlist);

        // Fixup selectors if necessary
        if (!mlist->isFixedUp()) {
            fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
        }
    }

然后通過(guò)fixupMethodList對(duì)方法列表進(jìn)行排序:

static void 
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
    runtimeLock.assertLocked();
    ASSERT(!mlist->isFixedUp());

    // fixme lock less in attachMethodLists ?
    // dyld3 may have already uniqued, but not sorted, the list
    if (!mlist->isUniqued()) {
        mutex_locker_t lock(selLock);
    
        // Unique selectors in list.
        for (auto& meth : *mlist) {
            const char *name = sel_cname(meth.name());
            meth.setName(sel_registerNameNoLock(name, bundleCopy));
        }
    }

    // Sort by selector address.
    // Don't try to sort small lists, as they're immutable.
    // Don't try to sort big lists of nonstandard size, as stable_sort
    // won't copy the entries properly.
    if (sort && !mlist->isSmallList() && mlist->entsize() == method_t::bigSize) {
        method_t::SortBySELAddress sorter;
        std::stable_sort(&mlist->begin()->big(), &mlist->end()->big(), sorter);
    }
    
    // Mark method list as uniqued and sorted.
    // Can't mark small lists, since they're immutable.
    if (!mlist->isSmallList()) {
        mlist->setFixedUp();
    }
}

可以看出方法排序是按SEL地址大小進(jìn)行排序的梯捕,這也是為什么我們?cè)?a href="http://www.reibang.com/p/b75ce9c94ee3" target="_blank">方法查找的時(shí)候遍歷方法列表時(shí)采用二分法查找的原因。

加載屬性列表(baseProperties)和協(xié)議列表(baseProtocols)
    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);
    }

這其中我看到一個(gè)出鏡率很高的變量rwe窝撵,rwe的實(shí)際結(jié)構(gòu)是class_rw_ext_t傀顾,這個(gè)rwe是在需要運(yùn)行時(shí)修改類結(jié)構(gòu)時(shí)才會(huì)被創(chuàng)建的。在_map_images基本不會(huì)創(chuàng)建(點(diǎn)擊了解class_rw_ext_t可多的信息)碌奉。而rwe是有可能在load_images階段創(chuàng)建短曾,下面會(huì)有介紹。

加載類別(categories)

說(shuō)到類Category加載赐劣,相對(duì)來(lái)說(shuō)比較復(fù)雜嫉拐。首先Category是類的擴(kuò)展,所以它加載必然跟類有關(guān)魁兼,接下來(lái)我們先看Category的底層結(jié)構(gòu)category_t:

struct category_t {
    const char *name;
    classref_t cls;
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
    WrappedPtr<method_list_t, PtrauthStrip> classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

可以看到Category不僅包含Class指針婉徘,還有很多信息,包括方法列表咐汞、屬性列表等盖呼。這些方法在加載的時(shí)候都是要合并到類的方法列表里面去的。那Category是什么時(shí)候加載的呢化撕?其實(shí)Category的也跟Class一樣几晤,有懶加載和非懶加載。Category的懶加載和非懶加載根類的懶加載和非懶加載有一定的關(guān)系植阴,經(jīng)過(guò)源碼運(yùn)行反復(fù)調(diào)試锌仅,總結(jié)了他們之間如下關(guān)系:

1章钾、當(dāng)類和它的Category都實(shí)現(xiàn)+load方法,類和category(如果有多個(gè)category热芹,只要有一個(gè)category實(shí)現(xiàn)+load贱傀,其他category是非懶加載),是非懶加載的伊脓。
2府寒、當(dāng)類實(shí)現(xiàn)+load,Category沒(méi)有實(shí)現(xiàn)+load报腔, 是非懶加載的株搔。
3、當(dāng)類沒(méi)有實(shí)現(xiàn)+load纯蛾,Category實(shí)現(xiàn)+load纤房,也是非懶加載的。
4翻诉、當(dāng)類和它的Category都沒(méi)有實(shí)現(xiàn)+load方法炮姨,是懶加載的。

那么這四種情況的category什么時(shí)候被加載的呢碰煌?我們?cè)谇懊嫣岬竭^(guò)舒岸,_objc_init會(huì)像dlyd注冊(cè)幾個(gè)函數(shù),其中就有一個(gè)load_images芦圾,這個(gè)函數(shù)是在鏡像文件初始化完成之后調(diào)用的蛾派。第1種情況是類在類初始化階段(_map_images)被加載,它的Category會(huì)在load_images階段被加載个少。而另外2洪乍、3種情況的Category會(huì)在_map_images階段類加載被讀取到class_ro_t中。而第4種情況則會(huì)在類第一次被訪問(wèn)的時(shí)候跟類一起被加載夜焦。接下來(lái)我們通過(guò)源碼調(diào)試驗(yàn)證一下壳澳。

1、當(dāng)類和它的Category都實(shí)現(xiàn)+load方法

  • 調(diào)試demo
    首先我們創(chuàng)建一個(gè)demo在源碼調(diào)試糊探,在demo中創(chuàng)建一個(gè)MyObject和它的一個(gè)category(addition),demo如下:
demo.jpeg

然后運(yùn)行源碼工程河闰。在_map_images流程的methodizeClass函數(shù)中有如下一段代碼:

    // Attach categories.
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

正常情況下科平,本來(lái)期待的是通調(diào)用attachToClass到attachCategories,在attachCategories里面實(shí)現(xiàn)把Category加載到類結(jié)構(gòu)中姜性。所以我們?cè)赼ttachCategories函數(shù)中專門(mén)為MyObject類打了個(gè)斷點(diǎn)瞪慧。通過(guò)斷點(diǎn)調(diào)試發(fā)現(xiàn)這個(gè)流程根本不是從_map_images走到attachCategories,而真正調(diào)用attachCategories時(shí)在load_images這個(gè)階段部念,一下是源碼調(diào)試階段:

attactCategory.jpeg

這樣Category在啟動(dòng)流程的加載時(shí)機(jī)我們就找到了弃酌。其流程就是

load_images -> loadAllCategories -> load_categories_nolock -> attachCategories氨菇。

這一流程只是針對(duì)類和分類實(shí)現(xiàn)了+load方法的情況,當(dāng)然在這個(gè)流程之前Category的類作為非懶加載類已經(jīng)在_map_images階段實(shí)現(xiàn)了加載妓湘。類加載完成之后在加載分類查蓉。加載分類并關(guān)聯(lián)到類是在函數(shù)attachCategories中實(shí)現(xiàn)的。

  • attachCategories源碼解析
    接下來(lái)在attachCategories源碼查看Category加載的流程:
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    const char *mangledName = cls->mangledName();
    if (strcmp("MyObject", mangledName) == 0) {
        if(!cls->isMetaClass()){//避免元類的干擾
            printf("我來(lái)了 MyObject");//MyObject是一個(gè)實(shí)現(xiàn)了+load方法的類榜贴,他還有一個(gè)category名字叫addtion也實(shí)現(xiàn)了+load方法豌研,
        }
    }
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rwe = cls->data()->extAllocIfNeeded();

    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }

    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

首先會(huì)先創(chuàng)建一個(gè)class_rw_ext_t結(jié)構(gòu)變量rwe:

auto rwe = cls->data()->extAllocIfNeeded();

rwe前面有提到過(guò)。然后遍歷Category列表唬党,因?yàn)榭赡苁嵌鄠€(gè)鹃共,然后把每個(gè)category的方法列表、屬性列表驶拱、協(xié)議類表等都分別添加到rwe對(duì)應(yīng)的列表中霜浴。這里依然以category的方法列表為例,來(lái)分析這個(gè)過(guò)程:

    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
......
}

首先通過(guò)函數(shù)methodsForMeta根據(jù)是否是元類獲取實(shí)例方法或者類方法列表蓝纲。然后通過(guò)prepareMethodLists先對(duì)方法進(jìn)行一波排序阴孟。接著調(diào)用attachLists函數(shù),把方法列表添加到rwe的methods中驻龟。接下來(lái)看attachLists是如何實(shí)現(xiàn)的:

    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;

            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i];
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i];
            free(array());
            setArray(newArray);
            validate();
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        } 
        else {
            // 1 list -> many lists
            Ptr<List> oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

這里我們可以看到methods是個(gè)二維數(shù)組温眉。而且新增的category方法列表總是被放在最前面。當(dāng)有新列表進(jìn)來(lái)時(shí)翁狐,如果沒(méi)有舊的方法列表則直接賦值类溢,如果有舊的方法列表,會(huì)重新計(jì)算數(shù)組大小重新創(chuàng)建一個(gè)新數(shù)組露懒,然后把舊的數(shù)據(jù)移到后面闯冷,新的加到前面。這樣新的方法總是能夠在最前面懈词。這也是為什么方法查找時(shí)和類的方法同名時(shí)總是先調(diào)用category方法的原因蛇耀。

2、當(dāng)類和它的Category其中之一實(shí)現(xiàn)+load方法
這時(shí)候類是非懶加載類坎弯,會(huì)在啟動(dòng)期間被加載纺涤。然后category在類初始化階段被加載到class_ro_t中。接下來(lái)源碼調(diào)試抠忘。首先創(chuàng)建一個(gè)類MyObject撩炊,并且創(chuàng)建兩個(gè)Category為CatA和CatB,然后分別實(shí)現(xiàn)兩個(gè)方法崎脉,然后我們針對(duì)當(dāng)前類的初始化的流程上打斷點(diǎn)拧咳,觀察方法列表,證明方法列表加載到class_ro_t中囚灼。demo如下:

2骆膝、3種情況.jpeg

接下來(lái)運(yùn)行程序祭衩,定位到MyObject讀取方法列表的代碼:

調(diào)試結(jié)果.png

根據(jù)調(diào)試結(jié)果證明這兩種情況的類確實(shí)是非懶加載類,啟動(dòng)時(shí)會(huì)加載阅签,同時(shí)也會(huì)加載其對(duì)應(yīng)的category掐暮。

3、當(dāng)類和它的Category都沒(méi)實(shí)現(xiàn)+load方法
在剛才的demo上去掉+load方法愉择,這樣類就不會(huì)在啟動(dòng)階段初始化了劫乱。接著我們?cè)趩?dòng)之后去調(diào)用類的方法:

第四種情況.jpeg

第四種情況調(diào)試結(jié)果.jpeg

總結(jié)

類加載過(guò)程比較復(fù)雜。類加載又分為懶加載和非懶加載锥涕。跟+load方法有關(guān)衷戈。實(shí)現(xiàn)不管是分類和本類實(shí)現(xiàn)+load方法,他都是非懶加載類层坠。會(huì)在啟動(dòng)階段被初始化殖妇,而非懶加載類則會(huì)在第一次被訪問(wèn)的時(shí)候調(diào)用。因此在這里面+load方法在使用上的特別注意破花,不能用的太多谦趣,會(huì)影響啟動(dòng)速度(點(diǎn)擊了解app啟動(dòng)優(yōu)化)。類的加載邏輯跟類的底層結(jié)構(gòu)密切相關(guān)座每,想了解加載過(guò)程首先得了解類的底層結(jié)構(gòu)(點(diǎn)擊了解類的底層結(jié)構(gòu))和APP的啟動(dòng)流程(點(diǎn)擊了解APP的啟動(dòng)流程)前鹅;

備注:本文調(diào)試的源碼是objc4-818.2版本的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末峭梳,一起剝皮案震驚了整個(gè)濱河市舰绘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌葱椭,老刑警劉巖捂寿,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異孵运,居然都是意外死亡秦陋,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)治笨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)驳概,“玉大人,你說(shuō)我怎么就攤上這事旷赖∷秤郑” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵杠愧,是天一觀的道長(zhǎng)待榔。 經(jīng)常有香客問(wèn)我逞壁,道長(zhǎng)流济,這世上最難降的妖魔是什么锐锣? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮绳瘟,結(jié)果婚禮上雕憔,老公的妹妹穿的比我還像新娘。我一直安慰自己糖声,他們只是感情好斤彼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著蘸泻,像睡著了一般琉苇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上悦施,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天并扇,我揣著相機(jī)與錄音,去河邊找鬼抡诞。 笑死穷蛹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的昼汗。 我是一名探鬼主播肴熏,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼顷窒!你這毒婦竟也來(lái)了蛙吏?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蹋肮,失蹤者是張志新(化名)和其女友劉穎出刷,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體坯辩,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡馁龟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了漆魔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坷檩。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖改抡,靈堂內(nèi)的尸體忽然破棺而出矢炼,到底是詐尸還是另有隱情,我是刑警寧澤阿纤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布句灌,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏胰锌。R本人自食惡果不足惜骗绕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望资昧。 院中可真熱鬧酬土,春花似錦、人聲如沸格带。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)叽唱。三九已至屈呕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間棺亭,已是汗流浹背凉袱。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留侦铜,地道東北人专甩。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像钉稍,于是被迫代替她去往敵國(guó)和親涤躲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容