如何精確度量 iOS App 的啟動(dòng)時(shí)間

在 WWDC 2016 和 2017 都有提到啟動(dòng)這塊的原理和性能優(yōu)化思路,可見(jiàn)啟動(dòng)時(shí)間遭赂,對(duì)于開(kāi)發(fā)者和用戶們來(lái)說(shuō)是多么的重要谦絮,本文就談?wù)勅绾尉_的度量 App 的啟動(dòng)時(shí)間,啟動(dòng)時(shí)間由 main 之前的啟動(dòng)時(shí)間和 main 之后的啟動(dòng)時(shí)間兩部分組成渣淳。

圖是 Apple 在 WWDC 上展示的 PPT,是對(duì) main 之前啟動(dòng)所做事的一個(gè)簡(jiǎn)單總結(jié)伴箩。main 之后的啟動(dòng)時(shí)間如何考量呢入愧?這個(gè)更多靠大家自己定義,有的人把 main 到 didFinishLaunching 結(jié)束的這一段時(shí)間作為指標(biāo),有的人把 main 到第一個(gè) ViewController 的 viewDidAppear 作為考量指標(biāo)棺蛛。不管如何怔蚌,我覺(jué)得都是一定程度上可以反映問(wèn)題的。

Xcode 測(cè)量 pre-main 時(shí)間

對(duì)于如何測(cè)試啟動(dòng)時(shí)間旁赊,Xcode 提供了一個(gè)很贊的方法桦踊,只需要在 Edit scheme -> Run -> Arguments 中將環(huán)境變量 DYLD_PRINT_STATISTICS 設(shè)為 1,就可以看到 main 之前各個(gè)階段的時(shí)間消耗终畅。

Total pre-main time: 341.32 milliseconds (100.0%)
         dylib loading time: 154.88 milliseconds (45.3%)
        rebase/binding time:  37.20 milliseconds (10.8%)
            ObjC setup time:  52.62 milliseconds (15.4%)
           initializer time:  96.50 milliseconds (28.2%)
           slowest intializers :
               libSystem.dylib :   4.07 milliseconds (1.1%)
    libMainThreadChecker.dylib :  30.75 milliseconds (9.0%)
                  AFNetworking :  19.08 milliseconds (5.5%)
                        LDXLog :  10.06 milliseconds (2.9%)
                        Bigger :   7.05 milliseconds (2.0%)

還有一個(gè)方法獲取更詳細(xì)的時(shí)間钞钙,只需將環(huán)境變量 DYLD_PRINT_STATISTICS_DETAILS 設(shè)為 1 就可以。

  total time: 1.0 seconds (100.0%)
  total images loaded:  243 (0 from dyld shared cache)
  total segments mapped: 721, into 93608 pages with 6173 pages pre-fetched
  total images loading time: 817.51 milliseconds (78.3%)
  total load time in ObjC:  63.02 milliseconds (6.0%)
  total debugger pause time: 683.67 milliseconds (65.5%)
  total dtrace DOF registration time:   0.07 milliseconds (0.0%)
  total rebase fixups:  2,131,938
  total rebase fixups time:  37.54 milliseconds (3.5%)
  total binding fixups: 243,422
  total binding fixups time:  29.60 milliseconds (2.8%)
  total weak binding fixups time:   1.75 milliseconds (0.1%)
  total redo shared cached bindings time:  29.32 milliseconds (2.8%)
  total bindings lazily fixed up: 0 of 0
  total time in initializers and ObjC +load:  93.76 milliseconds (8.9%)
                           libSystem.dylib :   2.58 milliseconds (0.2%)
               libBacktraceRecording.dylib :   3.06 milliseconds (0.2%)
                            CoreFoundation :   1.85 milliseconds (0.1%)
                                Foundation :   2.61 milliseconds (0.2%)
                libMainThreadChecker.dylib :  42.73 milliseconds (4.0%)
                                   ModelIO :   1.93 milliseconds (0.1%)
                              AFNetworking :  18.76 milliseconds (1.7%)
                                    LDXLog :   9.46 milliseconds (0.9%)
                        libswiftCore.dylib :   1.16 milliseconds (0.1%)
                   libswiftCoreImage.dylib :   1.51 milliseconds (0.1%)
                                    Bigger :   3.91 milliseconds (0.3%)
                              Reachability :   1.48 milliseconds (0.1%)
                             ReactiveCocoa :   1.56 milliseconds (0.1%)
                                SDWebImage :   1.41 milliseconds (0.1%)
                             SVProgressHUD :   1.23 milliseconds (0.1%)
