Android熱補(bǔ)丁動(dòng)態(tài)更新實(shí)踐

前言


好幾個(gè)月之前關(guān)于AndroidApp熱補(bǔ)丁修復(fù)火了一把代态,源于QQ空間團(tuán)隊(duì)的一篇文章安卓App熱補(bǔ)丁動(dòng)態(tài)修復(fù)技術(shù)介紹呐萌,然后各大廠的開(kāi)源項(xiàng)目都出來(lái)了,本文的實(shí)踐基于HotFix涎才,也就是QQ空間技術(shù)團(tuán)隊(duì)那篇文章所應(yīng)用的技術(shù)溪食,筆者會(huì)把整個(gè)過(guò)程的細(xì)節(jié)和思路在文章中詳說(shuō),研究這個(gè)的出發(fā)點(diǎn)也是為了能緊急修復(fù)app的bug区岗,而不需要重復(fù)發(fā)包略板,不需要用戶(hù)重新下載app就能把問(wèn)題解決,個(gè)人覺(jué)得這個(gè)還是蠻有價(jià)值的慈缔,雖然老板不知道….。

項(xiàng)目結(jié)構(gòu)

這里筆者創(chuàng)建一個(gè)新的項(xiàng)目”HotFixDemo”种玛,帶大家一步一步來(lái)完成Hotfix這個(gè)框架實(shí)現(xiàn)藐鹤,這個(gè)項(xiàng)目包含以下module:

app :我們的Android應(yīng)用程序Module瓤檐。

buildsrc :使用Groovy實(shí)現(xiàn)的項(xiàng)目,提供了一個(gè)類(lèi)娱节,用來(lái)實(shí)現(xiàn)修改class文件的操作挠蛉。

hackdex :提供了一個(gè)類(lèi),后面會(huì)用來(lái)打包成hack.dex肄满,也是buildsrc里面實(shí)現(xiàn)在所有類(lèi)的構(gòu)造函數(shù)插入的一段代碼所引用到的類(lèi)谴古。

hotfixlib :這個(gè)module最終會(huì)被app關(guān)聯(lián),里面提供實(shí)現(xiàn)熱補(bǔ)丁的核心方法

這個(gè)Demo里面的代碼跟HotFix框架基本無(wú)異稠歉,主要是告訴大家它實(shí)現(xiàn)的過(guò)程掰担,如果光看代碼,不實(shí)踐是無(wú)法把它應(yīng)用到你自己的app上去的怒炸,因?yàn)橛泻芏啾容^深入的知識(shí)需要你去理解带饱。

先說(shuō)原理

關(guān)于實(shí)現(xiàn)原理,QQ空間那篇文章已經(jīng)說(shuō)過(guò)了阅羹,這里我再重新闡述一遍:

Android使用的是PathClassLoader作為其類(lèi)的加載器

一個(gè)ClassLoader可以包含多個(gè)dex文件勺疼,每個(gè)dex文件是一個(gè)Element,多個(gè)dex排列成一個(gè)有序的dexElements數(shù)組

當(dāng)找類(lèi)的時(shí)候會(huì)遍歷dexElements數(shù)組捏鱼,從dex文件中找類(lèi)执庐,找到則返回,否則繼續(xù)下一個(gè)dex文件查找

熱補(bǔ)丁的方案导梆,其實(shí)就是將有問(wèn)題的類(lèi)單獨(dú)打包成一個(gè)dex文件(如:patch.dex)耕肩,然后將這個(gè)dex插入到dexElements數(shù)組的最前面去。

ok问潭,這個(gè)就是HotFix對(duì)app進(jìn)行熱補(bǔ)丁的原理猿诸,其實(shí)就是用ClassLoader加載機(jī)制,覆蓋掉有問(wèn)題的方法狡忙,然后我們所謂的補(bǔ)丁就是將有問(wèn)題的類(lèi)打成一個(gè)包梳虽。

再說(shuō)問(wèn)題

當(dāng)然要實(shí)現(xiàn)熱補(bǔ)丁動(dòng)態(tài)修復(fù)不會(huì)很容易,我們首要解決的一個(gè)問(wèn)題是:

當(dāng)虛擬機(jī)啟動(dòng)時(shí)灾茁,當(dāng)verify選項(xiàng)被打開(kāi)時(shí)窜觉,如果static方法、private方法北专、構(gòu)造函數(shù)等禀挫,其中的直接引用(第一層關(guān)系)到的類(lèi)都在同一個(gè)dex文件中,那么該類(lèi)會(huì)被打上CLASS_ISPREERIFIED標(biāo)記

