淺談ANDROID插件化

# Android #Plugin

一傻粘、認(rèn)識(shí)插件化

1.1 插件化起源

插件化技術(shù)最初源于免安裝運(yùn)行 Apk的想法巷查,這個(gè)免安裝的 Apk 就可以理解為插件,而支持插件的 app 我們一般叫 宿主抹腿。
想必大家都知道,在 Android 系統(tǒng)中旭寿,應(yīng)用是以 Apk 的形式存在的警绩,應(yīng)用都需要安裝才能使用。但實(shí)際上 Android 系統(tǒng)安裝應(yīng)用的方式相當(dāng)簡(jiǎn)單盅称,其實(shí)就是把應(yīng)用 Apk 拷貝到系統(tǒng)不同的目錄下肩祥、然后把 so 解壓出來而已。
常見的應(yīng)用安裝目錄有:

  • /system/app:系統(tǒng)應(yīng)用
  • /system/priv-app:系統(tǒng)應(yīng)用
  • /data/app:用戶應(yīng)用

那可能大家會(huì)想問缩膝,既然安裝這個(gè)過程如此簡(jiǎn)單混狠,Android 是怎么運(yùn)行應(yīng)用中的代碼的呢,我們先看 Apk 的構(gòu)成疾层,一個(gè)常見的 Apk 會(huì)包含如下幾個(gè)部分:

  • classes.dexJava 代碼字節(jié)碼
  • res:資源文件
  • libso 文件
  • assets:靜態(tài)資產(chǎn)文件
  • AndroidManifest.xml:清單文件

其實(shí) Android 系統(tǒng)在打開應(yīng)用之后将饺,也只是開辟進(jìn)程,然后使用 ClassLoader 加載 classes.dex 至進(jìn)程中痛黎,執(zhí)行對(duì)應(yīng)的組件而已予弧。
那大家可能會(huì)想一個(gè)問題,既然 Android 本身也是使用類似反射的形式加載代碼執(zhí)行湖饱,憑什么我們不能執(zhí)行一個(gè) Apk 中的代碼呢掖蛤?

1.2 插件化優(yōu)點(diǎn)

插件化讓 Apk 中的代碼(主要是指 Android 組件)能夠免安裝運(yùn)行,這樣能夠帶來很多收益:

  • 減少安裝Apk的體積井厌、按需下載模塊
  • 動(dòng)態(tài)更新插件
  • 宿主和插件分開編譯蚓庭,提升開發(fā)效率
  • 解決方法數(shù)超過65535的問題

想象一下,你的應(yīng)用擁有 Native 應(yīng)用一般極高的性能仅仆,又能獲取諸如 Web 應(yīng)用一樣的收益器赞。
嗯,理想很美好不是嘛蝇恶?

1.3 與組件化的區(qū)別

  • 組件化:是將一個(gè)App分成多個(gè)模塊拳魁,每個(gè)模塊都是一個(gè)組件(module),開發(fā)過程中可以讓這些組件相互依賴或獨(dú)立編譯撮弧、調(diào)試部分組件潘懊,但是這些組件最終會(huì)合并成一個(gè)完整的Apk去發(fā)布到應(yīng)用市場(chǎng)。

  • 插件化:是將整個(gè)App拆分成很多模塊贿衍,每個(gè)模塊都是一個(gè)Apk(組件化的每個(gè)模塊是一個(gè)lib)授舟,最終打包的時(shí)候?qū)⑺拗鰽pk和插件Apk分開打包,只需發(fā)布宿主Apk到應(yīng)用市場(chǎng)贸辈,插件Apk通過動(dòng)態(tài)按需下發(fā)到宿主Apk释树。

二、插件化的技術(shù)難點(diǎn)