total symbol trie searches:    133246
total symbol table binary searches:    0
total images defining weak symbols:  30
total images using weak symbols:  69

線上如何度量 pre-main 時(shí)間

如果不依靠 Xcode 我們也是可以對(duì) main 之前的時(shí)間進(jìn)行一個(gè)考量的声离。當(dāng)然,這個(gè)時(shí)間的度量更多關(guān)注的是開(kāi)發(fā)者可控的啟動(dòng)段瘫怜。也就是第一個(gè)圖展示的 Initializer 段术徊,在這段時(shí)間里處理 C++ 靜態(tài)對(duì)象的 initializer、ObjC Load 方法的執(zhí)行鲸湃。

度量 ObjC Load 方法

如何計(jì)算這一段時(shí)間呢赠涮?最容易想到的就是攔截打點(diǎn),如何攔截成為難點(diǎn)暗挑。這里把目光轉(zhuǎn)向 dyld 源碼笋除,看看有什么發(fā)現(xiàn)。整個(gè)初始化過(guò)程都是從 initializeMainExecutable 方法開(kāi)始的炸裆。dyld 會(huì)優(yōu)先初始化動(dòng)態(tài)庫(kù)垃它,然后初始化 App 的可執(zhí)行文件。

void initializeMainExecutable()
{
    // record that we've reached this step
    gLinkContext.startedInitializingMainExecutable = true;

    // run initialzers for any inserted dylibs
    ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    if ( rootCount > 1 ) {
        for(size_t i=1; i < rootCount; ++i) {
            sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
        }
    }
    
    // run initializers for main executable and everything it brings up 
    sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

那么不難想到烹看,只要在動(dòng)態(tài)庫(kù)的 load 函數(shù)中 Hook App 中所有的 Load 函數(shù)国拇,然后打點(diǎn)就可以啦。但是惯殊,現(xiàn)在很多項(xiàng)目庫(kù)都是使用 Cocoapods 管理的酱吝,并且很多都使用了 use_frameworks,那么也就是說(shuō)我們的 App 并不是一個(gè) 單一的可執(zhí)行文件土思,它是有主 image 文件和很多動(dòng)態(tài)庫(kù)共同組成的务热。按照剛才那種方法,是沒(méi)辦法統(tǒng)計(jì)到自己引入的動(dòng)態(tài)庫(kù)的 load 函數(shù)的執(zhí)行時(shí)間的己儒。下一步要考慮的就是崎岂,如何找到最早加載的動(dòng)態(tài)庫(kù)呢?然后在其 load 函數(shù)中做 Hook 就可以址愿。

動(dòng)態(tài)庫(kù)的 load 順序是與 Load Commands 順序和依賴(lài)關(guān)系息息相關(guān)的该镣。如圖所示:

就拿我們引入的動(dòng)態(tài)庫(kù)來(lái)說(shuō), AFNetworking 會(huì)優(yōu)先 load ,被依賴(lài)的動(dòng)態(tài)庫(kù)會(huì)優(yōu)先 load损合。下面是我自己打點(diǎn)測(cè)試的結(jié)果省艳,LDXlog 被 Bigger 依賴(lài),所以 AFNetworking 最早 load 嫁审,然后是 LDXlog跋炕,依次按照 Load Commands 順序加載。

2017-09-23 13:45:01.683817+0800 AAALoadHook[27267:1585198] AFNetworking
2017-09-23 13:45:01.696816+0800 AAALoadHook[27267:1585198] LDXLog
2017-09-23 13:45:01.707312+0800 AAALoadHook[27267:1585198] Bigger
2017-09-23 13:45:01.708875+0800 AAALoadHook[27267:1585198] Reachability
2017-09-23 13:45:01.710732+0800 AAALoadHook[27267:1585198] REACtive
2017-09-23 13:45:01.712066+0800 AAALoadHook[27267:1585198] SDWE
2017-09-23 13:45:01.713650+0800 AAALoadHook[27267:1585198] SVProgressHUD
2017-09-23 13:45:01.714499+0800 AAALoadHook[27267:1585198] 我是主工程

上面的測(cè)試讓我產(chǎn)生一個(gè)錯(cuò)覺(jué)律适,以為動(dòng)態(tài)庫(kù)加載是和字母順序相關(guān)的辐烂,其實(shí)并不是這樣,因?yàn)槲沂褂玫亩际?pod 管理的動(dòng)態(tài)庫(kù)捂贿,這個(gè)順序被 CocoaPods 排序過(guò)了纠修,所以才會(huì)有如此結(jié)果。在此感謝@冬瓜@monkey的干貨解答厂僧。

