Android如何一步步手動(dòng)實(shí)現(xiàn)熱修復(fù)

熱修復(fù)技術(shù)自從QQ空間團(tuán)隊(duì)搞出來之后便漸漸趨于成熟。

我們這個(gè)系列主要介紹如何一步步手動(dòng)實(shí)現(xiàn)基本的熱修復(fù)功能澳骤,無需使用第三方框架抖僵。

在開始學(xué)習(xí)之前,需要對(duì)基本的熱修復(fù)技術(shù)有些了解违柏,以下文章可以幫助到你:

一、dex文件的生成與加載

我們?cè)谶@部分主要做的流程有:

  • 1.編寫基本的Java文件并編譯為.class文件香椎。
  • 2.將.class文件轉(zhuǎn)為.dex文件漱竖。
  • 3.將轉(zhuǎn)好的dex文件放入創(chuàng)建好的Android工程內(nèi)并在啟動(dòng)時(shí)將其寫入本地。
  • 4.加載解壓后的.dex文件中的類畜伐,并調(diào)用其方法進(jìn)行測(cè)試馍惹。

Note: 在閱讀本節(jié)之前最好先了解一下類加載器的雙親委派模型、DexClassLoader的使用以及反射的知識(shí)點(diǎn)玛界。

編寫基本的Java文件并編譯為.class文件

首先我們?cè)谝粋€(gè)工程目錄下開始創(chuàng)建并編寫我們的Java文件万矾,你可能會(huì)選擇各種IDE來做這件事,但我在這里勸你不要這么做慎框,因?yàn)橛锌釉诘饶懔急贰5劝鸦玖鞒谈闱宄梢栽龠x擇更進(jìn)階的方法。這里我們可以選擇文本編輯器比如EditPlus來對(duì)Java文件進(jìn)行編輯笨枯。

新建一個(gè)Java文件薪丁,并命名為:com.sahadev.bean.ClassStudent.java,并在java文件內(nèi)鍵入以下代碼

public class com.sahadev.bean.ClassStudent {
    private String name;

    public com.sahadev.bean.ClassStudent() {

    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName(){
        return this.name + ".Mr";   
    }
}

Note: 這里要注意馅精,不要對(duì)類添加包名严嗜,因?yàn)樵诤笃趯?duì)class文件處理時(shí)會(huì)遇到問題,具體問題會(huì)稍后說明洲敢。上面的getName方法在返回時(shí)對(duì)this.name屬性添加了一段字符串漫玄,這里請(qǐng)注意,后面會(huì)用到压彭。

在文件創(chuàng)建好之后睦优,對(duì)Java文件進(jìn)行編譯:


將.class文件轉(zhuǎn)為.dex文件

好,現(xiàn)在我們使用class文件生成對(duì)應(yīng)的dex文件哮塞。生成dex文件所需要的工具為dx,dx工具位于sdk的build-tools文件夾內(nèi)凳谦,如下圖所示:


Tips: 為了方便使用忆畅,建議將dx的路徑添加到環(huán)境變量中。如果對(duì)dx工具不熟悉的,可以在終端中輸入dx --help以獲取幫助家凯。

dx工具的基本用法是:

dx --dex [--output=<file>] [<file>.class | <file>.{zip,jar,apk} | <directory>]

Tips: 剛開始自己摸索的時(shí)候缓醋,就沒有仔細(xì)看命令,導(dǎo)致后面兩個(gè)參數(shù)的順序顛倒了绊诲,搞出了一些讓人疑惑難解的問題送粱,最后又不得不去找dx工具的源碼調(diào)試,最后才發(fā)現(xiàn)自己的問題在哪掂之。如果有對(duì)dx工具感興趣的抗俄,可以對(duì)dx的包進(jìn)行反編譯或者獲取dx的相關(guān)源代碼進(jìn)行了解。dx.lib文件位于dx.bat的下級(jí)目錄lib文件夾中世舰,可以使用JD-GUI工具對(duì)其進(jìn)行查看或?qū)С龆ⅰH绻枰@取源代碼的,請(qǐng)使用以下命令進(jìn)行克赂埂:

git clone https://android.googlesource.com/platform/dalvik

我們使用以下命令生成dex文件:

dx --dex --output=user.dex com.sahadev.bean.ClassStudent.class