如下圖所示:

如果一個(gè)類(lèi)被打上了CLASS_ISPREERIFIED這個(gè)標(biāo)志拓颓,如果該類(lèi)引用的另外一個(gè)類(lèi)在另一個(gè)dex文件语婴,就會(huì)報(bào)錯(cuò)。簡(jiǎn)單來(lái)說(shuō),就是你在打補(bǔ)丁之前砰左,你所修復(fù)的類(lèi)已經(jīng)被打上標(biāo)記匿醒,你通過(guò)補(bǔ)丁去修復(fù)bug的時(shí)候這個(gè)時(shí)候你就不能完成校驗(yàn),就會(huì)報(bào)錯(cuò)缠导。

解決問(wèn)題

要解決上一節(jié)所提到的問(wèn)題就要在apk打包之前就阻止相關(guān)類(lèi)打上CLASS_ISPREERIFIED標(biāo)志廉羔,解決方案如下:

在所有類(lèi)的構(gòu)造函數(shù)插入一段代碼,如:

public class BugClass {

public BugClass() {

System.out.println(AntilazyLoad.class);

}

public String bug() {

return "bug class";

}

}

其中引用到的AntilazyLoad這個(gè)類(lèi)會(huì)單獨(dú)打包成hack.dex僻造,這樣當(dāng)安裝apk的時(shí)候憋他,classes.dex內(nèi)的類(lèi)都會(huì)引用一個(gè)不相同的dex中的AntilazyLoad類(lèi),這樣就解決CLASS_ISPREERIFIED標(biāo)記問(wèn)題了髓削。

實(shí)現(xiàn)細(xì)節(jié)

上面幾節(jié)講完原理竹挡、之后拋出了問(wèn)題,再提出解決方案蔬螟,相信大家對(duì)整個(gè)熱補(bǔ)丁修復(fù)框架有了一定的認(rèn)識(shí)此迅,至少我們知道它到底是怎么一回事。下面來(lái)講實(shí)現(xiàn)細(xì)節(jié):

創(chuàng)建兩個(gè)類(lèi)

package com.devilwwj.hotfixdemo;

/**

* com.devilwwj.hotfixdemo

* Created by devilwwj on 16/3/8.

*/

public class BugClass {

public String bug() {

return "bug class";

}

}

package com.devilwwj.hotfixdemo;

/**

* com.devilwwj.hotfixdemo

* Created by devilwwj on 16/3/8.

*/

public class LoadBugClass {

public String getBugString() {

BugClass bugClass = new BugClass();

return bugClass.bug();

}

}

我們需要做的是在這兩個(gè)類(lèi)的class文件的構(gòu)造方法中插入一段代碼:

System.out.println(AntilazyLoad.class);

創(chuàng)建hackdex模塊并創(chuàng)建AntilazyLoad類(lèi)

看圖就好了:

將AntilazyLoad單獨(dú)打成hack_dex.jar包

通過(guò)以下命令來(lái)實(shí)現(xiàn):

jar cvf hack.jar com.devilwwj.hackdex/*

這個(gè)命令會(huì)將AntilazyLoad類(lèi)打包成hack.jar文件

dx --dex --output hack_dex.jar hack.jar

這個(gè)命令使用dx工具對(duì)hack.jar進(jìn)行轉(zhuǎn)化旧巾,生成hack_dex.jar文件

dx工具在我們的sdk/build-tools下

最終我們把hack_dex.jar文件放到項(xiàng)目的assets目錄下:

使用javassist實(shí)現(xiàn)動(dòng)態(tài)代碼注入

創(chuàng)建buildSrc模塊耸序,這個(gè)項(xiàng)目是使用Groovy開(kāi)發(fā)的,需要配置Groovy SDK才可以編譯成功鲁猩。

在這里下載Groovy SDK:http://groovy-lang.org/download.html坎怪,下載之后,配置項(xiàng)目user Library即可廓握。

它里面提供了一個(gè)方法搅窿,用來(lái)向指定類(lèi)的構(gòu)造函數(shù)注入代碼:

/**

* 植入代碼

* @param buildDir 是項(xiàng)目的build class目錄,就是我們需要注入的class所在地

* @param lib 這個(gè)是hackdex的目錄隙券,就是AntilazyLoad類(lèi)的class文件所在地

*/