也就是說(shuō)扣草,只要把我們的統(tǒng)計(jì)庫(kù)命名為 A 開(kāi)頭的庫(kù)(我們的庫(kù)目前均使用 pod 管理),并在內(nèi)部加入打點(diǎn)就可以啦颜屠。再次總結(jié)下整體的思路:

  • 找到最早 load 的動(dòng)態(tài)庫(kù)
  • 在 load 函數(shù)中獲取 App 中的所有可執(zhí)行文件
  • hook 對(duì)應(yīng)的可執(zhí)行文件的 load 函數(shù)
  • 統(tǒng)計(jì)每個(gè) load 函數(shù)的時(shí)間辰妙、全部 load 函數(shù)的整體時(shí)間
  • 上報(bào)統(tǒng)計(jì)分析

由于代碼比較多,粘貼過(guò)來(lái)的話博客太長(zhǎng)了甫窟,所以想了解源碼的話密浑,可以點(diǎn)擊這個(gè)鏈接:https://github.com/joy0304/JoyDemo/tree/master/HookLoad

剛才的統(tǒng)計(jì)還有一些要注意的事項(xiàng),就是不能為了統(tǒng)計(jì)性能粗井,自己卻造成了性能問(wèn)題尔破,獲取所有的類(lèi)并且 Hook load 函數(shù)還是比較耗時(shí)的,控制不好反而增加了啟動(dòng)時(shí)間浇衬。

度量 C++ Static Initializers

剛才提到了初始化的入口是 initializeMainExecutable呆瞻,該函數(shù)會(huì)執(zhí)行 ImageLoader::runInitializers 方法,然后會(huì)調(diào)用 ImageLoader::doInitialization径玖,最后會(huì)執(zhí)行到 doModInitFunctions 方法痴脾。

void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
    if ( fHasInitializers ) {
        const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
        const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
        const struct load_command* cmd = cmds;
        for (uint32_t i = 0; i < cmd_count; ++i) {
            if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
                const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
                const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
                const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
                for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
                    const uint8_t type = sect->flags & SECTION_TYPE;
                    if ( type == S_MOD_INIT_FUNC_POINTERS ) {
                        Initializer* inits = (Initializer*)(sect->addr + fSlide);
                        const size_t count = sect->size / sizeof(uintptr_t);
                        
                        for (size_t j=0; j < count; ++j) {
                            Initializer func = inits[j];
                            // <rdar://problem/8543820&9228031> verify initializers are in image
                            if ( ! this->containsAddress((void*)func) ) {
                                dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
                            }
                        
                            func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
                        }
                    }
                }
            }
            cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
        }
    }
}

