iOS通過注入動態(tài)庫的方式實(shí)現(xiàn)極速編譯調(diào)試(InjectionIII讨衣、熱重載、熱編譯)原理解析

前言

iOS 原生代碼的編譯調(diào)試式镐,都是通過一遍又一遍地編譯重啟 APP來進(jìn)行的反镇。所以項(xiàng)目代碼量越大,編譯時(shí)間就越長娘汞。雖然我們可以將部分代碼先編譯成二進(jìn)制集成到工程里歹茶,來避免每次都全量編譯來加快編譯速度,但即使這樣你弦,每次編譯都還是需要重啟App惊豺,需要再走一遍調(diào)試流程。幸運(yùn)的是禽作,John Holdsworth 開發(fā)了一個(gè)叫做 InjectionIII 的工具可以動態(tài)地將 Swift 或 Objective-C 的代碼在已運(yùn)行的程序中執(zhí)行扮叨,以加快調(diào)試速度,同時(shí)保證程序不用重啟领迈。

看過幾篇寫 Injection 的文章,但都比較老碍沐,而且也沒有介紹全面狸捅,因此決定自己動手寫一下,從應(yīng)用到原理完整介紹一遍累提。

實(shí)踐步驟

1. 下載 InjectionIII尘喝,并安裝好

2. 運(yùn)行 InjectionIII

InjectionIII 運(yùn)行后 icon 如下,藍(lán)色針頭

3. 修改項(xiàng)目源碼

在 - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法里添加如下代碼

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

斋陪。朽褪。。无虚。缔赠。。

#if DEBUG
    [[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];
#endif

}

4. 運(yùn)行項(xiàng)目友题,選擇項(xiàng)目目錄

加載 bundle 的時(shí)候會讓你選擇項(xiàng)目目錄嗤堰,InjectionIII 就是監(jiān)控的這個(gè)目錄,里面文件變動會有通知度宦。

5. 注意

  • 只能在模擬器上看改動效果踢匣,真機(jī)上不行
  • 如果改了一個(gè)頁面代碼,要退出頁面然后進(jìn)入才能看到

原理介紹

一. 總體介紹

InjectionIII 分為server 和 client部分戈抄,client部分在你的項(xiàng)目啟動的時(shí)候會作為 bundle load 進(jìn)去离唬,server部分在Mac App那邊,server 和 client 都會在后臺發(fā)送和監(jiān)聽 Socket 消息划鸽,實(shí)現(xiàn)邏輯分別在 InjectionServer.mm 和 InjectionClient.mm 里的 runInBackground 方法里面输莺。InjectionIII 會監(jiān)聽源代碼文件的變化戚哎,如果文件被改動了,server 就會通過 Socket 通知 client 進(jìn)行 rebuildClass 重新對該文件進(jìn)行編譯模闲,打包成動態(tài)庫建瘫,也就是 .dylib 文件。然后通過 dlopen 把動態(tài)庫文件載入運(yùn)行的 App 里尸折,接下來 dlsym 會得到動態(tài)庫的符號地址啰脚,然后就可以處理類的替換工作。當(dāng)類的方法被替換后实夹,我們就可以開始重新繪制界面了橄浓。整個(gè)過程無需重新編譯和重載 App,使用動態(tài)庫方式極速調(diào)試的目的就達(dá)成了亮航。

原理用一張圖表示如下

備注:此圖作者戴銘

二. 編譯 InjectionIII 源碼

要了解一個(gè)工具荸实,最好的方式當(dāng)然直接看源碼了。InjectionIII 的源代碼鏈接如下:https://github.com/johnno1962/InjectionIII缴淋,可以下載下來對著源碼分析准给。
clone 源碼以后直接編譯會報(bào)錯(cuò),下面一一解決重抖。
首先如下圖露氮,證書問題,直接勾選 Automatically manage signing钟沛,同時(shí)選擇一下團(tuán)隊(duì)畔规,注意 InjectionIII 和 InjectionBundle 兩個(gè) target 都要選擇好。

接著編譯還是會出問題恨统,如下圖所示叁扫,說是找不到 XprobeSwift.swift 和 SwiftSwizzler.swift 兩個(gè)文件,到 Finder 里面根據(jù)目錄去找確實(shí)找不到畜埋,XprobePlugin 文件夾為空莫绣。

