Tinker合并流程

1. TinkerInstaller # install()

TinkerInstaller主要提供了两个install()方法,一个简单的,另一个复杂一点的适用于需要自定义更多功能的。

1-1 多参数的install()

先看这个多参数的install(),主要:

  1. 使用传入参数构建了一个Tinker对象,Tinker使用了构建者模式。
  2. 调用了Tinker的create()和install()。
public static Tinker install(ApplicationLike applicationLike, LoadReporter loadReporter, PatchReporter patchReporter,
                           PatchListener listener, Class<? extends AbstractResultService> resultServiceClass, AbstractPatch upgradePatchProcessor) {
    Tinker tinker = new Tinker.Builder(applicationLike.getApplication())
        .tinkerFlags(applicationLike.getTinkerFlags())
        .loadReport(loadReporter)
        .listener(listener)
        .patchReporter(patchReporter)
        .tinkerLoadVerifyFlag(applicationLike.getTinkerLoadVerifyFlag()).build();
    Tinker.create(tinker);
    tinker.install(applicationLike.getTinkerResultIntent(), resultServiceClass, upgradePatchProcessor);
    return tinker;
}
1-2 单参数的install()

单参数的install()也是相同的,只是构建Tinker对象时没有设置那些传入的参数。

public static Tinker install(ApplicationLike applicationLike) {
    Tinker tinker = new Tinker.Builder(applicationLike.getApplication()).build();
    Tinker.create(tinker);
    tinker.install(applicationLike.getTinkerResultIntent());
    return tinker;
}

这里就能看出来TinkerInstaller是一个外观模式,并没有执行初始化的工作,真正工作的是Tinker类,所以我们来看Tinker。

1-3 Tinker的单例模式

Tinker类是一个单例,用得是DCL,没有处理DCL失效问题,可能是因为发生的概率太小了且处理会让效率降低。最后是用Builder去构造对象,使用了构建者模式,Builder是Tinker内部类,去管理Tinker的参数。

public static Tinker with(Context context) {
    if (!sInstalled) {
        throw new TinkerRuntimeException("you must install tinker before get tinker sInstance");
    }
    if (sInstance == null) {
        synchronized (Tinker.class) {
            if (sInstance == null) {
                sInstance = new Builder(context).build();
            }
        }
    }
    return sInstance;
}

Builder在Tinker的内部,统一管理了一些参数,包括多参数install()传入的Reporter和Listener等。

private final Context context;
private final boolean mainProcess;
private final boolean patchProcess;
private int status = -1;
private LoadReporter  loadReporter;
private PatchReporter patchReporter;
private PatchListener listener;
private File          patchDirectory;
private File          patchInfoFile;
private File          patchInfoLockFile;
private Boolean       tinkerLoadVerifyFlag;
1-4 Builder的构造器

在构造方法中会先初始化一些,主要是context所在线程的判断和各种目录的初始。

public Builder(Context context) {
    if (context == null) {
        throw new TinkerRuntimeException("Context must not be null.");
    }
    this.context = context;
    this.mainProcess = TinkerServiceInternals.isInMainProcess(context);
    this.patchProcess = TinkerServiceInternals.isInTinkerPatchServiceProcess(context);
    this.patchDirectory = SharePatchFileUtil.getPatchDirectory(context);
    if (this.patchDirectory == null) {
        TinkerLog.e(TAG, "patchDirectory is null!");
        return;
    }
    this.patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory.getAbsolutePath());
    this.patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory.getAbsolutePath());
    TinkerLog.w(TAG, "tinker patch directory: %s", patchDirectory);
}
1-5 Builder # patchReporter()

之后就是构建者模式,提供了一系列设置参数的方法。比如这个设置patchReporter的方法。设置的方法不要多次调用,并不会覆盖掉之前的设置,只会抛已经设置过的异常。

public Builder patchReporter(PatchReporter patchReporter) {
    if (patchReporter == null) {
        throw new TinkerRuntimeException("patchReporter must not be null.");
    }
    if (this.patchReporter != null) {
        throw new TinkerRuntimeException("patchReporter is already set.");
    }
    this.patchReporter = patchReporter;
    return this;
}
1-6 Builder # build()

在build()中会判空,如果是之前还没有初始赋值的参数,就赋默认值。最后调用Tinker构造器传入初始化的参数创建Tinker对象并返回。

