我的Android重構(gòu)之旅:插件化篇

我的Android重構(gòu)之旅:架構(gòu)篇
我的Android重構(gòu)之旅:框架篇
我的Android重構(gòu)之旅:插件化篇

隨著項(xiàng)目的不斷成長渣玲,即便項(xiàng)目采用了 MVP 或是 MVVM 這類優(yōu)秀的架構(gòu)忘衍,也很難跟得上迭代的腳步铅搓,當(dāng) APP 端功能越來越龐大星掰、繁瑣蹋偏,人員不斷加入后威始,牽一發(fā)而動全局的事情時(shí)常發(fā)生黎棠,后續(xù)人員如同如履薄冰似的維護(hù)項(xiàng)目脓斩,為此我們必須考慮團(tuán)隊(duì)壯大后的開發(fā)模式随静,提前對業(yè)務(wù)進(jìn)行隔離燎猛,同時(shí)總結(jié)出插件化開發(fā)的流程沸停,完善 Android 端基礎(chǔ)框架愤钾。

本文是“我的Android重構(gòu)之旅”的第三篇能颁,也是讓我最為頭疼的一篇劲装,在本文中占业,我將會和大家聊一聊“插件化”的概念谦疾,以及我們在“插件化”框架上的選擇與碰到的一些問題念恍。

魯迅如是說道

Plug-in Hello World

插件化是指將 APK 分為宿主和插件的部分,在 APP 運(yùn)行時(shí)瞳氓,我們可以動態(tài)的載入或者替換插件部分匣摘。
宿主: 就是當(dāng)前運(yùn)行的APP音榜。
插件: 相對于插件化技術(shù)來說赠叼,就是要加載運(yùn)行的apk類文件嘴办。

插件化分為倆種形態(tài),一種插件與宿主 APP 無交互例如微信與微信小程序癞谒,一種插件與宿主極度耦合例如滴滴出行双仍,滴滴出行將用戶信息作為獨(dú)立的模塊朱沃,需要與其他模塊進(jìn)行數(shù)據(jù)的交互逗物,由于使用場景不一致翎卓,本文只針對插件與宿主有頻繁數(shù)據(jù)交互的情況失暴。

在我們開發(fā)的過程中逗扒,往往會碰到多人協(xié)作進(jìn)行模塊化的開發(fā)矩肩,我們期望能夠獨(dú)立運(yùn)行自己的模塊而又不受其他人模塊的影響述暂,還有一個(gè)更為常見的需求畦韭,我們在快速的產(chǎn)品迭代過程中艺配,我們往往希望能無縫銜接新的功能至用戶手機(jī)上转唉,過于頻繁的產(chǎn)品迭代或過長的開發(fā)周期赠法,這會使得我們在與竟品競爭時(shí)失去先機(jī)砖织。

Git 提交記錄

上圖是一款人臉識別產(chǎn)品的迭代記錄侧纯,由于上線的各個(gè)城市都有細(xì)微的邏輯差別,導(dǎo)致每次核心業(yè)務(wù)出現(xiàn) BUG 同事要一個(gè)個(gè) Push 至各各版本块请,然后通知各個(gè)城市的推廣商下載墩新,這時(shí)候我就在想抖棘,能不能把我們的應(yīng)用做成插件的形式動態(tài)下發(fā)呢切省,這樣就避免了每次都需要的版本升級朝捆,在某次 Push 版本的深夜驯用,我決定不能這樣下去了,我一定要用上插件化记餐。

插件化框架的選擇

下圖是主流的插件化片酝、組件化框架

特性 DynamicLoadApk DynamicAPK Small DroidPlugin VirtualAPK
支持四大組件 只支持Activity 只支持Activity 只支持Activity 全支持 全支持
組件無需在宿主manifest中預(yù)注冊 ×
插件可以依賴宿主 ×
支持PendingIntent × × ×
Android特性支持 大部分 大部分 大部分 幾乎全部 幾乎全部
兼容性適配 一般 一般 中等
插件構(gòu)建 部署aapt Gradle插件 Gradle插件

最終反復(fù)推敲決定使用滴滴出行的 VirtualAPK 作為我們的插件化框架,它有以下幾個(gè)優(yōu)點(diǎn):

  • 可與宿主工程通信
  • 兼容性強(qiáng)
  • 使用簡單
  • 編譯插件方便
  • 經(jīng)過大規(guī)模使用

