Android熱修復工具Tinker集成

Tinker介紹

Tinker是微信團隊開源的Android熱修復工具,支持dex, library和resources的熱更新。關于Tinker的基本的接入方法、Api和原理等,在官方wiki中有非常詳細的介紹。我這里重點描述一下基于我們項目的接入流程(客戶端和后臺),使用姿勢和遇到的問題,以及如何在Jenkins上構建補丁包。

Tinker接入

Tinker是目前熱修復方案中穩定性和兼容性最好的,畢竟源于微信團隊嘛!但也正是為了提高穩定性和兼容性,Tinker在接入成本上做了妥協,它不像以往的Andfix那樣可以一鍵接入,必須改造自己的Application,詳細可參考自定義Application類。其實改造的過程也并不復雜,只是多了一點學習成本。
但使用對Tinker進行再次封裝的第三方平臺的SDK還是可以實現一鍵接入的,比如TinkerPatch平臺Bugly熱更新功能,但這種方式對Application進行了反射,是有風險的:

TinkerPatch 平臺通過自動反射 Application,可以實現無縫接入。事實上,對于反射失敗的情況,我們會自動回退到代理 Application 生命周期模式,防止因為反射失敗而造成應用無法啟動的問題。
通過線上統計,大約有 1/1W的反射失敗率。我們更加推薦大家使用 Tinker 的方式改造自身的 Application, 使兼容性高。

而且我們需要自己搭建后臺來管理補丁包,所以不會使用第三方SDK,而是自己封裝了一套SDK,其實就是將Tinker的調用API和與后臺接口的通信功能進行了整合而已,封裝方式和后臺搭建也是基于github上的一個開源項目的:https://github.com/baidao/tinker-manager

我們的客戶端SDK已經放在了公司內部的Maven倉庫中:compile 'com.****.tinkerutils:utils:${version}'
我們已經搭建好的補丁管理平臺測試地址是:http://172.22.34.201/hotfix-console/

開始接入
gradle是Tinker推薦的接入方式,如果要使用命令行接入請參考這里。
第一步,引入Tinker插件和依賴
添加tinker-gradle-plugin到工程根目錄下的build.gradle的dependencies中:

buildscript {
    dependencies {
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7')
    }
}

然后在你的主module中的build.gradle文件里apply插件:

apply plugin: 'com.tencent.tinker.patch'

注意:這里的主module是指應用的啟動Application所在的module,即含有apply plugin: 'com.android.application'這句話的module,對應我們項目就是MyMoney,否則Tinker會拋出Exception

然后加入Tinker的lib依賴:

dependencies {
    //optional, help to generate the final application
    provided('com.tencent.tinker:tinker-android-anno:1.7.7')
    //tinker's main Android lib
    compile('com.tencent.tinker:tinker-android-lib:1.7.7') 
}

但因為我們使用自己的SDK,SDK中已經有了Tinker lib的依賴,所以,我們加入SDK的依賴即可:

dependencies {
    //optional, help to generate the final application
    provided('com.tencent.tinker:tinker-android-anno:1.7.7')
    //our tinker SDK
    compile 'com.****.tinkerutils:utils:${version}'
}

第二步,改造項目原有的Application
將我們現有的AppApplication直接繼承Tinker提供的DefaultApplicationLike類,參考自定義Application類,這樣我們的AppApplication就成了真實的Application(RealApplication,自定義或者通過注解自動生成)的代理類,這樣做就是為了將RealApplication隔離起來,防止誤修改,如此一來,在RealApplication中所做的所有初始化工作也就相當于轉移到了代理類中,間接實現了Application可修改進行熱修復的目的。
比如我們原來的AppApplication如下:

public class AppApplication extends Application {

    private static final String TAG = "AppApplication";

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            MultiDex.install(base);
            context = this;
        }catch (Exception e){
            DebugUtil.exception(TAG,e);
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 全局初始化代碼
        ... ...
    }

    // 復寫了Application的方法
    @Override
    public Resources getResources() {
        Resources res = super.getResources();
        if(res.getConfiguration().fontScale != 1){
            Configuration newConfig = res.getConfiguration();
            newConfig.fontScale = 1;
            res.updateConfiguration(newConfig, res.getDisplayMetrics());
        }
        return res;
    }

    @Override
    public void startActivities(Intent[] intents) {
        // do some option
        ... ...
        super.startActivities(intents);
    }
}