public Tinker build() {
    if (status == -1) {
        status = ShareConstants.TINKER_ENABLE_ALL;
    }
    // 如果调用的是但参数的install()就会在这里赋默认值
    if (loadReporter == null) {
        loadReporter = new DefaultLoadReporter(context);
    }
    if (patchReporter == null) {
        patchReporter = new DefaultPatchReporter(context);
    }
    if (listener == null) {
        listener = new DefaultPatchListener(context);
    }
    if (tinkerLoadVerifyFlag == null) {
        tinkerLoadVerifyFlag = false;
    }
    return new Tinker(context, status, loadReporter, patchReporter, listener, patchDirectory,
        patchInfoFile, patchInfoLockFile, mainProcess, patchProcess, tinkerLoadVerifyFlag);
}
1-7 Tinker # create()

回到1-2,Tinker对象初始化好后传入Tinker的create(),来看create()。这个方法就是赋值单例sInstance。

public static void create(Tinker tinker) {
    if (sInstance != null) {
        throw new TinkerRuntimeException("Tinker instance is already set.");
    }
    sInstance = tinker;
}
1-8 Tinker # install()

1-2在调用完create()后就会调用install(),Tinker的install()也有两个,单参数的TinkerInstaller#install()调用单参数的install(),多参数的调用多参数的,单参数也是调用多参数的install(),后面两个参数就生成默认对象传入了。

public void install(Intent intentResult) {
    install(intentResult, DefaultTinkerResultService.class, new UpgradePatch());
}

再看多参数的install(),它完成真正install的逻辑。主要工作:

  1. 置标记sInstalled为true。
  2. 将两个参数注入到TinkerPatchService中。
  3. 初始化TinkerLoadResult,调用onLoadResult()。
public void install(Intent intentResult, Class<? extends AbstractResultService> serviceClass,
                    AbstractPatch upgradePatch) {
    sInstalled = true;
    TinkerPatchService.setPatchProcessor(upgradePatch, serviceClass);
    TinkerLog.i(TAG, "try to install tinker, isEnable: %b, version: %s", isTinkerEnabled(), ShareConstants.TINKER_VERSION);
    if (!isTinkerEnabled()) {
        TinkerLog.e(TAG, "tinker is disabled");
        return;
    }
    if (intentResult == null) {
        throw new TinkerRuntimeException("intentResult must not be null.");
    }
    tinkerLoadResult = new TinkerLoadResult();
    tinkerLoadResult.parseTinkerResult(getContext(), intentResult);
    //after load code set
    loadReporter.onLoadResult(patchDirectory, tinkerLoadResult.loadCode, tinkerLoadResult.costTime);
    if (!loaded) {
        TinkerLog.w(TAG, "tinker load fail!");
    }
}

2. TinkerInstaller # onReceiveUpgradePatch()

同样因为外观模式,所以TinkerInstaller没有任何处理,直接交给Tinker对象中已经初始化好的PatchListener的onPatchReceived()处理。

public static void onReceiveUpgradePatch(Context context, String patchLocation) {
    Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
}
2-1 PatchListener # onPatchReceived()

PatchListener是一个接口,只有一个onPatchReceived()接口方法。DefaultPatchListener是PatchListener实现类。

public interface PatchListener {
    int onPatchReceived(String path);
}
2-2 DefaultPatchListener # onPatchReceived()

可以自己去实现PatchListener,但总归需要处理的工作是相似的。我们看一个默认实现,DefaultPatchListener。在onPatchReceived()方法里:

  1. 调用patchCheck()对patch文件进行一系列的安全性检查,去重写这个方法也就可以实现自己的检查逻辑了。
  2. 如果检查是安全地就开启TinkerPatchService,开始合并补丁包。
  3. 如果检查没通过就调用LoadReporter的相关方法通知。
@Override
public int onPatchReceived(String path) {
    File patchFile = new File(path);
    int returnCode = patchCheck(path, SharePatchFileUtil.getMD5(patchFile));
    if (returnCode == ShareConstants.ERROR_PATCH_OK) {
        TinkerPatchService.runPatchService(context, path);
    } else {
        Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
    }
    return returnCode;
}

到这里中心就要转去TinkerPatchService了。

