dexOpt & odex

(基于android12分析和测试)

一、现存问题

Android早期是aot的方式先编译成机器码,然后再运行的,这样会导致安装时间变长,后面的版本改成jit编译方式,在运行时编译,这样会导致运行速度较慢。android7.0之后的版本支持jit+oat的方式编译,支持app配置baseline-profile的方式设置热点代码。

二、理论基础

1、ART 执行方式

  1. 最初安装应用时不进行任何 AOT 编译。应用前几次运行时,系统会对其进行解译,并对经常执行的方法进行 JIT 编译。
  2. 当设备闲置和充电时,编译守护程序会运行,以便根据在应用前几次运行期间生成的配置文件对常用代码进行 AOT 编译。
  3. 下一次重新启动应用时将会使用配置文件引导型代码,并避免在运行时对已经过编译的方法进行 JIT 编译。在应用后续运行期间经过 JIT 编译的方法将会添加到配置文件中,然后编译守护程序将会对这些方法进行 AOT 编译。


    1695197336986.png

2、编译选项

dex2oat工具可以将dex文件生成vdex,odex或者.art文件。其中:

  1. .dex
    .java->.class->.dex (apk解压后的问题)
    java被编译成.class之后,使用d8工具(以前是dx)将class文件合成dex文件,dex一般是jar的50%大小。然后被打包成单个 .apk 文件。.dex 文件可以通过自动转换用 Java 编程语言编写的编译应用程序来创建。

  2. .odex一种文件格式
    .java->.class->.dex->.oat
    过 AOT 编译的方法代码,ART可以直接用的机器码。

  1. .vdex
    dex->.vdex
    对dex文件进行初步优化,调用dexOpt方法,转成vdex文件(文件名后缀依然是.dex),只是小小的优化了操作码,
    其中odex的文件是可以直接被运行的。生成那种类型的文件依赖dex2Oat工具,dex2Oat依赖一个核心参数“编译过滤器”

编译过滤器:(android 官方sdk android8.0之后没有再更新)

  • verify:仅运行 DEX 代码验证。
  • quicken:(从 Android 12 开始已移除)运行 DEX 代码验证,并优化一些 DEX 指令,以获得更好的解译器性能。(我在12还是看到有这样配置)
  • speed:运行 DEX 代码验证,并对所有方法进行 AOT 编译。
  • speed-profile:运行 DEX 代码验证,并对配置文件中列出的方法进行 AOT 编译。

实际上最新的android 12代码中增加了一些。

//android_12/art/libartbase/base/compiler_filter.h
enum Filter {
  kAssumeVerified,      // Skip verification but mark all classes as verified anyway.
  kExtract,             // Delay verication to runtime, do not compile anything.
  kVerify,              // Only verify classes.
  kSpaceProfile,        // Maximize space savings based on profile.
  kSpace,               // Maximize space savings.
  kSpeedProfile,        // Maximize runtime performance based on profile.
  kSpeed,               // Maximize runtime performance.
  kEverythingProfile,   // Compile everything capable of being compiled based on profile.
  kEverything,          // Compile everything capable of being compiled.
};
 
 
 
//android_12/art/libartbase/base/compiler_filter.cc
std::string CompilerFilter::NameOfFilter(Filter filter) {
  switch (filter) {
    case CompilerFilter::kAssumeVerified: return "assume-verified";
    case CompilerFilter::kExtract: return "extract";
    case CompilerFilter::kVerify: return "verify";
    case CompilerFilter::kSpaceProfile: return "space-profile";
    case CompilerFilter::kSpace: return "space";
    case CompilerFilter::kSpeedProfile: return "speed-profile";
    case CompilerFilter::kSpeed: return "speed";
    case CompilerFilter::kEverythingProfile: return "everything-profile";
    case CompilerFilter::kEverything: return "everything";
  }
  UNREACHABLE();
}

从这里可以得知,quicken被移除了,如果配置了重定向到verify 。配置speed可以将所有方法进行oat,从而加速代码的运行速度。配置speed-profile,可以选择性的让配置的方法进行AOT编译。

3、强制编译命令

系统支持用命令执行odex编译

命令:

adb shell cmd package compile

基于配置文件:

adb shell cmd package compile -m speed-profile -f my-package

全面编译:

adb shell cmd package compile -m speed -f my-package

代码执行流程 :

1695197534122.png

命令执行后会生成odex和vdex文件,放置在data/app/package/oat/arm[64]/xxx

重启进程后可以使用命令查:

/proc/pid/maps/ |grep "odex"

这样就看到应用加载了odex加载到了内存。

4、手动执行dex2Oat

android系统是自带dex2oat工具的,直接在平台执行dex2oat 命令可以直接生成对应的文件,默认位speed编译

dex2oat --dex-file=a.dex --oat-file=./oat/arm64/base.odex

不过我企图生成odex去覆盖原来的odex失败了。简单看了ART执行的逻辑,应该是校验不通过导致。(没有仔细研究,只是简单看看代码+推测)。

后面发现如果用PackageManagerService(pkms)去生成一个odex就可以用。于是我加了点log查看一样的命令参数,使用pkms同款编译参数后就可以用了。放上研究了一天的命令参数

dex2oat32 --dex-file=./data/app/xxx/xxx.dex --oat-file=/data/app/xxx/oat/arm/xxx.odex --classpath-dir=/data/app/xxx --class-loader-context=PCL[]{PCL[/system/framework/org.apache.http.legacy.jar]} --instruction-set=arm --instruction-set-features=default --instruction-set-variant=cortex-a7 --compiler-filter=speed --compilation-reason=cmdline --max-image-block-size=524288 --resolve-startup-const-strings=true --generate-mini-debug-info --runtime-arg -Xdeny-art-apex-data-files  --runtime-arg -Xtarget-sdk-version:31 --runtime-arg -Xhidden-api-policy:enabled -j4 --runtime-arg -Xms64m --runtime-arg -Xmx512m --compile-individually

三、PKMS 代码执行逻辑

1、pkms执行逻辑

1695200019816.png
int performDexOpt(AndroidPackage pkg, @NonNull PackageSetting pkgSetting,
        String[] instructionSets, CompilerStats.PackageStats packageStats,
        PackageDexUsage.PackageUseInfo packageUseInfo, DexoptOptions options) {
 
    if (PLATFORM_PACKAGE_NAME.equals(pkg.getPackageName())) {
        throw new IllegalArgumentException("System server dexopting should be done via "
                + " DexManager and PackageDexOptimizer#dexoptSystemServerPath");
    }
    if (pkg.getUid() == -1) {
        throw new IllegalArgumentException("Dexopt for " + pkg.getPackageName()
                + " has invalid uid.");
    }
    if (!canOptimizePackage(pkg)) {//过滤不允许oat的
        return DEX_OPT_SKIPPED;
    }
    synchronized (mInstallLock) {
        final long acquireTime = acquireWakeLockLI(pkg.getUid());
        try {
            return performDexOptLI(pkg, pkgSetting, instructionSets,
                    packageStats, packageUseInfo, options);
        } finally {
            releaseWakeLockLI(acquireTime);
        }
    }
}
 
// 进入performDexOptLI(pkg, pkgSetting, instructionSets, packageStats, packageUseInfo, options)
 
