热修复框架 - TinkerApplication启动(一) - 初始化过程

代码基于tinker 1.9.14.7。
Tinker热修复从使用上来看主要有三方面:

  • TinkerApplication 启动过程相关
  • TinkerInstaller.installTinker 初始化相关
  • TinkerInstaller.onReceiveUpgradePatch 加载patch包

这三点形成一个完整的热修复闭环,后续文章依次从这三个点铺开来分析,先对Tinker有个全面了解。本篇文章先从TinkerApplication 启动过程相关开始。

一、动态生成Application

接入 Tinker 官方推荐是利用 @DefaultLifeCycle 动态生成 Application。这里用到了apt在编译时生成类的方式。通过AnnotationProcessor 按resouces/TinkerAnnoApplication.tmpl模板生成。

public class %APPLICATION% extends TinkerApplication {
    public %APPLICATION%() {
        super(%TINKER_FLAGS%, "%APPLICATION_LIFE_CYCLE%", "%TINKER_LOADER_CLASS%", %TINKER_LOAD_VERIFY_FLAG%);
    }
}

动态生成的application是继承于TinkerApplication。

protected TinkerApplication(int tinkerFlags) {
    this(tinkerFlags, "com.tencent.tinker.entry.DefaultApplicationLike",
           TinkerLoader.class.getName(), false);
}

TinkerApplication初始化时关联DefaultApplicationLike,它作为生成的application的代理类供外部调用者使用。

接下来再看

private void onBaseContextAttached(Context base) {
    try {
        final long applicationStartElapsedTime = SystemClock.elapsedRealtime();
       final long applicationStartMillisTime = System.currentTimeMillis();
       loadTinker();
       mCurrentClassLoader = base.getClassLoader();
       mInlineFence = createInlineFence(this, tinkerFlags, delegateClassName,
               tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime,
               tinkerResultIntent);
       TinkerInlineFenceAction.callOnBaseContextAttached(mInlineFence, base);
       //reset save mode
       if (useSafeMode) {
            ShareTinkerInternals.setSafeModeCount(this, 0);
       }
    } catch (TinkerRuntimeException e) {
        throw e;
   } catch (Throwable thr) {
        throw new TinkerRuntimeException(thr.getMessage(), thr);
   }
}

在TinkerApplication的onBaseContextAttached中,主要做了两件事:

  • loadTinker: 反射执行TinkerLoader.tryLoad 方法。
  • createInlineFence,绑定代理DefaultApplicationLike。

二、TinkerLoader.tryLoad 加载补丁包