总结一下:

  1. TinkerInstaller使用了外观模式,没有真正逻辑处理,只是封装了Tinker的各种调用,真正处理的逻辑在Tinker中。
  2. Tinker使用了单例模式(DCL) + 构建者模式。
  3. TinkerInstaller提供了两个api:
    • install()用来创建并初始化Tinker对象,并调用了Tinker对象的create()和install()。
    • onReceiveUpgradePatch()中调用了Tinker对象中PatchListener的onPatchReceived()。
  4. Tinker的install()中也初始了TinkerPatchService,为后面做准备。
  5. PatchListener的onPatchReceived()用来检查patch文件合法性并开启执行修复工作的TinkerPatchService。

3. TinkerPatchService

TinkerPatchService就是加载合并patch文件的Service,继承了IntentService。

3-1 TinkerPatchService # setPatchProcessor()

回顾一下Tinker的install(),在install()中就传入了两个参数调用了setPatchProcessor()。再追溯参数来源其实是在我们自己编写代码调用TinkerInstaller的复杂install()传入的或是调用简单install()时Tinker的Builder为我们默认创建的。

多参数的源头:

// 回顾我们自己编写的代码,调用 TinkerInstaller#install()。
// 最终最后两个参数被注入到TinkerPatchService中。
AbstractPatch upgradePatchProcessor = new UpgradePatch();
TinkerInstaller.install(mAppLike, loadReporter, 
        patchReporter, mPatchListener,
        CustomResultService.class,// 决定在patch文件安装完毕后的操作
        upgradePatchProcessor// 决定patch文件的安装策略
);// 复杂的初始化方法

单参数的源头:

// Tinker # install()
public void install(Intent intentResult) {
    // 传入的是为我们默认实例的对象。
    install(intentResult, DefaultTinkerResultService.class, new UpgradePatch());
}

一直传递到setPatchProcessor(),最后向TinkerPatchService注入了这两个对象。

  • upgradePatch是我们直接创建的UpgradePatch()对象,表示patch文件的安装策略。
  • serviceClass是我们自定义的DefaultTinkerResultService类,表示修复完毕后的动作。
public static void setPatchProcessor(AbstractPatch upgradePatch, Class<? extends AbstractResultService> serviceClass) {
    upgradePatchProcessor = upgradePatch;
    resultServiceClass = serviceClass;
    //try to load
    try {
        Class.forName(serviceClass.getName());
    } catch (ClassNotFoundException e) {
          e.printStackTrace();
    }
}
3-2 TinkerPatchService # runPatchService()

接着上面看一下runPatchService(),就是启动TinkerPatchService的代码,intent中还传了一个patch文件路径path和一个在Tinker#install()中传入的一个ResultService类名。

public static void runPatchService(Context context, String path) {
    try {
        Intent intent = new Intent(context, TinkerPatchService.class);
        intent.putExtra(PATCH_PATH_EXTRA, path);
        intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
        context.startService(intent);
    } catch (Throwable throwable) {
        TinkerLog.e(TAG, "start patch service fail, exception:" + throwable);
    }
}
3-3 TinkerPatchService # onHandleIntent()
  1. 在这个方法中最重要的就是调用了tryPatch(),也就是更近一步的修复逻辑,因为onHandleIntent()是支持耗时操作的,所以完全可以猜想tryPatch()是同步方法。
  2. 其次重要就是在处理完tryPatch()之后会开启ResultService,执行修复完毕后的工作。
@Override
protected void onHandleIntent(Intent intent) {
    final Context context = getApplicationContext();
    Tinker tinker = Tinker.with(context);
    // PatchReporter回调
    tinker.getPatchReporter().onPatchServiceStart(intent);
    
    // ...一些异常判断处理...
    
    // 提升进程优先级,尽可能保证此Service不被kill
    increasingPriority();
    PatchResult patchResult = new PatchResult();
    try {
        if (upgradePatchProcessor == null) {
            throw new TinkerRuntimeException("upgradePatchProcessor is null.");
        }
        // 核心调用tryPatch()
        result = upgradePatchProcessor.tryPatch(context, path, patchResult);
    } catch (Throwable throwable) {
        e = throwable;
        result = false;
        // PatchReporter回调
        tinker.getPatchReporter().onPatchException(patchFile, e);
    }
    
    cost = SystemClock.elapsedRealtime() - begin;
    // PatchReporter回调
    tinker.getPatchReporter().onPatchResult(patchFile, result, cost);
    
    patchResult.isSuccess = result;
    patchResult.rawPatchFilePath = path;
    patchResult.costTime = cost;
    patchResult.e = e;
    
    // 开始执行ResultService,执行修复完毕后的工作
    AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));
}
3-4 TinkerPatchService # increasingPriority()

