ios啟動優(yōu)化:二進制重排

通過前面的探討赁豆,我們知道內(nèi)存分頁觸發(fā)中斷異常 Page Fault 后椎镣,會阻塞進程杂曲,這個問題是會對性能產(chǎn)生影響庶艾。
實際上在 iOS 系統(tǒng)中,生產(chǎn)環(huán)境的應(yīng)用擎勘,在發(fā)生缺頁中斷進行重新加載時 咱揍,iOS 系統(tǒng)還會對其做一次簽名驗證,因此 iOS 生產(chǎn)環(huán)境的 Page Fault 所產(chǎn)生的耗時要更多棚饵。
對用戶而言煤裙,使用App時第一個直接體驗就是啟動 App 時間,而啟動時期會有大量的噪漾、分類硼砰、三方等等需要加載和執(zhí)行,此時多個 Page Fault 所產(chǎn)生的的耗時往往是不能小覷的欣硼,下面我們就通過二進制重排來優(yōu)化啟動耗時题翰。

抖音團隊分享的一個 Page Fault,開銷在 0.6 ~ 0.8ms诈胜。實際測試發(fā)現(xiàn)不同頁會有所不同 , 也跟 cpu 負荷狀態(tài)有關(guān) , 在 0.1 ~ 1.0 ms 之間 豹障。
二進制重排這個方案最早也是 抖音團隊 分享的,不過他們的解決方案有瑕疵耘斩,下面我們會針對性的解決沼填。

一、原理

假設(shè)在啟動時期我們需要調(diào)用兩個函數(shù) method1method4括授,函數(shù)編譯在 mach-O 中的位置是根據(jù) ld ( Xcode 的鏈接器) 的編譯順序并非調(diào)用順序來的坞笙,因此很可能這兩個函數(shù)分布在不同的內(nèi)存頁上。

原理.png
如上圖荚虚,那么啟動時薛夜,page1page2 都需要從無到有加載到物理內(nèi)存中,從而觸發(fā)兩次 Page Fault版述。
二進制重排 的做法就是將 method1method4 放到一個內(nèi)存頁中梯澜,那么啟動時則只需要加載一次 page 即可,也就是只觸發(fā)一次 Page Fault渴析。
在實際項目中晚伙,我們可以將啟動時需要調(diào)用的函數(shù)放到一起 ( 比如 前10頁中 ) 以盡可能減少 Page Fault吮龄,進而減少啟動耗時。

二咆疗、調(diào)試 Page Fault

最好是卸載App漓帚,重新安裝,調(diào)試第一次啟動的效果午磁。

  1. 打開 Instruments尝抖,選擇 System Trace肾请。
  2. 選擇真機秉颗,選擇工程,選擇啟動嫉你,當(dāng)頁面加載出來的時候登颓,停止搅荞。
  3. 查看 Page Fault,如圖標(biāo)注挺据。
    Page Fault.png

File Backed Page In:即為 Page Fault取具,對應(yīng)的有count,一頁Page Fault最大耗時扁耐,最小耗時等參數(shù)暇检。

如果多次啟動調(diào)試,你會發(fā)現(xiàn)count的波動范圍很大婉称。所以如果想獲取準(zhǔn)確的數(shù)據(jù)块仆,最好重新安裝App或者打開多個App之后,再來調(diào)試王暗。
這是因為內(nèi)存管理機制悔据,殺掉進程時,他所占用的物理內(nèi)存空間俗壹,如果沒有被覆蓋使用科汗,那么這部分內(nèi)存有很大可能一直存在。重新打開绷雏,內(nèi)存就不需要全部初始化头滔。所以 冷熱啟動的界定不能以是否后臺殺死來簡單判斷

三涎显、二進制重排

3.1 Order File

前面說了這么多坤检,那么具體該怎么操作呢?蘋果其實已經(jīng)給我們提供了這個機制期吓。

Order File.png

實際上 二進制重排就是對即將生成的可執(zhí)行文件重新排列早歇,即它發(fā)生在鏈接階段
首先,Xcode 用的鏈接器叫做 ld 箭跳,ld 有一個參數(shù)叫 Order File晨另,我們可以通過這個參數(shù)配置一個 后綴名 為order的文件路徑。在這個 order 文件中谱姓,將你需要的符號按順序?qū)懺诶锩嬲蟆.?dāng)工程 build 的時候,Xcode 會讀取這個文件逝段,打的二進制包就會按照這個文件中的符號順序進行生成對應(yīng)的 mach-O
可以參考一下 libObjc 項目割捅,它已經(jīng)使用了二進制重排進行優(yōu)化奶躯。
libobjc.order.png