這里我為了防止出錯(cuò)胰蝠,提前在當(dāng)前目錄下新建好了user.dex文件。上述命令依賴編譯.class文件的JDK版本震蒋,如果使用的是JDK8編譯的class會(huì)提示以下問題:

PARSE ERROR:
unsupported class file version 52.0
...while parsing com.sahadev.bean.ClassStudent.class
1 error; aborting

這里的52.0意味著class文件不被支持茸塞,需要使用JDK8以下的版本進(jìn)行編譯,但是dx所需的環(huán)境還是需要為JDK8的查剖,這里我編譯class文件使用的是JDK7,請(qǐng)注意钾虐。

上面我們提到了為什么先不要在ClassStudent中使用包名,因?yàn)樵趫?zhí)行dx的時(shí)候會(huì)報(bào)以下異常梗搅,這是因?yàn)橐韵碌诙?xiàng)條件沒有通過禾唁,該代碼位于com.android.dx.cf.direct.DirectClassFile文件內(nèi):

    String thisClassName = thisClass.getClassType().getClassName();
    if(!(filePath.endsWith(".class") && filePath.startsWith(thisClassName) && (filePath.length()==(thisClassName.length()+6)))){
        throw new ParseException("class name (" + thisClassName + ") does not match path (" + filePath + ")");
    }

運(yùn)行截圖如下所示:


好了,到此為止我們的目錄應(yīng)該如下:


寫入dex到本地磁盤

接下來將生成好的user.dex文件放入Android工程的res\raw文件夾下:


在系統(tǒng)啟動(dòng)時(shí)將其寫入到磁盤无切,這里不再貼出具體的寫入代碼荡短,項(xiàng)目的MainActivity中包含了此部分代碼。

加載dex中的類并測(cè)試

在寫入完畢之后使用DexClassLoader對(duì)其進(jìn)行加載哆键。DexClassLoader的構(gòu)造方法需要4個(gè)參數(shù)掘托,這里對(duì)這4個(gè)參數(shù)進(jìn)行簡(jiǎn)要說明:

  • String dexPath:dex文件的絕對(duì)路徑。在這里我將其放入了應(yīng)用的cache文件夾下籍嘹。
  • String optimizedDirectory:優(yōu)化后的dex文件存放路徑闪盔。DexClassLoader在構(gòu)造完畢之后會(huì)對(duì)原有的dex文件優(yōu)化并生成一個(gè)新的dex文件,在這里我選擇的是.../cache/optimizedDirectory/目錄辱士。此外泪掀,API文檔對(duì)該目錄有嚴(yán)格的說明:Do not cache optimized classes on external storage.出于安全考慮,請(qǐng)不要將優(yōu)化后的dex文件放入外部存儲(chǔ)器中颂碘。
  • String libraryPath:dex文件所需要的庫文件路徑异赫。這里沒有依賴,使用空字符串代替。
  • ClassLoader parent:雙親委派模型中提到的父類加載器塔拳。這里我們使用默認(rèn)的加載器鼠证,通過getClassLoader()方法獲得。

在解釋完畢DexClassLoader的構(gòu)造參數(shù)之后靠抑,我們開始對(duì)剛剛的dex文件進(jìn)行加載:

DexClassLoader dexClassLoader = new DexClassLoader(apkPath, file.getParent() + "/optimizedDirectory/", "", classLoader);

接來下開始load我們剛剛寫入在dex文件中的ClassStudent類:

Class<?> aClass = dexClassLoader.loadClass("com.sahadev.bean.ClassStudent");

然后我們對(duì)其進(jìn)行初始化量九,并調(diào)用相關(guān)的get/set方法對(duì)其進(jìn)行驗(yàn)證,在這里我傳給ClassStudent對(duì)象一個(gè)字符串颂碧,然后調(diào)用它的get方法獲取在方法內(nèi)合并后的字符串:

    Object instance = aClass.newInstance();
    Method method = aClass.getMethod("setName", String.class);
    method.invoke(instance, "Sahadev");
            
    Method getNameMethod = aClass.getMethod("getName");
    Object invoke = getNameMethod.invoke(instance););

