58同城iOS混編項目無用代碼檢測方案介紹

摘要:本文主要介紹如何通過對Mach-O文件的解析以及反匯編的應(yīng)用實現(xiàn)OC&Swift的無用代碼檢測崭篡,重點介紹Swift的檢測方案。本文作為Swift Mach-O的應(yīng)用篇畸悬,建議先閱讀《從Mach-O角度談?wù)凷wift和OC的存儲差異》《Swift Hook新思路--虛函數(shù)表》了解相關(guān)概念和結(jié)構(gòu)顽腾。相關(guān)代碼已經(jīng)開源:WBBlades,如果感覺工具或方案對您有幫助不妨幫忙點個star忧设。

背景

近期很多大型APP都在做支持Swift與Objective-C的混編開發(fā)的工作,58集團(tuán)旗下的各個APP也在積極探索使用Swift語言開發(fā)颠通。因此可以預(yù)見址晕,在未來的幾年里集團(tuán)內(nèi)各個iOS項目中Swift代碼的占比會越來越高。因此我們需要考慮Swift代碼激增后所帶來的一些問題顿锰。如何檢測混編項目中無用代碼是我們面臨的諸多問題之一谨垃。

關(guān)于無用代碼檢測

無用代碼需不需要檢測?需不需要刪除硼控?無用代碼刪除在所有的性能優(yōu)化手段里基本上是ROI最低的刘陶。但是幾乎所有ROI較高的技術(shù)手段都是一次性優(yōu)化方案,經(jīng)過幾個版本迭代后再做優(yōu)化就會比較乏力牢撼。相比之下匙隔,針對代碼的檢測和刪除在很長的一段時間內(nèi)提供了很大的優(yōu)化空間。我們以58APP的10.15.1版本為例熏版,iPhone 7設(shè)備上的App Store正式包中主二進(jìn)制文件的大小占APP包大小的66%纷责,動態(tài)庫占15%捍掺,而資源占比不足20%。


圖片2.png

在越獄設(shè)備上獲取從App Store下載的包可以準(zhǔn)確查看當(dāng)臺設(shè)備上的包構(gòu)成(個人認(rèn)為這是最準(zhǔn)確的測算方式)再膳。58APP的資源占比較小是因為我們主要使用xcassert存儲圖片挺勿,這可以充分利用分片下發(fā)的能力。如果你的圖片存儲依舊使用bundle存儲喂柒,那么可能資源的比例會相對高一些不瓶,在這種情況下建議先將資源轉(zhuǎn)存到xcassert。

除了包大小優(yōu)化外灾杰,及時刪除無用代碼對啟動優(yōu)化也有一定的幫助蚊丐。另外,無用代碼檢測和刪除在項目維護(hù)上起到了很重要的作用艳吠。冗余的代碼往往意味著開發(fā)者需要更多額外的精力來評估需求的影響范圍麦备,及時刪除廢棄的代碼可以從一定程度上提升開發(fā)效率。無用代碼的靜態(tài)檢測并不是要將項目中的所有無用代碼都檢測出來讲竿,而是能為后續(xù)的檢測流程在龐大的源碼庫中提供一個圈選能力泥兰。因此靜態(tài)檢測需要提供相當(dāng)數(shù)量的疑似無用代碼集合,并且在這個集合中無用代碼的比例應(yīng)該盡可能的高题禀。靜態(tài)檢測的準(zhǔn)確度有限鞋诗,并不能作為單一手段,因此只能起到前置過濾的作用迈嘹。在58同城中削彬,除了WBBlades檢測外,還有根據(jù)業(yè)務(wù)代碼特征的二次過濾以及運(yùn)行時判斷等手段秀仲。

混編項目無用代碼檢測的幾種手段