那我們改造后應該是這樣:

@DefaultLifeCycle(application = "${yourpackage}.RealApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL)
public class AppApplication extends DefaultApplicationLike {

    private static final String TAG = "AppApplication";

    public AppApplication(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        try {
            //you must install multiDex whatever tinker is installed!
            MultiDex.install(base);
            //此處通過getApplication()拿到的其實就是RealApplication
            context = getApplication();
            //install tinker
            TinkerUtils.installTinker(getApplication(), this);
        }catch (Exception e){
            DebugUtil.exception(TAG,e);
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 設置tinker參數并向后臺請求補丁包
        TinkerUtils.setUpTinker(context);
        // 全局初始化代碼
        ... ...
    }

    @Override
    public Resources getResources(Resources res) {
        if(res.getConfiguration().fontScale != 1){
            Configuration newConfig = res.getConfiguration();
            newConfig.fontScale = 1;
            res.updateConfiguration(newConfig, res.getDisplayMetrics());
        }
        return res;
    }
    public Resources getResources() {
        return getApplication().getResources();
    }
}

上面是通過注解的方式來自動生成RealApplication,如果使用自定義的方式,則直接新建RealApplication類繼承TinkerApplication并創建對應構造方法即可,不用注解,也不用引入注解依賴。

public class RealApplication extends TinkerApplication {
    public RealApplication() {
      super(
        //tinkerFlags, tinker支持的類型,dex,library,還是全部都支持!
        ShareConstants.TINKER_ENABLE_ALL,
        //ApplicationLike的實現類,只能傳遞字符串 
        "tinker.sample.android.app.SampleApplicationLike",
        //Tinker的加載器,一般來說用默認的即可
        "com.tencent.tinker.loader.TinkerLoader",
        //tinkerLoadVerifyFlag, 運行加載時是否校驗dex,lib與res的Md5
        false);
    }  
}

官方提示:除了構造方法之外,你最好不要引入其他的類,這將導致它們無法通過補丁修改。

注意:改造完成后要用RealApplication替換掉AndroidManifest.xml中原來的AppApplication:

    <application
        android:name=".RealApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        ... ...

另外因為我們的SDK中有自定義AbstractResultService類,即TinkerResultService,所以也需要在清單文件中加上它,否則補丁合成會出問題

     <service
            android:name="com.feidee.tinkerutils.TinkerResultService"
            android:exported="false"/>

參考TinkerApplication源碼可以知道為什么如此修改Application:

    ... ...
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Thread.setDefaultUncaughtExceptionHandler(new TinkerUncaughtHandler(this));
        onBaseContextAttached(base);
    }

    private void onBaseContextAttached(Context base) {
        applicationStartElapsedTime = SystemClock.elapsedRealtime();
        applicationStartMillisTime = System.currentTimeMillis();
        loadTinker();
        ensureDelegate();
        //此處的applicationLike現在就是我們的AppApplication類
        applicationLike.onBaseContextAttached(base);
        //reset save mode
        if (useSafeMode) {
            String processName = ShareTinkerInternals.getProcessName(this);
            String preferName = ShareConstants.TINKER_OWN_PREFERENCE_CONFIG + processName;
            SharedPreferences sp = getSharedPreferences(preferName, Context.MODE_PRIVATE);
            sp.edit().putInt(ShareConstants.TINKER_SAFE_MODE_COUNT, 0).commit();
        }
    }
    ... ...

    @Override
    public void onCreate() {
        super.onCreate();
        ensureDelegate();
        //此處的applicationLike現在就是我們的AppApplication類
        applicationLike.onCreate();
    }
    ... ...

    @Override
    public Resources getResources() {
        Resources resources = super.getResources();
        if (applicationLike != null) {
            //此處的applicationLike現在就是我們的AppApplication類
            return applicationLike.getResources(resources);
        }
        return resources;
    }
    ... ...

改造原來AppApplication復寫的startActivities方法時,我發現DefaultApplicationLike類中并沒有類似getResources的代理方法,所以我只有將這個復寫放到了RealApplication中。

第三步,增加Tinker的gradle配置
Tinker的gradle參數配置很靈活,具體的參數設置事例可參考官方sample中的app/build.gradle,為了使gradle文件不至于太混雜,我將Tinker相關的配置單獨抽取出來放在新建的tinker_support.gradle文件中,然后在MyMoney/build.gradle文件中加入下面一行即可:

// tinker config
apply from: 'tinker_support.gradle'

為了更方便的構建,主要修改了以下配置參數:

//基準apk包的備份路徑,這里僅作備份用,每次構建apk時會將生成的apk文件、mapping和R文件自動拷貝一份到這個目錄下去
def bakPath = file("${buildDir}/bakApk/")
//構建補丁時獲取基準apk包的文件名
def getTinkerBaseApkFileName(def defaultName) {
    return hasProperty("TINKER_BASE_APK_NAME") ? TINKER_BASE_APK_NAME : defaultName
}
/**
 * you can use assembleRelease to build you base apk
 * use tinkerPatchRelease -POLD_APK=  -PAPPLY_MAPPING=  -PAPPLY_RESOURCE= to build patch
 * add apk from the build/bakApk
 */
ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = isRelease();
    tinkerBaseApkFileName = getTinkerBaseApkFileName("Mymoney_base.apk")// todo 構建時需要在此配置基準包的filename
    //proguard mapping file to build patch apk
    tinkerMappingFileName = tinkerBaseApkFileName.substring(0, tinkerBaseApkFileName.length() - 4) + "-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerSymbolFileName = tinkerBaseApkFileName.substring(0, tinkerBaseApkFileName.length() - 4) + "-R.txt"
}
/**
 * mapping 文件 路徑取得是 rootDir/tinker/mapping/
 * @return
 */