因此到 InjectionIII 的 github 上去看個(gè)究竟,發(fā)現(xiàn) SwiftTrace 和 XprobePlugin 是這樣的

點(diǎn)擊都能跳轉(zhuǎn)到對應(yīng)的倉庫那里悠鞍。好了兔综,知道原因了,看來這個(gè)文件夾下要把這個(gè)倉庫 clone 下來狞玛。
clone 了以后運(yùn)行項(xiàng)目软驰,還是報(bào)錯(cuò),如下

看了一下心肪,報(bào)錯(cuò)信息里面有 Xcode101.app锭亏,這顯然不對啊,應(yīng)該是 Xcode.app硬鞍,不然路徑肯定不對慧瘤,然后去 Run Script 里面看到了確實(shí)有 Xcode101戴已,如下圖

將 Xcode101 全部改為 Xcode,然后繼續(xù)編譯锅减,終于可以看到下面這個(gè)無比讓人欣慰的界面了糖儡。接下來就可以放肆地玩了。

最后說一下怔匣,如果我們要用源碼分析握联,當(dāng)然要將源碼編譯起來,打斷點(diǎn)看流程每瞒。這樣的話就在 willFinishLaunchingWithOptions 里面加載的路徑就要相應(yīng)修改了金闽,我這邊是這樣的。

