Tinker Dex熱修復(fù)源碼分析

Tencent Tinker Hotfix開源方案https://github.com/Tencent/tinker

0x10 應(yīng)用接入Tinker

請參考Tinker官方指導(dǎo)或者Tinker Github

0x20 Tinker工作原理

0x21 生成patch dex

  • 運行assembleRelease生成base apk
  • 修改base apk的代碼
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Tinker.MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.e(TAG, "i am on onCreate classloader:" + MainActivity.class.getClassLoader().toString());
        //test resource change
        // base apk代碼
        //Log.e(TAG, "i am on onCreate string:" + getResources().getString(R.string.test_resource));
        // patch apk代碼
        Log.e(TAG, "i am on patch onCreate");
        ...
    }
    ...
}
  • 修改Gradle腳本聲明base apk
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-release-1123-20-23-05.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-release-1123-20-23-05-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-release-1123-20-23-05-R.txt"
  • 運行tinkerPatchRelease生成補丁

  • 對于生成patch dex文件的原理努溃,由于水平所限,暫不討論

0x22 tinker在應(yīng)用內(nèi)的安裝

SampleApplication開始分析捻爷。

首先看TinkerLoadertryLoad()方法姻僧,tryLoad()方法用于加載patch后的classes.dex文件规丽。

    public Intent tryLoad(TinkerApplication app) {
        Intent resultIntent = new Intent();

        long begin = SystemClock.elapsedRealtime();
        // 實際調(diào)用tryLoadPatchFilesInternal
        tryLoadPatchFilesInternal(app, resultIntent);
        long cost = SystemClock.elapsedRealtime() - begin;
        ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
        return resultIntent;
    }

    private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
        final int tinkerFlag = app.getTinkerFlags();
        // 首先檢查是否可以加載,不滿足直接返回
        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
        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;
        }
        ......
    }

tinker的安裝初始化階段撇贺,我們假定patch dex還沒有生成赌莺,這樣加載失敗直接返回。后面分析patch dex生成后显熏,加載patch后的classes.dex的過程雄嚣,也就是hotfix如何生效的過程。
接下來是tinker的安裝過程喘蟆,暫不討論缓升。

0x22 合并patch dex生成oat文件

以tinker-sample-android Demo為例

我們從點擊load patch開始分析。

我們從點擊loadPatchButton開始分析蕴轨。

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.e(TAG, "i am on onCreate classloader:" + MainActivity.class.getClassLoader().toString());
        //test resource change
        Log.e(TAG, "i am on onCreate string:" + getResources().getString(R.string.test_resource));
//        Log.e(TAG, "i am on patch onCreate");

        Button loadPatchButton = (Button) findViewById(R.id.loadPatch);

        loadPatchButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 加載Patch Dex文件/sdcard/patch_signed_7zip.apk
                TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
            }
        });
        ......
    }

繼續(xù)看onReceiveUpgradePatch()的實現(xiàn)港谊。

    public static void onReceiveUpgradePatch(Context context, String patchLocation) {
       Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
   }

getPatchListener()返回SamplePatchListener對象,SamplePatchListener繼承自DefaultPatchListener橙弱,下面看onPatchReceived()的實現(xiàn)歧寺。

   public int onPatchReceived(String path) {
       File patchFile = new File(path);
       // Patch檢查
       int returnCode = patchCheck(path, SharePatchFileUtil.getMD5(patchFile));

       if (returnCode == ShareConstants.ERROR_PATCH_OK) {
           // 運行PatchService
           TinkerPatchService.runPatchService(context, path);
       } else {
           Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
       }
       return returnCode;
   }

TinkerPatchService負責合并patch dex與原始apk的classes.dex以及dex優(yōu)化燥狰,運行在獨立的進程中。TinkerPatchService繼承自IntentService斜筐,這樣最終在onHandleIntent()中處理服務(wù)請求龙致。

   protected void onHandleIntent(Intent intent) {
       final Context context = getApplicationContext();
       Tinker tinker = Tinker.with(context);
       tinker.getPatchReporter().onPatchServiceStart(intent);
       ......
       String path = getPatchPathExtra(intent);
       if (path == null) {
           TinkerLog.e(TAG, "TinkerPatchService can't get the path extra, ignoring.");
           return;
       }
       // patch文件
       File patchFile = new File(path);
       ......
       // 前臺服務(wù)
       increasingPriority();
       PatchResult patchResult = new PatchResult();
       try {
           if (upgradePatchProcessor == null) {
               throw new TinkerRuntimeException("upgradePatchProcessor is null.");
           }
           // 嘗試安裝patch
           result = upgradePatchProcessor.tryPatch(context, path, patchResult);
       } catch (Throwable throwable) {
           e = throwable;
           result = false;
           tinker.getPatchReporter().onPatchException(patchFile, e);
       }
       ......
   }        