是不是看到了ios應(yīng)用啟動加載過程中熟悉的方法。

1亿驾、order 文件里符號寫錯了或不存在會不會有問題:ld 會忽略這些符號嘹黔,如果提供了 link 選項 -order_file_statistics,他們會以 warning 的形式把這些沒找到的符號打印在日志里莫瞬。

2儡蔓、會不會影響上架:不會,order文件只是重新排列了所生成的 mach-O(可執(zhí)行文件) 中函數(shù)表與符號表的順序疼邀。

3.2 如何查看項目符合順序

  1. 可以設(shè)置 Write Link Map File 來設(shè)置是否輸出喂江,默認(rèn)是 noLink Map 是編譯期間產(chǎn)生的 旁振,( ld 的讀取二進制文件順序默認(rèn)是按照 Compile Sources 里的順序 )获询,它記錄了二進制文件的布局。
  2. 修改 Write Link Map FileYES拐袜,然后clean項目并重新編譯
  3. Products -> show in finder吉嚣,上上層文件夾,然后找到一個xxxxx-LinkMap-normal-arm64.txt 的txt文件蹬铺。
    Link map.png

    這個文件的# Symbols: 部分存儲了所有符號的順序尝哆,前面的 .o 等內(nèi)容忽略 。
    Symbols.png

    我們發(fā)現(xiàn)符號順序明顯是按照 Compile Sources 的文件順序來排列的甜攀。
    文件中最左側(cè)地址就是 方法真實實現(xiàn)地址(實際代碼地址)而并非符號地址 , 因此我們二進制重排并非只是修改符號地址 , 而是利用符號順序 , 重新排列整個代碼在文件的偏移地址 , 將啟動需要加載的方法地址放到前面內(nèi)存頁中 , 以此達到減少 page fault 的次數(shù)從而實現(xiàn)時間上的優(yōu)化秋泄。

終端查看符號表命令(不準(zhǔn)確,僅供參考)赴邻。找到可執(zhí)行文件:
nm (file):查看符號表
nm -p (file):按照orderfile順序
nm -up (file): 只看系統(tǒng)
nm -Up (file):只看自定義

3.3實戰(zhàn)

1印衔、 新建一個項目,添加方法:

binary.png
2姥敛、修改配置奸焙,編譯,找到xxx.txt文件
截圖.png

3、新建一個order文件:touch binary.order与帆,加入幾個方法

-[ViewController test3]
-[ViewController test2]
-[ViewController test1]

4了赌、修改Order File配置為:$(SRCROOT)/Binary/binary.order./Binary/binary.order

order file.png

5玄糟、clean勿她,編譯,再次查看xxx.txt文件阵翎。
截圖.png
oh my god逢并,我們所寫的這三個方法已經(jīng)被放到最前面了,也就是說郭卫,這三個方法被放到了距離 mach-O 中首地址偏移量最小位置砍聊。假設(shè)這三個方法原本在不同的三頁,那么意味著我們已經(jīng)優(yōu)化掉了兩個 Page Fault贰军。

3.4 獲取啟動執(zhí)行的函數(shù)

到這里玻蝌,離啟動優(yōu)化就只差一步了,如何獲取啟動運行的函數(shù)词疼?大致有三種方案俯树,僅供參考:

  1. hook objc_MsgSend:只能拿到 oc 以及 swift @objc dynamic 后的方法,并且由于可變參數(shù)個數(shù)贰盗,需要用匯編來獲取參數(shù) 许饿。
  2. 靜態(tài)掃描 machO 特定段和節(jié)里面所存儲的符號以及函數(shù)數(shù)據(jù)。
  3. clang 插樁:完全拿到 swift舵盈、oc米辐、cblock 全部函數(shù)书释。

四翘贮、Clang插樁

關(guān)于 clang 的插樁覆蓋的官方文檔如下 : clang 自帶代碼覆蓋工具 文檔中有詳細概述,以及簡短Demo演示爆惧。
思路:一是自己編寫 clang 插件狸页,另外一個就是利用 clang 本身已經(jīng)提供的一個工具來實現(xiàn)我們獲取所有符號的需求。

4.1 靜態(tài)插樁代碼

下面我們來探索一下這個靜態(tài)插樁代碼覆蓋工具的機制和原理扯再。
1芍耘、添加編譯設(shè)置:直接搜索 Other C Flags 來到 Apple Clang - Custom Compiler Flags 中 , 添加配置:-fsanitize-coverage=trace-pc-guard
2熄阻、在ViewController.m添加代碼:

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.

  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  //__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

3斋竞、運行(最好是一個空工程,注釋我們前面手動添加的方法)秃殉,查看打影映酢:

trace-pc-guard.png
通過打印startstop兩個指針地址浸剩,會發(fā)現(xiàn)他存儲的實際上是 1-15 幾個序號。
4鳄袍、添加一個oc方法绢要,我們再次打印startstop指針,你會發(fā)現(xiàn)序號變?yōu)?1-16拗小。
繼續(xù)添加一個c函數(shù)重罪,一個block,一個touch函數(shù)哀九,是不是驚喜的發(fā)現(xiàn)剿配,序號增加到 19 了。
89dfb9d8a201.png

此時阅束,我們是不是可以大膽的猜想:這個內(nèi)存區(qū)間保存的就是工程所有符號的個數(shù)惨篱。
5、繼續(xù)围俘,清空打印,點擊屏幕琢融。是不是發(fā)現(xiàn)有兩次輸出界牡,看代碼,此時有兩次方法的調(diào)用漾抬。最終我們發(fā)現(xiàn):調(diào)用幾個方法宿亡,就會打印幾次 guard:

此時查看匯編纳令,你會發(fā)現(xiàn):在每個函數(shù)調(diào)用的第一句實際代碼挽荠,會被添加進去了一個 bl 指令, 調(diào)用到 __sanitizer_cov_trace_pc_guard 這個函數(shù)中來 平绩。
bl圈匆,匯編跳轉(zhuǎn)指令,即調(diào)用方法捏雌。bl之前是棧平衡與寄存器數(shù)據(jù)準(zhǔn)備跃赚,不用關(guān)心。

這就是靜態(tài)插樁:靜態(tài)插樁實際上是在編譯期性湿,在每一個函數(shù)內(nèi)部第一行代碼處纬傲,添加 hook 代碼 ( 即我們添加的 __sanitizer_cov_trace_pc_guard 函數(shù) ) ,實現(xiàn)全局的方法 hook肤频,即AOP效果叹括。

4.2 獲取函數(shù)符號

通過上面的分析我們知道,所有函數(shù)的第一步都會調(diào)用__sanitizer_cov_trace_pc_guard宵荒,那我們是不是可以通過這個函數(shù)獲取函數(shù)符號呢汁雷?
熟悉匯編的應(yīng)該知道:函數(shù)嵌套時 , 在跳轉(zhuǎn)子函數(shù)時净嘀,都會保存下一條指令的地址在 x30 ( 又叫 lr 寄存器) 里 。

例如 , A 函數(shù)中調(diào)用了 B 函數(shù)摔竿,在 arm 匯編中即 bl + 0x**** 指令面粮,該指令會首先將下一條匯編指令的地址保存在 x30 寄存器中。然后在跳轉(zhuǎn)到 bl 后面?zhèn)鬟f的指定地址去執(zhí)行继低。
bl 能實現(xiàn)跳轉(zhuǎn)到某個地址的匯編指令熬苍,其原理就是修改 pc 寄存器的值來指向到要跳轉(zhuǎn)的地址,而且實際上 B 函數(shù)中也會對 x29 / x30 寄存器的值做保護袁翁,防止子函數(shù)又跳轉(zhuǎn)其他函數(shù)會覆蓋掉 x30 的值 , 當(dāng)然葉子函數(shù)除外柴底。
當(dāng) B 函數(shù)執(zhí)行 ret 也就是返回指令時,就會去讀取 x30 寄存器的地址粱胜,跳轉(zhuǎn)過去柄驻,因此也就回到了上一層函數(shù)的下一步。
__sanitizer_cov_trace_pc_guard 函數(shù)中的這一句代碼:

void *PC = __builtin_return_address(0); 

它的作用其實就是去讀取 x30 中所存儲的要返回時下一條指令的地址焙压。所以他名稱叫做 __builtin_return_address鸿脓。換句話說,這個地址就是我當(dāng)前這個函數(shù)執(zhí)行完畢后涯曲,要返回到哪里去野哭。
bt 函數(shù)調(diào)用棧也是這種思路來實現(xiàn)的。也就是說 , 我們可以在 __sanitizer_cov_trace_pc_guard 這個函數(shù)中 , 通過 __builtin_return_address 函數(shù)拿到原函數(shù)調(diào)用 __sanitizer_cov_trace_pc_guard 這句匯編代碼的下一條指令的地址幻件。

c5eaed5e0295.png

如圖拨黔,PC的指向就是,當(dāng)test1函數(shù)執(zhí)行完__sanitizer_cov_trace_pc_guard后绰沥,下一行代碼NSLog篱蝇。

那么問題又來了,如果通過函數(shù)內(nèi)部內(nèi)存地址徽曲,獲取函數(shù)名稱呢零截?

