坑爹的MultiDex

一、問題

1、65535 問題

當 App 的功能越來越豐富、使用的庫越來越多時,其包含的 Java 方法總數也越來越多,這時候就會出現 65535 問題。
在構建 apk 的時候限制了一個 dex 文件能包含的方法數,其總數不能超過 65535(則 64K,1K = 2^10 = 1024 , 64 * 1024 = 65535)。MultiDex, 顧名思義,是指多 dex 實現,大多數 App,解壓其 apk 后,一般只有一個 classes.dex 文件,采用 MultiDex 的 App 解壓后可以看到有 classes.dex,classes2.dex,… classes(N).dex,這樣每個 dex 都可以最大承載 64k 個方法,很大限度地緩解了單 dex 方法數限制。

2、LinearAlloc問題

現在這個問題已經不常見了,它多發生在 2.x 版本的設備上,安裝時會提示 INSTALL_FAILED_DEXOPT。這個問題發生在安裝期間,在使用 Dalvik 虛擬機的設備上安裝 APK 時,會通過 DexOpt 工具將 Dex 文件優化為 odex 文件,即 Optimized Dex,這樣可以提高執行效率 (不同的設備需要不同的 odex 格式,所以這個過程只能安裝 apk 后進行)。
LinearAlloc 是一個固定大小的緩沖區,dexopt 使用 LinearAlloc 來存儲應用的方法信息,在 Android 的不同版本中有 4M/5M/8M/16M 等不同大小,目前主流 4.x 系統上都已到 8MB 或 16MB,但是在 Gingerbread 或以下系統(2.2 和 2.3)LinearAlloc 分配空間只有 5M 大小的。當應用的方法信息過多導致超出緩沖區大小時,會造成 dexopt 崩潰,造成 INSTALL_FAILED_DEXOPT 錯誤。

二、啟用MultiDex解決問題

1、配置 build.gradle

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.0"

    defaultConfig {
        ...
        minSdkVersion 14
        targetSdkVersion 21
        ...

        multiDexEnabled true // Enable MultiDex.
    }
    ...
}

dependencies {
  compile 'com.android.support:multidex:1.0.1'
}

2、在代碼里啟動 MultiDex

在 Java 代碼里啟動 MultiDex,有兩種方式可以搞定。
方式一,使用 MultiDexApplication

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="xxx">
    <application
        ...
        android:name="android.support.multidex.MultiDexApplication">
        ...
    </application>
</manifest>

方式二,在自己的 Application#attachBaseContext(Context) 方法里添加以下代碼。

public class MyApplication extends Application {
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this); // Enable MultiDex.
    }
}

三、實現原理


實現原理.png

MultiDex的入口是MultiDex.install(Context),先從這里入手

1、MultiDex.install

public static void install(Context context) {
    // 經過一系列檢查
    try {
        ApplicationInfo applicationInfo = getApplicationInfo(context);
        if (applicationInfo == null) {
            Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
                  + " MultiDex support library is disabled.");
            return;
        }
        // 調用真正進行dex install的方法
        doInstallation(context,
                       new File(applicationInfo.sourceDir),
                       new File(applicationInfo.dataDir),
                       CODE_CACHE_SECONDARY_FOLDER_NAME,
                       NO_KEY_PREFIX,
                       true);

    } catch (Exception e) {
        Log.e(TAG, "MultiDex installation failure", e);
        throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
    }
}

經過一系列檢查之后調用doInstallation發方法開始真正的dex install操作