upgradePatchProcessorUpgradePatch類的對象,下面看trypatch()的實現(xiàn)顷链。

   public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
       Tinker manager = Tinker.with(context);

       final File patchFile = new File(tempPatchPath);
       ......
       if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
           TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
           return false;
       }
       ......
   }         

tryRecoverDexFiles()實現(xiàn)了dex文件的合并與oat文件的生成目代,本文只討論oat文件的生成過程。下面直接看dexOptimizeDexFiles()方法嗤练。

   private static boolean dexOptimizeDexFiles(Context context, List<File> dexFiles, String optimizeDexDirectory, final File patchFile) {
       // 參數(shù)dexFiles表示需要優(yōu)化的dex文件(patch合成后)列表
       // optimizeDexDirectory為oat文件的存放路徑
       final Tinker manager = Tinker.with(context);

       optFiles.clear();

       if (dexFiles != null) {
           File optimizeDexDirectoryFile = new File(optimizeDexDirectory);
           ......
           // add opt files
           for (File file : dexFiles) {
               String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile);
               optFiles.add(new File(outputPathName));
           }
           ......
           // try parallel dex optimizer
           // 串行優(yōu)化多個dex文件
           TinkerDexOptimizer.optimizeAll(
                   dexFiles, optimizeDexDirectoryFile,
               new TinkerDexOptimizer.ResultCallback() {
                   long startTime;

                   @Override
                   public void onStart(File dexFile, File optimizedDir) {
                       startTime = System.currentTimeMillis();
                       TinkerLog.i(TAG, "start to parallel optimize dex %s, size: %d", dexFile.getPath(), dexFile.length());
                   }

                   @Override
                   public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
                       // Do nothing.
                       TinkerLog.i(TAG, "success to parallel optimize dex %s, opt file:%s, opt file size: %d, use time %d",
                           dexFile.getPath(), optimizedFile.getPath(), optimizedFile.length(), (System.currentTimeMillis() - startTime));
                   }

                   @Override
                   public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
                       TinkerLog.i(TAG, "fail to parallel optimize dex %s use time %d",
                           dexFile.getPath(), (System.currentTimeMillis() - startTime));
                       failOptDexFile.add(dexFile);
                       throwable[0] = thr;
                   }
               }
           );
           .......
       }
   }                        

optimizeAll()方法對多個dex文件排序后榛了,調(diào)用OptimizeWorkerrun()方法優(yōu)化dex。下面看run()方法的實現(xiàn)煞抬。

        public boolean run() {
            try {
                if (!SharePatchFileUtil.isLegalFile(dexFile)) {
                    if (callback != null) {
                        callback.onFailed(dexFile, optimizedDir,
                            new IOException("dex file " + dexFile.getAbsolutePath() + " is not exist!"));
                        return false;
                    }
                }
                if (callback != null) {
                    callback.onStart(dexFile, optimizedDir);
                }
                String optimizedPath = SharePatchFileUtil.optimizedPathFor(this.dexFile, this.optimizedDir);
                if (useInterpretMode) {
                    interpretDex2Oat(dexFile.getAbsolutePath(), optimizedPath);
                } else {
                    DexFile.loadDex(dexFile.getAbsolutePath(), optimizedPath, 0);
                }
                if (callback != null) {
                    callback.onSuccess(dexFile, optimizedDir, new File(optimizedPath));
                }
            } catch (final Throwable e) {
                Log.e(TAG, "Failed to optimize dex: " + dexFile.getAbsolutePath(), e);
                if (callback != null) {
                    callback.onFailed(dexFile, optimizedDir, e);
                    return false;
                }
            }
            return true;
        }

