RePlugin源码阅读

0.目录

RePlugin是360公司推出的开源的Android插件化和热更新框架, 广泛运用于360旗下的Android客户端项目.

其项目源码和中文说明可参看其GitHub页面: https://github.com/Qihoo360/RePlugin/blob/dev/README_CN.md

本系列文章将主要针对其当前版本2.1.0源码进行学习和分析, 目的是学习如何去实现一个插件化和热更新框架.

RePlugin源码中一共分为4个子项目:

  • replugin-host-gradle host项目的gradle插件

  • replugin-host-library host项目需要依赖的android library

  • replugin-plugin-gradle plugin项目的gradle插件

  • replugin-plugin-library plugin项目需要依赖的android library

接下来会分别对这4个子项目的代码进行阅读和学习.


1.Host Gradle

1. replugin-host-gradle项目

1.1 简要说明

本文主要对RePlugin的插件项目源码进行学习和分析. 目的是了解RePlugin在Android构建时做了什么事情来支持其插件化系统.

本文需要对以下知识有一个初步了解:

1.2 插件简介

RePlugin-host-gradle项目为一个Gralde插件的项目, 主要是在Android构建的国中做如下事情:

  • 收集内置插件列表信息, 生成 /assert/plugins-builtin.json 文件
  • 读取RePlugin Gradle配置, 生成 RePluginHostConfig.java 文件
  • 修改AndroidManifest文件, 主要是用来占坑

注意: 这个项目主要使用 groovy 语言来编写gradle插件的.

1.3 插件实现类

开发Gradle插件, 需要在 src/main/resources/META-INF/gradle-plugins/ 目录下提供一个 properties 文件, 用于指定插件的实现类.

在本项目中的插件配置文件为: replugin-host-gradle.properties, 在其中定义了插件的实现类:

implementation-class=com.qihoo360.replugin.gradle.host.Replugin

每个Gradle插件的实现类, 必须实现 org.gradle.api.Plugin 接口即可, 该接口只有一个apply方法,

例如在本项目中的 Replugin 实现类如下:

import org.gradle.api.Plugin


public class Replugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        // ...
    }
}

1.4 Gradle自定义配置项

在接入RePlugin的过程里, 需要在项目的build.gradle文件中去配置RePlugin插件的一些配置项, 例如:

/** * 配置项均为可选配置,默认无需添加 * 更多可选配置项参见replugin-host-gradle的RepluginConfig类 * 可更改配置项参见 自动生成RePluginHostConfig.java */
repluginHostConfig {
    /** * 是否使用 AppCompat 库 * 不需要个性化配置时,无需添加 */
    useAppCompat = true
    /** * 背景不透明的坑的数量 * 不需要个性化配置时,无需添加 */
    countNotTranslucentStandard = 6
    countNotTranslucentSingleTop = 2
    countNotTranslucentSingleTask = 3
    countNotTranslucentSingleInstance = 2
}

具体的配置项可以参考: https://github.com/Qihoo360/RePlugin/wiki/%E4%B8%BB%E7%A8%8B%E5%BA%8F%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97

因此在Gralde插件的项目里, 首先需要添加这些配置项.

使用的方法是在 project.extensions 中创建一个 Configs 即可, 例如本项目中:

project.extensions.create("repluginHostConfig", RepluginConfig)

这里的RepluginConfig就是一个普通的类, 定义了所有可在gralde文件里配置的项, 例如:

class RepluginConfig {


    /** 自定义进程的数量(除 UI 和 Persistent 进程) */
    def countProcess = 3


    /** 是否使用常驻进程? */
    def persistentEnable = true


    /** 常驻进程名称(也就是上面说的 Persistent 进程,开发者可自定义)*/
    def persistentName = ':GuardService'


    /** 背景不透明的坑的数量 */
    def countNotTranslucentStandard = 6
    def countNotTranslucentSingleTop = 2
    def countNotTranslucentSingleTask = 3
    def countNotTranslucentSingleInstance = 2


    // ...
}

1.5 Android Gradle Plugin

这里需要额外的了解一下Android Gradle Plugin, 这个插件是所有Android应用主要使用的, 例如所有Android项目里的 build.gradle 里都会使用该plugin:

buildscript {
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
    }
}


apply plugin: 'com.android.application'

而这里的 'com.android.application' 插件也通过实现 org.gradle.api.Plugin 来实现的. 它的具体的代码如下:

package com.android.build.gradle


/**
* Gradle plugin class for 'application' projects.
*/
class AppPlugin extends BasePlugin implements Plugin<Project> {
    @Inject
    public AppPlugin(Instantiator instantiator, ToolingModelBuilderRegistry registry) {
        super(instantiator, registry)
    }


    @Override
    protected Class<? extends BaseExtension> getExtensionClass() {
        return AppExtension.class
    }


    @Override
    void apply(Project project) {
        super.apply(project)
    }


    // ...
}

在AppPlugin新建了一个project extension, 名字叫做 "android", 实现类是 AppExtension , 它就是我们一般在Android项目的 build.gradle 里定义的:

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"

    defaultConfig {
        applicationId "com.qihoo360.replugin.sample.host"
        minSdkVersion 9
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
    }


    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

因为Android Gradle的内容很多, 这里并不展开, 只需要看一个概念, 就是 buildTypes 的属性, 每个Android项目的gradle里都可以配置多种构建变种, 用于不同的场景, 打出不同的包, 例如默认会有两个构建变种:

  • release
  • debug

当我们默认使用Android Studio运行APP的时候, 默认使用的就是使用debug前面的debug包, 而发布的时候会使用开发者前面来打release包.

具体的相关信息可以参考Android官方文档: build-variants https://developer.android.com/studio/build/build-variants

我们这里需要先了解buildTypes的目的是为了知道, 在添加Gradle Task的时候, 也需要根据不同的变种, 新建不同的Task. 接下来就来看RePlugin具体新建了哪些Task.

1.6 Gradle Task

RePlugin插件一共添加了如下的几个自定义的Task:

  • ShowPlugins
    • rpShowPluginsDebug
    • rpShowPluginsRelease
  • GenerateBuiltinJson
    • rpGenerateDebugBuiltinJson
    • rpGenerateReleaseBuiltinJson
  • GenerateHostConfig
    • rpGenerateDebugHostConfig
    • rpGenerateReleaseHostConfig

ShowPlugins Task主要是用来显示内置插件列表信息, 在执行的时候它会在Gradle Console里打印出当前host下所有内置插件的信息.
GenerateBuiltinJson Task 主要是在构建的时候, 生成内置插件列表的Json文件.
GenerateHostConfig Task 主要是读取RePlugin Gradle的配置项, 然后生成Config的Java文件.

而正如上一节提到的, RePlugin新建的三种Task, 也根据buildType不同, 而新建了不同的Task.

这里我们来看具体如何根据不同的buildType来新建任务:

@Override
public void apply(Project project) {
    println "${TAG} Welcome to replugin world ! "


    this.project = project


    if (project.plugins.hasPlugin(AppPlugin)) { // apply plugin: 'com.android.application'


        def android = project.extensions.getByType(AppExtension) // android extensions


        android.applicationVariants.all { variant ->


            addShowPluginTask(variant) // 添加showPluginTask , 该Task用于整理并收集内置插件列表, 并将内置插件信息其保存到json文件中
   }
  }
}

这里首先检查了项目是否有使用 'com.android.application' 插件, 然后获取到 android 的 extension 实例, 遍历它的所有构建变种 android.applicationVariants .

然后在每个构建变种里面都新建一个 ShowPlugin 的Task. 接下来我们来看具体是如何实现这个Task的.

1.7 ShowPlugins Task

创建一个Task的方法, 例如创建ShowPlugins Task的方法如下:

    // 添加 【查看所有插件信息】 任务
    def addShowPluginTask(def variant) {
        def variantData = variant.variantData  // android当前的构建变种信息
        def scope = variantData.scope           // 变种scope对象
        def showPluginsTaskName = scope.getTaskName(AppConstant.TASK_SHOW_PLUGIN, "")   // 创建变种的task名称
        def showPluginsTask = project.task(showPluginsTaskName) // create new task with name


        showPluginsTask.doLast {
            // 在所有asset资源被merge以后, 遍历该目录, 找到所有插件的文件, 并读取其中的信息, 整理成插件列表
            // 以JSON的形式写入到 /assert/plugins-builtin.json 文件中
            IFileCreator creator = new PluginBuiltinJsonCreator(project, variant, config)
            def dir = creator.getFileDir() // mergeAssetsTask.outputDir


            if (!dir.exists()) {
                println "${AppConstant.TAG} The ${dir.absolutePath} does not exist "
                println "${AppConstant.TAG} pluginsInfo=null"
                return
            }


            String fileContent = creator.getFileContent()
            if (null == fileContent) {
                return
            }


            // 将插件列表信息以Json格式保存到: /assert/plugins-builtin.json
            new File(dir, creator.getFileName()).write(fileContent, 'UTF-8')
        }
        showPluginsTask.group = AppConstant.TASKS_GROUP


        //get mergeAssetsTask name
        String mergeAssetsTaskName = variant.getVariantData().getScope().getMergeAssetsTask().name
        //get real gradle task
        def mergeAssetsTask = project.tasks.getByName(mergeAssetsTaskName)


        //depend on mergeAssetsTask so that assets have been merged
        if (mergeAssetsTask) {
            showPluginsTask.dependsOn mergeAssetsTask
        }


    }