這段代碼實(shí)在是長(zhǎng),它會(huì)從 mod_init_func 這個(gè) section 中讀取所有的函數(shù)指針梳星,然后執(zhí)行函數(shù)調(diào)用赞赖,這些函數(shù)指針對(duì)應(yīng)的正是我們的 C++ Static Initializers 和 __attribute__((constructor))修飾的函數(shù)。

因?yàn)樗鼈兊膱?zhí)行順序在 load 函數(shù)之后冤灾,所以可以在 load 函數(shù)中把 mod_init_func 中的地址都替換成我們的 hook 函數(shù)指針前域,然后再把原函數(shù)指針保存到一個(gè)全局?jǐn)?shù)據(jù)中,當(dāng)執(zhí)行我們的 hook 函數(shù)時(shí)韵吨,從全局?jǐn)?shù)組中取出原函數(shù)地址執(zhí)行匿垄。在這里張貼下主要代碼,更多可以參考這個(gè)鏈接:https://github.com/everettjf/Yolo/blob/master/HookCppInitilizers/hook_cpp_init.mm

void myInitFunc_Initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct MyProgramVars* vars){
        ++g_cur_index;
        
        OriginalInitializer func = (OriginalInitializer)g_initializer->at(g_cur_index);
        
        CFTimeInterval start = CFAbsoluteTimeGetCurrent();
        
        func(argc,argv,envp,apple,vars);
        
        CFTimeInterval end = CFAbsoluteTimeGetCurrent();
}

static void hookModInitFunc(){
        Dl_info info;
        dladdr((const void *)hookModInitFunc, &info);
        
#ifndef __LP64__
        const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;
        unsigned long size = 0;
        MemoryType *memory = (uint32_t*)getsectiondata(mhp, "__DATA", "__mod_init_func", & size);
#else
        const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;
        unsigned long size = 0;
        MemoryType *memory = (uint64_t*)getsectiondata(mhp, "__DATA", "__mod_init_func", & size);
#endif
        for(int idx = 0; idx < size/sizeof(void*); ++idx){
                MemoryType original_ptr = memory[idx];
                g_initializer->push_back(original_ptr);
                memory[idx] = (MemoryType)myInitFunc_Initializer;
        }
}

剛才 hook load 函數(shù)時(shí)遇到的問(wèn)題,對(duì)于 C++ Static Initializers 會(huì)不會(huì)存在呢椿疗?是存在的漏峰,我想要在一個(gè)動(dòng)態(tài)庫(kù)中統(tǒng)計(jì) App 中所有可執(zhí)行文件的 C++ Static Initializers 的執(zhí)行時(shí)間,但是 dyld 中有這么一段代碼:

if ( type == S_MOD_INIT_FUNC_POINTERS ) {
    Initializer* inits = (Initializer*)(sect->addr + fSlide);
    const size_t count = sect->size / sizeof(uintptr_t);
    
    for (size_t j=0; j < count; ++j) {
        Initializer func = inits[j];
        // <rdar://problem/8543820&9228031> verify initializers are in image
        if ( ! this->containsAddress((void*)func) ) {
            dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
        }
    
        func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
    }
}

if ( ! this->containsAddress((void*)func) ) 這里會(huì)做一個(gè)判斷届榄,判斷函數(shù)地址是否在當(dāng)前 image 的地址空間中浅乔,因?yàn)槲覀兪窃谝粋€(gè)獨(dú)立的動(dòng)態(tài)庫(kù)中做函數(shù)地址替換,替換后的函數(shù)地址都是我們動(dòng)態(tài)庫(kù)中的铝条,并沒(méi)有在其他 image 中靖苇,所以當(dāng)其他 image 執(zhí)行到這個(gè)判斷時(shí),就拋出了異常班缰。這個(gè)問(wèn)題好像無(wú)解贤壁,所以我們的 C++ Static Initializers 時(shí)間統(tǒng)計(jì)稍有不足。

Xcode For Static Initializers

Apple 在 https://developer.apple.com/videos/play/wwdc2017/413/ 中公布了一個(gè)新的追蹤 Static Initializers 時(shí)間消耗的方案埠忘, Instruments 增加了一個(gè)叫做 Static Initializer Tracing 的工具芯砸,可以方便排查每個(gè) Static Initializer 的時(shí)間消耗。(我還沒(méi)更新最新版本给梅,暫不實(shí)踐)