private int performDexOptLI(AndroidPackage pkg, @NonNull PackageSetting pkgSetting,
        String[] targetInstructionSets, CompilerStats.PackageStats packageStats,
        PackageDexUsage.PackageUseInfo packageUseInfo, DexoptOptions options) {
...
 
    int result = DEX_OPT_SKIPPED;
    for (int i = 0; i < paths.size(); i++) {
        // Skip paths that have no code.
        if (!pathsWithCode[i]) {
            continue;
        }
        if (classLoaderContexts[i] == null) {
            throw new IllegalStateException("Inconsistent information in the "
                    + "package structure. A split is marked to contain code "
                    + "but has no dependency listed. Index=" + i + " path=" + paths.get(i));
        }
 
        // Append shared libraries with split dependencies for this split.
        String path = paths.get(i);
        if (options.getSplitName() != null) {
            // We are asked to compile only a specific split. Check that the current path is
            // what we are looking for.
            if (!options.getSplitName().equals(new File(path).getName())) {
                continue;
            }
        }
 
        String profileName = ArtManager.getProfileName(
                i == 0 ? null : pkg.getSplitNames()[i - 1]); //找profile文件
 
        ...
 
        final String compilerFilter = getRealCompilerFilter(pkg,
            options.getCompilerFilter(), isUsedByOtherApps);//对一些过滤编译器做调整
        ...
 
        for (String dexCodeIsa : dexCodeInstructionSets) {
            int newResult = dexOptPath(pkg, pkgSetting, path, dexCodeIsa, compilerFilter,
                    profileAnalysisResult, classLoaderContexts[i], dexoptFlags, sharedGid,
                    packageStats, options.isDowngrade(), profileName, dexMetadataPath,
                    options.getCompilationReason());
        ...
    return result;
}
 
//进入dexOptPath
 
private int dexOptPath(AndroidPackage pkg, @NonNull PackageSetting pkgSetting, String path,
        String isa, String compilerFilter, int profileAnalysisResult, String classLoaderContext,
        int dexoptFlags, int uid, CompilerStats.PackageStats packageStats, boolean downgrade,
        String profileName, String dexMetadataPath, int compilationReason) {
   ...
    String oatDir = getPackageOatDirIfSupported(pkg,
            pkgSetting.getPkgState().isUpdatedSystemApp());
 
    ...
        mInstaller.dexopt(path, uid, pkg.getPackageName(), isa, dexoptNeeded, oatDir,
                dexoptFlags, compilerFilter, pkg.getVolumeUuid(), classLoaderContext,
                seInfo, false /* downgrade*/, pkg.getTargetSdkVersion(),
                profileName, dexMetadataPath,
                getAugmentedReasonName(compilationReason, dexMetadataPath != null)); //调用Installer 执行dexOpt过程
 
        ...
        return DEX_OPT_PERFORMED;
    } catch (InstallerException e) {
        Slog.w(TAG, "Failed to dexopt", e);
        return DEX_OPT_FAILED;
    }
}

调用performDexOpt需要传入以下参数

performDexOpt(AndroidPackage pkg, @NonNull PackageSetting pkgSetting,

String[] instructionSets,

CompilerStats.PackageStats packageStats,

PackageDexUsage.PackageUseInfo packageUseInfo,

DexoptOptions options)

其中DexoptOptions 定义了编译的可选项,其构造方法如下,有个比较重要的属性compilationReason,构造方法2也是通过reason获取compilerFilter的。

//构造1
public DexoptOptions(String packageName, String compilerFilter, int flags) {
    this(packageName, /*compilationReason*/ -1, compilerFilter, /*splitName*/ null, flags);
}
 
//构造2
public DexoptOptions(String packageName, int compilationReason, int flags) {
    this(packageName, compilationReason, getCompilerFilterForReason(compilationReason),
            /*splitName*/ null, flags);
}
 
//构造3
public DexoptOptions(String packageName, int compilationReason, String compilerFilter,
            String splitName, int flags) {
    int validityMask =
            DEXOPT_CHECK_FOR_PROFILES_UPDATES |
            DEXOPT_FORCE |
            DEXOPT_BOOT_COMPLETE |
            DEXOPT_ONLY_SECONDARY_DEX |
            DEXOPT_ONLY_SHARED_DEX |
            DEXOPT_DOWNGRADE |
            DEXOPT_AS_SHARED_LIBRARY |
            DEXOPT_IDLE_BACKGROUND_JOB |
            DEXOPT_INSTALL_WITH_DEX_METADATA_FILE |
            DEXOPT_FOR_RESTORE;
    if ((flags & (~validityMask)) != 0) {
        throw new IllegalArgumentException("Invalid flags : " + Integer.toHexString(flags));
    }
 
    mPackageName = packageName;
    mCompilerFilter = compilerFilter;
    mFlags = flags;
    mSplitName = splitName;
    mCompilationReason = compilationReason;
}

通过reason获取compilerFilter的。也就是从这个通过prop定义的值完成设置编译过滤器。

public static String getCompilerFilterForReason(int reason) {
   return getAndCheckValidity(reason);
}

/ Load the property for the given reason and check for validity. This will throw an
// exception in case the reason or value are invalid.
private static String getAndCheckValidity(int reason) {
   String sysPropValue = SystemProperties.get(getSystemPropertyName(reason));
   if (sysPropValue == null || sysPropValue.isEmpty()
           || !(sysPropValue.equals(DexoptOptions.COMPILER_FILTER_NOOP)
                   || DexFile.isValidCompilerFilter(sysPropValue))) {
       throw new IllegalStateException("Value \"" + sysPropValue +"\" not valid "
               + "(reason " + REASON_STRINGS[reason] + ")");
   } else if (!isFilterAllowedForReason(reason, sysPropValue)) {
       throw new IllegalStateException("Value \"" + sysPropValue +"\" not allowed "
               + "(reason " + REASON_STRINGS[reason] + ")");
   }

   return sysPropValue;
}

PKMS 中定义了14中编译原因,对应了每种触发dexOpt的原因,和执行dexOpt定义的编译过滤器。

// Compilation reasons.
public static final int REASON_FIRST_BOOT = 0;
public static final int REASON_BOOT_AFTER_OTA = 1;
public static final int REASON_POST_BOOT = 2;
public static final int REASON_INSTALL = 3;
public static final int REASON_INSTALL_FAST = 4;
public static final int REASON_INSTALL_BULK = 5;
public static final int REASON_INSTALL_BULK_SECONDARY = 6;
public static final int REASON_INSTALL_BULK_DOWNGRADED = 7;
public static final int REASON_INSTALL_BULK_SECONDARY_DOWNGRADED = 8;
public static final int REASON_BACKGROUND_DEXOPT = 9;
public static final int REASON_AB_OTA = 10;
public static final int REASON_INACTIVE_PACKAGE_DOWNGRADE = 11;
public static final int REASON_CMDLINE = 12;
public static final int REASON_SHARED = 13;

2、BackgroundDexOptService

BackgroundDexOptService 是一个JobService,用于定期执行任务。在SystemServer#startOtherService()方法中启动,启动后执行两个任务。

任务一:监听开机广播,在开机10分钟-60分钟内完成post-boot的dex优化

任务二:每隔一天执行一次idle 场景下dex优化。

1695200395519.png
//SystemServer.java 
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
...
t.traceBegin("StartBackgroundDexOptService");
try {
    BackgroundDexOptService.schedule(context);
} catch (Throwable e) {
    reportWtf("starting StartBackgroundDexOptService", e);
}
t.traceEnd();
...
}
// BackgroundDexOptService.java
public static void schedule(Context context) {
    if (isBackgroundDexoptDisabled()) {//读属性"pm.dexopt.disable_bg_dexopt" ,目前是false
        return;
    }
 
    final JobScheduler js = context.getSystemService(JobScheduler.class);
 
    // Schedule a one-off job which scans installed packages and updates
    // out-of-date oat files. Schedule it 10 minutes after the boot complete event,
    // so that we don't overload the boot with additional dex2oat compilations.
    context.registerReceiver(new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            js.schedule(new JobInfo.Builder(JOB_POST_BOOT_UPDATE, sDexoptServiceName)//BackgroundDexOptService 的第一个任务
                    .setMinimumLatency(TimeUnit.MINUTES.toMillis(10)) //最短执行时间10min
                    .setOverrideDeadline(TimeUnit.MINUTES.toMillis(60)) //最迟执行时间:60mins
                    .build());
            context.unregisterReceiver(this);
            if (DEBUG) {
                Slog.i(TAG, "BootBgDexopt scheduled");
            }
        }
    }, new IntentFilter(Intent.ACTION_BOOT_COMPLETED));//监听开机启动广播
 
    // Schedule a daily job which scans installed packages and compiles
    // those with fresh profiling data.
    js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName) //BackgroundDexOptService 的第二个任务
                .setRequiresDeviceIdle(true) //设备在idle状态
                .setRequiresCharging(true) //充电中
                .setPeriodic(IDLE_OPTIMIZATION_PERIOD) //执行周期 一天
                .build());
 
    if (DEBUG) {
        Slog.d(TAG, "BgDexopt scheduled");
    }
}
 
 
//关注第一个job:到点执行进入onStartJob方法
public boolean onStartJob(JobParameters params) {
    if (DEBUG) {
        Slog.i(TAG, "onStartJob");
    }
 
    // NOTE: PackageManagerService.isStorageLow uses a different set of criteria from
    // the checks above. This check is not "live" - the value is determined by a background
    // restart with a period of ~1 minute.
    PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package");
    if (pm.isStorageLow()) {//Environment.getDataDirectory().getUsableSpace() < getMemoryLowThreshold(); 为data空间的20%或者低于500MB
        Slog.i(TAG, "Low storage, skipping this run");
        return false;
    }
 
    final ArraySet<String> pkgs = pm.getOptimizablePackages();
    if (pkgs.isEmpty()) {
        Slog.i(TAG, "No packages to optimize");
        return false;
    }
 
    mThermalStatusCutoff =
        SystemProperties.getInt("dalvik.vm.dexopt.thermal-cutoff", THERMAL_CUTOFF_DEFAULT);//2
 
    boolean result;
    if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
        result = runPostBootUpdate(params, pm, pkgs);//进入这里
    } else {
        result = runIdleOptimization(params, pm, pkgs);
    }
 
    return result;
}
 