如果你要加載一個(gè)插件审轮,并且這個(gè)插件無需和宿主有任何耦合,也無需和宿主進(jìn)行通信榆苞,并且你也不想對這個(gè)插件重新打包坐漏,那么推薦選擇DroidPlugin赊琳。

Android 插件化技術(shù)的典型應(yīng)用

插件化原理

VirtualAPK 對插件沒有額外的約束,原生的apk即可作為插件趁尼。插件工程編譯生成 Apk 后酥泞,即可通過宿主 App 加載芝囤,每個(gè)插件apk被加載后悯姊,都會在宿主中創(chuàng)建一個(gè)單獨(dú)的 LoadedPlugin 對象悯许。如下圖所示岸晦,通過這些 LoadedPlugin 對象启上,VirtualAPK 就可以管理插件并賦予插件新的意義,使其可以像手機(jī)中安裝過的 App 一樣運(yùn)行倒慧。

我們在引入一款框架的時(shí)候往往不能只單純的了解如何使用纫谅,應(yīng)去深入的了解它是如何工作的,特別是插件化這種熱門的技術(shù)询吴,十分感謝開源項(xiàng)目給了我們一把探尋 Android 世界的金鑰匙亮元,下面將和大家簡易的分析下 VirtualAPK 的原理奉瘤。

VirtualAPK 的工作過程

四大組件對于安卓人員都是再熟悉不過了盗温,我們都清楚四大組建都是需要在 AndroidManifest 中注冊的肌访,而對于 VirtualAPK 來說是不可能預(yù)先知曉名字惩激,提前注冊在宿主 Apk 中的风钻,所以現(xiàn)在基本都采用 hack 方案解決骡技,VirtualAPK 大致方案如下:

  • Activity:在宿主 Apk 中提前占坑,然后通過 Hook Activity 的啟動過程昼窗,“欺上瞞下”啟動插件 Apk 中的 Activity唆途,因?yàn)?Activity 存在不同的 LaunchMode 以及一些特殊的熟悉肛搬,所以需要多個(gè)占坑的“李鬼” Activity温赔。
  • Service:通過代理 Service 的方式去分發(fā)陶贼;主進(jìn)程和其他進(jìn)程骇窍,VirtualAPK 使用了兩個(gè)代理Service。
  • BroadcastReceiver:靜態(tài)轉(zhuǎn)動態(tài)驱犹。
  • ContentProvider:通過一個(gè)代理Provider進(jìn)行分發(fā)雄驹。

在本文医舆,我們主要分析 Activity 的占坑過程,如果需要更深入的了解 VirtualAPK 請點(diǎn)我

Activity 流程

我們?nèi)绻獑⒂?VirtualAPK 的話惫东,需要先調(diào)用pluginManager.loadPlugin(apk)廉沮,進(jìn)行加載插件,然后我們繼續(xù)向下調(diào)用

   // 調(diào)用 LoadedPlugin 加載插件 Activity 信息
   LoadedPlugin plugin = LoadedPlugin.create(this, this.mContext, apk);
   // 加載插件的 Application
   plugin.invokeApplication();

我們可以發(fā)現(xiàn)插件 Activity 的解析是交由LoadedPlugin.create 去完成的徐矩,完成之后保存至 mPlugins 這個(gè) Map 當(dāng)中方便下次調(diào)用與解綁插件滞时,我們繼續(xù)往下探索

        // 拷貝Resources
        this.mResources = createResources(context, apk);
        // 使用DexClassLoader加載插件并與現(xiàn)在的Dex進(jìn)行合并
        this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
        // 如果已經(jīng)初始化不解析
        if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) {
            throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
        }
        // 解析APK
        this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
        // 拷貝插件中的So
        tryToCopyNativeLib(apk);
        // 保存插件中的 Activity 參數(shù)
        Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
        for (PackageParser.Activity activity : this.mPackage.activities) {
            activityInfos.put(activity.getComponentName(), activity.info);
        }
        this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
        this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);

LoadedPlugin 中將我們插件中的資源合并進(jìn)了宿主 App 中,至此插件 App 的加載過程就已經(jīng)完成了丧蘸,這里大家肯定會有疑惑漂洋,該Activity必然沒有在Manifest中注冊,這么啟動不會報(bào)錯(cuò)嗎力喷?

這就要涉及到 Activity 的啟動流程了刽漂,我們在startActivity之后系統(tǒng)最終會調(diào)用 Instrumentation 的 execStartActivity 方法弟孟,然后再通過 ActivityManagerProxy 與 AMS 進(jìn)行交互。