main 之后的時(shí)間度量

main 到 didFinishLaunching 結(jié)束或者第一個(gè) ViewController 的viewDidAppear 都是作為 main 之后啟動(dòng)時(shí)間的一個(gè)度量指標(biāo)。這個(gè)時(shí)間統(tǒng)計(jì)直接打點(diǎn)計(jì)算就可以双揪,不過(guò)當(dāng)遇到時(shí)間較長(zhǎng)需要排查問(wèn)題時(shí)动羽,只統(tǒng)計(jì)兩個(gè)點(diǎn)的時(shí)間其實(shí)不方便排查,目前見(jiàn)到比較好用的方式就是為把啟動(dòng)任務(wù)規(guī)范化渔期、粒子化运吓,針對(duì)每個(gè)任務(wù)都有打點(diǎn)統(tǒng)計(jì),這樣方便后期問(wèn)題的定位和優(yōu)化疯趟。

優(yōu)化拘哨?

其實(shí)優(yōu)化的,很多公司都有博客寫(xiě)道信峻。既然談到了啟動(dòng)監(jiān)控就稍微寫(xiě)一點(diǎn)個(gè)人覺(jué)得比較使用的優(yōu)化方案吧倦青。

  • 目前很多項(xiàng)目使用 use_frameworks 的 pod 動(dòng)態(tài)庫(kù),系統(tǒng)的動(dòng)態(tài)庫(kù)有共享緩存等優(yōu)化方案盹舞,但是我們的動(dòng)態(tài)庫(kù)變多了的話會(huì)非常耗時(shí)产镐,所以合并動(dòng)態(tài)庫(kù)是一個(gè)有效且可行的方案
  • 把啟動(dòng)任務(wù)細(xì)分,不需要及時(shí)初始化踢步,不需要在主線程初始化的癣亚,都選擇異步延時(shí)加載
  • 監(jiān)控好 load 和 Static Initializers 的時(shí)間消耗,一不小心就容易出現(xiàn)幾百毫秒的時(shí)間消耗
  • 還有很多其他公司實(shí)踐的方案获印,我都收集了下來(lái)述雾,可以參考:https://github.com/joy0304/Joy-Blog/blob/master/iOSCollection.md
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子玻孟,更是在濱河造成了極大的恐慌唆缴,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件取募,死亡現(xiàn)場(chǎng)離奇詭異琐谤,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)玩敏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)斗忌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人旺聚,你說(shuō)我怎么就攤上這事织阳。” “怎么了砰粹?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵唧躲,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我碱璃,道長(zhǎng)弄痹,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任嵌器,我火速辦了婚禮肛真,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘爽航。我一直安慰自己蚓让,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布讥珍。 她就那樣靜靜地躺著历极,像睡著了一般。 火紅的嫁衣襯著肌膚如雪衷佃。 梳的紋絲不亂的頭發(fā)上趟卸,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音氏义,去河邊找鬼衰腌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛觅赊,可吹牛的內(nèi)容都是我干的右蕊。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼吮螺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼饶囚!你這毒婦竟也來(lái)了帕翻?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤萝风,失蹤者是張志新(化名)和其女友劉穎嘀掸,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體规惰,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡睬塌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了歇万。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揩晴。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖贪磺,靈堂內(nèi)的尸體忽然破棺而出硫兰,到底是詐尸還是另有隱情,我是刑警寧澤寒锚,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布劫映,位于F島的核電站,受9級(jí)特大地震影響刹前,放射性物質(zhì)發(fā)生泄漏泳赋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一喇喉、第九天 我趴在偏房一處隱蔽的房頂上張望祖今。 院中可真熱鬧,春花似錦轧飞、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至制妄,卻和暖如春掸绞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背耕捞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工衔掸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人俺抽。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓敞映,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親磷斧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子振愿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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