熟悉安全攻防,逆向的同學(xué)可能會清楚秃臣。我們?yōu)榱朔乐鼓承┨囟ǖ姆椒ū粍e人使用 fishhook hook 掉瞻润,會利用 dlopen 打開動態(tài)庫,拿到一個句柄甜刻,進而拿到函數(shù)的內(nèi)存地址直接調(diào)用绍撞。那我們可以反過來使用。

dlopen.h 相同 , 在 dlfcn.h 中有一個方法如下 :

typedef struct dl_info {
        const char      *dli_fname;     /* 所在文件 */
        void            *dli_fbase;     /* 文件地址 */
        const char      *dli_sname;     /* 符號名稱 */
        void            *dli_saddr;     /* 函數(shù)起始地址 */
} Dl_info;

//這個函數(shù)能通過函數(shù)內(nèi)部地址找到函數(shù)符號
int dladdr(const void *, Dl_info *);

我們在項目中實踐一下得院,先導(dǎo)入頭文件 #import <dlfcn.h>傻铣,然后修改代碼如下 :

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;  // Duplicate the guard check.

    void *PC = __builtin_return_address(0);

    Dl_info info;
    dladdr(PC, &info);

    printf("\nfname:%s \nfbase:%p \nsname:%s\nsaddr:%p \n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);

    char PcDescr[1024];
    //__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

打印結(jié)果:

fname:/Users/00393998/Library/Developer/CoreSimulator/Devices/23342248-4844-41AB-9851-2023D815FAA2/data/Containers/Bundle/Application/9A86A08B-5411-4909-B62B-B27097CA2EC9/Binary.app/Binary 
fbase:0x10beee000 
sname:-[ViewController touchesBegan:withEvent:]
saddr:0x10beef9d0 
guard: 0x10bef468c 6 PC ?

fname:/Users/00393998/Library/Developer/CoreSimulator/Devices/23342248-4844-41AB-9851-2023D815FAA2/data/Containers/Bundle/Application/9A86A08B-5411-4909-B62B-B27097CA2EC9/Binary.app/Binary 
fbase:0x10beee000 
sname:testFunc
saddr:0x10beef9b0 
guard: 0x10bef4688 5 PC \367\371\356??

4.3 寫入order文件

寫入文件時有許多需要注意的地方,即坑點

1祥绞、多線程

考慮到這個方法會來特別多次非洲,使用鎖會影響性能鸭限,這里使用蘋果底層的原子隊列 ( 底層實際上是個棧結(jié)構(gòu),利用隊列結(jié)構(gòu) + 原子性來保證順序 ) 來實現(xiàn)两踏。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //遍歷出隊
    while (true) {
        //offset 通過next指針在結(jié)構(gòu)體的偏移量败京,進而知道next的指向
        //offsetof 就是針對某個結(jié)構(gòu)體找到某個屬性相對這個結(jié)構(gòu)體的偏移量
       // offsetof(SymbolNode, next) 可以替換為 8
        SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
        if (node == NULL) break;
        Dl_info info;
        dladdr(node->pc, &info);
        
        printf("%s \n",info.dli_sname);
    }
}
//原子隊列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定義符號結(jié)構(gòu)體
typedef struct{
    void * pc;
    void * next;
}SymbolNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;  // Duplicate the guard check.
    void *PC = __builtin_return_address(0);
    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    
    //入隊
    // offsetof 用在這里是為了入隊添加下一個節(jié)點找到 前一個節(jié)點next指針的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}
2、死循環(huán)

上述這種 clang 插樁的方式梦染,會在while循環(huán)中同樣插入 hook 代碼赡麦。
通過匯編會查看到 while 循環(huán),會被多次靜態(tài)加入 __sanitizer_cov_trace_pc_guard 調(diào)用帕识,導(dǎo)致死循環(huán)泛粹。
解決方式:Other C Flags 修改為如下:-fsanitize-coverage=func,trace-pc-guardfunc:表示僅 hook函數(shù)時調(diào)用肮疗。

cbnz:匯編執(zhí)行晶姊,while循環(huán)。

3伪货、load方法

load 方法時们衙,__sanitizer_cov_trace_pc_guard 函數(shù)的參數(shù) guard0,所以打印并沒有發(fā)現(xiàn) load碱呼。屏蔽掉 __sanitizer_cov_trace_pc_guard 函數(shù)中的:if (!*guard) return;

拓展:如果我們希望從某個函數(shù)之后/之前開始優(yōu)化蒙挑,那么我們可以通過一個全局靜態(tài)變量,在特定的時機修改其值巍举,在 __sanitizer_cov_trace_pc_guard 這個函數(shù)中做好對應(yīng)的處理即可。