private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
                                   String secondaryFolderName, String prefsKeyPrefix,
                                   boolean reinstallOnPatchRecoverableException) throws IOException {
    //保證方法僅調用一次,如果這個方法已經調用過一次,就不能再調用了。
    synchronized (installedApk) {
        if (installedApk.contains(sourceApk)) {
            return;
        }
        installedApk.add(sourceApk);
        // 如果當前Android版本>20已經自身支持了MultiDex,依然可以執行MultiDex操作,但是會有警告。
        if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
            Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
                  + Build.VERSION.SDK_INT + ": SDK version higher than "
                  + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
                  + "runtime with built-in multidex capabilty but it's not the "
                  + "case here: java.vm.version=\""
                  + System.getProperty("java.vm.version") + "\"");
        }
        // 獲取當前的ClassLoader實例,后面要做的工作,就是把其他dex文件加載后,
        // 把其DexFile對象添加到這個ClassLoader里
        ClassLoader loader = getDexClassloader(mainContext);
        if (loader == null) {
            return;
        }

        try {
            // 清除舊的dex文件,這里不是清除上次加載的dex文件緩存。
            // 獲取dex緩存目錄是,會優先獲取/data/data/${packageName}/code-cache作為緩存目錄。
            // 如果獲取失敗,則使用/data/data/${packageName}/files/code-cache目錄。
            // 這里清除的是第二個目錄。
            clearOldDexDir(mainContext);
        } catch (Throwable t) {
            Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
                  + "continuing without cleaning.", t);
        }
        //獲取一個存放dex的目錄,路徑是"/data/data/${packageName}/code_cache/secondary-dexes",用來存放優化后的dex文件
        File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
        // 使用MultiDexExtractor這個工具類把APK中的dex提取到dexDir目錄中
        MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
        IOException closeException = null;
        try {
            //返回的files集合有可能為空,表示沒有secondaryDex
            //不強制重新加載,也就是說如果已經提取過了,可以直接從緩存目錄中拿來使用,這么做速度比較快
            List<? extends File> files =
                extractor.load(mainContext, prefsKeyPrefix, false);
            try {
                // 如果提取的文件是有效的,就安裝secondaryDex
                installSecondaryDexes(loader, dexDir, files);
            } catch (IOException e) {
                if (!reinstallOnPatchRecoverableException) {
                    throw e;
                }
                //如果提取出的文件是無效的,那么就強制重新加載,這么做的話速度就慢了一點,有一些IO開銷
                files = extractor.load(mainContext, prefsKeyPrefix, true);
                installSecondaryDexes(loader, dexDir, files);
            }
        } finally {
            try {
                extractor.close();
            } catch (IOException e) {
                // Delay throw of close exception to ensure we don't override some exception
                // thrown during the try block.
                closeException = e;
            }
        }
        if (closeException != null) {
            throw closeException;
        }
    }
}

方法開始使用synchronized關鍵字保證方法僅調用一次,如果這個方法已經調用過一次,就不能再調用了。如果當前Android版本>20已經自身支持了MultiDex,依然可以執行MultiDex操作,但是會有警告。開始提取dex文件之前先調用 clearOldDexDir 清除舊的dex文件,這里不是清除上次加載的dex文件緩存。這里清除的文件目錄是/data/data/${packageName}/files/code-cache (getDexDir 獲取dex緩存目錄是,會優先獲取/data/data/${packageName}/code-cache作為緩存目錄,如果獲取失敗,則使用/data/data/${packageName}/files/code-cache目錄)。
使用MultiDexExtractor這個工具類把APK中的dex提取到dexDir目錄中,MultiDexExtractor返回的files集合有可能為空,表示沒有secondaryDex,
不強制重新加載,也就是說如果已經提取過了,可以直接從緩存目錄中拿來使用,這么做速度比較快

2、提取Dex文件

再來看一下從APK文件中抽取出.dex文件的邏輯。下面是MultiDexExtractor的load()方法:

List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)
    throws IOException {
    //加上文件鎖,防止多進程沖突。
    if (!cacheLock.isValid()) {
        throw new IllegalStateException("MultiDexExtractor was closed");
    }

    List<ExtractedDex> files;
    // sourceApk 路徑為"/data/app/${packageName}-xxx/base.apk"
    // 先判斷是否強制重新解壓,這里第一次會優先使用已解壓過的dex文件,如果加載失敗就強制重新解壓。
    // 此外,通過crc和文件修改時間,判斷如果Apk文件已經被修改(覆蓋安裝),就會跳過緩存重新解壓dex文件
    if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {
        try {
            // 加載緩存的dex文件
            files = loadExistingExtractions(context, prefsKeyPrefix);
        } catch (IOException ioe) {
            Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
                  + " falling back to fresh extraction", ioe);
            // 加載失敗的話重新解壓,并保存解壓出來的dex文件的信息。
            files = performExtractions();
            putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
                             files);
        }
    } else {
        if (forceReload) {
            Log.i(TAG, "Forced extraction must be performed.");
        } else {
            Log.i(TAG, "Detected that extraction must be performed.");
        }
        //重新解壓,并保存解壓出來的dex文件的信息。
        files = performExtractions();
        putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
                         files);
    }

    Log.i(TAG, "load found " + files.size() + " secondary dex files");
    return files;
}