最后我們實(shí)現(xiàn)的代碼可能是這樣的:

    /**
     * 加載指定路徑的dex
     *
     * @param apkPath
     */
    private void loadClass(String apkPath) {
        ClassLoader classLoader = getClassLoader();

        File file = new File(apkPath);

        try {

            DexClassLoader dexClassLoader = new DexClassLoader(apkPath, file.getParent() + "/optimizedDirectory/", "", classLoader);
            Class<?> aClass = dexClassLoader.loadClass("com.sahadev.bean.ClassStudent");
            mLog.i(TAG, "com.sahadev.bean.ClassStudent = " + aClass);

            Object instance = aClass.newInstance();
            Method method = aClass.getMethod("setName", String.class);
            method.invoke(instance, "Sahadev");

            Method getNameMethod = aClass.getMethod("getName");
            Object invoke = getNameMethod.invoke(instance);

            mLog.i(TAG, "invoke result = " + invoke);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

最后附上我們的運(yùn)行截圖:


二荠列、Class文件的替換

在完成基本外部類加載之后,我們這一節(jié)開始對(duì)工程內(nèi)部的類實(shí)行替換稚伍。

Tips: 本章主要依賴文章http://blog.csdn.net/vurtne_ye/article/details/39666381中的未實(shí)現(xiàn)代碼實(shí)現(xiàn)弯予,實(shí)現(xiàn)思路也源自該文章,在閱讀本文之前可以先行了解个曙。

這一節(jié)我們主要實(shí)現(xiàn)的流程有:

  • 在類的內(nèi)部創(chuàng)建與外部dex相同的類文件锈嫩,但在調(diào)用getName()方法返回字符串時(shí)會(huì)稍有區(qū)別,以便進(jìn)行結(jié)果驗(yàn)證
  • 使用DexClassLoader加載外部的user.dex
  • 將DexClassLoader中的dexElements放在PathClassLoader的dexElements之前
  • 驗(yàn)證替換結(jié)果

因?yàn)樯瞎?jié)課中專門聲明了不可以對(duì)類聲明包名垦搬,但是這樣在Android工程中無法引用沒有包名的類呼寸,所以把不能聲明包名的問題解決了一下。

上一節(jié)課主要遇到的問題是在編譯Java文件時(shí)沒有使用正當(dāng)?shù)拿詈锓 ?duì)含有包名的Java文件應(yīng)當(dāng)使用以下命令:

javac -d ./ ClassStudent.java

經(jīng)過上面命令編譯后的.class文件便可以順利通過dx工具的轉(zhuǎn)換对雪。

我們還是按照第一節(jié)的步驟將轉(zhuǎn)換后的user.dex文件放入工程中并寫入本地磁盤,以便稍后使用米绕。

在開始之前還是先說一下具體的實(shí)現(xiàn)思路:一個(gè)類在使用之前必須要經(jīng)過加載器的加載才能使用瑟捣,在加載器加載類之前會(huì)調(diào)用自身的findClass()方法進(jìn)行查找。然而在Android中類的查找使用的是BaseDexClassLoader栅干,BaseDexClassLoader對(duì)findClass()方法進(jìn)行了重寫:

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);

        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }

        return clazz;
    }

pathList是類DexPathList的實(shí)例迈套,這里pathList.findClass的實(shí)現(xiàn)如下:

    public Class findClass(String name) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext);
                if (clazz != null) {
                    return clazz;
                }
            }
        }

        return null;
    }

由此我們可以得知類的查找是通過遍歷dexElements來進(jìn)行查找的。所以為了實(shí)現(xiàn)替換效果碱鳞,我們需要將DexClassLoader中的Element對(duì)象放到dexElements數(shù)組的第0個(gè)位置桑李,這樣才能在BaseDexClassLoader查找類時(shí)先找到DexClassLoader所用的user.dex中的類。

Tips: 如果對(duì)上面這句話看不懂的窿给,沒關(guān)系贵白,可以先了解一下類的加載機(jī)制與ClassLoader的雙親委派模型。

好了崩泡,有了基本的實(shí)現(xiàn)思路之后禁荒,我們接下來對(duì)思路進(jìn)行實(shí)踐。