run()方法中霜大,如果采用interpret-only編譯dex,直接使用Android提供的dex2oat命令生成oat文件革答;否則使用DexFile.loadDex()方法生成oat文件战坤。interpertDex2Oat()以及DexFile.loadDex()均會等待dex2oat進程結(jié)束才會返回。

Tinker之前采用并行優(yōu)化dex文件蝗碎,多個dex2oat進程同時編譯湖笨,系統(tǒng)負載太高,可能會引發(fā)ANR等問題蹦骑,現(xiàn)在代碼中已是串行。另外臀防,對于不緊急的patch眠菇,是否可以使用JobScheduler當系統(tǒng)空閑時來優(yōu)化dex文件。DexFile.loadDex()最終也是通過dex2oat來生成oat文件袱衷,但默認的compiler-filter是speed捎废。

0x23 Hotfix如何生效

tinker安裝時,TinkerLoadertryLoad()會調(diào)用tryLoadPatchFilesInternal()嘗試加載oat文件致燥,我們從這里開始分析登疗。

    private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
        final int tinkerFlag = app.getTinkerFlags();
        // 一系列檢查
        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
        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;
        }
        ......
        //now we can load patch jar
        // 滿足條件加載oat文件
        if (isEnabledForDex) {
            boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);
        ......
        }
        ......
    }

下面分析loadTinkerJars().

    public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA) {
        if (loadDexList.isEmpty() && classNDexInfo.isEmpty()) {
            Log.w(TAG, "there is no dex to load");
            return true;
        }
        ......
        ArrayList<File> legalFiles = new ArrayList<>();
        // 非classesN.dex的dex文件
        for (ShareDexDiffPatchInfo info : loadDexList) {
            //for dalvik, ignore art support dex
            if (isJustArtSupportDex(info)) {
                continue;
            }

            String path = dexPath + info.realName;
            File file = new File(path);

            if (application.isTinkerLoadVerifyFlag()) {
                long start = System.currentTimeMillis();
                String checkMd5 = getInfoMd5(info);
                if (!SharePatchFileUtil.verifyDexFileMd5(file, checkMd5)) {
                    //it is good to delete the mismatch file
                    ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
                    intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
                        file.getAbsolutePath());
                    return false;
                }
                Log.i(TAG, "verify dex file:" + file.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
            }
            legalFiles.add(file);
        }
        // verify merge classN.apk
        if (isVmArt && !classNDexInfo.isEmpty()) {
            File classNFile = new File(dexPath + ShareConstants.CLASS_N_APK_NAME);
            long start = System.currentTimeMillis();

            if (application.isTinkerLoadVerifyFlag()) {
                // classesN.dex文件,請參考multidex
                for (ShareDexDiffPatchInfo info : classNDexInfo) {
                    if (!SharePatchFileUtil.verifyDexFileMd5(classNFile, info.rawName, info.destMd5InArt)) {
                        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
                        intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
                            classNFile.getAbsolutePath());
                        return false;
                    }
                }
            }
            Log.i(TAG, "verify dex file:" + classNFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));

            legalFiles.add(classNFile);
        }
        ......
        try {
            // 安裝dex文件嫌蚤,合法的dex文件保存在參數(shù)legalFiles辐益,optimizeDir為dex文件對應(yīng)的oat文件路徑
            SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
        } catch (Throwable e) {
            Log.e(TAG, "install dexes failed");
//            e.printStackTrace();
            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
            return false;
        }

        return true;
    }        

下面看installDexes(),它是patch dex生效的關(guān)鍵所在脱吱,這里只分析SDK >= 24的情況智政。

    public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
        throws Throwable {
        Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());

        if (!files.isEmpty()) {
            files = createSortedAdditionalPathEntries(files);
            ClassLoader classLoader = loader;
            if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
                // 創(chuàng)建新的應(yīng)用類加載器
                classLoader = AndroidNClassLoader.inject(loader, application);
            }
            //because in dalvik, if inner class is not the same classloader with it wrapper class.
            //it won't fail at dex2opt
            if (Build.VERSION.SDK_INT >= 23) {
                // 修改類加載器的DexPathList
                V23.install(classLoader, files, dexOptDir);
            } else if (Build.VERSION.SDK_INT >= 19) {
                V19.install(classLoader, files, dexOptDir);
            } else if (Build.VERSION.SDK_INT >= 14) {
                V14.install(classLoader, files, dexOptDir);
            } else {
                V4.install(classLoader, files, dexOptDir);
            }
            //install done
            sPatchDexCount = files.size();
            Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);

            if (!checkDexInstall(classLoader)) {
                //reset patch dex
                SystemClassLoaderAdder.uninstallPatchDex(classLoader);
                throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
            }
        }
    }

