32.iOS底層學習之啟動優(yōu)化

本章提綱:
1笆豁、pre-Main階段的性能檢測
2郎汪、虛擬內(nèi)存
3、二進制重排
4闯狱、Clang插裝

1煞赢、pre-Main階段的性能檢測

應用的啟動過程一般以Main函數(shù)為臨界點,分為Main函數(shù)之前和Main函數(shù)之后扩氢。
Main函數(shù)之前我們稱為pre-Main耕驰。
Xcode為檢測pre-Main的耗時提供了環(huán)境變量,以便開發(fā)者了解pre-Main的時間。
在Xcode中的Schemes->Run->Arguments中添加DYLD_PRINT_STATISTICS的環(huán)境變量為YES朦肘。然后運行程序饭弓,可以看到如下打印:

Total pre-main time: 540.09 milliseconds (100.0%)
dylib loading time: 159.35 milliseconds (29.5%)
rebase/binding time: 39.06 milliseconds (7.2%)
ObjC setup time: 28.37 milliseconds (5.2%)
initializer time: 313.30 milliseconds (58.0%)
slowest intializers :
libSystem.B.dylib : 7.52 milliseconds (1.3%)
libMainThreadChecker.dylib : 48.67 milliseconds (9.0%)
GPUToolsCore : 26.26 milliseconds (4.8%)
libglInterpose.dylib : 113.10 milliseconds (20.9%)
KSAdSDK : 105.15 milliseconds (19.4%)
xxxx : 80.49 milliseconds (14.9%)

  • dylib loading time
    動態(tài)庫的載入耗時媒抠。系統(tǒng)的動態(tài)庫存在于共享緩存弟断,但是自定義的動態(tài)庫就要通過依賴關系一個一個的加載。
    蘋果官方建議項目中不要超過6個自定義的動態(tài)庫趴生,超過的部分最好進行多個動態(tài)庫合并阀趴,以此來減少動態(tài)庫的加載時間。

  • rebase/binding time
    這是一個非常核心而且重要的概念苍匆。重定位/符號綁定耗時刘急。涉及到虛擬內(nèi)存的相關技術,會在下面詳細介紹浸踩。
    rebase(重定位):采用了ASLR技術叔汁,保證地址的隨機化,加強了內(nèi)存訪問的安全性检碗。
    binding(符號綁定):使用外部符號据块,編譯時無法找到函數(shù)地址。在運行時折剃,dyld加載共享緩存另假,加載鏈接動態(tài)庫之后,進行binding操作怕犁,重新綁定外部符號边篮。

  • ObjC setup time
    注冊OC類的耗時。應用啟動時奏甫,系統(tǒng)會生成OC類和分類的兩張相關映射表苟耻,IMP到SEL的映射,分類的方法等合并到相關表中的等操作會造成一部分的耗時扶檐。
    減少項目中類和分類的數(shù)量可以優(yōu)化這部分的時間。
    減少類和分類中的Load方法的使用胁艰,讓類以懶加載的方式加載款筑。

  • initializer time
    執(zhí)行load以及C++構(gòu)造函數(shù)的耗時

  • slowest intializers
    最耗時的幾個動態(tài)庫。

2腾么、虛擬內(nèi)存

聊到虛擬內(nèi)存我們就要聊起早期的計算機結(jié)構(gòu)奈梳。早期的是馮·諾依曼計算機結(jié)構(gòu),在1945年就被提出了解虱,在當時是很新穎的結(jié)構(gòu)了攘须,它是第一次將存儲器和運算器分離,開啟了以存儲器為核心的現(xiàn)代計算機的篇章殴泰。

馮·諾依曼計算機結(jié)構(gòu)

但是馮·諾依曼結(jié)構(gòu)有它自己的問題于宙,就是存儲器之間的讀取速度遠遠小于CPU的工作效率浮驳。讀取效率低,CPU的運算能力又太快捞魁,就造成了CPU性能的浪費至会。為了解決這個問題,現(xiàn)行的解決方式就是采用多級存儲谱俭,來平衡存儲器的讀寫速率奉件,容量,價格昆著。