public static void process(String buildDir, String lib) {

println(lib);

ClassPool classes = ClassPool.getDefault()

classes.appendClassPath(buildDir)

classes.appendClassPath(lib)

// 將需要關(guān)聯(lián)的類(lèi)的構(gòu)造方法中插入引用代碼

CtClass c = classes.getCtClass("com.devilwwj.hotfixdemo.BugClass")

if (c.isFrozen()) {

c.defrost()

}

println("====添加構(gòu)造方法====")

def constructor = c.getConstructors()[0];

constructor.insertBefore("System.out.println(com.devilwwj.hackdex.AntilazyLoad.class);")

c.writeFile(buildDir)

CtClass c1 = classes.getCtClass("com.devilwwj.hotfixdemo.LoadBugClass")

if (c1.isFrozen()) {

c1.defrost()

}

println("====添加構(gòu)造方法====")

def constructor1 = c1.getConstructors()[0];

constructor1.insertBefore("System.out.println(com.devilwwj.hackdex.AntilazyLoad.class);")

c1.writeFile(buildDir)

}

配置app項(xiàng)目的build.gradle

上一小節(jié)創(chuàng)建的module提供相應(yīng)的方法來(lái)讓我們對(duì)項(xiàng)目的類(lèi)進(jìn)行代碼注入男应,我們需要在build.gradle來(lái)配置讓它自動(dòng)來(lái)做這件事:

apply plugin: 'com.android.application'

task('processWithJavassist') << {

String classPath = file('build/intermediates/classes/debug')// 項(xiàng)目編譯class所在目錄

com.devilwwj.patch.PatchClass.process(classPath, project(':hackdex').buildDir.absolutePath + "/intermediates/classes/debug") // 第二個(gè)參數(shù)是hackdex的class所在目錄

}

android {

compileSdkVersion 23

buildToolsVersion "23.0.1"

defaultConfig {

applicationId "com.devilwwj.hotfixdemo"

minSdkVersion 14

targetSdkVersion 23

versionCode 1

versionName "1.0"

}

buildTypes {

debug {

minifyEnabled false

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

}

release {

minifyEnabled false

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

}

}

applicationVariants.all { variant ->

variant.dex.dependsOn << processWithJavassist // 在執(zhí)行dx命令之前將代碼打入到class中

}

}

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

testCompile 'junit:junit:4.12'

compile 'com.android.support:appcompat-v7:23.1.1'

compile 'com.android.support:design:23.1.1'

compile project(':hotfixlib')

}

這時(shí)候我們r(jià)un項(xiàng)目,反編譯build/output/apk下的app-debug.apk文件娱仔,你就可以看到代碼已經(jīng)成功植入了沐飘。

mac下的反編譯工具:

https://sourceforge.net/projects/jadx/?source=typ_redirect

反編譯的結(jié)果如下圖:

其實(shí)你也可以直接在項(xiàng)目中看:

創(chuàng)建hotfixlib模塊,并關(guān)聯(lián)到項(xiàng)目中

這差不多是最后一步了牲迫,也是最核心的一步耐朴,提供將heck_dex.jar動(dòng)態(tài)插入到dexElements的方法。

核心代碼:

package com.devilwwj.hotfixlib;

import android.annotation.TargetApi;

import android.content.Context;

import java.io.File;

import java.lang.reflect.Array;

import java.lang.reflect.Field;

import java.lang.reflect.InvocationTargetException;

import dalvik.system.DexClassLoader;

import dalvik.system.PathClassLoader;

/**

* com.devilwwj.hotfixlib

* Created by devilwwj on 16/3/9.

*/

public final class HotFix {

public static void patch(Context context, String patchDexFile, String patchClassName) {

if (patchDexFile != null && new File(patchDexFile).exists()) {

try {

if (hasLexClassLoader()) {

injectAliyunOs(context, patchDexFile, patchClassName);

} else if (hasDexClassLoader()) {

injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);

} else {

injectBelowApiLevel14(context, patchDexFile, patchClassName);

}

} catch (Throwable th) {

}

}

}

private static boolean hasLexClassLoader() {

try {

Class.forName("dalvik.system.LexClassLoader");

return true;

} catch (ClassNotFoundException e) {

e.printStackTrace();

return false;

}

}

private static boolean hasDexClassLoader() {

try {

Class.forName("dalvik.system.BaseDexClassLoader");

return true;

} catch (ClassNotFoundException e) {

e.printStackTrace();

return false;

}

}