这里所有使用Android的构建变种scope来生成task name.

/**
* A scope containing data for a specific variant.
*/
public class VariantScopeImpl implements VariantScope {


    @Override
    @NonNull
    public String getTaskName(@NonNull String prefix, @NonNull String suffix) {
        return prefix + StringHelper.capitalize(getVariantConfiguration().getFullName()) + suffix;
    }


}

也就是这里传入的task名为"rpShowPlugins", 根据构建变种最终的task name则为"rpShowPluginsDebug" , 和 "rpShowPluginsRelease" .

然后新建Task: project.task(taskName), 并设置task的几个属性: doLast, group, dependsOn. 这三个都是gradle task自带的属性, 其中doLast表示在task的action list都执行完了以后最终指定的动作, 在该函数中主要做的事情就是遍历 /assert/plugins 目录下的所有 jar 包文件(因为RePlugin要求所有内置的Plugin, 都必须将apk文件重命令为 .jar 包文件, 并放置到 /assert/plugins 目录下),
然后依次的去读取这个jar包文件(其实是apk文件)里的AndroidManifest.xml文件, 获取插件的基本信息, 然后将所有插件的信息, 写到一个 /assert/plugins-builtin.json 文件里(gen目录下, 而非src目录).

该文件里列出了所有插件的信息:

[
    {
        "high": null, /** 插件最高兼容版本 */
        "frm": null, /** 框架版本号 */
        "ver": 104, /** 插件版本号 */
        "low": null, /** 插件最低兼容版本 */
        "pkg": "com.qihoo360.replugin.sample.demo1", /** 插件包名 */
        "path": "plugins/demo1.jar",   /** 插件文件路径 */
        "name": "demo1" /** 插件名 */
    }
]

这些信息都是在插件项目的AndroidManifest里进行配置的, 配置的方式如下:

<meta-data
    android:name="com.qihoo360.plugin.name"
    android:value="[你的插件别名]" />


<meta-data
    android:name="com.qihoo360.plugin.version.low"
    android:value="[你的插件协议版本号]" />
<meta-data
    android:name="com.qihoo360.plugin.version.high"
    android:value="[你的插件协议版本号]" />

具体的配置项可参看RePlugin官方文档: https://github.com/Qihoo360/RePlugin/wiki/%E6%8F%92%E4%BB%B6%E7%9A%84%E4%BF%A1%E6%81%AF

这里使用了第三方库来解析apk文件里的manifest文件, 第三方库为: https://github.com/hsiafan/apk-parser

ApkFile apkFile = new ApkFile(pluginFile) // 开源的APK解析库: <https://github.com/hsiafan/apk-parser>


// 解析Manifest中的应用包名, 版本号, 和RePlugin的配置项
String manifestXmlStr = apkFile.getManifestXml()
ByteArrayInputStream inputStream = new ByteArrayInputStream(manifestXmlStr.getBytes("UTF-8"))


SAXParserFactory factory = SAXParserFactory.newInstance()
SAXParser parser = factory.newSAXParser()
parser.parse(inputStream, this)

把manifest当做XML做解析, 读取其中的meta-data即可.

ShowPlugins Task设置为依赖Android的mergeAssert Task, 因为需要在子项目的assert资源都合并了以后再开始遍历. 这里由兴趣的可以扩展的了解一下Android Gradle提供的一个Task各自是做什么, 并且以怎样的顺序执行, 来对Android项目的构建有一个全面的了解. 具体看参考 android gradle项目里的 AndroidTask 类. 项目源码在:
https://android.googlesource.com/platform/tools/build