这里重点看看TinkerLoader.tryLoad,tryLoad直接调用tryLoadPatchFilesInternal,这个方法非常长,那么来耐心看下:

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
   final int tinkerFlag = app.getTinkerFlags();
   //确保tinker enable 且非patch进程
   if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
       Log.w(TAG, "tryLoadPatchFiles: tinker is disable, just return");
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
       return;
   }

   if (ShareTinkerInternals.isInPatchProcess(app)) {
       Log.w(TAG, "tryLoadPatchFiles: we don't load patch with :patch process itself, just return");
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
       return;
   }

   //tinker
   //PatchDirectory:/data/data/tinker.sample.android/tinker
   File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
   if (patchDirectoryFile == null) {
       Log.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
       //treat as not exist
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
       return;
   }
   String patchDirectoryPath = patchDirectoryFile.getAbsolutePath();

   // 检查tinker目录是否存在
   //check patch directory whether exist
   if (!patchDirectoryFile.exists()) {
       Log.w(TAG, "tryLoadPatchFiles:patch dir not exist:" + patchDirectoryPath);
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
       return;
   }
   //tinker/patch.info 补丁信息文件
   File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);
   //check patch info file whether exist
   if (!patchInfoFile.exists()) {
       Log.w(TAG, "tryLoadPatchFiles:patch info not exist:" + patchInfoFile.getAbsolutePath());
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_NOT_EXIST);
       return;
   }

   //获取patch.info并包装为SharePatchInfo
   //old = 641e634c5b8f1649c75caf73794acbdf
   //new = 2c150d8560334966952678930ba67fa8
   File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectoryPath);
   patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
   if (patchInfo == null) {
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
       return;
   }
   final boolean isProtectedApp = patchInfo.isProtectedApp;
   resultIntent.putExtra(ShareIntentUtil.INTENT_IS_PROTECTED_APP, isProtectedApp);
   String oldVersion = patchInfo.oldVersion;
   String newVersion = patchInfo.newVersion;
   String oatDex = patchInfo.oatDir;
   if (oldVersion == null || newVersion == null || oatDex == null) {
       //it is nice to clean patch
       Log.w(TAG, "tryLoadPatchFiles:onPatchInfoCorrupted");
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
       return;
   }
   boolean mainProcess = ShareTinkerInternals.isInMainProcess(app);
   boolean isRemoveNewVersion = patchInfo.isRemoveNewVersion;
   //是否清除新patch
   if (mainProcess) {
       final String patchName = SharePatchFileUtil.getPatchVersionDirectory(newVersion);
       // So far new version is not loaded in main process and other processes.
       // We can remove new version directory safely.
       if (isRemoveNewVersion) {
           Log.w(TAG, "found clean patch mark and we are in main process, delete patch file now.");
           if (patchName != null) {
               // oldVersion.equals(newVersion) means the new version has been loaded at least once
               // after it was applied.
               final boolean isNewVersionLoadedBefore = oldVersion.equals(newVersion);
               if (isNewVersionLoadedBefore) {
                   // Set oldVersion and newVersion to empty string to clean patch
                   // if current patch has been loaded before.
                   oldVersion = "";
               }
               newVersion = oldVersion;
               patchInfo.oldVersion = oldVersion;
               patchInfo.newVersion = newVersion;
               patchInfo.isRemoveNewVersion = false;
               SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
               String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
               SharePatchFileUtil.deleteDir(patchVersionDirFullPath);
               if (isNewVersionLoadedBefore) {
                   ShareTinkerInternals.killProcessExceptMain(app);
                   ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
                   return;
               }
           }
       }
       if (patchInfo.isRemoveInterpretOATDir) {
           // delete interpret odex
           // for android o, directory change. Fortunately, we don't need to support android o interpret mode any more
           Log.i(TAG, "tryLoadPatchFiles: isRemoveInterpretOATDir is true, try to delete interpret optimize files");
           patchInfo.isRemoveInterpretOATDir = false;
           SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
           ShareTinkerInternals.killProcessExceptMain(app);
           String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
           SharePatchFileUtil.deleteDir(patchVersionDirFullPath + "/" + ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);
       }
   }
   resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OLD_VERSION, oldVersion);
   resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_NEW_VERSION, newVersion);
   boolean versionChanged = !(oldVersion.equals(newVersion));
   boolean oatModeChanged = oatDex.equals(ShareConstants.CHANING_DEX_OPTIMIZE_PATH);
   oatDex = ShareTinkerInternals.getCurrentOatMode(app, oatDex);
   resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, oatDex);
   // 根据版本变化和是否是主进程的条件决定是否加载新patch
   String version = oldVersion;
   if (versionChanged && mainProcess) {
       version = newVersion;
   }
   if (ShareTinkerInternals.isNullOrNil(version)) {
       Log.w(TAG, "tryLoadPatchFiles:version is blank, wait main process to restart");
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_BLANK);
       return;
   }
   //patch-641e634c
   String patchName = SharePatchFileUtil.getPatchVersionDirectory(version);
   if (patchName == null) {
       Log.w(TAG, "tryLoadPatchFiles:patchName is null");
       //we may delete patch info file
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
       return;
   }
   //tinker/patch.info/patch-641e634c
   String patchVersionDirectory = patchDirectoryPath + "/" + patchName;
   File patchVersionDirectoryFile = new File(patchVersionDirectory);
   if (!patchVersionDirectoryFile.exists()) {
       Log.w(TAG, "tryLoadPatchFiles:onPatchVersionDirectoryNotFound");
       //we may delete patch info file
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
       return;
   }
   //tinker/patch.info/patch-641e634c/patch-641e634c.apk
   final String patchVersionFileRelPath = SharePatchFileUtil.getPatchVersionFile(version);
   //获取diff patch文件
   File patchVersionFile = (patchVersionFileRelPath != null ? new File(patchVersionDirectoryFile.getAbsolutePath(), patchVersionFileRelPath) : null);
   if (!SharePatchFileUtil.isLegalFile(patchVersionFile)) {
       Log.w(TAG, "tryLoadPatchFiles:onPatchVersionFileNotFound");
       //we may delete patch info file
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_FILE_NOT_EXIST);
       return;
   }
   ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);
   // 1. 检查补丁包 apk 的签名
   // 2. 检查基准包的 tinker id 与补丁包中是否一致
   // 3. 检查 tinker 设置与补丁包中的类型是否符合

   int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
   if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
       Log.w(TAG, "tryLoadPatchFiles:checkTinkerPackage");
       resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, returnCode);
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
       return;
   }
   resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_CONFIG, securityCheck.getPackagePropertiesIfPresent());
   final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
   //com.huawei.ark.app.ArkApplicationInfo arthot是对华为的一种单独修复支持
   final boolean isArkHotRuning = ShareTinkerInternals.isArkHotRuning();
   if (!isArkHotRuning && isEnabledForDex) {
       //tinker/patch.info/patch-641e634c/dex
       boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
       if (!dexCheck) {
           //file not found, do not load patch
           Log.w(TAG, "tryLoadPatchFiles:dex check fail");
           return;
       }
   }
   final boolean isEnabledForArkHot = ShareTinkerInternals.isTinkerEnabledForArkHot(tinkerFlag);
   if (isArkHotRuning && isEnabledForArkHot) {
       boolean arkHotCheck = TinkerArkHotLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
       if (!arkHotCheck) {
           // file not found, do not load patch
           Log.w(TAG, "tryLoadPatchFiles:dex check fail");
           return;
       }
   }
   final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
   if (isEnabledForNativeLib) {
       //tinker/patch.info/patch-641e634c/lib
       boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
       if (!libCheck) {
           //file not found, do not load patch
           Log.w(TAG, "tryLoadPatchFiles:native lib check fail");
           return;
       }
   }
   //check resource
   final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
   Log.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
   if (isEnabledForResource) {
       boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
       if (!resourceCheck) {
           //file not found, do not load patch
           Log.w(TAG, "tryLoadPatchFiles:resource check fail");
           return;
       }
   }
   //art环境且系统做ota升级,需要重新loadDexFile,因为之前的odex会失效。
   //only work for art platform oat,because of interpret, refuse 4.4 art oat
   //android o use quicken default, we don't need to use interpret mode
   boolean isSystemOTA = ShareTinkerInternals.isVmArt()
       && ShareTinkerInternals.isSystemOTA(patchInfo.fingerPrint)
       && Build.VERSION.SDK_INT >= 21 && !ShareTinkerInternals.isAfterAndroidO();
   resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_SYSTEM_OTA, isSystemOTA);
   //we should first try rewrite patch info file, if there is a error, we can't load jar
   if (mainProcess) {
       if (versionChanged) {
           patchInfo.oldVersion = version;
       }
       if (oatModeChanged) {
           patchInfo.oatDir = oatDex;
           patchInfo.isRemoveInterpretOATDir = true;
       }
   }
   if (!checkSafeModeCount(app)) {
       resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, new TinkerRuntimeException("checkSafeModeCount fail"));
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_UNCAUGHT_EXCEPTION);
       Log.w(TAG, "tryLoadPatchFiles:checkSafeModeCount fail");
       return;
   }
   //now we can load patch jar
   if (!isArkHotRuning && isEnabledForDex) {
       //加载dex补丁
       boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);
       if (isSystemOTA) {
           // update fingerprint after load success
           patchInfo.fingerPrint = Build.FINGERPRINT;
           patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
           // reset to false
           oatModeChanged = false;
           if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
               ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
               Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
               return;
           }
           // update oat dir
           resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
       }
       if (!loadTinkerJars) {
           Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
           return;
       }
   }

   if (isArkHotRuning && isEnabledForArkHot) {
       boolean loadArkHotFixJars = TinkerArkHotLoader.loadTinkerArkHot(app, patchVersionDirectory, resultIntent);
       if (!loadArkHotFixJars) {
           Log.w(TAG, "tryLoadPatchFiles:onPatchLoadArkApkFail");
           return;
       }
   }
   //now we can load patch resource
   if (isEnabledForResource) {
       //加载资源补丁
       boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
       if (!loadTinkerResources) {
           Log.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
           return;
       }
   }
   // Init component hotplug support.
   if ((isEnabledForDex || isEnabledForArkHot) && isEnabledForResource) {
       ComponentHotplug.install(app, securityCheck);
   }
   if (!AppInfoChangedBlocker.tryStart(app)) {
       Log.w(TAG, "tryLoadPatchFiles:AppInfoChangedBlocker install fail.");
       ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_BAIL_HACK_FAILURE);
       return;
   }
   // Before successfully exit, we should update stored version info and kill other process
   // to make them load latest patch when we first applied newer one.
   if (mainProcess && (versionChanged || oatModeChanged)) {
       //update old version to new
       if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
           ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
           Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
           return;
       }
       ShareTinkerInternals.killProcessExceptMain(app);
   }
   //all is ok!
   ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);
   Log.i(TAG, "tryLoadPatchFiles: load end, ok!");
}