Activity 是否注冊在 Manifest 的校驗(yàn)是由 AMS 進(jìn)行的,所以我們在于 AMS 交互前,提前將 ActivityManagerProxy 提交給 AMS 的 ComponentName替換為我們占坑的名字即可。
通常我們可以選擇 Hook Instrumentation 或者 Hook ActivityManagerProxy 都可以達(dá)到目標(biāo),VirtualAPK 選擇了 Hook Instrumentation 眉抬。

 private void hookInstrumentationAndHandler() {
        try {
            Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
            if (baseInstrumentation.getClass().getName().contains("lbe")) {
                // reject executing in paralell space, for example, lbe.
                System.exit(0);
            }
            // 用于處理替換 Activity 的名稱
            final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);
            Object activityThread = ReflectUtil.getActivityThread(this.mContext);
            // Hook Instrumentation 替換 Activity 名稱
            ReflectUtil.setInstrumentation(activityThread, instrumentation);
            // Hook handleLaunchActivity
            ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
            this.mInstrumentation = instrumentation;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

上面我們已經(jīng)成功的 Hook 了 Instrumentation ,接下來就是需要我們的李鬼上場了

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
        // 只有是插件中的Activity 才進(jìn)行替換
        if (intent.getComponent() != null) {
            Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(),
                    intent.getComponent().getClassName()));
            // 使用"李鬼"進(jìn)行替換
            this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
        }
        ActivityResult result = realExecStartActivity(who, contextThread, token, target,
                    intent, requestCode, options);
        return result;
    }

我們來看一看 markIntentIfNeeded(intent); 到底做了什么

    public void markIntentIfNeeded(Intent intent) {
        if (intent.getComponent() == null) {
            return;
        }
        String targetPackageName = intent.getComponent().getPackageName();
        String targetClassName = intent.getComponent().getClassName();
        // 保存我們原有數(shù)據(jù)
        if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
            intent.putExtra(Constants.KEY_IS_PLUGIN, true);
            intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
            intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
            dispatchStubActivity(intent);
        }
    }

    private void dispatchStubActivity(Intent intent) {
        ComponentName component = intent.getComponent();
        String targetClassName = intent.getComponent().getClassName();
        LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
        ActivityInfo info = loadedPlugin.getActivityInfo(component);
        // 判斷是否是插件中的Activity
        if (info == null) {
            throw new RuntimeException("can not find " + component);
        }
        int launchMode = info.launchMode;
        // 并入主題
        Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
        themeObj.applyStyle(info.theme, true);
        // 將插件中的 Activity 替換為占坑的 Activity
        String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
        Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
        intent.setClassName(mContext, stubActivity);
    }

可以看到上面將我們原本的信息保存至 Intent 中颈墅,然后調(diào)用了 getStubActivity(targetClassName, launchMode, themeObj); 進(jìn)行了替換


    public static final String STUB_ACTIVITY_STANDARD = "%s.A$%d";
    public static final String STUB_ACTIVITY_SINGLETOP = "%s.B$%d";
    public static final String STUB_ACTIVITY_SINGLETASK = "%s.C$%d";
    public static final String STUB_ACTIVITY_SINGLEINSTANCE = "%s.D$%d";

    public String getStubActivity(String className, int launchMode, Theme theme) {
        String stubActivity= mCachedStubActivity.get(className);
        if (stubActivity != null) {
            return stubActivity;
        }

        TypedArray array = theme.obtainStyledAttributes(new int[]{
                android.R.attr.windowIsTranslucent,
                android.R.attr.windowBackground
        });
        boolean windowIsTranslucent = array.getBoolean(0, false);
        array.recycle();
        if (Constants.DEBUG) {
            Log.d("StubActivityInfo", "getStubActivity, is transparent theme ? " + windowIsTranslucent);
        }
        stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
        switch (launchMode) {
            case ActivityInfo.LAUNCH_MULTIPLE: {
                stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
                if (windowIsTranslucent) {
                    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
                }
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_TOP: {
                usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_TASK: {
                usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
                break;
            }
            case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
                usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
                stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
                break;
            }

            default:break;
        }

        mCachedStubActivity.put(className, stubActivity);
        return stubActivity;
    }
       <!-- Stub Activities -->
       <activity android:name=".B$1" android:launchMode="singleTop"/>
       <activity android:name=".C$1" android:launchMode="singleTask"/>
       <activity android:name=".D$1" android:launchMode="singleInstance"/>
        其余略····

StubActivityInfo 根據(jù)同的 launchMode 啟動相應(yīng)的“李鬼” Activity 至此腿箩,我們已經(jīng)成功的 欺騙了 AMS ,啟動了我們占坑的 Activity 但是只成功了一半滑潘,為什么這么說呢追逮?因?yàn)槠垓_過了 AMS,AMS 執(zhí)行完成后漾唉,最終要啟動的并非是占坑的 Activity 般此,所以我們還要能正確的啟動目標(biāo)Activity。