該結(jié)構(gòu)下的CPU的尋址方式:內(nèi)存可以被看成一個數(shù)組县貌,數(shù)組元素是一個字節(jié)大小的空間,而數(shù)組索引則是所謂的物理地址凑懂。最簡單直接的方式就是CPU直接通過物理地址去訪問對應的內(nèi)存煤痕,也叫做物理尋址。

這種尋址方式有非常嚴重的安全問題征候。因為直接暴露的是物理地址杭攻,所以進程通過地址偏移可以訪問到任何屋里地址,用戶進程想干嘛就干嘛疤坝。這是非常不安全的兆解。

現(xiàn)代處理器使用的是虛擬尋址的方式。CPU通過訪問虛擬地址跑揉,經(jīng)過翻譯獲得物理地址才能訪問內(nèi)存锅睛。這個翻譯過程由CPU中的內(nèi)存管理單元(Memory Management Unit,縮寫為MMU)完成历谍。

現(xiàn)代的操作系統(tǒng)都引入了虛擬內(nèi)存现拒。對于每個進程來說,操作系統(tǒng)可以為其提供一個獨立的私有的連續(xù)的地址空間望侈。對于進程來說印蔬,它的可見部分只有分配給它的虛擬內(nèi)存。而虛擬內(nèi)存實際可能映射到物理內(nèi)存以及硬盤的任何區(qū)域脱衙。由于硬盤的讀寫速度不如內(nèi)存快侥猬,所以操作系統(tǒng)會優(yōu)先使用物理內(nèi)存空間,但是當物理內(nèi)存空間不夠時捐韩,就會將部分內(nèi)存數(shù)據(jù)交換到硬盤上去存儲退唠,這也是所謂的Swap內(nèi)存交換機制。有了內(nèi)存交換機制以后荤胁,相比起物理尋址瞧预,虛擬內(nèi)存實際上利用了磁盤空間擴展了內(nèi)存空間。

虛擬內(nèi)存的優(yōu)勢同時也彰顯了出來:
1、保護了進程的地址空間垢油,將進程和物理地址完全阻隔開盆驹,無法跨進程訪問。
2秸苗、由于操作系統(tǒng)分配的虛擬內(nèi)存是連續(xù)的召娜,簡化了內(nèi)存管理。
3惊楼、利用硬盤空間拓展了內(nèi)存空間玖瘸。
4、可以按需加載內(nèi)容到內(nèi)存中檀咙,避免內(nèi)存浪費雅倒。

內(nèi)存分頁

虛擬內(nèi)存和物理內(nèi)存存在映射關系,為了方便映射和管理弧可,虛擬內(nèi)存和物理內(nèi)存都被分割成大小相同的單位蔑匣,物理內(nèi)存的最小單位稱為幀(Frame),而虛擬內(nèi)存的最小單位被稱為頁(Page)棕诵。

在iOS中裁良,一頁的大小為16KB,當進程被加載到內(nèi)存中是校套,虛擬內(nèi)存會給該進程開辟最大4個G的虛擬內(nèi)存空間价脾。

內(nèi)存分頁的最大意義在于:
1、支持了物理內(nèi)存的離散使用笛匙;
2侨把、提高MMU的翻譯效率,采用一些頁面調(diào)度(Paging)算法妹孙,利用翻譯過程中也存在局部性原理秋柄,將大概率被使用的幀地址加入到TLB或者頁表之中,提高翻譯效率蠢正。

缺頁中斷

現(xiàn)代計算機都是分級緩存的骇笔,內(nèi)存命中的查找也是分級的。

  • 首先會在TLB(Translation Lookaside Buffer)中進行查詢嚣崭,這個表位于CPU內(nèi)部蜘拉,查詢速度最快;
  • 如果沒有命中有鹿,那么接下來會在頁表(Page Table)中進行查詢,頁表位于物理內(nèi)存中谎脯,所以查詢速度較慢葱跋,如果發(fā)現(xiàn)目標不在物理內(nèi)存中,那么成為缺頁
  • 如果物理內(nèi)存沒有命中查找娱俺,此時會去磁盤中查找稍味,如果還找不到就報錯了。

所以當發(fā)生缺頁時荠卷,操作系統(tǒng)會阻塞當前進程模庐,把需要的數(shù)據(jù)載入到物理內(nèi)存中,然后再尋址讀取油宜。當缺頁頻繁發(fā)生時掂碱,也是非常耗時的。