在说两个核心过程之前先来看这个方法,这个方法在tryPatch()之前被调用,主要是利用系统漏洞让Service优先级高一些避免轻易被回收。

private void increasingPriority() {
    TinkerLog.i(TAG, "try to increase patch process priority");
    try {
        Notification notification = new Notification();
        if (Build.VERSION.SDK_INT < 18) {
            startForeground(notificationId, notification);
        } else {
            startForeground(notificationId, notification);
            // start InnerService
            startService(new Intent(this, InnerService.class));
        }
    } catch (Throwable e) {
        TinkerLog.i(TAG, "try to increase patch process priority error:" + e);
    }
}
public static class InnerService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
        try {
            startForeground(notificationId, new Notification());
        } catch (Throwable e) {
            TinkerLog.e(TAG, "InnerService set service for push exception:%s.", e);
        }
        // kill
        stopSelf();
    }
    
    @Override
    public void onDestroy() {
        stopForeground(true);
        super.onDestroy();
    }
    
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

4. UpgradePatch # tryPatch()

在上面分析的TinkerPatchService#onHanleIntent():

result = upgradePatchProcessor.tryPatch(context, path, patchResult);
4-1 AbstractPatch

upgradePatchProcessor是AbstractPatch类的,只有一个抽象方法tryPatch()。

public abstract class AbstractPatch {

    public abstract boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult);
}

upgradePatchProcessor是AbstractPatch的实例对象,在Tinker的install()调用时传入,也就是多参数TinkerInstaller的install()传入的,实现类是UpgradePatch,看tryPatch()实现,真的是非常长,我们分成两个部分来看,显示检查再是算法调用。

4-2 UpgradePatch # tryPatch()中的检查逻辑

tryPatch()的返回值便是合成Patch成功与否,在方法的开始都是一些判断,对Tinker的检查、文件的检查、签名检查、TinkerId检查、文件md5检查等,一旦检查不安全就直接返回false。

@Override
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
    Tinker manager = Tinker.with(context);
    final File patchFile = new File(tempPatchPath);
    
    // 检查Tinker参数和SharedPreferences是否可用
    if (!manager.isTinkerEnabled() || !ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:patch is disabled, just return");
        return false;
    }
    
    // 判断patch存在、可读、是文件、大小大于0
    if (!SharePatchFileUtil.isLegalFile(patchFile)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:patch file is not found, just return");
        return false;
    }
    
    //check the signature, we should create a new checker
    ShareSecurityCheck signatureCheck = new ShareSecurityCheck(context);
    // 解压patch去检查签名、TinkerId、和patch压缩包中文件全不全
    int returnCode = ShareTinkerInternals.checkTinkerPackage(context, manager.getTinkerFlags(), patchFile, signatureCheck);
    if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchPackageCheckFail");
        manager.getPatchReporter().onPatchPackageCheckFail(patchFile, returnCode);
        return false;
    }
    
    // 获取patch的md5
    String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
    if (patchMd5 == null) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:patch md5 is null, just return");
        return false;
    }
    //use md5 as version
    // 存patch文件的md5
    patchResult.patchVersion = patchMd5;
    TinkerLog.i(TAG, "UpgradePatch tryPatch:patchMd5:%s", patchMd5);
    
    //check ok, we can real recover a new patch
    // 从缓存之前存过的取文件信息
    final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();
    File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);
    File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory);
    // 看之前是不是有patch
    SharePatchInfo oldInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
    //it is a new patch, so we should not find a exist
    SharePatchInfo newInfo;
    
    //already have patch
    // 构建newInfo
    if (oldInfo != null) {
        // 如果有就检查信息全不全
        if (oldInfo.oldVersion == null || oldInfo.newVersion == null || oldInfo.oatDir == null) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchInfoCorrupted");
            manager.getPatchReporter().onPatchInfoCorrupted(patchFile, oldInfo.oldVersion, oldInfo.newVersion);
            return false;
        }
        // 检查md5不空且长度正确
        if (!SharePatchFileUtil.checkIfMd5Valid(patchMd5)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchVersionCheckFail md5 %s is valid", patchMd5);
            manager.getPatchReporter().onPatchVersionCheckFail(patchFile, oldInfo, patchMd5);
            return false;
        }
        // if it is interpret now, use changing flag to wait main process
        final String finalOatDir = oldInfo.oatDir.equals(ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH)
            ? ShareConstants.CHANING_DEX_OPTIMIZE_PATH : oldInfo.oatDir;
        newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, Build.FINGERPRINT, finalOatDir);
    } else {
        newInfo = new SharePatchInfo("", patchMd5, Build.FINGERPRINT, ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH);
    }
    
    // ......
    
    //copy file
    File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));
    try {
        // check md5 first
        if (!patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {
            // 检查md5正确后拷贝文件,因为后面操作可能会发生以外而删除patch文件。
            // 所以在这里拷贝一份,后面的操作对拷贝的patch来操作。
            SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);
            TinkerLog.w(TAG, "UpgradePatch copy patch file, src file: %s size: %d, dest file: %s size:%d", patchFile.getAbsolutePath(), patchFile.lengt
                destPatchFile.getAbsolutePath(), destPatchFile.length());
        }
    } catch (IOException e) {
          e.printStackTrace();
        TinkerLog.e(TAG, "UpgradePatch tryPatch:copy patch file fail from %s to %s", patchFile.getPath(), destPatchFile.getPath());
        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, destPatchFile, patchFile.getName(), ShareConstants.TYPE_PATCH_FILE);
        return false;
    }
    
    // ......检查成功的后序合并算法调用

    
}
4-3 UpgradePatch # tryPatch()中合并算法的调用