我們在 Hook Instrumentation 的同時(shí)一并 Hook 了 handleLaunchActivity,所以我們之間到 Instrumentation 的 newActivity 方法查看啟動 Activity 的流程。

  @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        try {
            // 是否能直接加載这揣,如果能就是宿主中的 Activity
            cl.loadClass(className);
        } catch (ClassNotFoundException e) {
            // 取得正確的 Activity
            LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
            String targetClassName = PluginUtil.getTargetActivity(intent);
            Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName));
            // 判斷是否是 VirtualApk 啟動的插件 Activity
            if (targetClassName != null) {
                Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
                // 啟動插件 Activity
                activity.setIntent(intent);
                try {
                    // for 4.1+
                    ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
                } catch (Exception ignored) {
                    // ignored.
                }
                return activity;
            }
        }
        // 宿主的 Activity 直接啟動
        return mBase.newActivity(cl, className, intent);
    }

好了,到此Activity就可以正常啟動了芥挣。

小結(jié)

VritualApk 整理思路很清晰,在這里我們只介紹了 Activity 的啟動方式都弹,感興趣的同學(xué)可以去網(wǎng)上了解下其余三大組建的代理方式。不論如何如果想使用插件化框架,一定要了解其中的實(shí)現(xiàn)原理油狂,文檔上描述的并不是所有的細(xì)節(jié)溪烤,很多一些屬性什么的掂铐,以及由于其實(shí)現(xiàn)的方式造成一些特性的不支持炮叶。

引入插件化之痛

由于項(xiàng)目的宿主與插件需要進(jìn)行較為緊密的交互,在插件化的同時(shí)需要對項(xiàng)目進(jìn)行模塊化矩距,但是模塊化并不能一蹴而就,在模塊化的過程中經(jīng)常出現(xiàn)艇潭,牽一發(fā)而動全身的問題,在經(jīng)歷過無數(shù)個(gè)通宵的夜晚后,我總結(jié)出了模塊化的幾項(xiàng)準(zhǔn)則盗扇。

插件化的使用

VirtualAPK 本身的使用并不困難颠锉,困難的是需要逐步整理項(xiàng)目的模塊戈毒,在這期間問題百出,因?yàn)樽陨頉]有相關(guān)經(jīng)驗(yàn)在網(wǎng)上看了很多關(guān)于模塊化的文章迹蛤,最終我找到有贊模塊化的文章,對他們總結(jié)出來的經(jīng)驗(yàn)深刻認(rèn)同落包。

在項(xiàng)目模塊化時(shí)應(yīng)該遵循以下幾個(gè)準(zhǔn)則

  • 確定業(yè)務(wù)邏輯邊界
  • 模塊的更改上保持克制
  • 公共資源及時(shí)抽取

確定業(yè)務(wù)邏輯邊界
在模塊化之前旭寿,我們先要詳細(xì)的分析業(yè)務(wù)邏輯疾层,App 作為業(yè)務(wù)鏈的末端琉历,由于角色所限,開發(fā)人員對業(yè)務(wù)的理解比后端要淺授舟,所謂欲速則不達(dá)桩盲,重構(gòu)不能急伤靠,理清楚業(yè)務(wù)邏輯之后再動手该窗。

項(xiàng)目改造前結(jié)構(gòu)

在模塊化進(jìn)行時(shí)贪绘,我們需要將業(yè)務(wù)模塊進(jìn)行隔離狸窘,業(yè)務(wù)模塊之間不能互相依賴能存在數(shù)據(jù)傳輸,只能單向依賴宿主項(xiàng)目奴曙,為了達(dá)到這個(gè)效果 我們需要借用市面上的路由方案 ARouter 洽糟,由于篇幅原因沉御,我在這里不做過多介紹伐谈,感興趣的同學(xué)可以自行搜索距贷。