4凝垛、其他處理
  1. 由于用的先進后出原因 , 我們要 倒敘 一下
  2. 去重
  3. order 文件格式要求:c函數(shù)懊悯、block 前面還需要加 _下劃線。
    核心代碼(不要忘記編譯配置哦):
//引入頭文件
#import <dlfcn.h>
#import <libkern/OSAtomic.h>


//核心代碼
#pragma mark - 獲取order文件

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    while (YES) {
        //offsetof 就是針對某個結(jié)構(gòu)體找到某個屬性相對這個結(jié)構(gòu)體的偏移量
        SymbolNode * node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));
        if (node == NULL) break;
        Dl_info info;
        dladdr(node->pc, &info);
        
        NSString * name = @(info.dli_sname);
        
        // 添加 _
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        
        //去重
        if (![symbolNames containsObject:symbolName]) {
            [symbolNames addObject:symbolName];
        }
    }

    //取反
    NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
    NSLog(@"%@",symbolAry);
    
    //將結(jié)果寫入到文件
    NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"binary.order"];
    NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
    BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    if (result) {
        NSLog(@"%@",filePath);
    }else{
        NSLog(@"文件寫入出錯");
    }
    
}
//原子隊列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號結(jié)構(gòu)體
typedef struct{
    void * pc;
    void * next;
}SymbolNode;


#pragma mark - 靜態(tài)插樁代碼

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                         uint32_t *stop) {
    static uint64_t N;  // Counter for the guards.
    if (start == stop || *start) return;  // Initialize only once.
    printf("INIT: %p %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++)
        *x = ++N;  // Guards should start from 1.
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    //if (!*guard) return;  // Duplicate the guard check.
    
    void *PC = __builtin_return_address(0);
    
    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    
    //入隊
    // offsetof 用在這里是為了入隊添加下一個節(jié)點找到 前一個節(jié)點next指針的位置
    OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
}

最后運行梦皮,下載.order文件到本地炭分,就可以愉快的玩耍了。

五剑肯、補充

5.1 swift / OC 混編工程問題

通過如上方式適合純 OC 工程獲取符號捧毛。由于 swift 的編譯器前端是自己的 swift 編譯前端程序,因此配置稍有不同让网。搜索 Other Swift Flags呀忧,添加兩條配置即可:-sanitize-coverage=func、 -sanitize=undefined溃睹。swift類同樣可以通過這個方式獲取而账。

5.2 cocoapod 工程問題

cocoapod 工程引入的庫,會產(chǎn)生多 target因篇,我們在主target添加的配置是不會生效的泞辐,我們需要針對需要的target做對應(yīng)的設(shè)置笔横。
對于直接手動導(dǎo)入到工程里的 sdk,不管是 靜態(tài)庫 .a 還是 動態(tài)庫咐吼,會默認(rèn)使用主工程的設(shè)置吹缔,也就是可以拿到符號的。

參考:
抖音研發(fā)實踐:基于二進制文件重排的解決方案 APP啟動速度提升超15%

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锯茄,一起剝皮案震驚了整個濱河市厢塘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌撇吞,老刑警劉巖俗冻,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異牍颈,居然都是意外死亡迄薄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門煮岁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讥蔽,“玉大人,你說我怎么就攤上這事画机∫鄙。” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵步氏,是天一觀的道長响禽。 經(jīng)常有香客問我,道長荚醒,這世上最難降的妖魔是什么芋类? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮界阁,結(jié)果婚禮上侯繁,老公的妹妹穿的比我還像新娘。我一直安慰自己泡躯,他們只是感情好贮竟,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著较剃,像睡著了一般咕别。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上写穴,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天顷级,我揣著相機與錄音,去河邊找鬼确垫。 笑死弓颈,一個胖子當(dāng)著我的面吹牛帽芽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播翔冀,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼导街,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了纤子?” 一聲冷哼從身側(cè)響起搬瑰,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎控硼,沒想到半個月后泽论,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡卡乾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年翼悴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幔妨。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡鹦赎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出误堡,到底是詐尸還是另有隱情古话,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布锁施,位于F島的核電站陪踩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏悉抵。R本人自食惡果不足惜肩狂,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望基跑。 院中可真熱鬧婚温,春花似錦描焰、人聲如沸媳否。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽篱竭。三九已至,卻和暖如春步绸,著一層夾襖步出監(jiān)牢的瞬間掺逼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工瓤介, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吕喘,地道東北人赘那。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像氯质,于是被迫代替她去往敵國和親募舟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355