想讓插件的Apk真正運(yùn)行起來,首先要先能找到插件Apk的存放位置奢啥,然后我們要能解析加載Apk里面的代碼秸仙。
但是光能執(zhí)行Java代碼是沒有意義的,在Android系統(tǒng)中有四大組件是需要在系統(tǒng)中注冊(cè)的桩盲,具體來說是在 Android 系統(tǒng)的 ActivityManagerService (AMS)PackageManagerService (PMS) 中注冊(cè)的寂纪,而四大組件的解析和啟動(dòng)都需要依賴 AMSPMS,如何欺騙系統(tǒng)赌结,讓他承認(rèn)一個(gè)未安裝的 Apk 中的組件捞蛋,如何讓宿主動(dòng)態(tài)加載執(zhí)行插件Apk中 Android 組件(即 ActivityService柬姚、BroadcastReceiver拟杉、ContentProviderFragment)等是插件化最大的難點(diǎn)量承。
另外搬设,應(yīng)用資源引用(特指 R 中引用的資源,如 layout撕捍、values 等)也是一大問題焕梅,想象一下你在宿主進(jìn)程中使用反射加載了一個(gè)插件 Apk,代碼中的 R 對(duì)應(yīng)的 id 卻無法引用到正確的資源卦洽,會(huì)產(chǎn)生什么后果贞言。
總結(jié)一下,其實(shí)做到插件化的要點(diǎn)就這幾個(gè):

  • 如何加載并執(zhí)行插件 Apk 中的代碼(ClassLoader Injection
  • 讓系統(tǒng)能調(diào)用插件 Apk 中的組件(Runtime Container
  • 正確識(shí)別插件 Apk 中的資源(Resource Injection

當(dāng)然還有其他一些小問題阀蒂,但可能不是所有場(chǎng)景下都會(huì)遇到该窗,我們后面再單獨(dú)說。

三蚤霞、ClassLoader Injection

ClassLoader 是插件化中必須要掌握的酗失,因?yàn)槲覀冎?code>Android 應(yīng)用本身是基于魔改的 Java 虛擬機(jī)的,而由于插件是未安裝的 apk昧绣,系統(tǒng)不會(huì)處理其中的類规肴,所以需要使用 ClassLoader 加載 Apk,然后反射里面的代碼夜畴。

3.1 java 中的 ClassLoader

  • BootstrapClassLoader 負(fù)責(zé)加載 JVM 運(yùn)行時(shí)的核心類拖刃,比如 JAVA_HOME/lib/rt.jar 等等
  • ExtensionClassLoader 負(fù)責(zé)加載 JVM 的擴(kuò)展類,比如 JAVA_HOME/lib/ext 下面的 jar 包
  • AppClassLoader 負(fù)責(zé)加載 classpath 里的 jar 包和目錄

3.2 android 中的 ClassLoader

在Android系統(tǒng)中ClassLoader是用來加載dex文件的贪绘,有包含 dex 的 apk 文件以及 jar 文件兑牡,dex 文件是一種對(duì)class文件優(yōu)化的產(chǎn)物,在Android中應(yīng)用打包時(shí)會(huì)把所有class文件進(jìn)行合并税灌、優(yōu)化(把不同的class文件重復(fù)的東西只保留一份)均函,然后生成一個(gè)最終的class.dex文件

  • PathClassLoader 用來加載系統(tǒng)類和應(yīng)用程序類亿虽,可以加載已經(jīng)安裝的 apk 目錄下的 dex 文件
public class PathClassLoader extends BaseDexClassLoader {
     public PathClassLoader(String dexPath, ClassLoader parent) {
       super(dexPath, null, null, parent);
     }
    
     public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
       super(dexPath, null, libraryPath, parent);
     }
 }
  • DexClassLoader 用來加載 dex 文件,可以從存儲(chǔ)空間加載 dex 文件苞也。
public class DexClassLoader extends BaseDexClassLoader {
   public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
       super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

我們?cè)诓寮幸话闶褂玫氖?DexClassLoader洛勉。

3.3 雙親委派機(jī)制

每一個(gè) ClassLoader 中都有一個(gè) parent 對(duì)象,代表的是父類加載器如迟,在加載一個(gè)類的時(shí)候坯认,會(huì)先使用父類加載器去加載,如果在父類加載器中沒有找到氓涣,自己再進(jìn)行加載,如果 parent 為空陋气,那么就用系統(tǒng)類加載器來加載劳吠。通過這樣的機(jī)制可以保證系統(tǒng)類都是由系統(tǒng)類加載器加載的。 下面是 ClassLoader 的 loadClass 方法的具體實(shí)現(xiàn)巩趁。

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
   // First, check if the class has already been loaded
   Class<?> c = findLoadedClass(name);
   if (c == null) {
     try {
       if (parent != null) {
         // 先從父類加載器中進(jìn)行加載
         c = parent.loadClass(name, false);
       } else {
         c = findBootstrapClassOrNull(name);
       }
     } catch (ClassNotFoundException e) {
       // ClassNotFoundException thrown if class not found
       // from the non-null parent class loader
     }

     if (c == null) {
       // 沒有找到痒玩,再自己加載
       c = findClass(name);
     }
   }
   return c;
 }