#if DEBUG
    [[NSBundle bundleWithPath:@"/Users/xxxxxx/Library/Developer/Xcode/DerivedData/InjectionIII-fvgzelftiqykfxebnrehvynhccwz/Build/Products/Debug/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];
#endif

可以在 Products 下選中 InjectionIII.app 然后 Show in Finder剿骨,參考我的目錄一步一步點(diǎn)進(jìn)去找到 iOSInjection.bundle代芜。


三. 源碼分析

1. InjectionIII 項(xiàng)目運(yùn)行前

InjectionIII 項(xiàng)目有兩個(gè) target,一個(gè) InjectionIII,一個(gè) InjectionBundle。如下圖


可以看看 InjectionIII Build Phases 下面的 Run Script,從中能找到項(xiàng)目具體對這個(gè) target 做了什么,腳本如下

SYMROOT=/tmp/Injection
export PLATFORM_DIR_OS=$DEVELOPER_DIR/Platforms/iPhoneSimulator.platform &&
"$DEVELOPER_BIN_DIR"/xcodebuild SYMROOT=$SYMROOT PRODUCT_NAME=iOSInjection LD_RUNPATH_SEARCH_PATHS="$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator $PLATFORM_DIR_OS/Developer/Library/Frameworks @loader_path/Frameworks" -arch x86_64 -sdk iphonesimulator -config Debug -target InjectionBundle &&
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild SYMROOT=/tmp/Injection10 PRODUCT_NAME=iOSInjection10 LD_RUNPATH_SEARCH_PATHS="$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator $PLATFORM_DIR_OS/Developer/Library/Frameworks @loader_path/Frameworks" -arch x86_64 -sdk iphonesimulator -config Debug -target InjectionBundle &&
rsync -au $SYMROOT/Debug-iphonesimulator/iOSInjection.bundle /tmp/Injection10/Debug-iphonesimulator/iOSInjection10.bundle "$CODESIGNING_FOLDER_PATH/Contents/Resources" &&

export PLATFORM_DIR_OS=$DEVELOPER_DIR/Platforms/AppleTVSimulator.platform &&
"$DEVELOPER_BIN_DIR"/xcodebuild SYMROOT=$SYMROOT PRODUCT_NAME=tvOSInjection LD_RUNPATH_SEARCH_PATHS="$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/appletvsimulator $PLATFORM_DIR_OS/Developer/Library/Frameworks @loader_path/Frameworks" -arch x86_64 -sdk appletvsimulator -config Debug -target InjectionBundle &&
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild SYMROOT=/tmp/Injection10 PRODUCT_NAME=tvOSInjection10 LD_RUNPATH_SEARCH_PATHS="$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/appletvsimulator $PLATFORM_DIR_OS/Developer/Library/Frameworks @loader_path/Frameworks" -arch x86_64 -sdk appletvsimulator -config Debug -target InjectionBundle &&
rsync -au $SYMROOT/Debug-appletvsimulator/tvOSInjection.bundle /tmp/Injection10/Debug-appletvsimulator/tvOSInjection10.bundle "$CODESIGNING_FOLDER_PATH/Contents/Resources" &&

export PLATFORM_DIR_OS=$DEVELOPER_DIR/Platforms/MacOS.platform &&
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild SYMROOT=/tmp/Injection10 PRODUCT_NAME=macOSInjection10 LD_RUNPATH_SEARCH_PATHS="$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator $PLATFORM_DIR_OS/Developer/Library/Frameworks @loader_path/Frameworks" -arch x86_64 -config Debug -target InjectionBundle &&
rsync -au /tmp/Injection10/Debug/macOSInjection10.bundle "$CODESIGNING_FOLDER_PATH/Contents/Resources" &&
find $CODESIGNING_FOLDER_PATH/Contents/Resources/*.bundle -name '*.h' -delete

內(nèi)容比較多缩焦,重點(diǎn)關(guān)注的是iPhoneSimulator.platform 平臺

SYMROOT=/tmp/Injection
export PLATFORM_DIR_OS=$DEVELOPER_DIR/Platforms/iPhoneSimulator.platform &&
"$DEVELOPER_BIN_DIR"/xcodebuild SYMROOT=$SYMROOT PRODUCT_NAME=iOSInjection LD_RUNPATH_SEARCH_PATHS="$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator $PLATFORM_DIR_OS/Developer/Library/Frameworks @loader_path/Frameworks" -arch x86_64 -sdk iphonesimulator -config Debug -target InjectionBundle &&
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild SYMROOT=/tmp/Injection10 PRODUCT_NAME=iOSInjection10 LD_RUNPATH_SEARCH_PATHS="$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator $PLATFORM_DIR_OS/Developer/Library/Frameworks @loader_path/Frameworks" -arch x86_64 -sdk iphonesimulator -config Debug -target InjectionBundle &&
rsync -au $SYMROOT/Debug-iphonesimulator/iOSInjection.bundle /tmp/Injection10/Debug-iphonesimulator/iOSInjection10.bundle "$CODESIGNING_FOLDER_PATH/Contents/Resources"

可以看到首先使用 xcodebuild 命令將 InjectionBundle 編譯成名字為 iOSInjection.bundle 的動態(tài)庫,放到 /tmp/Injection 目錄下

然后使用 rsync (rsync命令介紹--可以使用 rsync 同步本地硬盤中的不同目錄)命令將 iOSInjection.bundle 同步到 InjectionIII.app 目錄下 "$CODESIGNING_FOLDER_PATH/Contents/Resources"
如下圖

我們需要熱加載的項(xiàng)目的 willFinishLaunchingWithOptions 方法里面要加載 iOSInjection.bundle。這個(gè)作為客戶端和 InjectionIII 通信。注意,bundle 是不能被鏈接的 dylib遵班,只能在運(yùn)行時(shí)使用 dlopen() 加載屠升。

2. Injection 初始化

  • 服務(wù)端初始化
    在 InjectionIII 啟動時(shí)調(diào)用 SimpleSocket 的 startServer 方法并傳入端口號 在后臺運(yùn)行開啟服務(wù)端socket 服務(wù)用于和客戶端的通訊,并運(yùn)行 InjectionServer 類的 runInBackground 方法進(jìn)行初始化操作狭郑,彈出選擇項(xiàng)目目錄對話框腹暖,如果之前選擇過的話就不會彈出。
+ (void)startServer:(NSString *)address {
    [self performSelectorInBackground:@selector(runServer:) withObject:address];
}

+ (void)runServer:(NSString *)address {
    struct sockaddr_storage serverAddr;
    [self parseV4Address:address into:&serverAddr];

    int serverSocket = [self newSocket:serverAddr.ss_family];
    if (serverSocket < 0)
        return;

    if (bind(serverSocket, (struct sockaddr *)&serverAddr, serverAddr.ss_len) < 0)
        [self error:@"Could not bind service socket: %s"];
    else if (listen(serverSocket, 5) < 0)
        [self error:@"Service socket would not listen: %s"];
    else
        while (TRUE) {
            struct sockaddr_storage clientAddr;
            socklen_t addrLen = sizeof clientAddr;

            int clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &addrLen);
            if (clientSocket > 0) {
                @autoreleasepool {
                    struct sockaddr_in *v4Addr = (struct sockaddr_in *)&clientAddr;
                    NSLog(@"Connection from %s:%d\n",
                          inet_ntoa(v4Addr->sin_addr), ntohs(v4Addr->sin_port));
                    [[[self alloc] initSocket:clientSocket] run];
                }
            }
            else
                [NSThread sleepForTimeInterval:.5];
        }
}

  • 客戶端初始化
    在 InjectionIII 啟動后翰萨,打開需要調(diào)試的 Xcode 工程脏答,Xcode 工程必須在其App啟動方法里加載 InjectionIII 目錄下對應(yīng)的 bundle 動態(tài)庫,bundle 是不能被鏈接的 dylib亩鬼,只能在運(yùn)行時(shí)使用 dlopen() 加載殖告。此時(shí)運(yùn)行需要調(diào)試的 Xcode 工程,App 會加載 bundle 動態(tài)庫雳锋,并執(zhí)行動態(tài)庫里 InjectionClient 類的 +load 方法黄绩。在 InjectionClient 類的 +load 方法里會調(diào)用其 connectTo 方法傳入對應(yīng)的端口號來連接服務(wù)端的 socket 服務(wù)用于通訊,并運(yùn)行其runInBackground 方法進(jìn)行初始化操作玷过。
+ (void)load {
    // connect to InjetionIII.app using sicket
    if (InjectionClient *client = [self connectTo:INJECTION_ADDRESS])
        [client run];
    else
        printf("?? Injection loaded but could not connect. Is InjectionIII.app running?\n");

}
  • Injection 初始化詳細(xì)步驟
    首先服務(wù)端和客戶端會讀取一些數(shù)據(jù)傳給對方保存在 SwiftEval 單例中方便后期進(jìn)行代碼注入爽丹,傳送的數(shù)據(jù)包括:Injection App 的沙盒目錄筑煮、調(diào)試 Xcode 工程的物理路徑、目標(biāo) App 芯片類型和沙盒路徑粤蝎、Xcode App 物理路徑和調(diào)試工程的 build 物理路徑 等真仲。接下來服務(wù)端會通過 FileWatcher 開啟調(diào)試工程目錄下文件改變的監(jiān)聽,當(dāng)文件發(fā)生改變后會執(zhí)行傳入的 injector block 方法來進(jìn)行代碼注入初澎。最后客戶端和服務(wù)端都會通過 socket 的 readInt 來持續(xù)獲取交互命令來執(zhí)行對應(yīng)的操作秸应。

項(xiàng)目啟動以后可以在控制臺執(zhí)行 image list -o -f 查看加載的動態(tài)庫,可以看到 iOSInjection.bundle 確實(shí)已經(jīng)以動態(tài)庫的形式加載進(jìn)來了谤狡。

3. 重新編譯灸眼、打包動態(tài)庫和簽名

InjectionIII 運(yùn)行以后會在后臺監(jiān)聽 socket 消息,每隔0.5秒檢查一次是否有客戶端連接過來墓懂,等我們app 啟動以后加載了 iOSInjection.bundle焰宣,就會啟動 client 跟 server 建立連接,然后就可以發(fā)送消息了捕仔。

當(dāng)我們在調(diào)試工程中修改了代碼并保存后匕积,F(xiàn)ileWatcher 會立即收到文件改變的回調(diào),F(xiàn)ileWatcher 使用 Mac OS 上的 FSEvents 框架實(shí)現(xiàn)榜跌,并執(zhí)行如下圖的 injector block 方法闪唆。

在該方法中會判斷是否為自動注入,如果是則執(zhí)行 injectPending 方法钓葫,通過 socket 對客戶端下發(fā)InjectionInject 代碼注入命令并傳入需要代碼注入的文件名物理路徑悄蕾。如果不是自動注入那么就在控制臺輸出“xx文件已保存,輸入ctrl-=進(jìn)行注入”告訴我們手動注入的觸發(fā)方式础浮。

當(dāng)客戶端收到代碼注入命令后會調(diào)用 SwiftInjection 類的 injectWithOldClass: classNameOrFile: 方法進(jìn)行代碼注入帆调,如下圖:

    public class func inject(oldClass: AnyClass?, classNameOrFile: String) {
        do {
            let tmpfile = try SwiftEval.instance.rebuildClass(oldClass: oldClass,
                                    classNameOrFile: classNameOrFile, extra: nil)
            try inject(tmpfile: tmpfile)
        }
        catch {
        }
    }

這個(gè)方法分為兩步,第一步是調(diào)用 SwiftEval 單例的 rebuildClass 方法來進(jìn)行修改文件的重新編譯豆同、打包動態(tài)庫和簽名番刊,第二步是加載對應(yīng)的動態(tài)庫進(jìn)行方法的替換。這里我們先看第一步的操作步驟影锈。

首先根據(jù)修改的類文件名在 Injection App 的沙盒路徑生成對應(yīng)的編譯腳本芹务,腳本命名為eval+數(shù)字,數(shù)字以100為基數(shù)鸭廷,每次遞增1枣抱。腳本生成調(diào)用方法如下圖:

injectionNumber += 1
        let tmpfile = "\(tmpDir)/eval\(injectionNumber)"
        let logfile = "\(tmpfile).log"

        guard var (compileCommand, sourceFile) = try SwiftEval.compileByClass[classNameOrFile] ??
            findCompileCommand(logsDir: logsDir, classNameOrFile: classNameOrFile, tmpfile: tmpfile) ??
            SwiftEval.longTermCache[classNameOrFile].flatMap({ ($0 as! String, classNameOrFile) }) else {
            throw evalError("""
                Could not locate compile command for \(classNameOrFile)
                (Injection does not work with Whole Module Optimization.
                There are also restrictions on characters allowed in paths.
                All paths are also case sensitive is another thing to check.)
                """)
        }

其中 findCompileCommand 為生成 sh 腳本的具體方法,主要是針對當(dāng)前修改類設(shè)置對應(yīng)的編譯腳本命令辆床。由于腳本太長沃但,這里就不貼上來了,有興趣的同學(xué)可以自行查看佛吓。

使用改動類的編譯腳本可以生成其.o文件宵晚,具體如下圖:

let toolchain = ((try! NSRegularExpression(pattern: "\\s*(\\S+?\\.xctoolchain)", options: []))
            .firstMatch(in: compileCommand, options: [], range: NSMakeRange(0, compileCommand.utf16.count))?
            .range(at: 1)).flatMap { compileCommand[$0] } ?? "\(xcodeDev)/Toolchains/XcodeDefault.xctoolchain"

let osSpecific: String
if compileCommand.contains("iPhoneSimulator.platform") {
    osSpecific = "-isysroot \(xcodeDev)/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -mios-simulator-version-min=9.0 -L\(toolchain)/usr/lib/swift/iphonesimulator -undefined dynamic_lookup"http:// -Xlinker -bundle_loader -Xlinker \"\(Bundle.main.executablePath!)\""

這里針對模擬器環(huán)境進(jìn)行腳本配置垂攘,配置完成后使用 clang 命令把對應(yīng)的.o文件生成相同名字的動態(tài)庫,具體如下圖:

        guard shell(command: """
            \(xcodeDev)/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -arch "\(arch)" -bundle \(osSpecific) -dead_strip -Xlinker -objc_abi_version -Xlinker 2 -fobjc-arc \(tmpfile).o -L "\(frameworks)" -F "\(frameworks)" -rpath "\(frameworks)" -o \(tmpfile).dylib >>\(logfile) 2>&1
            """) else {
            throw evalError("Link failed, check \(tmpDir)/command.sh\n\(try! String(contentsOfFile: logfile))")
        }

由于蘋果會對加載的動態(tài)庫進(jìn)行簽名校驗(yàn)淤刃,所以我們下一步需要對這個(gè)動態(tài)庫進(jìn)行簽名晒他,使用 signer block 方法來進(jìn)行簽名操作,簽名方法如下:

    // make available implementation of signing delegated to macOS app
    [SwiftEval sharedInstance].signer = ^BOOL(NSString *_Nonnull dylib) {
        [self writeCommand:InjectionSign withString:dylib];
        return [reader readString].boolValue;
    };

由于簽名需要使用 Xcode 環(huán)境逸贾,所以客戶端是無法進(jìn)行的陨仅,只能通過 socket 告訴服務(wù)端來進(jìn)行操作。當(dāng)服務(wù)端收到 InjectionSign 簽名命令后會調(diào)用 SignerService 類的 codesignDylib 來對相應(yīng)的動態(tài)庫進(jìn)行簽名操作铝侵,具體簽名腳本操作如下:

服務(wù)端代碼如下

case InjectionSign: {
    NSString *sockStr = [self readString];
    BOOL signedOK = [SignerService codesignDylib:sockStr];
    [self writeCommand:InjectionSigned withString: signedOK ? @"1": @"0"];
    break;
}
+ (BOOL)codesignDylib:(NSString *)dylib {
    NSString *command = [NSString stringWithFormat:@""
                         "(export CODESIGN_ALLOCATE=/Applications/Xcode.app"
                         "/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate; "
                         "/usr/bin/codesign --force -s '-' \"%@\")", dylib];
    return system(command.UTF8String) >> 8 == EXIT_SUCCESS;
}

至此修改文件的重新編譯灼伤、打包動態(tài)庫和簽名操作就全部完成了,接下來就是我們最熟悉的加載動態(tài)庫進(jìn)行方法替換了咪鲜。

4. 加載動態(tài)庫進(jìn)行方法替換

  • 加載并注入動態(tài)庫
    上面提到了在調(diào)用了 SwiftEval 類的 rebuildClass 方法進(jìn)行編譯打包動態(tài)庫和簽名后狐赡,會再調(diào)用SwiftInjection 類的 inject 方法來進(jìn)行動態(tài)庫的加載和方法的替換,讓我們一起看看具體的實(shí)現(xiàn)步驟疟丙。在獲取到改變后的新類的符號地址后就可以通過 runtime 的方式來進(jìn)行方法的替換了颖侄。

  • 方法的替換
    在拿到新類的符號地址后,我們把新類里所有的類方法和實(shí)例方法都替換到對應(yīng)的舊類中享郊,使用的是SwiftInjection 的 injection 方法览祖,具體實(shí)現(xiàn)如下圖:

最后我們修改的代碼就在不需要重啟 App 重新編譯的情況下生效了,當(dāng)然為了執(zhí)行修改過的代碼炊琉,需要退出當(dāng)前頁面展蒂,再進(jìn)來才可以看到效果。

參考文章

Injection源碼深度解析
App 如何通過注入動態(tài)庫的方式實(shí)現(xiàn)極速編譯調(diào)試苔咪?
Injection:iOS熱重載背后的黑魔法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锰悼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子悼泌,更是在濱河造成了極大的恐慌,老刑警劉巖夹界,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件馆里,死亡現(xiàn)場離奇詭異,居然都是意外死亡可柿,警方通過查閱死者的電腦和手機(jī)鸠踪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來复斥,“玉大人营密,你說我怎么就攤上這事∧慷В” “怎么了评汰?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵纷捞,是天一觀的道長。 經(jīng)常有香客問我被去,道長主儡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任惨缆,我火速辦了婚禮糜值,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘坯墨。我一直安慰自己寂汇,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布捣染。 她就那樣靜靜地躺著骄瓣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪液斜。 梳的紋絲不亂的頭發(fā)上累贤,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機(jī)與錄音少漆,去河邊找鬼臼膏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛示损,可吹牛的內(nèi)容都是我干的渗磅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼检访,長吁一口氣:“原來是場噩夢啊……” “哼始鱼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起脆贵,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤医清,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后卖氨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體会烙,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年筒捺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了柏腻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡系吭,死狀恐怖五嫂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤沃缘,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布躯枢,位于F島的核電站,受9級特大地震影響孩灯,放射性物質(zhì)發(fā)生泄漏闺金。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一峰档、第九天 我趴在偏房一處隱蔽的房頂上張望败匹。 院中可真熱鬧,春花似錦讥巡、人聲如沸掀亩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽槽棍。三九已至,卻和暖如春抬驴,著一層夾襖步出監(jiān)牢的瞬間炼七,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工布持, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留豌拙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓题暖,卻偏偏與公主長得像按傅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子胧卤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

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