1.8 GenerateBuiltinJson Task

这个Task和ShowPlugins Task的任务基本是一样的, 也是去获取插件列表数据, 然后将其写到json文件.

唯一的区别是, ShowPlugins Task的主要是为了去将结果打印到Gradle Console, 便于开发者去查看当前的插件信息.

而GenerateBuiltinJson是在项目构建的过程去, 去生成这个json文件, 并将其打包到apk里.

因此这个Task依赖的依然是Android的MergeAssert Task, 但是它将自己加到的MergeAssert Task任务之后(finalizedBy)去执行, 从而将其加入了整个构建的流程中:

//depends on mergeAssets Task
String mergeAssetsTaskName = variant.getVariantData().getScope().getMergeAssetsTask().name
def mergeAssetsTask = project.tasks.getByName(mergeAssetsTaskName)
if (mergeAssetsTask) {
    generateBuiltinJsonTask.dependsOn mergeAssetsTask
    mergeAssetsTask.finalizedBy generateBuiltinJsonTask
}

1.9 GenerateHostConfig Task

这个任务的主要作用是去读取项目的build.gradle中关于RePlugin的配置项, 然后将其写到一个java文件里, 并将其编译到项目中去, 使得可以从java里(RePlugin library项目)去访问到这个配置项.

然而这个Task依赖的是 Android 的 GenerateBuildConfigTask , 并也是将其添加到这个任务之后进行执行, 使其进入构建流程.

Android的 GenerateBuildConfigTask 主要是为了生成 BuildConfig.java 文件, 里面包括了该构建变种的一些配置文件, 其中用得多的就是 BuildConfig.DEBUG 来判断当前是不是debug版本.

在这里的 GenerateHostConfigTask 任务里, 主要是使用 RePluginHostConfigCreator 类来负责生成 RePluginHostConfig.java 文件.
这里Java文件包括了所有RePlugin在Gradle里的配置项. 生成后的文件如下:

package com.qihoo360.replugin.gen;


/**
* 注意:此文件由插件化框架自动生成,请不要手动修改。
*/
public class RePluginHostConfig {


    // 常驻进程名字
    public static String PERSISTENT_NAME = "${config.persistentName}";


    // 是否使用“常驻进程”(见PERSISTENT_NAME)作为插件的管理进程。若为False,则会使用默认进程
    public static boolean PERSISTENT_ENABLE = ${config.persistentEnable};


    // 背景透明的坑的数量(每种 launchMode 不同)
    public static int ACTIVITY_PIT_COUNT_TS_STANDARD = ${config.countTranslucentStandard};
    public static int ACTIVITY_PIT_COUNT_TS_SINGLE_TOP = ${config.countTranslucentSingleTop};
    public static int ACTIVITY_PIT_COUNT_TS_SINGLE_TASK = ${config.countTranslucentSingleTask};
    public static int ACTIVITY_PIT_COUNT_TS_SINGLE_INSTANCE = ${
            config.countTranslucentSingleInstance
        };


  // ...
}

这里所有的配置项都对应了最开始我们提到的 repluginHostConfig 配置项.

刚才提到这个任务会在 Android 的 GenerateBuildConfigTask 之后执行, 并且它会在在生成的 BuildConfig.java 文件的同目录去写一个它自己的 RePluginHostConfig.java 文件. 这个目录一般情况下都是应用的packagename的根目录, 及和R文件同样的目录. 这个目录通过如下方法访问:

File buildConfigGeneratedDir = this.variant.getVariantData().getScope().getBuildConfigSourceOutputDir()

1.10 修改Manifest文件

RePlugin Host插件的最重要的任务, 就是去修改Manifest文件. 把RePlugin框架需要的一些组件添加到Manifest里.

这里主要是通过 variant.outputs.proccessManifest 任务去修改Manifest文件的, 其方式就是在AndroidManifest.xml被processManifest任务处理完毕以后, 再打开AndroidManifest.xml文件, 并将新的内容直接写到 </application> 之前以实现注册自己的组件.