頁面置換

由于物理內(nèi)存是有限的慎冤,當物理內(nèi)存沒有空間時疼燥,操作系統(tǒng)會通過算法找到最不經(jīng)常使用的物理頁驅(qū)逐回磁盤,為新的內(nèi)存頁讓出空間蚁堤。這個過程稱為頁面置換醉者,也稱內(nèi)存交換

然而EG思础!iOS并不支持內(nèi)存交換機制3识印剥槐!
大多數(shù)移動設備都不支持內(nèi)存交換機制。移動設備上的大容量存儲器通常是閃存(Flash)掂咒,它的讀寫速度遠遠小于電腦所使用的的硬盤才沧,這就導致了在移動設備上,就算使用了內(nèi)存交換也不能提升性能绍刮。其次温圆,移動設備本身容量就經(jīng)常短缺,閃存的讀寫壽命也非常有限孩革,所以這種情況下還有進行內(nèi)存交換就非常不劃算了岁歉。

ASLR

程序的代碼在不修改的情況下,每次加載到虛擬內(nèi)存的地址是一樣的膝蜈,這樣的方式并不安全锅移,為了解決地址固定的問題,出現(xiàn)了ASLR技術饱搏。
ASLR(Address space layout randomization):地址空間配置隨機加載非剃,是一種防范內(nèi)存損壞漏洞被利用的計算機安全技術。

地址空間配置隨機加載利用隨機方式配置數(shù)據(jù)地址空間推沸,使某些敏感數(shù)據(jù)配置到一個惡意程序無法事先獲知的地址备绽,令攻擊者難以進行攻擊券坞。

以上就簡單的介紹了下虛擬內(nèi)存的相關知識。接下來是二進制重排部分肺素。

3恨锚、二進制重排

3.1缺頁中斷時間消耗的檢測

前面我們已經(jīng)提到了缺頁中斷,接下來我們通過Profile來檢測一下缺頁中斷的發(fā)生倍靡。
Xcode頂部菜單Product->Profile->Instruments->System Trace

image.png

可以看到我們的項目冷啟動時猴伶,缺頁次數(shù)大概是1200多次,耗時130毫秒塌西,如果項目再大一些他挎,缺頁發(fā)生的更多那么也是一個不小的影響啟動時間的一個因素。

3.2二進制重排原理

創(chuàng)建測試項目雨让,查看代碼的順序雇盖,在Build Settings->Write Link Map File,設置為YES栖忠,然后編譯項目崔挖,來到工程的Build目錄下,找到LinkMap文件

image.png

Build目錄找不到的話從Xcode->Preferences->Locations庵寞,可以看到Derived Data的路徑狸相,可以直接跳轉(zhuǎn)過去。

具體看到LinkMap文件保存了項目再編譯鏈接時的符號順序捐川,以方法/函數(shù)為單位排列脓鹃。

image.png

可以看到和編譯的文件順序是一樣的,目前ViewController中只有一個方法viewDidLoad古沥,所以在這個文件下面ViewController只排列了這一個方法瘸右。

如果按照默認配置,在啟動時會加載大量的與啟動無關的代碼岩齿,導致缺頁太颤。那么如果可以將啟動時需要的方法/函數(shù)排在最前面,就能降低缺頁的發(fā)生盹沈,從而提高應用的啟動速度龄章,這就是二進制重排的核心原理。

3.2二進制重排準備

在工程目錄下創(chuàng)建一個.order文件乞封,按照固定的格式做裙,將啟動時需要的方法/函數(shù)順序排列,然后再去把排列好的.order文件放到Xcode中使用肃晚。在.order中寫入測試順序

-[ViewController viewDidLoad]
_main

最后通過LinkMap文件查看來驗證.order是否生效锚贱。
在Xcode中進行配置.order文件,在Build Settings->Order File中配置

image.png

結(jié)果新的LinkMap中的前兩位的順序確實是我寫入Lucky.order文件的順序关串。
image.png

以上就完成了重排的準備工作拧廊,并且測試也生效了杂穷,接下來的難點就是,怎么能獲取到啟動時需要調(diào)用的所有方法和函數(shù)卦绣。

4、Clang插莊