這個過程主要是獲取可以安裝的dex文件列表,可以是上次解壓出來的緩存文件,也可以是重新從Apk包里面提取出來的。需要注意的是,如果是重新解壓,這里會有明顯的耗時,而且解壓出來的dex文件,會被壓縮成.zip壓縮包,壓縮的過程也會有明顯的耗時(這里壓縮dex文件可能是為了節省空間)。
如果dex文件是重新解壓出來的,則會保存dex文件的信息,包括解壓的apk文件的crc值、修改時間以及dex文件的數目,以便下一次啟動直接使用已經解壓過的dex緩存文件,而不是每一次都重新解壓。
根據前后順序的話,App第一次運行的時候需要從APK中提取取dex文件,先來看一下MultiDexExtractor的performExtractions()方法:

private List<ExtractedDex> performExtractions() throws IOException {
    // 抽取出的dex文件名前綴是"${apkName}.classes"
    final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;

    clearDexDir();

    List<ExtractedDex> files = new ArrayList<ExtractedDex>();

    final ZipFile apk = new ZipFile(sourceApk);
    try {

        int secondaryNumber = 2;
        
        // 獲取"classes${secondaryNumber}.dex"格式的文件
        ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
        // 如果dexFile不為null就一直遍歷
        while (dexFile != null) {
            // 抽取后的文件名是"${apkName}.classes${secondaryNumber}.zip"
            String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
            // 創建文件
            ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
            // 添加到集合中
            files.add(extractedFile);

            Log.i(TAG, "Extraction is needed for file " + extractedFile);
            // 抽取過程中存在失敗的可能,可以多次嘗試,使用isExtractionSuccessful作為是否成功的標志
            int numAttempts = 0;
            boolean isExtractionSuccessful = false;
            while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
                numAttempts++;

                // 抽出去apk中對應序號的dex文件,存放到extractedFile這個zip文件中,只包含它一個dex文件
                // extract方法就是一個IO操作
                extract(apk, dexFile, extractedFile, extractedFilePrefix);

                // 判斷是夠抽取成功
                try {
                    extractedFile.crc = getZipCrc(extractedFile);
                    isExtractionSuccessful = true;
                } catch (IOException e) {
                    isExtractionSuccessful = false;
                    Log.w(TAG, "Failed to read crc from " + extractedFile.getAbsolutePath(), e);
                }

                // Log size and crc of the extracted zip file
                Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed")
                      + " '" + extractedFile.getAbsolutePath() + "': length "
                      + extractedFile.length() + " - crc: " + extractedFile.crc);
                if (!isExtractionSuccessful) {
                    // Delete the extracted file
                    extractedFile.delete();
                    if (extractedFile.exists()) {
                        Log.w(TAG, "Failed to delete corrupted secondary dex '" +
                              extractedFile.getPath() + "'");
                    }
                }
            }
            if (!isExtractionSuccessful) {
                throw new IOException("Could not create zip file " +
                                      extractedFile.getAbsolutePath() + " for secondary dex (" +
                                      secondaryNumber + ")");
            }
            secondaryNumber++;
            dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
        }
    } finally {
        try {
            apk.close();
        } catch (IOException e) {
            Log.w(TAG, "Failed to close resource", e);
        }
    }

    return files;
}

當MultiDexExtractor的performExtractions()方法調用完畢的時候就把APK中所有的dex文件抽取出來,并以一定文件名格式的zip文件保存在緩存目錄中。然后再把一些關鍵的信息通過調用putStoredApkInfo(Context context, long timeStamp, long crc, int totalDexNumber)方法保存到SP中。
當APK之后再啟動的時候就會從緩存目錄中去加載已經抽取過的dex文件。接著來看一下MultiDexExtractor的loadExistingExtractions()方法:

private List<ExtractedDex> loadExistingExtractions(
    Context context,
    String prefsKeyPrefix)
    throws IOException {
    Log.i(TAG, "loading existing secondary dex files");
    // 抽取出的dex文件名前綴是"${apkName}.classes"
    final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
    SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
    // 從SharedPreferences中獲取.dex文件的總數量,調用這個方法的前提是已經抽取過dex文件,所以SP中是有值的
    int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + KEY_DEX_NUMBER, 1);
    final List<ExtractedDex> files = new ArrayList<ExtractedDex>(totalDexNumber - 1);
    // 從第2個dex開始遍歷,這是因為主dex由Android系統自動加載的,從第2個開始即可
    for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
        // 文件名,格式是"${apkName}.classes${secondaryNumber}.zip"
        String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
        // 根據緩存目錄和文件名得到抽取后的文件
        ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
        // 如果是一個文件就保存到抽取出的文件列表中
        if (extractedFile.isFile()) {
            extractedFile.crc = getZipCrc(extractedFile);
            long expectedCrc = multiDexPreferences.getLong(
                prefsKeyPrefix + KEY_DEX_CRC + secondaryNumber, NO_VALUE);
            long expectedModTime = multiDexPreferences.getLong(
                prefsKeyPrefix + KEY_DEX_TIME + secondaryNumber, NO_VALUE);
            long lastModified = extractedFile.lastModified();
            if ((expectedModTime != lastModified)
                || (expectedCrc != extractedFile.crc)) {
                throw new IOException("Invalid extracted dex: " + extractedFile +
                                      " (key \"" + prefsKeyPrefix + "\"), expected modification time: "
                                      + expectedModTime + ", modification time: "
                                      + lastModified + ", expected crc: "
                                      + expectedCrc + ", file crc: " + extractedFile.crc);
            }
            files.add(extractedFile);
        } else {
            throw new IOException("Missing extracted secondary dex file '" +
                                  extractedFile.getPath() + "'");
        }
    }

    return files;
}

3、安裝Dex文件

提取完dex后,接下來就是安裝過程

private static void installSecondaryDexes(ClassLoader loader, File dexDir,
                                          List<? extends File> files){
    if (!files.isEmpty()) {
        if (Build.VERSION.SDK_INT >= 19) {
            V19.install(loader, files, dexDir);
        } else if (Build.VERSION.SDK_INT >= 14) {
            V14.install(loader, files);
        } else {
            V4.install(loader, files);
        }
    }
}

因為在不同的SDK版本上,DexClassLoader加載dex文件的方式有所不同,所以這里做了V4/V14/V19的兼容
下面主要分析SDK19以上安裝過程:

private static final class V19 {

    static void install(ClassLoader loader,
                        List<? extends File> additionalClassPathEntries,
                        File optimizedDirectory){
        // 反射獲取到DexClassLoader的pathList字段;
        Field pathListField = findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // 將剛剛提取出來的zip文件包裝成Element對象,并擴展DexPathList中的dexElements數組字段;
        expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                                                                     new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                                                                     suppressedExceptions));
        if (suppressedExceptions.size() > 0) {
            for (IOException e : suppressedExceptions) {
                Log.w(TAG, "Exception in makeDexElement", e);
            }
            Field suppressedExceptionsField =
                findField(dexPathList, "dexElementsSuppressedExceptions");
            IOException[] dexElementsSuppressedExceptions =
                (IOException[]) suppressedExceptionsField.get(dexPathList);

            if (dexElementsSuppressedExceptions == null) {
                dexElementsSuppressedExceptions =
                    suppressedExceptions.toArray(
                    new IOException[suppressedExceptions.size()]);
            } else {
                IOException[] combined =
                    new IOException[suppressedExceptions.size() +
                                    dexElementsSuppressedExceptions.length];
                suppressedExceptions.toArray(combined);
                System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
                                 suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
                dexElementsSuppressedExceptions = combined;
            }

            suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);

            IOException exception = new IOException("I/O exception during makeDexElement");
            exception.initCause(suppressedExceptions.get(0));
            throw exception;
        }
    }

    private static Object[] makeDexElements(
        Object dexPathList, ArrayList<File> files, File optimizedDirectory,
        ArrayList<IOException> suppressedExceptions)
        throws IllegalAccessException, InvocationTargetException,
    NoSuchMethodException {
        // 反射調用DexPathList對象中的makeDexElements方法,將剛剛提取出來的zip文件包裝成Element對象
        Method makeDexElements =
            findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
                       ArrayList.class);

        return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
                                                 suppressedExceptions);
    }
}