def getMappingFilePath() {
//    String baseMappingPath = project.projectDir.toString() + "/document/mapping/"
    String baseMappingPath = "${rootDir}/tinker/mapping/"
    String tailPath = ext.tinkerMappingFileName
    String middlePath = ""
    if (hasProperty("channelCode")) {
        middlePath = channelCode + "/"
    }
    return baseMappingPath + middlePath + tailPath
}
/**
 * R 文件 路徑取得是 rootDir/tinker/symbol/
 */
def getSymbolFilePath() {
    String baseSymbolPath = "${rootDir}/tinker/symbol/"
    String tailSymbolPath = ext.tinkerSymbolFileName
    String middleSymbolPath = ""
    if (hasProperty("channelCode")) {
        middleSymbolPath = channelCode + "/"
    }
    return baseSymbolPath + middleSymbolPath + tailSymbolPath
}
/**
 * 基準apk文件  rootDir/tinker/apk/
 * @return
 */
def getBaseApkFilePath() {
    String baseApkPath = "${rootDir}/tinker/apk/"
    String tailApkPath = ext.tinkerBaseApkFileName
    String middleApkPath = ""
    if (hasProperty("channelCode")) {
        middleApkPath = channelCode + "/"
    }
    return baseApkPath + middleApkPath + tailApkPath
}

// tinkerId是唯一標識,這里默認指定為apk的版本號
def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : ext.apkVersionName
... ...
//如果在本地目錄下找不到基準apk,就去我們放基準包的地方去下載
def downloadBaseApkFile(def address, def savePath) {
    new File(savePath).withOutputStream { out ->
        out << new URL(address).openStream()
    }
}

以上更改把基準包的獲取路徑改到了根目錄下的tinker目錄中,把tinkerEnabled的值付給isRelease()函數,這樣在實際應用發布版本時,我們手動在創建一個tinker目錄及其相應文件夾(只需創建一次即可),然后將我們構建好的apk、相應的mapping和R文件(需改名為${tinkerBaseApkFileName}-mapping.txt和${tinkerBaseApkFileName}-R.txt),這樣做的好處時基準包可以放在本地不被clean掉,方便在本地手動構建時進行管理。

第四步,安裝和初始化Tinker
上面改造后的AppApplication中有兩行代碼用于Tinker的安裝和初始化:

//install tinker 
TinkerUtils.installTinker(getApplication(), this);
// 設置tinker參數并向后臺請求補丁包
TinkerUtils.setUpTinker(context);

TinkerUtils代碼如下:

public class TinkerUtils {
    private static final String TAG = "Tinker";
    public static void installTinker(Context context, ApplicationLike applicationLike) {
        // 安裝tinker
        SampleTinkerManager.initCurrentChannelValue(ChannelUtil.getChannel());
        SampleTinkerManager.setTinkerApplicationLike(applicationLike);
        SampleTinkerManager.initFastCrashProtect();
        //should set before com.dx168.patchsdk.sample.tinker is installed
        SampleTinkerManager.setUpgradeRetryEnable(true);
        //installTinker after load multiDex
        //or you can put com.tencent.com.dx168.patchsdk.sample.tinker.** to main dex
        SampleTinkerManager.installTinker(applicationLike);
        Tinker.with(context);
        //使用Hack的方式,如果補丁中有so庫 那么直接加載補丁中的armeabi下的so庫(將tinker library中的armeabi注冊到系統的library path中。)
        TinkerLoadLibrary.installNavitveLibraryABI(context, "armeabi");
    }
    public static void setUpTinker(Context context) {
        if (ChannelUtil.isGoogleVersion()) {
            return;
        }
        //在補丁管理后臺注冊的id和key,參數值配置在gradle文件中
        String appId = BuildConfig.TINKER_APP_ID;
        String appSecret = BuildConfig.TINKER_APP_SECRET;
        String tinkerUrl = BuildConfig.TINKER_PATCH_URL;
        PatchManager.getInstance().init(context, tinkerUrl, appId, appSecret, new ActualPatchManager() {
            @Override
            public void cleanPatch(Context context) {
                TinkerInstaller.cleanPatch(context);
                DebugUtil.debug(TAG, "local patch sdk >>>>> cleanPatch");
            }
            @Override
            public void applyPatch(Context context, String patchPath) {
                TinkerInstaller.onReceiveUpgradePatch(context, patchPath);
                DebugUtil.debug(TAG, "local patch sdk >>>>> applyPatch: " + patchPath);
            }
        });
        PatchManager.getInstance().setTag(ChannelUtil.getChannel());//可用于灰度發布
        PatchManager.getInstance().setChannel(ChannelUtil.getChannel());
        PatchManager.getInstance().queryAndApplyPatch(new PatchListener() {
            @Override
            public void onQuerySuccess(String response) {
                DebugUtil.debug(TAG, "local patch sdk >>>>>  onQuerySuccess response={ignore in log}");
            }
            @Override
            public void onQueryFailure(Throwable e) {
                DebugUtil.debug(TAG, "local patch sdk >>>>>  onQueryFailure e=" + Log.getStackTraceString(e));
            }
            @Override
            public void onDownloadSuccess(String path) {
                DebugUtil.debug(TAG, "local patch sdk >>>>>  onDownloadSuccess path=" + path);
            }
            @Override
            public void onDownloadFailure(Throwable e) {
                DebugUtil.debug(TAG, "local patch sdk >>>>>  onDownloadFailure e=" + Log.getStackTraceString(e));
            }
            @Override
            public void onApplySuccess() {
                DebugUtil.debug(TAG, "local patch sdk >>>>>  onApplySuccess");
            }
            @Override
            public void onApplyFailure(String msg) {
                DebugUtil.debug(TAG, "local patch sdk >>>>> onApplyFailure msg=" + msg);
            }
            @Override
            public void onCompleted() {
                DebugUtil.debug(TAG, "local patch sdk >>>>> onCompleted");
            }
        });
    }
}

構建補丁包

如上個步驟所說,構建好基準包后,將apk、mapping和R文件改好名字后放在tinker相應目錄中,就可以開始構建補丁包了。打包方式:

直接使用task:tinkerPatchVariantName(例如tinkerPatchDebug、tinkerPatchRelease)即可自動根據Variant選擇相應的編譯類型,同時它還貼心的為我們完成以下幾個操作:
1.將TINKER_ID自動插入AndroidManifest的meta項,輸出路徑為build/intermediates/tinker_intermediates/AndroidManifest.xml;
2.如果minifyEnabled為true,將自動將Tinker的proguard規則添加到proguardFiles中,輸出路徑為build/intermediates/tinker_intermediates/tinker_proguard.pro,這里你不需要將它們拷貝到自己的proguard配置文件中;
3.如果multiDexEnabled為true,將自動生成Tinker需要放在主dex的keep規則。在tinker 1.7.6版本之前,你需要手動將生成規則拷貝到自己的multiDexKeepProguard文件中。例如Sample中的multiDexKeepProguard file("keep_in_main_dex.txt")。在1.7.6版本之后,這里會通過腳本自動處理,無須手動填寫。
4.把dexOptions的jumboMode打開。