//执行post-boot更新
private boolean runPostBootUpdate(final JobParameters jobParams,
        final PackageManagerService pm, final ArraySet<String> pkgs) {
    if (mExitPostBootUpdate.get()) {
        // This job has already been superseded. Do not start it.
        return false;
    }
    new Thread("BackgroundDexOptService_PostBootUpdate") {//新起一个线程执行postBootUpdate
        @Override
        public void run() {
            postBootUpdate(jobParams, pm, pkgs);
        }
 
    }.start();
    return true;
}
 
//子线程运行执行所有可优化包的dex优化
private void postBootUpdate(JobParameters jobParams, PackageManagerService pm,
        ArraySet<String> pkgs) {
    final BatteryManagerInternal batteryManagerInternal =
            LocalServices.getService(BatteryManagerInternal.class);
    final long lowThreshold = getLowStorageThreshold(this);
 
    mAbortPostBootUpdate.set(false);
 
    ArraySet<String> updatedPackages = new ArraySet<>();
    for (String pkg : pkgs) {
        if (mAbortPostBootUpdate.get()) {
            // JobScheduler requested an early abort.
            return;
        }
        if (mExitPostBootUpdate.get()) {
            // Different job, which supersedes this one, is running.
            break;
        }
        if (batteryManagerInternal.getBatteryLevelLow()) {//低电量不执行
            // Rather bail than completely drain the battery.
            break;
        }
        long usableSpace = mDataDir.getUsableSpace();
        if (usableSpace < lowThreshold) {//存储不足
            // Rather bail than completely fill up the disk.
            Slog.w(TAG, "Aborting background dex opt job due to low storage: " +
                    usableSpace);
            break;
        }
        if (DEBUG) {
            Slog.i(TAG, "Updating package " + pkg);
        }
 
        // Update package if needed. Note that there can be no race between concurrent
        // jobs because PackageDexOptimizer.performDexOpt is synchronized.
 
        // checkProfiles is false to avoid merging profiles during boot which
        // might interfere with background compilation (b/28612421).
        // Unfortunately this will also means that "pm.dexopt.boot=speed-profile" will
        // behave differently than "pm.dexopt.bg-dexopt=speed-profile" but that's a
        // trade-off worth doing to save boot time work.
        int result = pm.performDexOptWithStatus(new DexoptOptions(//进入PKMS
                pkg,
                PackageManagerService.REASON_POST_BOOT,//原因
                DexoptOptions.DEXOPT_BOOT_COMPLETE));
        if (result == PackageDexOptimizer.DEX_OPT_PERFORMED)  {
            updatedPackages.add(pkg);
        }
    }
    notifyPinService(updatedPackages);
    notifyPackagesUpdated(updatedPackages);
    // Ran to completion, so we abandon our timeslice and do not reschedule.
    jobFinished(jobParams, /* reschedule */ false);
}
 