下面的方法是我們主要的注入方法:


    public String inject(String apkPath) {
        boolean hasBaseDexClassLoader = true;

        File file = new File(apkPath);
        try {
            Class.forName("dalvik.system.BaseDexClassLoader");
        } catch (ClassNotFoundException e) {
            hasBaseDexClassLoader = false;
        }
        if (hasBaseDexClassLoader) {
            PathClassLoader pathClassLoader = (PathClassLoader) getClassLoader();
            DexClassLoader dexClassLoader = new DexClassLoader(apkPath, file.getParent() + "/optimizedDirectory/", "", pathClassLoader);
            try {
                Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
                Object pathList = getPathList(pathClassLoader);
                setField(pathList, pathList.getClass(), "dexElements", dexElements);
                return "SUCCESS";
            } catch (Throwable e) {
                e.printStackTrace();
                return android.util.Log.getStackTraceString(e);
            }
        }
        return "SUCCESS";
    }

這段代碼原封不動(dòng)采用于http://blog.csdn.net/vurtne_ye/article/details/39666381文章中最后的實(shí)現(xiàn)代碼角撞,但是該文章并沒有給出具體的注入細(xì)節(jié)呛伴。我們接下里的過程就是對(duì)沒有給全的細(xì)節(jié)進(jìn)行補(bǔ)充與講解寥掐。

這段代碼的核心在于將DexClassLoader中的dexElements與PathClassLoader中的dexElements進(jìn)行合并,然后將合并后的dexElements替換原先的dexElements磷蜀。最后我們?cè)谑褂肅lassStudent類的時(shí)候便可以直接使用外部的ClassStudent,而不會(huì)再使用默認(rèn)的ClassStudent類百炬。默認(rèn)情況下會(huì)加載默認(rèn)的ClassStudent類褐隆。

首先我們通過classLoader獲取各自的pathList對(duì)象:

    public Object getPathList(BaseDexClassLoader classLoader) {
        Class<? extends BaseDexClassLoader> aClass = classLoader.getClass();

        Class<?> superclass = aClass.getSuperclass();
        try {

            Field pathListField = superclass.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object object = pathListField.get(classLoader);

            return object;
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

在使用以上反射的時(shí)候要注意,pathList屬性屬于基類BaseDexClassLoader剖踊。所以如果直接對(duì)DexClassLoader或者PathClassLoader獲取pathList屬性的話庶弃,會(huì)得到null。

其次是獲取pathList對(duì)應(yīng)的dexElements德澈,這里要注意dexElements是個(gè)數(shù)組對(duì)象:

    public Object getDexElements(Object object) {
        if (object == null)
            return null;

        Class<?> aClass = object.getClass();
        try {
            Field dexElements = aClass.getDeclaredField("dexElements");
            dexElements.setAccessible(true);
            return dexElements.get(object);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;

    }

接下來我們將兩個(gè)數(shù)組對(duì)象合并成為一個(gè):

    public Object combineArray(Object object, Object object2) {
        Class<?> aClass = Array.get(object, 0).getClass();

        Object obj = Array.newInstance(aClass, 2);

        Array.set(obj, 0, Array.get(object2, 0));
        Array.set(obj, 1, Array.get(object, 0));

        return obj;
    }

上面這段代碼我們根據(jù)數(shù)組對(duì)象的類型創(chuàng)建了一個(gè)新的大小為2的新數(shù)組歇攻,并將兩個(gè)數(shù)組的第一個(gè)元素取出,將dex中的dexElement放在了第0個(gè)位置梆造。這樣可以確保在查找類時(shí)優(yōu)先從dex的dexElement中查找缴守。

最后將原先的dexElements覆蓋:

    public void setField(Object pathList, Class aClass, String fieldName, Object fieldValue) {

        try {
            Field declaredField = aClass.getDeclaredField(fieldName);
            declaredField.setAccessible(true);
            declaredField.set(pathList, fieldValue);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }

運(yùn)行驗(yàn)證結(jié)果:

大功告成!

類的加載機(jī)制簡(jiǎn)要介紹

一個(gè)類在被加載到內(nèi)存之前要經(jīng)過加載镇辉、驗(yàn)證屡穗、準(zhǔn)備等過程。經(jīng)過這些過程之后忽肛,虛擬機(jī)才會(huì)從方法區(qū)將代表類的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為內(nèi)存中的Class村砂。

我們這節(jié)內(nèi)容的重點(diǎn)在于一個(gè)類是如何被加載的,所以我們從類的加載入口開始屹逛。

類的加載是由虛擬機(jī)觸發(fā)的础废,類的加載入口位于ClassLoader的loadClassInternal()方法:

    // This method is invoked by the virtual machine to load a class.
    private Class<?> loadClassInternal(String name)
        throws ClassNotFoundException
    {
        // For backward compatibility, explicitly lock on 'this' when
        // the current class loader is not parallel capable.
        if (parallelLockMap == null) {
            synchronized (this) {
                 return loadClass(name);
            }
        } else {
            return loadClass(name);
        }
    }

這段方法還有段注釋說明:這個(gè)方法由虛擬機(jī)調(diào)用用來加載一個(gè)類。我們看到這個(gè)類的內(nèi)部最后調(diào)用了loadClass()方法罕模。那我們進(jìn)入loadClass()方法看看:

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

loadClass()方法方法內(nèi)部調(diào)用了loadClass()的重載方法:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

loadClass()方法大概做了以下工作:

  • 首先查找該類是否已經(jīng)被加載.

  • 如果該ClassLoader有父加載器评腺,那么調(diào)用父加載器的loadClass()方法.

  • 如果沒有父加載器,則調(diào)用findBootstrapClassOrNull()方法進(jìn)行加載手销,該方法會(huì)使用引導(dǎo)類加載器進(jìn)行加載歇僧。普通類是不會(huì)被該加載器加載到的,所以這里一般返回null.

  • 如果前面的步驟都沒找到锋拖,那調(diào)用自身的findClass()方法進(jìn)行查找诈悍。

好,ClassLoader的findClass()方法是個(gè)空方法兽埃,所以這個(gè)過程一般是由子加載器實(shí)現(xiàn)的侥钳。Java的加載器這么設(shè)計(jì)是有一定的淵源的,感興趣的讀者可以自行查找書籍了解柄错。

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

在Android中舷夺,ClassLoader的直接子類是BaseDexClassLoader苦酱,我們看一下BaseDexClassLoader的findClass()實(shí)現(xiàn):

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);

        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }

        return clazz;
    }

Tips: 有需要虛擬機(jī)以及類加載器全套代碼的,請(qǐng)使用以下命令克隆:

git clone https://android.googlesource.com/platform/dalvik-snapshot

相關(guān)代碼位于項(xiàng)目的ics-mr1分支上给猾。

看到這里我們可以知道疫萤,Android中類的查找是通過這個(gè)pathList進(jìn)行查找的,而pathList又是個(gè)什么鬼呢敢伸?

在BaseDexClassLoader中聲明了以下變量:

    /** structured lists of path elements */
    private final DexPathList pathList;

所以我們可以看看DexPathList的findClass()方法做了什么:

    public Class findClass(String name) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext);
                if (clazz != null) {
                    return clazz;
                }
            }
        }

        return null;
    }