反射獲取ClassLoader中的pathList字段;
反射調用DexPathList對象中的makeDexElements方法,將剛剛提取出來的zip文件包裝成Element對象;
將包裝成的Element對象擴展到DexPathList中的dexElements數組字段里;
makeDexElements中有dexopt的操作,是一個耗時的過程,產物是一個優化過的odex文件。
至此:提取出來的dex文件也被加到了ClassLoader里,而那些Class也就可以被ClassLoader所找到并使用。

四、存在的問題

MultiDex 并不是萬全的方案,Google 貌似不太熱衷于舊版本的兼容工作,通過閱讀 MultiDex Support 庫的源碼,我們也能發現其代碼寫得貌似沒有那么嚴謹。
目前來說,使用 MultiDex 可能存在以下問題。

1、NoClassDefFoundError

如果你在調用 MultiDex#install(Context) 做了別的工作,而這些工作需要用到的類卻存在于別的 dex 文件里面(Secondary Dexes),就會出現類找不到的運行時異常。
正確的做法是把這些需要用到的類標記在 multidex.keep 清單文件里面,再在 build.gradle 里面啟用該清單文件。

android {

  defaultConfig {
    multiDexEnabled true
    multiDexKeepProguard file('multidex.pro')
    multiDexKeepFile file('main_dex.txt')
   }
}

dependencies {
  compile 'com.android.support:multidex:1.0.3'
}

multiDexKeepProguard使用的是類似于混淆文件的過濾規則,除了這個配置項之外還有multiDexKeepFile,這個要求你在清單文件里把所有的類都羅列出來。

2、卡頓/ANR問題

目前 Android 5.0 以上的設備已經自身支持了 MultiDex 功能,也就是說在安裝 apk 的時候,系統已經會幫我們把 apk 里面的所有 dex 文件都做好 Optimize 處理,所以不需要我們在代碼里啟用 MultiDex 了。但是對于 Android 5.0 以下的設備,依然要求我們啟用 MultiDex。而這些系統的設備在第一次運行 App 的時候,需要對所有的 Secondary Dexes 文件都進行一次解壓以及 Optimize 處理(生成 odex 文件),這段時間會有明顯的耗時,所有會產生明顯的卡頓現象


dex異步加載.png

1、在Application的attachBaseContext啟動新進程執行dexOpt

protected void attachBaseContext(Context base) {
    // 只有5.0以下需要執行 MultiDex.install
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        MULTI_DEX = MULTI_DEX + "_" + getVersionCode(base);
        if (SystemUtil.isInMainProcess(base)) {
            // 判斷有沒有執行過dexOpt
            if (!dexOptDone(base)) {
                preLoadDex(base);
            }
        }
        if (!KwaiApp.isMultiDeXProcess(base)) {
            MultiDex.install(base);
        }
    }
    super.attachBaseContext(base);
}

/**
   * 是否進行過DexOpt操作。
   * 
   * @param context
   * @return
   */
private boolean dexOptDone(Context context) {
    SharedPreferences sp = context.getSharedPreferences(MULTI_DEX, MODE_MULTI_PROCESS);
    return sp.getBoolean(MULTI_DEX, false);
}

/**
   * 在單獨進程中提前進行DexOpt的優化操作;主進程進入等待狀態。
   *
   * @param base
   */
public void preLoadDex(Context base) {
    // 在新進程中啟動PreLoadDexActivity
    Intent intent = new Intent(base, PreLoadDexActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    base.startActivity(intent);
    while (!dexOptDone(base)) {
        try {
            // 主線程開始等待;直到優化進程完成了DexOpt操作。
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2、 子進程中執行dexOpt

public class PreLoadDexActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    super.onCreate(savedInstanceState);
    // 取消掉系統默認的動畫。
    overridePendingTransition(0, 0);
    setContentView(R.layout.tv_splash_layout);
    new Thread() {
      @Override
      public void run() {
        try {
          // 在子線程中調用
          MultiDex.install(getApplication());
          SharedPreferences sp = getSharedPreferences(App.MULTI_DEX, MODE_MULTI_PROCESS);
          sp.edit().putBoolean(App.MULTI_DEX, true).commit();
          finish();
        } catch (Exception e) {
          finish();
        }
      }
    }.start();
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    System.exit(0);
  }
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373