項(xiàng)目改造前結(jié)構(gòu)

項(xiàng)目改造后宿主只留下最簡單的公共基礎(chǔ)邏輯配阵,其他部分都由插件的形式裝載拂铡,這樣使得我們在版本更新的過程中自由度很高壹无,從項(xiàng)目結(jié)構(gòu)上我們看起來很像所有插件都依賴了宿主 App 的代碼,但實(shí)際上在打包的過程中 VirtualAPK 會幫助我們剔除重復(fù)資源感帅。

打包完成的插件

模塊的更改上保持克制
在模塊化進(jìn)行時(shí)斗锭,不要過分的追求完美的目標(biāo),簡單粗暴一點(diǎn)失球,后續(xù)再逐漸改善岖是,很多業(yè)務(wù)邏輯經(jīng)常會和其他業(yè)務(wù)邏輯產(chǎn)生牽連,它們倆會處于一個(gè)相對曖昧的關(guān)系实苞,這種時(shí)候我們不要去強(qiáng)行的分割它們的業(yè)務(wù)邊界豺撑,過分的分割往往會因?yàn)榫幋a人員對于模塊的不清晰導(dǎo)致項(xiàng)目改造的全盤崩潰。

公共資源及時(shí)抽取
VirtualAPK 會幫助我們剔除重復(fù)資源黔牵,對于一些曖昧不清的資源我們可以索性將它放入宿主項(xiàng)目中聪轿,如果將過多的資源存于插件項(xiàng)目中,這樣會導(dǎo)致我們的插件失去應(yīng)有的靈活性和資源的復(fù)用性猾浦。

總結(jié)

最初在公司內(nèi)部推廣插件化的時(shí)候陆错,同事們嘩然一片大多數(shù)都是對插件化的質(zhì)疑,在這里我要感謝我原來的領(lǐng)導(dǎo)金赦,在關(guān)鍵時(shí)刻給我的支持幫我頂住了大家質(zhì)疑的聲音音瓷,在十多個(gè)日日夜夜的修改重構(gòu)后,插件化后的第一個(gè)上線的版本素邪,插件化靈活的優(yōu)勢體現(xiàn)的淋漓盡致外莲,每個(gè)插件只有60 KB 的大小猪半,對服務(wù)端的帶寬幾乎沒有絲毫的壓力兔朦,幫助我們快速的進(jìn)行了產(chǎn)品的迭代 、Bug的修復(fù)磨确。
本文中沽甥,只是我自己在項(xiàng)目插件化的一些經(jīng)驗(yàn)與想法,并沒有深入的介紹如何使用 VirtualAPK 感興趣的同學(xué)可以讀一下 VirtualAPK 的 WiKi 乏奥,希望本文的設(shè)計(jì)思路能帶給你一些幫助摆舟。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市邓了,隨后出現(xiàn)的幾起案子恨诱,更是在濱河造成了極大的恐慌,老刑警劉巖骗炉,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件照宝,死亡現(xiàn)場離奇詭異,居然都是意外死亡句葵,警方通過查閱死者的電腦和手機(jī)厕鹃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門兢仰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人剂碴,你說我怎么就攤上這事把将。” “怎么了忆矛?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵察蹲,是天一觀的道長。 經(jīng)常有香客問我催训,道長递览,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任瞳腌,我火速辦了婚禮绞铃,結(jié)果婚禮上嫂侍,老公的妹妹穿的比我還像新娘儿捧。我一直安慰自己,他們只是感情好挑宠,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布菲盾。 她就那樣靜靜地躺著,像睡著了一般各淀。 火紅的嫁衣襯著肌膚如雪懒鉴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天碎浇,我揣著相機(jī)與錄音临谱,去河邊找鬼。 笑死奴璃,一個(gè)胖子當(dāng)著我的面吹牛悉默,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播苟穆,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼抄课,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了雳旅?” 一聲冷哼從身側(cè)響起跟磨,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎攒盈,沒想到半個(gè)月后抵拘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沦童,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年仑濒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了叹话。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡墩瞳,死狀恐怖驼壶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情喉酌,我是刑警寧澤热凹,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站泪电,受9級特大地震影響般妙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜相速,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一碟渺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧突诬,春花似錦苫拍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蔬捷,卻和暖如春垄提,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背周拐。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工铡俐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人速妖。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓高蜂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親罕容。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

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