3.4 如何加載插件中的類

要加載插件中的類,我們首先要?jiǎng)?chuàng)建一個(gè) DexClassLoader议慰,先看下 DexClassLoader 的構(gòu)造函數(shù)需要那些參數(shù)蠢古。

public class DexClassLoader extends BaseDexClassLoader {
 public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
 // ...
 }
}

構(gòu)造函數(shù)需要四個(gè)參數(shù): dexPath 是需要加載的 dex / apk / jar 文件路徑 optimizedDirectory 是 dex 優(yōu)化后存放的位置,在 ART 上别凹,會(huì)執(zhí)行 oat 對(duì) dex 進(jìn)行優(yōu)化草讶,生成機(jī)器碼,這里就是存放優(yōu)化后的 odex 文件的位置 librarySearchPath 是 native 依賴的位置 parent 就是父類加載器炉菲,默認(rèn)會(huì)先從 parent 加載對(duì)應(yīng)的類

創(chuàng)建出 DexClassLaoder 實(shí)例以后堕战,只要調(diào)用其 loadClass(className) 方法就可以加載插件中的類了。具體的實(shí)現(xiàn)在下面:

// 從 assets 中拿出插件 apk 放到內(nèi)部存儲(chǔ)空間
 private fun extractPlugin() {
   var inputStream = assets.open("plugin.apk")
   File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
 }

 private fun init() {
   extractPlugin()
   pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
   nativeLibDir = File(filesDir, "pluginlib").absolutePath
   dexOutPath = File(filesDir, "dexout").absolutePath
   // 生成 DexClassLoader 用來加載插件類
   pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir,  this::class.java.classLoader)
 }
類加載流程.png

3.5 執(zhí)行插件類的方法

通過反射來執(zhí)行類的方法

val loadClass = pluginClassLoader.loadClass(activityName)
loadClass.getMethod("test",null).invoke(loadClass)

我們稱這個(gè)過程叫做 ClassLoader 注入拍霜。完成注入后嘱丢,所有來自宿主的類使用宿主的 ClassLoader 進(jìn)行加載,所有來自插件 Apk 的類使用插件 ClassLoader 進(jìn)行加載祠饺,而由于 ClassLoader 的雙親委派機(jī)制越驻,實(shí)際上系統(tǒng)類會(huì)不受 ClassLoader 的類隔離機(jī)制所影響,這樣宿主 Apk 就可以在宿主進(jìn)程中使用來自于插件的組件類了道偷。

四缀旁、Runtime Container

我們之前說到 Activity 插件化最大的難點(diǎn)是如何欺騙系統(tǒng),讓他承認(rèn)一個(gè)未安裝的 Apk 中的組件勺鸦。 因?yàn)椴寮莿?dòng)態(tài)加載的诵棵,所以插件的四大組件不可能注冊(cè)到宿主的 Manifest 文件中,而沒有在 Manifest 中注冊(cè)的四大組件是不能和系統(tǒng)直接進(jìn)行交互的祝旷。 如果直接把插件的 Activity 注冊(cè)到宿主 Manifest 里就失去了插件化的動(dòng)態(tài)特性履澳,因?yàn)槊看尾寮行略?Activity 都要修改宿主 Manifest 并且重新打包嘶窄,那就和直接寫在宿主中沒什么區(qū)別了。