简单总结这个长方法的工作:

  • 1.一系列检查:tinker功能是否打开、tinker文件夹是否存在、patch.info文件是否存在。
  • 2.通过patch.info校验patch有效性,决定是否加载new patch。
  • 3.补丁包校验:签名检查、tinkerid是否与基准包一致等。
  • 4.检查tinkerFlag确认开启了哪些修复功能:
application初始化时配置的tinkerFlag:
ShareConstants.java
public static final int TINKER_DISABLE             = 0x00;
public static final int TINKER_DEX_MASK            = 0x01;
public static final int TINKER_NATIVE_LIBRARY_MASK = 0x02;
public static final int TINKER_RESOURCE_MASK       = 0x04;
public static final int TINKER_ARKHOT_MASK = 0x08;
public static final int TINKER_DEX_AND_LIBRARY     = TINKER_DEX_MASK | TINKER_NATIVE_LIBRARY_MASK | TINKER_ARKHOT_MASK;
public static final int TINKER_ENABLE_ALL          = TINKER_DEX_MASK | TINKER_NATIVE_LIBRARY_MASK | TINKER_RESOURCE_MASK | TINKER_ARKHOT_MASK;
  • 5.加载dex、arthot、resource补丁。这里并没有尝试加载so,arthot是针对华为的,在1.9.6之后的某个版本专门针对华为新增的修复类型。