下面首先看AndroidNClassLoaderinject()方法。

    public static AndroidNClassLoader inject(PathClassLoader originClassLoader, Application application) throws Exception {
        // originClassLoader為應(yīng)用默認的類加載器
        // application為應(yīng)用的Application對象
        AndroidNClassLoader classLoader = createAndroidNClassLoader(originClassLoader, application);
        // 把新的應(yīng)用類加載器設(shè)置到LoadedApk以及Thread中
        reflectPackageInfoClassloader(application, classLoader);
        return classLoader;
    }

下面看createAndroidNClassLoader()的實現(xiàn)箱蝠。

    private static AndroidNClassLoader createAndroidNClassLoader(PathClassLoader originalClassLoader, Application application) throws Exception {
        //let all element ""
        // 創(chuàng)建繼承自PathClassLoader的應(yīng)用類加載器
        final AndroidNClassLoader androidNClassLoader = new AndroidNClassLoader("",  originalClassLoader, application);
        final Field pathListField = ShareReflectUtil.findField(originalClassLoader, "pathList");
        final Object originPathList = pathListField.get(originalClassLoader);

        // To avoid 'dex file register with multiple classloader' exception on Android O, we must keep old
        // dexPathList in original classloader so that after the newly loaded base dex was bound to
        // AndroidNClassLoader we can still load class in base dex from original classloader.

        // 用原類加載器的pathList創(chuàng)建新的類加載器的pathList
        Object newPathList = recreateDexPathList(originPathList, androidNClassLoader);

        // Update new classloader's pathList.
        pathListField.set(androidNClassLoader, newPathList);

        return androidNClassLoader;
    }

下面看V23.install()的實現(xiàn)续捂。

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                    File optimizedDirectory)
            throws IllegalArgumentException, IllegalAccessException,
            NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            /* The patched class loader is expected to be a descendant of
             * dalvik.system.BaseDexClassLoader. We modify its
             * dalvik.system.DexPathList pathList field to append additional DEX
             * file entries.
             */
            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
                new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makePathElement", e);
                    throw e;
                }

            }
        }

install()主要負責加載新增的dex文件垦垂,以及將新增的dex文件與原來的dex文件列表合并起來。此后牙瓢,應(yīng)用的類加載工作由新類加載器接管劫拗。

如果dex文件沒有優(yōu)化安裝,makePathElements會觸發(fā)dex2oat.

0x24 小結(jié)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末矾克,一起剝皮案震驚了整個濱河市杨幼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌聂渊,老刑警劉巖差购,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異汉嗽,居然都是意外死亡欲逃,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門饼暑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來稳析,“玉大人,你說我怎么就攤上這事弓叛≌镁樱” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵撰筷,是天一觀的道長陈惰。 經(jīng)常有香客問我,道長毕籽,這世上最難降的妖魔是什么抬闯? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮关筒,結(jié)果婚禮上溶握,老公的妹妹穿的比我還像新娘。我一直安慰自己蒸播,他們只是感情好睡榆,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著袍榆,像睡著了一般胀屿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蜡塌,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天碉纳,我揣著相機與錄音,去河邊找鬼馏艾。 笑死劳曹,一個胖子當著我的面吹牛奴愉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播铁孵,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼锭硼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蜕劝?” 一聲冷哼從身側(cè)響起檀头,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎岖沛,沒想到半個月后暑始,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡婴削,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年廊镜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唉俗。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡贺辰,死狀恐怖甲献,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赁还,我是刑警寧澤礁阁,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布坐漏,位于F島的核電站充岛,受9級特大地震影響盹憎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寺酪,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一坎背、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧寄雀,春花似錦、人聲如沸陨献。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽眨业。三九已至急膀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間龄捡,已是汗流浹背卓嫂。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留聘殖,地道東北人晨雳。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓行瑞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親餐禁。 傳聞我的和親對象是個殘疾皇子血久,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

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