4.1 為什么沒有注冊(cè)的 Activity 不能和系統(tǒng)交互

這里的不能直接交互的含義有兩個(gè)

  1. 系統(tǒng)會(huì)檢測(cè) Activity 是否注冊(cè) 如果我們啟動(dòng)一個(gè)沒有在 Manifest 中注冊(cè)的 Activity距贷,會(huì)發(fā)現(xiàn)報(bào)如下 error:
android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?

這個(gè) log 在 Instrumentation 的 checkStartActivityResult 方法中可以看到:

public class Instrumentation {
   public static void checkStartActivityResult(int res, Object intent) {
      if (!ActivityManager.isStartResultFatalError(res)) {
       return;
   }

   switch (res) {
     case ActivityManager.START_INTENT_NOT_RESOLVED:
     case ActivityManager.START_CLASS_NOT_FOUND:
       if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
       throw new ActivityNotFoundException(
       "Unable to find explicit activity class "
       + ((Intent)intent).getComponent().toShortString()
       + "; have you declared this activity in your AndroidManifest.xml?");
       throw new ActivityNotFoundException(
       "No Activity found to handle " + intent);
       ...
     }
   }
}
  1. Activity 的生命周期無法被調(diào)用柄冲,其實(shí)一個(gè) Activity 主要的工作,都是在其生命周期方法中調(diào)用了忠蝗,既然上一步系統(tǒng)檢測(cè)了 Manifest 注冊(cè)文件现横,啟動(dòng) Activity 被拒絕,那么其生命周期方法也肯定不會(huì)被調(diào)用了阁最。從而插件 Activity 也就不能正常運(yùn)行了戒祠。

4.2 運(yùn)行時(shí)容器技術(shù)

由于Android中的組件(Activity,Service速种,BroadcastReceiver和ContentProvider)是由系統(tǒng)創(chuàng)建的姜盈,并且由系統(tǒng)管理生命周期。 僅僅構(gòu)造出這些類的實(shí)例是沒用的配阵,還需要管理組件的生命周期馏颂。其中以Activity最為復(fù)雜,不同框架采用的方法也不盡相同棋傍。插件化如何支持組件生命周期的管理救拉。 大致分為兩種方式:

  • 運(yùn)行時(shí)容器技術(shù)(ProxyActivity代理)
  • 預(yù)埋StubActivity,hook系統(tǒng)啟動(dòng)Activity的過程

我們的解決方案很簡(jiǎn)單瘫拣,即運(yùn)行時(shí)容器技術(shù)亿絮,簡(jiǎn)單來說就是在宿主 Apk 中預(yù)埋一些空的 Android 組件,以 Activity 為例麸拄,我預(yù)置一個(gè) ContainerActivity extends Activity 在宿主中壹无,并且在 AndroidManifest.xml 中注冊(cè)它。
它要做的事情很簡(jiǎn)單感帅,就是幫助我們作為插件 Activity 的容器斗锭,它從 Intent 接受幾個(gè)參數(shù),分別是插件的不同信息失球,如:

  • pluginName
  • pluginApkPath
  • pluginActivityName

等岖是,其實(shí)最重要的就是 pluginApkPathpluginActivityName,當(dāng) ContainerActivity 啟動(dòng)時(shí)实苞,我們就加載插件的 ClassLoader豺撑、Resource,并反射 pluginActivityName 對(duì)應(yīng)的 Activity 類黔牵。當(dāng)完成加載后聪轿,ContainerActivity 要做兩件事:
* 轉(zhuǎn)發(fā)所有來自系統(tǒng)的生命周期回調(diào)至插件 Activity
* 接受 Activity 方法的系統(tǒng)調(diào)用,并轉(zhuǎn)發(fā)回系統(tǒng)
我們可以通過復(fù)寫 ContainerActivity 的生命周期方法來完成第一步猾浦,而第二步我們需要定義一個(gè) PluginActivity陆错,然后在編寫插件 Apk 中的 Activity 組件時(shí)灯抛,不再讓其集成 android.app.Activity,而是集成自我們的 PluginActivity音瓷。