在通过了这一系列检查之后就到了真正合并文件算法的时候了,合并的文件分为三种:dex文件、.so文件和资源文件分别对应下面三个调用,只要一个修复工作失败了,就返回false,修复算法我们在下一篇分析。

@Override
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
    
    // ......一系列检查工作
    
    //we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
    if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
        return false;
    }
    if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
        return false;
    }
    if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
        return false;
    }
    // check dex opt file at last, some phone such as VIVO/OPPO like to change dex2oat to interpreted
    if (!DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, check dex opt file failed");
        return false;
    }
    // ......写新的合并Patch信息
}

5. AbstractResultService

回顾3-3,在tryPatch()调用完成后,最后一句:

AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));

开启了修复完成后的工作Service,DefaultTinkerResultService是默认实现,或者也可以自定义,在TinkerInstaller#install()传入。

5-1 AbstractResultService # runResultService()

resultServiceClass是一路传递过来的类名,到这里就是启动了,默认给的是DefaultTinkerResultService

public static void runResultService(Context context, PatchResult result, String resultServiceClass) {
    if (resultServiceClass == null) {
        throw new TinkerRuntimeException("resultServiceClass is null.");
    }
    try {
        Intent intent = new Intent();
        intent.setClassName(context, resultServiceClass);
        intent.putExtra(RESULT_EXTRA, result);
        context.startService(intent);
    } catch (Throwable throwable) {
        TinkerLog.e(TAG, "run result service fail, exception:" + throwable);
    }
}
5-2 DefaultTinkerResultService # onHandleIntent()

DefaultTinkerResultService没有重写该方法,父类实现直接调用的onPatchResult()

@Override
protected void onHandleIntent(Intent intent) {
    if (intent == null) {
        TinkerLog.e(TAG, "AbstractResultService received a null intent, ignoring.");
        return;
    }
    PatchResult result = (PatchResult) ShareIntentUtil.getSerializableExtra(intent, RESULT_EXTRA);
    onPatchResult(result);
}
5-3 DefaultTinkerResultService # onPatchResult()

首先会关闭PatchService,然后删除patch文件,最后将应用进程杀死,再开就生效了。但是这样的体验不好,所以如果想要自己的逻辑,就可以自定义DefaultTinkerResultService,重写onPatchService()。

@Override
public void onPatchResult(PatchResult result) {
    
    // ......一些判断和日志打印
    
    //first, we want to kill the recover process
    // 关闭TinkerPatchService
    TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
    
    // if success and newPatch, it is nice to delete the raw file, and restart at once
    // only main process can load an upgrade patch!
    if (result.isSuccess) {
        // 如果修复成功了,就把patch删掉
        deleteRawPatchFile(new File(result.rawPatchFilePath));
        if (checkIfNeedKill(result)) {
            // 这就是为什么不自定义ResultService时,修复完成应用会闪退
            android.os.Process.killProcess(android.os.Process.myPid());
        } else {
            TinkerLog.i(TAG, "I have already install the newly patch version!");
        }
    }
}

到这里整个流程就结束了,默认的话此时进程已被杀死,再次启动才能够生效。就再分析分析启动过程中发生的事情。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容