private static void injectAliyunOs(Context context, String patchDexFile, String patchClassName) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {

PathClassLoader obj = (PathClassLoader) context.getClassLoader();

String replaceAll = new File(patchDexFile).getName().replaceAll("\\.[a-zA-Z0-9]+", ".lex");

Class cls = Class.forName("dalvik.system.LexClassLoader");

Object newInstance = cls.getConstructor(new Class[]{String.class, String.class, String.class, ClassLoader.class}).newInstance(

new Object[]{context.getDir("dex", 0).getAbsolutePath()

+ File.separator + replaceAll, context.getDir("dex", 0).getAbsolutePath(), patchDexFile, obj});

cls.getMethod("loadClass", new Class[]{String.class}).invoke(newInstance, new Object[]{patchClassName});

setField(obj, PathClassLoader.class, "mPaths", appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath")));

setField(obj, PathClassLoader.class, "mFiles", combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles")));

setField(obj, PathClassLoader.class, "mZips", combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips")));

setField(obj, PathClassLoader.class, "mLexs", combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs")));

}

@TargetApi(14)

private static void injectBelowApiLevel14(Context context, String str, String str2) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

PathClassLoader obj = (PathClassLoader) context.getClassLoader();

DexClassLoader dexClassLoader = new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader());

dexClassLoader.loadClass(str2);

setField(obj, PathClassLoader.class, "mPaths",

appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class, "mRawDexPath")));

setField(obj, PathClassLoader.class, "mFiles",

combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class, "mFiles")));

setField(obj, PathClassLoader.class, "mZips",

combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class, "mZips")));

setField(obj, PathClassLoader.class, "mDexs",

combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class, "mDexs")));

obj.loadClass(str2);

}

/**

* 將dex注入dexElements數(shù)組中

* @param context

* @param str

* @param str2

* @throws ClassNotFoundException

* @throws NoSuchFieldException

* @throws IllegalAccessException

*/

private static void injectAboveEqualApiLevel14(Context context, String str, String str2) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

Object a = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));

Object a2 = getPathList(pathClassLoader);

setField(a2, a2.getClass(), "dexElements", a);

pathClassLoader.loadClass(str2);

}

/**

* 通過(guò)PathClassLoader拿到pathList

* @param obj

* @return

* @throws ClassNotFoundException

* @throws NoSuchFieldException

* @throws IllegalAccessException

*/

private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");

}

/**

* 通過(guò)pathList取得dexElements對(duì)象

* @param obj

* @return

* @throws NoSuchFieldException

* @throws IllegalAccessException

*/

private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {

return getField(obj, obj.getClass(), "dexElements");

}

/**

* 通過(guò)反射拿到指定對(duì)象

* @param obj

* @param cls

* @param str

* @return

* @throws NoSuchFieldException

* @throws IllegalAccessException

*/

private static Object getField(Object obj, Class cls, String str) throws NoSuchFieldException, IllegalAccessException {

Field declaredField = cls.getDeclaredField(str);

declaredField.setAccessible(true);

return declaredField.get(obj);

}

/**

* 通過(guò)反射設(shè)置屬性

* @param obj

* @param cls

* @param str

* @param obj2

* @throws NoSuchFieldException

* @throws IllegalAccessException

*/

private static void setField(Object obj, Class cls, String str, Object obj2) throws NoSuchFieldException, IllegalAccessException {

Field declaredField = cls.getDeclaredField(str);

declaredField.setAccessible(true);

declaredField.set(obj, obj2);

}

/**

* 合并數(shù)組

* @param obj

* @param obj2

* @return

*/

private static Object combineArray(Object obj, Object obj2) {

Class componentType = obj2.getClass().getComponentType();

int length = Array.getLength(obj2);

int length2 = Array.getLength(obj) + length;

Object newInstance = Array.newInstance(componentType, length2);

for (int i = 0; i < length2; i++) {

if (i < length) {

Array.set(newInstance, i, Array.get(obj2, i));

} else {

Array.set(newInstance, i, Array.get(obj, i - length));

}

}

return newInstance;

}

/**

* 添加到數(shù)組

* @param obj

* @param obj2

* @return

*/

private static Object appendArray(Object obj, Object obj2) {

Class componentType = obj.getClass().getComponentType();

int length = Array.getLength(obj);

Object newInstance = Array.newInstance(componentType, length + 1);

Array.set(newInstance, 0, obj2);

for (int i = 0; i < length + 1; i++) {

Array.set(newInstance, i, Array.get(obj, i - 1));

}

return newInstance;

}

}

準(zhǔn)備補(bǔ)丁盹憎,最后測(cè)試結(jié)果