public class ContainerActivity extends Activity {
        private PluginActivity pluginActivity;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            String pluginActivityName = getIntent().getString("pluginActivityName", "");
            pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
            if (pluginActivity == null) {
                super.onCreate(savedInstanceState);
                return;
            }

            pluginActivity.onCreate();
        }

        @Override
        protected void onResume() {
            if (pluginActivity == null) {
                super.onResume();
                return;
            }
            pluginActivity.onResume();
        }

        @Override
        protected void onPause() {
            if (pluginActivity == null) {
                super.onPause();
                return;
            }
            pluginActivity.onPause();
        }

        // ...
    }
public class PluginActivity {
        private ContainerActivity containerActivity;

        public PluginActivity(ContainerActivity containerActivity) {
            this.containerActivity = containerActivity;
        }

        @Override
        public <T extends View> T findViewById(int id) {
            return containerActivity.findViewById(id);
        }
        // ...
    }
// 插件 `Apk` 中真正寫的組件
    public class TestActivity extends PluginActivity {
        // ......
    }

是不是感覺有點(diǎn)看懂了对嚼,雖然真正搞的時(shí)候還有很多小坑,但大概原理就是這么簡(jiǎn)單绳慎,啟動(dòng)插件組件需要依賴容器纵竖,容器負(fù)責(zé)加載插件組件并且完成雙向轉(zhuǎn)發(fā),轉(zhuǎn)發(fā)來自系統(tǒng)的生命周期回調(diào)至插件組件杏愤,同時(shí)轉(zhuǎn)發(fā)來自插件組件的系統(tǒng)調(diào)用至系統(tǒng)靡砌。

4.3 字節(jié)碼替換

該方式雖然能夠很好的實(shí)現(xiàn)啟動(dòng)插件Activity的目的,但是由于開發(fā)式侵入性很強(qiáng)珊楼,插件中的Activity必須繼承PluginActivity通殃,如果想把之前的模塊改造成插件需要很多額外的工作。

class TestActivity extends Activity {}
    ->
    class TestActivity extends PluginActivity {}

有沒有什么辦法能讓插件組件的編寫與原來沒有任何差別呢亥曹?

Shadow 的做法是字節(jié)碼替換插件,這是一個(gè)非常棒的想法恨诱,簡(jiǎn)單來說媳瞪,Android 提供了一些 Gradle 插件開發(fā)套件,其中有一項(xiàng)功能叫 Transform Api照宝,它可以介入項(xiàng)目的構(gòu)建過程蛇受,在字節(jié)碼生成后、dex 文件生成前厕鹃,對(duì)代碼進(jìn)行某些變換兢仰,具體怎么做的不說了,可以自己看文檔剂碴。

實(shí)現(xiàn)的功能嘛把将,就是用戶配置 Gradle 插件后,正常開發(fā)忆矛,依然編寫:

class TestActivity extends Activity {}

然后完成編譯后察蹲,最后的字節(jié)碼中,顯示的卻是:

class TestActivity extends PluginActivity {}

到這里基本的框架就差不多結(jié)束了催训。

五洽议、Resource Injection

最后要說的是資源注入,其實(shí)這一點(diǎn)相當(dāng)重要漫拭,Android 應(yīng)用的開發(fā)其實(shí)崇尚的是邏輯與資源分離的理念亚兄,所有資源(layoutvalues 等)都會(huì)被打包到 Apk 中采驻,然后生成一個(gè)對(duì)應(yīng)的 R 類审胚,其中包含對(duì)所有資源的引用 id匈勋。
資源的注入并不容易,好在 Android 系統(tǒng)給我們留了一條后路菲盾,最重要的是這兩個(gè)接口:

  • PackageManager#getPackageArchiveInfo:根據(jù) Apk 路徑解析一個(gè)未安裝的 ApkPackageInfo
  • PackageManager#getResourcesForApplication:根據(jù) ApplicationInfo 創(chuàng)建一個(gè) Resources 實(shí)例