我們構建Release包時,直接執行下面命令即可:

./gradlew tinkerPatchRelease

構建很快,輸出目錄為build/outputs/tinkerPatch/release,會產生兩個帶簽名的apk格式的補丁patch_signed.apkpatch_signed_7zip.apk,構建log會提示我們哪個補丁更小并建議我們使用小的。更改代碼和資源文件造成的改動量會影響補丁包的大小,只改一行代碼的情況下,補丁包大約為4k。

測試

測試主要從以下幾個方面進行:

  • 集成Tinker后,打包測試apk是否有可能存在的bug
  • 測試補丁下發流程及合成(補丁拉取時機是每次app進程重新啟動時,拉取后會自動合成,合成后在鎖屏或者app正好處于后臺的情況下會自動殺掉app進程,補丁在進程重啟后生效。如合成失敗,會自動重試一次)
  • 測試包含不同類型修改的補丁(Tinker目前版本不支持清單文件的修改)
  • 修改Application(此處即指改造后的繼承DefaultApplicationLike的類)
  • 修改其他代碼
  • 修改資源文件
  • 測試補丁是否對渠道信息有影響
  • 測試對同一個基準apk下發多個補丁的情況(目前的策略后臺會根據補丁上傳的時間自動修改補丁的版本號,當高版本的補丁被下發時,已合成的補丁會自動被清除,再嘗試合成新補?。?/li>
  • app版本升級(在升級版本時我們也無須手動去清除補丁,框架已經為我們做了這件事情)

在后臺創建好app,拿到對應的key配置到項目中,并創建對應基準apk的版本,上傳對應版本的補丁包(后臺會自動改名,所以下發的補丁包不會包含.apk的后綴名),選擇是否灰度等,即可下發補丁。

補丁管理后臺

Debug打印日志可以看到補丁的拉取和合成過程


補丁下載及合成過程

Jenkins構建支持

因為構建補丁包時,有三個變量,即基準apk、mapping和R文件,所以我們可以使用Jenkins提供的參數化構建。

構建命令如下,配置TINKER_BASE_APK_NAME為Mymoney_base.apk

修改構建的輸出目錄為:

MyMoney/build/outputs/tinkerPatch/release/patch_signed_7zip.apk,MyMoney/build/outputs/tinkerPatch/release/patch_signed.apk

在開始構建之前,我們需要上傳相應文件來設置我們添加的三個文件參數,因為Jenkins會將我們設置好的文件參數指向我們上傳的文件,而參數名稱已經根據TINKER_BASE_APK_NAME寫死而且符合規范,所以我們每次構建都無需對項目配置和Jenkins配置做任何修改,只需要上傳對應基準文件即可(也無需修改文件名了)。


構建完成后就可以生成相應的補丁包:

遇到的問題

  1. 在Debug構建測試Tinker時,會出現不能斷點調試的情況,這是因為我在測試時Debug模式將minifyEnabled設置為了true,所以無法在斷點時識別代碼。

  2. 在Release模式時,將tinkerEnabled設置為false,會報找不到Application的錯誤:



    原因也是開啟了混淆,不過官方的demo也一樣有這個問題。鑒于在Release的情況下,似乎不會將Tinker關閉,可忽略這個問題。

  3. 提示有png被修改,但是其實沒改過。wiki中有提到這個問題,除了將cruncherEnabled關閉外,可能的原因是使用Run的方式構建了apk。

  4. 集成Tinker后第一次啟動app崩潰,并且不打印任何錯誤堆棧。原來以為是分包的問題,經過多次測試,發現應該是在Tinker安裝之前進行了多余的操作,另外Tinker的依賴最好放在啟動Application所在的module中,因為Tinker的安裝和構建都依賴Application。如果將其依賴放在其他lib庫所在的module中,可能引起未知crash。

其他可能遇到的問題參考wiki:常見問題

擴展

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370

推薦閱讀更多精彩內容