在OC開發(fā)環(huán)境中融痛,無用代碼的檢測方案比較多,但是OC&Swift混編環(huán)境的無用代碼檢測方案相對較少神僵。原因是OC與Swift無論是在編譯前端還是編譯后的二進(jìn)制文件上雁刷,都存在較大的差異。這就導(dǎo)致OC的檢測方案不一定適用于Swift保礼,而Swift的檢測方案也不一定適用于OC沛励。目前業(yè)界常用的技術(shù)手段包括AppCode工具檢測以及以例如Pecker這樣的基于 IndexStoreDBSwiftSyntax的靜態(tài)檢測方案,通過SwiftSyntax獲取所有符號并通過IndexStoreDB獲取符號之間的索引關(guān)系炮障,從而確定哪些代碼之間的引用關(guān)系目派,得到無用代碼集合。當(dāng)然除了這兩種技術(shù)方案外胁赢,還有很多其他的方案企蹭,例如:源碼文本分析、針對framework目標(biāo)文件優(yōu)化的技術(shù)方案、基于Mach-O文件分析的技術(shù)等等谅摄。至于選擇哪種技術(shù)方案主要取決于當(dāng)前的工具在什么場景下使用徒河,甚至不同代碼量的APP最優(yōu)方案也不同。58APP在接入Swift語言之前就已經(jīng)確定了基于Mach-O分析的無用代碼檢測方案螟凭,這主要是該方案比較容易接入到版本流程中虚青。因此它呀,為了保持技術(shù)方案的統(tǒng)一螺男,在項目混編后我們依舊采用基于Mach-O文件分析的方式來實現(xiàn)無用代碼檢測。

OC是如何實現(xiàn)無用代碼檢測的纵穿?

OC的無用代碼檢測和優(yōu)化方案有很多種下隧,優(yōu)化方案遍布編譯、鏈接谓媒、Product淆院、運(yùn)行等各個階段。58同城采用的是對Product的掃描以及運(yùn)行時核驗的雙重保障機(jī)制句惯。其中對Product的掃描就是通過WBBlades掃描Mach-O文件來實現(xiàn)的土辩。基本思路就是對classlist和classrefs做差集抢野,形成初步的無用類集合拷淘,并根據(jù)業(yè)務(wù)代碼特征做二次適配。例如:作為基類或者成員變量的類指孤、通過完整字符串實現(xiàn)動態(tài)調(diào)用的類启涯、RN或者Hybrid的Module通過load方法注冊的類等都會被當(dāng)做有用的代碼,不會出現(xiàn)在無用代碼集合中恃轩,減少了二次核驗的成本结洼。但是此套方案無法直接應(yīng)用與Swift語言開發(fā)的項目,接下來我們來探討下原因及解決方案叉跛。

Swfit的類調(diào)用

在OC的檢測方案中松忍,很大程度上是依賴classlist和classrefs做差集來實現(xiàn)的。其他技術(shù)手段不過是作為補(bǔ)充技術(shù)手段筷厘。如果沒有classrefs這樣一個section為我們提供主要信息鸣峭,那么整個方案的技術(shù)基礎(chǔ)就會受到動搖。那我們首先要弄清楚類如何使用會被存儲到classrefs中敞掘。首先我們來看個示例:

WBBladesClass *b = nil; 
id c = [WBBladesClass new];
Class d = NSClassFromString(@"WBBladesClass");

上面示例中只有通過[WBBladesClass new]顯式的方法調(diào)用時叽掘,OC的類才會被存儲classrefs中。

那Swift的類是不是也存在這樣的特性呢玖雁?

在Swift調(diào)用環(huán)境中更扁,被顯式調(diào)用的類并不會被加入的classrefs這個section中。下面的代碼經(jīng)過編譯鏈接后,查看MachOView發(fā)現(xiàn)TestClass0和TestClass1這兩個類并不在classrefs中浓镜。

class TestClass0: NSObject {
    dynamic func hello() {
        let obj = TestClass0.init()
    }
}
class TestClass1 {
    func hello() {
        let obj = TestClass1.init()
    }
}

但是溃列,如果類被導(dǎo)出到OC環(huán)境中使用,那么這個Swift類就會被加入到classrefs中膛薛。

class TestClass2 : NSObject{}
//在OC環(huán)境中調(diào)用則會被加入到classrefs中
+ (void)load{
    id obj = [TestClass2 new];
}
只有TestClass2被加入到classrefs

因此可以說明classrefs只適用于OC的語言環(huán)境听隐,即使刨除Struct、enum等類型不談哄啄,classlist和classrefs做差集的方案也不適用于Swift的無用代碼檢測雅任。

那如何才能識別出來一個Swift類型被調(diào)用呢?

那么問題來了咨跌,如果沒有classrefs做記錄沪么,如何才能知道一個Swift的類被使用了呢?之前我們在《從Mach-O角度談?wù)凷wift和OC的存儲差異》和 《Swift Hook新思路--虛函數(shù)表》中詳細(xì)介紹了Swift的類的存儲結(jié)構(gòu)锌半。