我們要做的就是在上面 ContainerActivity#onCreate 中加載插件 Apk 的時(shí)候颓影,用這兩個(gè)方法創(chuàng)建出來一份插件資源實(shí)例。具體來說就是先用 PackageManager#getPackageArchiveInfo 拿到插件 ApkPackageInfo懒鉴,有了 PacakgeInfo 之后我們就可以自己組裝一份 ApplicationInfo诡挂,然后通過 PackageManager#getResourcesForApplication 來創(chuàng)建資源實(shí)例,大概代碼像這樣:

PackageManager packageManager = getPackageManager();
   PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
       pluginApkPath,
       PackageManager.GET_ACTIVITIES
       | PackageManager.GET_META_DATA
       | PackageManager.GET_SERVICES
       | PackageManager.GET_PROVIDERS
       | PackageManager.GET_SIGNATURES
   );
   packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
   packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

   Resources injectResources = null;
   try {
       injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
   } catch (PackageManager.NameNotFoundException e) {
       // ...
   }

拿到資源實(shí)例后临谱,我們需要將宿主的資源和插件資源 Merge 一下璃俗,編寫一個(gè)新的 Resources 類,用這樣的方式完成自動(dòng)代理:

public class PluginResources extends Resources {
        private Resources hostResources;
        private Resources injectResources;

        public PluginResources(Resources hostResources, Resources injectResources) {
            super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
            this.hostResources = hostResources;
            this.injectResources = injectResources;
        }

        @Override
        public String getString(int id, Object... formatArgs) throws NotFoundException {
            try {
                return injectResources.getString(id, formatArgs);
            } catch (NotFoundException e) {
                return hostResources.getString(id, formatArgs);
            }
        }

        // ...
    }

然后我們?cè)?ContainerActivity 完成插件組件加載后悉默,創(chuàng)建一份 Merge 資源城豁,再?gòu)?fù)寫 ContainerActivity#getResources,將獲取到的資源替換掉:

public class ContainerActivity extends Activity {
        private Resources pluginResources;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            // ...
            pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
            // ...
        }

        @Override
        public Resources getResources() {
            if (pluginActivity == null) {
                return super.getResources();
            }
            return pluginResources;
        }
    }

這樣就完成了資源的注入抄课。

【參考文章】
《巧文解析 Android 插件化黑科技的實(shí)現(xiàn)原理》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末唱星,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子跟磨,更是在濱河造成了極大的恐慌间聊,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抵拘,死亡現(xiàn)場(chǎng)離奇詭異哎榴,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)僵蛛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門尚蝌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人充尉,你說我怎么就攤上這事飘言。” “怎么了驼侠?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵热凹,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我泪电,道長(zhǎng)般妙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任相速,我火速辦了婚禮碟渺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘突诬。我一直安慰自己苫拍,他們只是感情好芜繁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绒极,像睡著了一般骏令。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上垄提,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天榔袋,我揣著相機(jī)與錄音,去河邊找鬼铡俐。 笑死凰兑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的审丘。 我是一名探鬼主播吏够,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼滩报!你這毒婦竟也來了锅知?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤脓钾,失蹤者是張志新(化名)和其女友劉穎售睹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惭笑,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡侣姆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年生真,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了沉噩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡柱蟀,死狀恐怖川蒙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情长已,我是刑警寧澤畜眨,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站术瓮,受9級(jí)特大地震影響康聂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜胞四,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一恬汁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辜伟,春花似錦氓侧、人聲如沸脊另。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)偎痛。三九已至,卻和暖如春独郎,著一層夾襖步出監(jiān)牢的瞬間踩麦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工囚聚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留靖榕,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓顽铸,卻偏偏與公主長(zhǎng)得像茁计,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子谓松,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353