/* ------------------------------------------------------------- */
// 在 生成AndroidManifest.xml的task结束后, 将需要添加到manifest中的文件内容添加到 </application> 前面
// output
variant.outputs.each { output ->
    output.processManifest.doLast {
        output.processManifest.outputs.files.each { File file ->
            def manifestFile = null;
            //在gradle plugin 3.0.0之前,file是文件,且文件名为AndroidManifest.xml
            //在gradle plugin 3.0.0之后,file是目录,且不包含AndroidManifest.xml,需要自己拼接
            //除了目录和AndroidManifest.xml之外,还可能会包含manifest-merger-debug-report.txt等不相干的文件,过滤它
            if ((file.name.equalsIgnoreCase("AndroidManifest.xml") && !file.isDirectory()) || file.isDirectory()) {
                if (file.isDirectory()) {
                    //3.0.0之后,自己拼接AndroidManifest.xml
                    manifestFile = new File(file, "AndroidManifest.xml")
                } else {
                    //3.0.0之前,直接使用
                    manifestFile = file
                }
                //检测文件是否存在
                if (manifestFile != null && manifestFile.exists()) {
                    println "${AppConstant.TAG} handle manifest: ${manifestFile}"
                    def updatedContent = manifestFile.getText("UTF-8").replaceAll("</application>", newManifest + "</application>")
                    manifestFile.write(updatedContent, 'UTF-8')
                }
            }
        }
    }
}


这里的 variant.output 的实现类是 ApkVariantOutputImpl , 其属性 processManifest 的实现类是 ManifestProcessorTask , 该任务主要是用来处理 Manifest 文件. 其包括在该文件中添加 miniSdkVersion, targetSdkVersion等, 还包括merge多个AndroidManifest.xml为一个文件.

这里在Manifest里新增的内容, 主要是通过 ComponentsGenerator 来类实现的, 其中主要是在创建一个 <application> 里的xml内容.

    /**
     * 动态生成插件化框架中需要的组件
     *
     * @param applicationID 宿主的 applicationID
     * @param config 用户配置
     * @return String       插件化框架中需要的组件
     */
    def static generateComponent(def applicationID, def config) {
        // 是否使用 AppCompat 库(涉及到默认主题)
        if (config.useAppCompat) {
            themeNTS = THEME_NTS_USE_APP_COMPAT
        } else {
            themeNTS = THEME_NTS_NOT_USE_APP_COMPAT
        }


        def writer = new StringWriter()
        def xml = new MarkupBuilder(writer) // gradle里专门用来创建xml的类


        /* UI 进程 */
        xml.application {


            /* 需要编译期动态修改进程名的组件*/


            String pluginMgrProcessName = config.persistentEnable ? config.persistentName : applicationID


            // 常驻进程Provider
            provider(
                    "${name}":"[com.qihoo360.replugin.component.process.ProcessPitProviderPersist](http://com.qihoo360.replugin.component.process.processpitproviderpersist/)",
                    "${authorities}":"${applicationID}.loader.p.main",
                    "${exp}":"false",
                    "${process}":"${pluginMgrProcessName}"
    //...

这里体现了 groovy 语言的优势, 其生成xml可以直接在代码里写, 而不需要拼接字符串, 上面的代码转换成xml就基本相当于:

<application>


    <provider
        android:name="[com.qihoo360.replugin.component.process.ProcessPitProviderPersist](http://com.qihoo360.replugin.component.process.processpitproviderpersist/)"
        android:authorities="your.packagename.loader.p.main"
        android:exported="false"
        android:process=":GuardService" />


</application>

在这里添加了三个provider:

还有一个Service:

和很多个不同属性的activity:

  • 透明的N个Activity: 不同launchMode的坑: singleTop, singleTask, singleInstance
  • 不透明的N个Activity: 不同launchMode的坑: singleTop, singleTask, singleInstance
  • 不同TaskAffinity的activity的坑
  • 不同process的activity的坑

以及多个自定义进程的各种provider, service, 和activity的坑.

这里的各种activity的占坑, 就是RePlugin支持activity热更新的关键. 在启动某个没有在Manifest注册的activity的时候, RePlugin会使用事先占好的坑来启动.

1.11 小结

到此为止, RePlugin Host Plugin项目的代码基本就读完了, 从中主要可以了解如何开发一个Gradle插件, 已经Android Gradle插件的一些基本概念.
对于RePlugin的插件系统有一点点了解, 并对RePlugin的Activity占坑有一个简单的了解.

后面需要去阅读 RePlugin Host Library 项目来了解具体的RePlugin实现方案.


NOTE ATTRIBUTES

Created Date: 2018-04-18 08:49:35
Last Evernote Update Date: 2018-05-17 09:12:06

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