struct ClassContextDescriptor{
    uint32_t Flag;
    uint32_t Parent;
    int32_t  Name;
    int32_t  AccessFunction;
    int32_t  FieldDescriptor;
    int32_t  SuperclassType;
    uint32_t MetadataNegativeSizeInWords;
    uint32_t MetadataPositiveSizeInWords;
    uint32_t NumImmediateMembers;
    uint32_t NumFields;
    uint32_t FieldOffsetVectorOffset;
    <泛型簽名> //字節(jié)數(shù)與泛型的參數(shù)和約束數(shù)量有關(guān)
    <MaybeAddResilientSuperclass>//有則添加4字節(jié)
    <MaybeAddMetadataInitialization>//有則添加4*3字節(jié)
    VTableList[]//先用4字節(jié)存儲offset/pointerSize禽车,再用4字節(jié)描述數(shù)量,隨后N個4+4字節(jié)描述函數(shù)類型及函數(shù)地址刊殉。
    OverrideTableList[]//先用4字節(jié)描述數(shù)量殉摔,隨后N個4+4+4字節(jié)描述當(dāng)前被重寫的類、被重寫的函數(shù)描述记焊、當(dāng)前重寫函數(shù)地址逸月。
}

在這里可能有同學(xué)會有疑問,上述結(jié)構(gòu)與調(diào)試時的結(jié)構(gòu)不相符亚亲。調(diào)試時彻采,Swift的類的結(jié)構(gòu)應(yīng)該如下所示:

struct SwiftMetadataClass {
    NSInteger kind;
    id superclass;
    NSInteger reserveword1;
    NSInteger reserveword2;
    NSUInteger rodataPointer;
    UInt32 classFlags;
    UInt32 instanceAddressPoint;
    UInt32 instanceSize;
    UInt16 instanceAlignmentMask;
    UInt16 runtimeReservedField;
    UInt32 classObjectSize;
    UInt32 classObjectAddressPoint;
    NSInteger nominalTypeDescriptor;
    NSInteger ivarDestroyer;
    ...//N個函數(shù)地址
};

在runtime中我們通過類.self獲取到的是struct SwiftMetadataClass ,而我們提到存儲結(jié)構(gòu)指的是ClassContextDescriptor捌归,兩者并不是一個結(jié)構(gòu)肛响。

//通過這樣的強(qiáng)制轉(zhuǎn)換能清楚的發(fā)現(xiàn)TestClass2的supperclass等信息
struct SwiftMetadataClass* swiftClass = 
(__bridge struct SwiftMetadataClass * )(TestClass2.self);

SwiftMetadataClass是Swift的運(yùn)行態(tài)數(shù)據(jù),在Mach-O文件中惜索,類的SwiftMetadataClass結(jié)構(gòu)體存儲在DATA段中特笋。細(xì)心的同學(xué)會發(fā)現(xiàn),Swift的Mach-O相比OC多了一個名為(__ TEXT,__const)的section巾兆。這個section中存儲的就是Swift的TypeContextDescriptorClassContextDescriptor的父類)結(jié)構(gòu)猎物。TypeContextDescriptorSwiftMetadataClass而言更接近源碼形態(tài),通過TypeContextDescriptor我們能很容易知道這個代碼在哪個Module中定義的角塑、有多少屬性蔫磨、屬性都是什么類型、是否是泛型圃伶、有多少函數(shù)堤如、又有哪些函數(shù)是重寫父類的等等蒲列。但是由于MachOView并沒有很好地適配好Swift的Mach-O文件,我們看到的這個section是未經(jīng)格式化展示的二進(jìn)制數(shù)據(jù)搀罢。

ClassContextDescriptor和SwiftMetadataClass又有什么關(guān)系呢蝗岖?

簡而言之SwiftMetadataClass.nominalTypeDescriptor指向的就是這個類的ClassContextDescriptor,而ClassContextDescriptor則是通過ClassContextDescriptor.AccessFunction的函數(shù)調(diào)用獲取到對應(yīng)的SwiftMetadataClass地址榔至。

 let tclass = TestClass1.self