进入PKMS 的performDexOptWithStatus方法。
/* package */ int performDexOptWithStatus(DexoptOptions options) {
    return performDexOptTraced(options);
}
 
 
private int performDexOptTraced(DexoptOptions options) {
    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
    try {
        return performDexOptInternal(options);
    } finally {
        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
    }
}
 
 
private int performDexOptInternal(DexoptOptions options) {
    AndroidPackage p;
    PackageSetting pkgSetting;
    synchronized (mLock) {
        p = mPackages.get(options.getPackageName());
        pkgSetting = mSettings.getPackageLPr(options.getPackageName());
        if (p == null || pkgSetting == null) {
            // Package could not be found. Report failure.
            return PackageDexOptimizer.DEX_OPT_FAILED;
        }
        mPackageUsage.maybeWriteAsync(mSettings.getPackagesLocked());
        mCompilerStats.maybeWriteAsync();
    }
    final long callingId = Binder.clearCallingIdentity();
    try {
        synchronized (mInstallLock) {
            return performDexOptInternalWithDependenciesLI(p, pkgSetting, options); //进入这里
        }
    } finally {
        Binder.restoreCallingIdentity(callingId);
    }
}
 
 
private int performDexOptInternalWithDependenciesLI(AndroidPackage p,
        @NonNull PackageSetting pkgSetting, DexoptOptions options) {
    // System server gets a special path.
    if (PLATFORM_PACKAGE_NAME.equals(p.getPackageName())) {
        return mDexManager.dexoptSystemServer(options);//android 走系统
    }
 
    // Select the dex optimizer based on the force parameter.
    // Note: The force option is rarely used (cmdline input for testing, mostly), so it's OK to
    //       allocate an object here.
    PackageDexOptimizer pdo = options.isForce()
            ? new PackageDexOptimizer.ForcedUpdatePackageDexOptimizer(mPackageDexOptimizer)
            : mPackageDexOptimizer; //如果有携带f就要强制编译,无其他逻辑
 
    // Dexopt all dependencies first. Note: we ignore the return value and march on
    // on errors.
    // Note that we are going to call performDexOpt on those libraries as many times as
    // they are referenced in packages. When we do a batch of performDexOpt (for example
    // at boot, or background job), the passed 'targetCompilerFilter' stays the same,
    // and the first package that uses the library will dexopt it. The
    // others will see that the compiled code for the library is up to date.
    Collection<SharedLibraryInfo> deps = findSharedLibraries(pkgSetting);
    final String[] instructionSets = getAppDexInstructionSets(
            AndroidPackageUtils.getPrimaryCpuAbi(p, pkgSetting),
            AndroidPackageUtils.getSecondaryCpuAbi(p, pkgSetting));
    if (!deps.isEmpty()) {
        DexoptOptions libraryOptions = new DexoptOptions(options.getPackageName(),
                options.getCompilationReason(), options.getCompilerFilter(),
                options.getSplitName(),
                options.getFlags() | DexoptOptions.DEXOPT_AS_SHARED_LIBRARY);
        for (SharedLibraryInfo info : deps) {
            AndroidPackage depPackage = null;
            PackageSetting depPackageSetting = null;
            synchronized (mLock) {
                depPackage = mPackages.get(info.getPackageName());
                depPackageSetting = mSettings.getPackageLPr(info.getPackageName());
            }
            if (depPackage != null && depPackageSetting != null) {
                // TODO: Analyze and investigate if we (should) profile libraries.
                pdo.performDexOpt(depPackage, depPackageSetting, instructionSets, //先对依赖库进行dex优化
                        getOrCreateCompilerPackageStats(depPackage),
                        mDexManager.getPackageUseInfoOrDefault(depPackage.getPackageName()),
                        libraryOptions);
            } else {
                // TODO(ngeoffray): Support dexopting system shared libraries.
            }
        }
    }
 
    return pdo.performDexOpt(p, pkgSetting, instructionSets,//进入PackageDexOptimizer#performDexOpt流程,上面已经分析过。
            getOrCreateCompilerPackageStats(p),
            mDexManager.getPackageUseInfoOrDefault(p.getPackageName()), options);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容