如果只對于OC方法飞蚓,可以對objc_msgSend方法進行Hook滤港,但是系統(tǒng)調(diào)用的方法中會有一些c、c++的方法函數(shù)趴拧,以及一些block回調(diào)溅漾,這些通過objc_msgSend是無法攔截到的。

LLVM內(nèi)置了一個簡單的代碼覆蓋率檢測的工具(SanitizerCoverage)著榴。它在函數(shù)級添履、基本塊和邊緣級上插入了對用戶自定義函數(shù)的調(diào)用,通過方式脑又,可以順利對OC方法暮胧、C函數(shù)、Block塊问麸、Swift等函數(shù)進行更加全面的攔截往衷。
(官方文檔鏈接)https://clang.llvm.org/docs/SanitizerCoverage.html

4.1配置SanitizerCoverage

搭建測試項目,在Build Settings->Other C Flags中严卖,增加-fsanitize-coverage=trace-pc-guard的配置席舍。
根據(jù)官方文檔的示例,在測試項目中添加以下代碼:

#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
    static uint64_t N;
    if (start == stop || *start) return;
    printf("INIT: %p %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++)
        *x = ++N;
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;

    void *PC = __builtin_return_address(0);
    char PcDescr[1024];

//    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
@end

如果不添加__sanitizer_cov_trace_pc_guard_init方法和__sanitizer_cov_trace_pc_guard編譯會報錯哮笆。

image.png

添加完就可以正常編譯運行了来颤。
打印如下:
image.png

  • __sanitizer_cov_trace_pc_guard_init
    函數(shù)__sanitizer_cov_trace_pc_guard_init是回調(diào)函數(shù),startstop表示一個section的首地址和結(jié)束地址稠肘。這個方法能反應項目中的符號個數(shù)福铅。

// This callback is inserted by the compiler as a module constructor
// into every DSO. 'start' and 'stop' correspond to the
// beginning and end of the section with the guards for the entire
// binary (executable or DSO). The callback will be called at least
// once per DSO and may be called multiple times with the same parameters.

  • __sanitizer_cov_trace_pc_guard
    而函數(shù)__sanitizer_cov_trace_pc_guard則是可以監(jiān)聽到編譯器所有的emit,例如官方給的注釋中的例子:

/ This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
// if(*guard)
// __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
// __sanitizer_cov_trace_pc_guard(guard);

4.2 __sanitizer_cov_trace_pc_guard的測試

我們來測試一下是不是函數(shù)启具,方法本讥,block都會被攔截,添加如下測試代碼:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"touchesBegan方法執(zhí)行");
    test();
}

void(^block)(void) = ^(void){
    NSLog(@"Block執(zhí)行");
};

void test(){
    NSLog(@"test函數(shù)執(zhí)行");
    block();
}

image.png

可以看到這些方法確實都被函數(shù)__sanitizer_cov_trace_pc_guard能攔截到鲁冯。通過查看匯編指令:
image.png

image.png

image.png

可以看到這幾個測試方法后邊都有callq指令拷沸,調(diào)用的都是__sanitizer_cov_trace_pc_guard

可以初步的了解到薯演,Clang插裝的原理是撞芍,只要添加了插裝的標記,編譯器就會在當前項目中跨扮,在所有的方法序无、函數(shù)验毡、block的代碼實現(xiàn)的邊緣,插入一句__sanitizer_cov_trace_pc_guard達到方法帝嗡、函數(shù)晶通、block的全覆蓋。

4.4獲取符號名稱

官方示例代碼中哟玷,用了__builtin_return_address函數(shù)狮辽,該函數(shù)的作用會獲取到當前的返回地址,也就是函數(shù)的調(diào)用者巢寡。
通過Dl_info

typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;

dli_fname:當前的路徑
dli_fbase:地址
dli_sname:調(diào)用的函數(shù)名稱
dli_saddr:函數(shù)地址
所以我們通過dli_sname來拿到函數(shù)名稱喉脖。接下來的工作就是拿到這些名稱(去重),然后把名稱寫入到前面說的.order文件中去抑月,也就完成了重排的工作树叽。

4.5實踐
  • 存儲返回地址
    為了保證線程安全,定義一個原子隊列谦絮,隊列中存儲帶有返回地址的結(jié)構(gòu)體题诵。