斷點查看執(zhí)行就會發(fā)現(xiàn)抵赢,在獲取到類地址之前,總會先調(diào)用TestClass1的metadata accessor函數(shù)(其實就是TestClass1的ClassContextDescriptor.AccessFunction

bl  0x100a3b32c ; type metadata accessor for BBB.TestClass1

這就意味著唧取,我們只要在匯編代碼中找到某個類的AccessFunction被調(diào)用了也就知道這個類是被使用的類铅鲤。

如何在匯編代碼中查找AccessFunction?

在Mach-O文件中兵怯,代碼都是以機(jī)器指令的形式存儲的彩匕,我們不能直接獲取到助記符和操作數(shù)腔剂。因此需要借助反匯編庫進(jìn)行反匯編操作媒区,將指令轉(zhuǎn)為匯編代碼。有了匯編代碼掸犬,我們只要在每個函數(shù)的指令區(qū)間內(nèi)查找是否有某個類的AccessFunction地址袜漩,就能知道這個函數(shù)中是否調(diào)用了某個類。

怎么才能知道每個函數(shù)的函數(shù)指令區(qū)間湾碎?

這一步很簡單宙攻,在Debug模式的Mach-O文件中,符號表會告訴我們每個符號對應(yīng)的地址介褥。函數(shù)作為符號的一種座掘,當(dāng)然符號表也記錄了函數(shù)的地址,即下面結(jié)構(gòu)體中的n_value柔滔。

/*
 * This is the symbol table entry structure for 64-bit architectures.
 */
struct nlist_64 {
    union {
        uint32_t  n_strx; /* index into the string table */
    } n_un;
    uint8_t n_type;        /* type flag, see below */
    uint8_t n_sect;        /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};

但是每個符號只知道這個符號的名字以及符號的起始地址溢陪。以函數(shù)為例,函數(shù)通過符號表只能知道函數(shù)名以及函數(shù)的起始地址睛廊,雖然可以通過靜態(tài)分析ret指令來粗略地判斷函數(shù)結(jié)尾形真,但在Swift的匯編代碼中這種方式存在較大的偏差。因此需要換種方案超全,采用較為直接的方式咆霜,借助符號表將匯編指令切割分段來實現(xiàn)函數(shù)指令區(qū)間的判斷,這也是WBBlades需要分析Debug包的原因之一嘶朱。


未命名2.001.jpeg

具體做法是蛾坯,首先對符號表按地址進(jìn)行排序,然后把下個符號的起始地址當(dāng)做當(dāng)前函數(shù)的截止點疏遏。這樣就實現(xiàn)了函數(shù)指令區(qū)間的切割脉课。

遇到的有意思的問題

在為WBBlades做Swift適配時發(fā)現(xiàn)了很多有意思的問題挂疆,也是開發(fā)過程中踩到的一系列坑。

  • section判斷不嚴(yán)謹(jǐn)下翎。

之前字節(jié)跳動發(fā)過一篇文章《今日頭條優(yōu)化實踐: iOS 包大小二進(jìn)制優(yōu)化缤言,一行代碼減少 60 MB 下載大小》,可能有些APP做了section遷移视事。如果APP做了section遷移的話胆萧,原本處于一個段的兩個section變成了不同segment中的兩個section。由于不同的段的base address可能不同俐东,因此一旦地址計算出現(xiàn)跨段的時候跌穗,都需要做base address地址修正,否則文件的偏移地址可能會取錯虏辫。另外蚌吸,由于段名可能存在自定義的情況,因此也不能通過段名+節(jié)名的方式來確定唯一的一個section砌庄。需要通過段的權(quán)限+節(jié)名來確定section羹唠。

if ((segmentCommand.maxprot & (VM_PROT_WRITE | VM_PROT_READ)) ==
 (VM_PROT_WRITE | VM_PROT_READ)) {
    //能夠具有讀寫權(quán)限的的段,即可認(rèn)為為__DATA,__CONST_DATA,__AUTH_CONST等
 }

當(dāng)然娄昆,這種判斷方式也不是完全準(zhǔn)確佩微,因為section遷移后,新增的段默認(rèn)是讀寫權(quán)限萌焰,這也意味著原先的TEXT中的數(shù)據(jù)哺眯,遷移后可能變成了VM_PROT_WRITE | VM_PROT_READ。這也是段遷移后需要重新設(shè)置權(quán)限的原因扒俯。

  • 獲取類名循環(huán)遍歷Parent可能發(fā)生異常
//類似這樣的代碼(Type的Parent可能不屬于Type)
func extensions(of value: Any) {
  struct Extensions : AnyExtensions {}
   return
}

Swift與OC有個區(qū)別就是在Swift中很多地方都可以定義類或結(jié)構(gòu)體奶卓。例如上面的代碼中,就是在一個函數(shù)中定義了一個結(jié)構(gòu)體撼玄。這時在遍歷Extensions這個結(jié)構(gòu)體時需要注意夺姑,它的Parent并不是一個Model Type類型,因此需要在套用結(jié)構(gòu)體解析二進(jìn)制的時候需要判斷處理下互纯。

  • 復(fù)雜的泛型結(jié)構(gòu)

之所以說泛型復(fù)雜是因為泛型的簽名是不定長的數(shù)據(jù)瑟幕。它取決于泛型的參數(shù)格式和條件個數(shù)。泛型到底占多少字節(jié)留潦,可以參考下面的布局說明只盹。

內(nèi)容 字節(jié)數(shù) 備注
addMetadataInstantiationCache 4B class only
addMetadataInstantiationPattern 4B class only
GenericParamCount 2B
GenericRequirementCount 2B
GenericKeyArgumentCount 2B
GenericExtraArgumentCount 2B
params GenericParamCount
pandding (unsigned)-GenericParamCount & 3 填補(bǔ),4字節(jié)對齊
EachParam 3 * 4 * GenericRequirementCount
  • Anonymous布局

Anonymous官方解釋如下

/// This context descriptor represents an anonymous possibly-generic context
/// such as a function body.
Anonymous = 2,

與類兔院、結(jié)構(gòu)體等布局不同殖卑,Anonymous在二進(jìn)制中的布局如下:

Flag(4Byte) + Parent(4Byte) + 泛型簽名(不定長)+ mangleName(4Byte)

但是Anonymous 不一定會存在mangleName,因此在解析Anonymous 還需要判斷是否存在 mangleName坊萝。

/// Flags for anonymous type context descriptors. These values are used as the
/// kindSpecificFlags of the ContextDescriptorFlags for the anonymous context.
class AnonymousContextDescriptorFlags : public FlagSet<uint16_t> {
  enum {
    /// Whether this anonymous context descriptor is followed by its
    /// mangled name, which can be used to match the descriptor at runtime.
    HasMangledName = 0,
  };
...
};

如果此時TypeContext為Anonymous孵稽,那么需要查看Flag的前2字節(jié)是否為0许起。如果為0則Anonymous 沒有mangleName。

  • 其他

在Swift中通過fileprivate菩鲜、open等修飾的代碼园细,都會多少些許不同,我們在開源代碼中都做了適配處理接校。另外猛频,有些時候Swift訪問并不是通過AccessFunc,而是直接訪問類的地址蛛勉。這種情況一般會在符號表中存在demangling cache variable for type metadata for開頭的符號鹿寻。

支持范圍

WBBlades做二進(jìn)制掃描檢測時,對APP中包含以下情況的代碼作了測試诽凌。示例中的? 的代碼能被識別為被使用到毡熏。其中V1.1是在適配Swift二進(jìn)制之前,V2.0是經(jīng)過適配之后侣诵。


能否被識別為有用

使用方法

  • 需要檢測的APP需要在Debug環(huán)境下打出一個arm64真機(jī)包痢法。
  • 編譯WBBlades,生成WBBlades可執(zhí)行文件窝趣。https://github.com/wuba/WBBlades
  • 將WBBlades可執(zhí)行文件拖入系統(tǒng)終端疯暑,并輸入-unused ,再將真機(jī)包拖入終端哑舒。
  • Enter,等待幾分鐘幻馁,會在桌面輸出結(jié)果文件洗鸵。如果Swift代碼較多,可能耗時較長仗嗦。

應(yīng)用情況及展望

目前58同城APP中大概存在2w+個類膘滨,1k+個Swift的類型定義。靜態(tài)檢測后發(fā)現(xiàn)OC代碼的無用代碼比例在8%左右稀拐,Swift代碼的無用代碼比例相對較低火邓,大概在2%左右。通過人工復(fù)核后我們發(fā)現(xiàn)部分業(yè)務(wù)線的代碼檢測準(zhǔn)確度較高德撬,準(zhǔn)確率80%+铲咨,而部分業(yè)務(wù)線的篩查結(jié)果準(zhǔn)確度較低。造成準(zhǔn)確率降低的主要原因是多個字符串拼接成類名進(jìn)行動態(tài)調(diào)用以及在Swift中使用反射導(dǎo)致蜓洪,這種情況如果不知道代碼的拼接規(guī)則很難通過通用的手段來檢測纤勒。后續(xù)我們會逐漸完善工具,在輸出掃描結(jié)果的同時給出每個無用代碼在二進(jìn)制文件中的字節(jié)數(shù)隆檀,方便開發(fā)者做決策使用摇天。

總結(jié)

Swift是一門非常神奇且深奧的語言粹湃,上層使用靈活是以底層復(fù)雜適配為代價的。筆者也是在逐漸摸索和學(xué)習(xí)中泉坐,因此可能難免帶著OC的思維來看Swift這門語言为鳄,例如在工具開發(fā)之前筆者一直考慮的是如何檢測Swift無用類,但是實際上Struct腕让、Enum等類型等在開發(fā)中同樣重要济赎。因此,WBBlades還在持續(xù)優(yōu)化记某。目前WBBldes得到了58集團(tuán)內(nèi)外共14個團(tuán)隊或個人的協(xié)助司训,正在持續(xù)的體驗和收集問題。如果您有好的想法或者問題液南,可以在GitHub上留言溝通壳猜。

作者介紹

鄧竹立:用戶價值增長中心-平臺技術(shù)部-iOS技術(shù)部 資深開發(fā)工程師,WBBlades開源工具作者

參考文獻(xiàn)

https://developer.apple.com/documentation/kernel/nlist_64

https://github.com/apple/swift/blob/d68d406dae39ea1677d586714b3991b8f2037dab/lib/IRGen/GenMeta.cpp

http://www.reibang.com/p/158574ab8809

http://www.reibang.com/p/ef0ff6ee6bc6
https://mp.weixin.qq.com/s/egrQxxJSympB-L6BdVDQVA

https://github.com/alibaba/HandyJSON

http://www.reibang.com/p/0cbbbe783ac9

https://juejin.cn/post/6939763940243030024

https://github.com/apple/swift/blob/7123d2614b5f222d03b3762cb110d27a9dd98e24/include/swift/Reflection/Records.h

https://juejin.cn/post/6911121493573402638

https://github.com/woshiccm/Pecker

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末滑凉,一起剝皮案震驚了整個濱河市统扳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌畅姊,老刑警劉巖咒钟,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異若未,居然都是意外死亡朱嘴,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進(jìn)店門粗合,熙熙樓的掌柜王于貴愁眉苦臉地迎上來萍嬉,“玉大人,你說我怎么就攤上這事隙疚∪雷罚” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵供屉,是天一觀的道長行冰。 經(jīng)常有香客問我,道長伶丐,這世上最難降的妖魔是什么悼做? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮撵割,結(jié)果婚禮上贿堰,老公的妹妹穿的比我還像新娘。我一直安慰自己啡彬,他們只是感情好羹与,可當(dāng)我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布故硅。 她就那樣靜靜地躺著,像睡著了一般纵搁。 火紅的嫁衣襯著肌膚如雪吃衅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天腾誉,我揣著相機(jī)與錄音徘层,去河邊找鬼。 笑死利职,一個胖子當(dāng)著我的面吹牛趣效,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播猪贪,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼跷敬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了热押?” 一聲冷哼從身側(cè)響起西傀,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎桶癣,沒想到半個月后拥褂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牙寞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年饺鹃,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碎税。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡尤慰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出雷蹂,到底是詐尸還是另有隱情,我是刑警寧澤杯道,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布匪煌,位于F島的核電站,受9級特大地震影響党巾,放射性物質(zhì)發(fā)生泄漏萎庭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一齿拂、第九天 我趴在偏房一處隱蔽的房頂上張望驳规。 院中可真熱鬧,春花似錦署海、人聲如沸吗购。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捻勉。三九已至镀梭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間踱启,已是汗流浹背报账。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留埠偿,地道東北人透罢。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像冠蒋,于是被迫代替她去往敵國和親羽圃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,573評論 2 359

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