補(bǔ)丁是我們程序修復(fù)bug的包筛峭,如果我們已經(jīng)上線的包出現(xiàn)了bug,你需要緊急修復(fù)陪每,那你就找到有bug的那個(gè)類(lèi)影晓,將它修復(fù)镰吵,然后將這個(gè)修復(fù)的class文件打包成jar包,讓服務(wù)端將這個(gè)補(bǔ)丁包放到指定位置俯艰,你的就程序就可以將這補(bǔ)丁包下載到sdcard捡遍,之后就是程序自動(dòng)幫你打補(bǔ)丁把問(wèn)題修復(fù)锌订。

比如我們上面提到的BugClass:

未修復(fù)之前:

public class BugClass {

public String bug() {

return "bug class";

}

}

修復(fù)之后:

public class BugClass {

public String bug() {

return "小巫將bug修復(fù)啦V裎铡!辆飘!";

}

}

你要做的就是替換這個(gè)類(lèi)啦辐,怎么做?

先打包:

記昨谙睢:一定要經(jīng)過(guò)dx工具轉(zhuǎn)化芹关,然后路徑一定要對(duì)

patch_dex.jar就是我們的補(bǔ)丁包,這里我們?yōu)榱搜菔景阉诺巾?xiàng)目的assets目錄下:

之后紧卒,就是測(cè)試效果了侥衬,看動(dòng)態(tài)圖:

好,到這里就大公搞成了跑芳,我們的bug被修復(fù)了啦轴总。

總結(jié)

本次實(shí)踐過(guò)程是基于HotFix框架,在這里感謝開(kāi)源的作者博个,因?yàn)椴粷M(mǎn)足于拿作者的東西直接用怀樟,然后不知道為什么,所以筆者把整個(gè)過(guò)程都跑了一遍盆佣,正所謂實(shí)踐出真知往堡,原本以為很難的東西通過(guò)反復(fù)實(shí)踐也會(huì)變得不那么難,期間實(shí)踐自然不會(huì)那么順利共耍,筆者就遇到一個(gè)坑虑灰,比如Groovy編譯,hack_dex包中的類(lèi)找不到等等痹兜,但最后都一一解決了穆咐,研究完這個(gè)熱更新框架,再去研究其他熱更新框架也是一樣的佃蚜,因?yàn)樵矶家粯佑褂椋跃筒患m結(jié)研究哪個(gè)了,之后筆者也會(huì)把這個(gè)技術(shù)用到項(xiàng)目中去谐算,不用每次發(fā)包也是心情愉悅的熟尉,最后感謝各位看官耐心看,有啥問(wèn)題就直接留言吧洲脂。

參考:

http://blog.csdn.net/lmj623565791/article/details/49883661

http://www.reibang.com/p/56facb3732a7

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末斤儿,一起剝皮案震驚了整個(gè)濱河市剧包,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌往果,老刑警劉巖疆液,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異陕贮,居然都是意外死亡堕油,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)肮之,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)掉缺,“玉大人,你說(shuō)我怎么就攤上這事戈擒】裘鳎” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵筐高,是天一觀的道長(zhǎng)搜囱。 經(jīng)常有香客問(wèn)我,道長(zhǎng)柑土,這世上最難降的妖魔是什么蜀肘? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮冰单,結(jié)果婚禮上幌缝,老公的妹妹穿的比我還像新娘。我一直安慰自己诫欠,他們只是感情好涵卵,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著荒叼,像睡著了一般轿偎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上被廓,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天坏晦,我揣著相機(jī)與錄音,去河邊找鬼嫁乘。 笑死昆婿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蜓斧。 我是一名探鬼主播仓蛆,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼挎春!你這毒婦竟也來(lái)了看疙?” 一聲冷哼從身側(cè)響起豆拨,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎能庆,沒(méi)想到半個(gè)月后施禾,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡搁胆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年弥搞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丰涉。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拓巧,死狀恐怖斯碌,靈堂內(nèi)的尸體忽然破棺而出一死,到底是詐尸還是另有隱情,我是刑警寧澤傻唾,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布投慈,位于F島的核電站,受9級(jí)特大地震影響冠骄,放射性物質(zhì)發(fā)生泄漏伪煤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一凛辣、第九天 我趴在偏房一處隱蔽的房頂上張望抱既。 院中可真熱鬧,春花似錦扁誓、人聲如沸防泵。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)捷泞。三九已至,卻和暖如春寿谴,著一層夾襖步出監(jiān)牢的瞬間锁右,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工讶泰, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留咏瑟,地道東北人痪署。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓码泞,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親惠桃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子浦夷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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