這里通過遍歷dexElements中的Element對(duì)象進(jìn)行查找扯饶,最終走的是DexFile的loadClassBinaryName()方法:

    public Class loadClassBinaryName(String name, ClassLoader loader) {
        return defineClass(name, loader, mCookie);
    }

    private native static Class defineClass(String name, ClassLoader loader, int cookie);

到此為止,我們就將一個(gè)類真正的加載過程梳理完了池颈。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末尾序,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子躯砰,更是在濱河造成了極大的恐慌每币,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件琢歇,死亡現(xiàn)場(chǎng)離奇詭異兰怠,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)李茫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門痕慢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涌矢,你說我怎么就攤上這事掖举。” “怎么了娜庇?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵塔次,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我名秀,道長(zhǎng)励负,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任匕得,我火速辦了婚禮继榆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘汁掠。我一直安慰自己略吨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布考阱。 她就那樣靜靜地躺著翠忠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪乞榨。 梳的紋絲不亂的頭發(fā)上秽之,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天当娱,我揣著相機(jī)與錄音,去河邊找鬼考榨。 笑死跨细,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的河质。 我是一名探鬼主播扼鞋,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼愤诱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起捐友,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤淫半,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后匣砖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體科吭,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年猴鲫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了对人。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拂共,死狀恐怖牺弄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宜狐,我是刑警寧澤势告,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站抚恒,受9級(jí)特大地震影響咱台,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜俭驮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一回溺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧混萝,春花似錦遗遵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至厘熟,卻和暖如春屯蹦,著一層夾襖步出監(jiān)牢的瞬間维哈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工登澜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阔挠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓脑蠕,卻偏偏與公主長(zhǎng)得像购撼,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子谴仙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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