6.art环境且系统做ota升级,需要重新loadDexFile,因为之前的odex会失效。

另外,要强调的是:tinker文件夹是在执行合成的时候生成的,后续再分析,这里先看看文件夹内容:

cepheus:/data/data/com.stan.tinkersdkdemo/tinker # ls -al
total 36
drwx------ 3 u0_a350 u0_a350 4096 2020-08-12 10:14 .
drwx------ 8 u0_a350 u0_a350 4096 2020-08-12 10:14 ..
-rw------- 1 u0_a350 u0_a350    0 2020-08-12 10:15 info.lock
drwx------ 4 u0_a350 u0_a350 4096 2020-08-12 10:14 patch-8b79c8cc
-rw------- 1 u0_a350 u0_a350  367 2020-08-12 10:15 patch.info // patch信息描述文件

cepheus:/data/data/com.stan.tinkersdkdemo/tinker # cat patch.info
#from old version:8b79c8cc9881601c1e53e20bbe320026 to new version:8b79c8cc9881601c1e53e20bbe320026
#Wed Aug 12 10:15:25 GMT+08:00 2020
old=8b79c8cc9881601c1e53e20bbe320026 //旧包md5
is_remove_interpret_oat_dir=0
print=Xiaomi/cepheus/cepheus\:10/QKQ1.190825.002/9.10.29\:user/release-keys
dir=odex
is_protected_app=0
is_remove_new_version=0
new=8b79c8cc9881601c1e53e20bbe320026 //新包md5

cepheus:/data/data/com.stan.tinkersdkdemo/tinker/patch-8b79c8cc # ls -al
total 40
drwx------ 4 u0_a350 u0_a350 4096 2020-08-12 10:14 .
drwx------ 3 u0_a350 u0_a350 4096 2020-08-12 10:14 ..
drwx------ 3 u0_a350 u0_a350 4096 2020-08-12 10:14 dex
drwx------ 2 u0_a350 u0_a350 4096 2020-08-12 10:14 odex
-rw------- 1 u0_a350 u0_a350 3443 2020-08-12 10:14 patch-8b79c8cc.apk //合成前的diff patch包

cepheus:/data/data/com.stan.tinkersdkdemo/tinker/patch-8b79c8cc/dex # ls -al
total 2328
drwx------ 3 u0_a350 u0_a350    4096 2020-08-12 10:14 .
drwx------ 4 u0_a350 u0_a350    4096 2020-08-12 10:14 ..
drwxrwx--x 3 u0_a350 u0_a350    4096 2020-08-12 10:14 oat
-rw------- 1 u0_a350 u0_a350 2351677 2020-08-12 10:14 tinker_classN.apk //合成后的patch包

再看看patch包构成:

信息保存在assets中

  • package_meta.txt 补丁包的基本信息
  • dex_meta.txt dex补丁的信息
  • so_meta.txt so补丁的信息
  • res_meta.txt 资源补丁的信息

package_meta.txt

#base package config field
#Wed Aug 12 10:13:45 CST 2020
NEW_TINKER_ID=tinker_id_1.1
TINKER_ID=tinker_id_1.1
is_protected_app=0
patchMessage=fix the 1.0 version's bugs
patchVersion=1.0

dex_meta.txt

classes.dex,,4d97b3f8bcfdffb05e3ed5db5cbb8f83,4d97b3f8bcfdffb05e3ed5db5cbb8f83,35b4ca58525ecb64e20cd315d5832c70,3533914992,3549684149,jar
test.dex,,56900442eb5b7e1de45449d0685e6e00,56900442eb5b7e1de45449d0685e6e00,0,0,0,jar

TinkerApplication启动过程核心功能主要分两点:

  • 1通过@DefaultLifeCycle 利用apt动态生成 Application。这里tinker使用ApplicationLike作为Application的代理,它绑定了Application的生命周期,提供给客户端使用。
  • 2 TinkerLoader.tryLoad 尝试加载补丁包。

这里加载补丁包类型:

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