//定義原子隊列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號結(jié)構(gòu)體
typedef struct {
    void * pc;
    void * next;
} SYNode;

通過SYNode來存儲,方法__sanitizer_cov_trace_pc_guard中通過函數(shù)__builtin_return_address得到的pc挨稿。
函數(shù)__sanitizer_cov_trace_pc_guard的實現(xiàn)如下:

//HOOK一切的回調(diào)函數(shù)3鹎帷!
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    void *PC = __builtin_return_address(0);
    //創(chuàng)建結(jié)構(gòu)體
    SYNode * node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    //結(jié)構(gòu)體入棧     
    //offsetof:參數(shù)1傳入類型奶甘,將下一個節(jié)點的地址返回給參數(shù)2
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
  • 獲取函數(shù)符號并去重排序
    獲取完畢返回的地址篷店,我們進行排序和去重處理
 //定義數(shù)組
    NSMutableArray<NSString *> * symbleNames = [NSMutableArray array];
    while (YES) {
  //循環(huán)體內(nèi)!進行了攔截3艏摇疲陕!
        SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode,next));
        if (node == NULL) {
            break;
        }
        
        Dl_info info;
        dladdr(node->pc, &info);
        NSString * name = @(info.dli_sname);//獲取函數(shù)名稱,并轉(zhuǎn)字符串
        //oc方法直接返回钉赁,其余的前面加"_"
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        //符號加到符號數(shù)組里
        [symbleNames addObject:symbolName];
    }

    //反向遍歷數(shù)組
    NSEnumerator * em = [symbleNames reverseObjectEnumerator];

//去重
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
    NSString * name;
    while (name = [em nextObject]) {
        if (![funcs containsObject:name]) {//數(shù)組沒有name
            [funcs addObject:name];
        }
    }
    //去掉自己蹄殃!
    [funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
  • 寫入文件并配置
    處理完要進行重排的相關符號,下一步就是把這些寫入.order文件中你踩。
 //寫入文件
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"Lucky.order"];
    NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
    NSLog(@"%@",funcStr);

寫入完畢之后诅岩,我們根據(jù)前邊編譯.order的經(jīng)驗來編譯,至此我們就完成了重排和插裝的過程带膜!可以對實際項目進行測試一下是不是有作用吩谦。


慢慢都堅持這么久了,繼續(xù)加油膝藕!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末式廷,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子芭挽,更是在濱河造成了極大的恐慌滑废,老刑警劉巖蝗肪,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蠕趁,居然都是意外死亡薛闪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進店門俺陋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逛绵,“玉大人,你說我怎么就攤上這事倔韭。” “怎么了瓢对?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵寿酌,是天一觀的道長。 經(jīng)常有香客問我硕蛹,道長醇疼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任法焰,我火速辦了婚禮秧荆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘埃仪。我一直安慰自己乙濒,他們只是感情好,可當我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布卵蛉。 她就那樣靜靜地躺著颁股,像睡著了一般。 火紅的嫁衣襯著肌膚如雪傻丝。 梳的紋絲不亂的頭發(fā)上甘有,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天,我揣著相機與錄音葡缰,去河邊找鬼亏掀。 笑死,一個胖子當著我的面吹牛泛释,可吹牛的內(nèi)容都是我干的滤愕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼胁澳,長吁一口氣:“原來是場噩夢啊……” “哼该互!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起韭畸,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤宇智,失蹤者是張志新(化名)和其女友劉穎蔓搞,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體随橘,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡喂分,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了机蔗。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒲祈。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖萝嘁,靈堂內(nèi)的尸體忽然破棺而出梆掸,到底是詐尸還是另有隱情,我是刑警寧澤牙言,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布酸钦,位于F島的核電站,受9級特大地震影響咱枉,放射性物質(zhì)發(fā)生泄漏卑硫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一蚕断、第九天 我趴在偏房一處隱蔽的房頂上張望欢伏。 院中可真熱鬧,春花似錦亿乳、人聲如沸硝拧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽河爹。三九已至,卻和暖如春桐款,著一層夾襖步出監(jiān)牢的瞬間咸这,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工魔眨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留媳维,地道東北人。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓遏暴,卻偏偏與公主長得像侄刽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子朋凉,可洞房花燭夜當晚...
    茶點故事閱讀 44,652評論 2 354

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