Android筆記 (5): 封裝Volley實現(xiàn)自動化網(wǎng)絡處理(中)

上一章中,我們通過利用Handler和封裝Volley,實現(xiàn)了自動化網(wǎng)絡請求處理,但是其中還是有缺陷:

  • Handler可能導致內存泄露
  • 請求過程中顯示的對話框太丑
  • 網(wǎng)絡請求結果返回的狀態(tài)碼沒統(tǒng)一處理

這一章就來搞定這些問題。

Handler優(yōu)化

首先,我以前也是按照上一章的樣子使用Hanlder的,也正因此踩過這個坑,所以這里特別提出來。Android Lint會給這樣的用法給出提示:

In Android, Handler classes should be static or leaks might occur.

至于為什么會造成內存泄露,以及解決思路,請參照下文。
Android中使用Handler造成內存泄露的分析和解決

接下來我們動手解決這個問題,先新建com.joyin.volleydemo.utils.hander包,在下面建IHandleMessage.javaMyHandler.java兩個文件。

IHandleMessage.java

package com.joyin.volleydemo.utils.hander;

import android.os.Message;

/**
 * Created by joyin on 16-4-3.
 */
public interface IHandleMessage {
    void onHandleMessage(Message message);
}

MyHandler.java

package com.joyin.volleydemo.utils.hander;

import android.os.Handler;
import android.os.Message;

import java.lang.ref.WeakReference;

/**
 * Created by joyin on 16-4-3.
 */
public class MyHandler<T extends IHandleMessage> extends Handler {

    private WeakReference<T> mTarget;

    public MyHandler(T t) {
        mTarget = new WeakReference<T>(t);
    }

    @Override
    public void handleMessage(Message msg) {
        T target = mTarget.get();
        if (target != null) {
            target.onHandleMessage(msg);
        }
    }
}

MyHandler中采用泛型的好處在于,無論是Activity還是Fragment等,只要實現(xiàn)了IHandleMessage接口,都可以實例化MyHandler對象來使用,在handleMessage()中也采取了保護措施。

接下來我們新建com.joyin.volleydemo.activity.BaseActivity類。

package com.joyin.volleydemo.activity;

import android.os.Bundle;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;

import com.joyin.volleydemo.utils.hander.IHandleMessage;
import com.joyin.volleydemo.utils.hander.MyHandler;

/**
 * Created by joyin on 16-4-3.
 */
abstract public class BaseActivity extends AppCompatActivity implements IHandleMessage {

    public MyHandler<BaseActivity> mHandler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler = new MyHandler<>(this);
    }

    @Override
    public void onHandleMessage(Message msg) {

    }
}

修改MainActivity繼承自BaseActivity,并且刪除mHandler的定義,將原本handleMessage中的代碼移到onHandleMessage中,最終MainActivity的代碼如下:

package com.joyin.volleydemo.activity;

import android.os.Bundle;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;

import com.alibaba.fastjson.JSON;
import com.android.volley.Request;
import com.joyin.volleydemo.R;
import com.joyin.volleydemo.data.api.IpInfo;
import com.joyin.volleydemo.utils.network.RequestHandler;

import java.util.HashMap;
import java.util.Map;

public class MainActivity extends BaseActivity {

    TextView mTvCountry, mTvCountryId, mTvIP;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initViews();
        initData();
    }

    private void initViews() {
        mTvCountry = (TextView) findViewById(R.id.tv_country);
        mTvCountryId = (TextView) findViewById(R.id.tv_country_id);
        mTvIP = (TextView) findViewById(R.id.tv_ip);
    }

    private void initData() {
        String url = "http://ip.taobao.com/service/getIpInfo.php";
        Map<String, String> params = new HashMap<>();
        params.put("ip", "21.22.11.33");
        RequestHandler.addRequestWithDialog(Request.Method.GET, MainActivity.this, mHandler, RESULT_GET_IP_INFO, null, url, params, null);
    }

    private void setIpInfoToView(IpInfo ipInfo) {
        mTvCountry.setText(ipInfo.getData().getCountry());
        mTvCountryId.setText(ipInfo.getData().getCountry_id());
        mTvIP.setText(ipInfo.getData().getIp());
    }

    private static final int RESULT_GET_IP_INFO = 101;

    @Override
    public void onHandleMessage(Message msg) {
        switch (msg.what) {
            case RESULT_GET_IP_INFO:
                String result = (String) msg.obj;
                Log.d("demo", result);
                IpInfo ipInfo = JSON.parseObject(result, IpInfo.class);
                setIpInfoToView(ipInfo);
                break;
        }
    }
}

至此,Handler優(yōu)化已經(jīng)完成。

返回碼統(tǒng)一處理

首先,我們看一下上一章請求接口返回的結果

請求參數(shù)與返回結果

現(xiàn)在我們將參數(shù)改為一個不合法的,可以看到返回錯誤。

請求錯誤的參數(shù)

看到這里,應該能清楚了,當code是0的時候表示返回結果正確,code為1表示錯誤。這種情況,我們在界面上彈出Toast,內容為“無效IP地址”,那么就可以通過xml文件配置,文件名為:return_codes.xml,放在assets/configs目錄下。

XML配置

首先修改build.gradle文件,在android{}結構中加入如下代碼指定assets目錄:

    sourceSets {
        main {
            assets.srcDirs = ['assets']
        }
    }

app/assets/configs/return_codes.xml

<?xml version="1.0" encoding="utf-8"?>
<return_data>
    <item>
        <code>1</code>
        <data>無效IP地址</data>
    </item>
    <item>
        <code>101</code>
        <data>服務器錯誤</data>
    </item>
    <item>
        <code>102</code>
        <data>用戶未登錄</data>
    </item>
    <item>
        <code>103</code>
        <data>商品已下架</data>
    </item>
</return_data>

在里面添加了幾項該接口不會返回的數(shù)據(jù),僅作為參考。其實這里也可以為item加上屬性,用于判斷toast內容是xml中data項配置的,還是返回的data字段里面的。代碼都不難,通過xml解析即可完成,我這里就不重點講了。

通過配置實現(xiàn)錯誤處理

如果僅僅是通過校驗返回的code來彈出對應Toast,功能太單一,而且有的后臺請求是不需要對錯誤進行處理的,所以還記得一開始網(wǎng)絡請求就多了一個Bundle類型的參數(shù)嗎?這個參數(shù)不參與網(wǎng)絡流程,但是對于請求結束后的配置相當重要。接下來新建com.joyin.volleydemo.utils.network.NetworkError類。

package com.joyin.volleydemo.utils.network;

import android.os.Bundle;

import com.alibaba.fastjson.JSONObject;
import com.joyin.volleydemo.utils.ui.ToastUtil;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * Created by joyin on 16-4-3.
 */
public class NetworkError {

    public static HashMap<String, String> mErrorMap = null;

    /**
     * 網(wǎng)絡請求的bundle參數(shù)分析如下
     * ignoreError (boolean,默認false),是否忽略所有errorCode,如后臺調用
     * ignoreToastErrorCode (ArrayList<String>),list里面的errorCode會被忽略掉
     */
    public static void error(String errorCode, JSONObject jsonObject, Bundle bundle) {
        if (bundle != null) {
            // ignoreError若為true,則忽略所有errorCode
            if (bundle.getBoolean("ignoreError", false)) {
                // 該請求無需錯誤處理
                return;
            }
        }

        if (mErrorMap == null) {
            return;
        }

        if (!checkIgnoreCodes(bundle, errorCode)) {
            parseDefaultErrorCode(errorCode, jsonObject);
        }
    }

    /**
     * 遍歷code,彈出對應錯誤信息Toast
     */
    private static void parseDefaultErrorCode(String errorCode, JSONObject jsonObject) {
        if (mErrorMap != null && mErrorMap.containsKey(errorCode)) {
            ToastUtil.show(mErrorMap.get(errorCode));
            return;
        }
        ToastUtil.show(jsonObject.toString());
    }

    /**
     * 檢查該code是否需忽略
     *
     * @return 驗證是否通過
     */
    private static boolean checkIgnoreCodes(Bundle bundle, String errorCode) {
        if (bundle != null) {
            // 若errorCode存在于該list,則由調用者自己處理
            ArrayList<String> ignoreList = bundle.getStringArrayList("ignoreToastErrorCode");
            if (ignoreList != null && !ignoreList.isEmpty()) {
                if (ignoreList.contains(errorCode)) {
                    return true;
                }
            }
        }
        return false;
    }
}

其中ErrorMap的鍵值對就是code-data鍵值對。

替換系統(tǒng)ToastUtil

由于Android系統(tǒng)原生Toast有一個特點,如果你界面上有一個Button,每點擊一次,則執(zhí)行一次

Toast.makeText(MainActivity.this, "toast content", Toast.LENGTH_SHORT).show();

那么當用戶連續(xù)點擊的時候,就會一直重復彈出Toast,這點相信大家都明白,用戶體驗很低。我們新建com.joyin.volleydemo.utils.ui.ToastUtil類。

package com.joyin.volleydemo.utils.ui;

import android.widget.Toast;

import com.joyin.volleydemo.app.MyApplication;

/**
 * Created by joyin on 16-4-3.
 */
public class ToastUtil {
    private ToastUtil() {

    }

    private static Toast mToast;

    public static void show(int resId) {
        show(MyApplication.getInstance().getString(resId));
    }

    public static void show(String msg) {
        if (mToast == null) {
            mToast = Toast.makeText(MyApplication.getInstance(), msg, Toast.LENGTH_SHORT);
        } else {
            mToast.setText(msg);
        }
        mToast.show();
    }
}

通過這樣的方式彈出Toast,如果是連續(xù)幾次操作,那么后面的消息會覆蓋前面的內容,而不是像以前一樣,等待前面的Toast結束,再重新彈出后續(xù)Toast。

合理打印log

這里穿插一下,我們應當在debug狀態(tài)下打印出請求返回的信息,release版本安全性較高,不應打印這些log,新建com.joyin.volleydemo.utils.app.LogUtil類。

package com.joyin.volleydemo.utils.app;

import android.util.Log;

import com.joyin.volleydemo.BuildConfig;
import com.joyin.volleydemo.R;
import com.joyin.volleydemo.app.MyApplication;

/**
 * Created by joyin on 16-4-3.
 */
public class LogUtil {
    private LogUtil() {

    }

    public static final boolean DEBUG = BuildConfig.DEBUG;
    public static final String TAG = MyApplication.getInstance().getString(R.string.config_logcat_tag);


    public static void d(String msg) {
        d(TAG, msg);
    }

    public static void d(String tag, String msg) {
        if (DEBUG) {
            Log.d(tag, msg);
        }
    }

    public static void e(String msg) {
        e(TAG, msg);
    }

    public static void e(String tag, String msg) {
        if (DEBUG) {
            Log.e(tag, msg);
        }
    }

    public static void v(String msg) {
        v(TAG, msg);
    }

    public static void v(String tag, String msg) {
        if (DEBUG) {
            Log.v(tag, msg);
        }
    }

    public static void exception(String msg) {
        e(msg);
    }
}

在res/values/下新建configs.xml,將R.string.config_logcat_tag加入其中(直接添加在strings.xml中也可以,但推薦配置類的參數(shù)單獨建文件,各司其職)。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="config_logcat_tag">demo</string>
</resources>

調整RequestHandler類,onVolleyResponse方法里面加上

LogUtil.d(response);
onVolleyResponse

解析xml

接下來回到正題,新建com.joyin.volleydemo.utils.parse.xml.ErrorCodeParser類。

package com.joyin.volleydemo.utils.parse.xml;

import android.text.TextUtils;
import android.util.Xml;

import com.joyin.volleydemo.app.MyApplication;
import com.joyin.volleydemo.utils.network.NetworkError;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;

/**
 * Created by joyin on 16-4-3.
 */
public class ErrorCodeParser {

    public static void init() {
        try {
            NetworkError.mErrorMap = getErrorCodeMessageMap();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (XmlPullParserException e) {
            e.printStackTrace();
        }
    }

    private static HashMap<String, String> getErrorCodeMessageMap() throws IOException, XmlPullParserException {
        HashMap<String, String> map = null;

        XmlPullParser parser = Xml.newPullParser();
        InputStream in = MyApplication.getInstance().getAssets().open("configs/return_codes.xml");
        parser.setInput(in, "UTF-8");
        int eventType = parser.getEventType();

        String key = "";
        String value = "";

        while (eventType != XmlPullParser.END_DOCUMENT) {
            String nodeName = parser.getName();
            switch (eventType) {
                case XmlPullParser.START_DOCUMENT:
                    map = new HashMap<>();
                    break;
                case XmlPullParser.START_TAG:
                    if (nodeName.equals("code")) {
                        key = parser.nextText();
                    } else if (nodeName.equals("data")) {
                        value = parser.nextText();
                    }
                    break;
                case XmlPullParser.END_TAG:
                    if (nodeName.equals("item") && !TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) {
                        map.put(key, value);
                    }
                    break;
            }
            eventType = parser.next();
        }
        return map;
    }
}

代碼很簡單,就是解析錯誤信息xml數(shù)據(jù),然后將其賦予NetworkError類的全局變量。同時,在MyApplication類onCreate方法中調用ErrorCodeParser.init()。

    @Override
    public void onCreate() {
        super.onCreate();
        mInstance = this;
        mRequestQueue = Volley.newRequestQueue(this);
        ErrorCodeParser.init();
    }

完成錯誤處理

文章開頭就給出正確和錯誤兩種返回值,在這里,我們就認為code為0代表成功,其他的code表示失敗,同時向handler發(fā)出錯誤碼為-1的消息。那么現(xiàn)在繼續(xù)來修改RequestHandler中onVolleyResponse方法的代碼。

    private static void onVolleyResponse(String response, Handler handler, int what, Bundle bundle) {
        LogUtil.d(response);
        JSONObject json = JSON.parseObject(response);
        if (json != null && json.containsKey("code")) {
            int code = json.getIntValue("code");
            if (code != 0) {
                // 如果code不為0,則走錯誤處理流程
                Message msg = handler.obtainMessage(NetworkError.NET_ERROR_CUSTOM);
                msg.setData(bundle);
                handler.sendMessage(msg);
                NetworkError.error("" + code, json, bundle);
                return;
            }
        }
        Message msg = handler.obtainMessage(what, response);
        msg.setData(bundle);
        handler.sendMessage(msg);
    }

同時在NetworkError中定義參數(shù)

public static final int NET_ERROR_CUSTOM = -1;

并且,為了更合理的理解,將原本RequestHandler類中定義的NET_ERROR_VOLLEY也移到NetworkError中。

最后,驗證一下

修改ip參數(shù)為不合法的ip地址

onHandleMessage方法中增加一個case

case NetworkError.NET_ERROR_CUSTOM:
    mTvCountry.setText("獲取請求失敗");
    break;
onHandleMessage方法
效果圖

修改loading框

目前我們采用的的加載框比較丑陋,在這里我們將定義自己的對話框。

獲取素材

首先,我們的素材從哪里來呢?在這里給大家安利兩個網(wǎng)站:

cheatsheet

素材

我們可以在這個網(wǎng)站上找到圖標素材,比如我們要找的,就是fa-spinner類型。


fa-spinner

將素材轉換為png圖片。訪問網(wǎng)站,F(xiàn)A2PNG,輸入spinner,你會發(fā)現(xiàn)有很多類似的,我們選擇icomoon-spinner2這個素材。


下拉列表中有很多選項

右邊是預覽圖,左邊是選項,前景色(這里填入默認的#0064ff),背景色(我們選透明),圖片尺寸,以及margin尺寸。


效果圖

點擊Generate后,即可生成圖片。我們這個項目中,直接Download即可。
生成圖片

icon_loading_rotate.png

將圖片改名為icon_loading_rotate.png,我們的loading框就做成一個轉圈的動畫,接下來我們來實現(xiàn)該對話框。
將得到的icon_loading_rotate.png放入drawable-xxhdpi目錄下,新建res/anim/loading_rotate.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="false">
    <rotate
        android:duration="1500"
        android:fromDegrees="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:pivotX="50%"
        android:pivotY="50%"
        android:repeatCount="-1"
        android:repeatMode="restart"
        android:toDegrees="+360" />
</set>

新建布局文件dialog_loading.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_dialog_loading"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="10dp">

    <ImageView
        android:id="@+id/icon_loading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:src="@drawable/icon_loading_rotate" />

</LinearLayout>

新建res/drawable/bg_dialog_loading.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="5dp" />
    <solid android:color="#88333333" />
</shape>

res/values/styles.xml文件中添加:

    <style name="default_dialog" parent="android:style/Theme.Dialog">
        <item name="android:windowFrame">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowContentOverlay">@null</item>
    </style>

因為我們目前使用的是loading框,而項目中往往還會有消息提示框等,所以我們將具體代碼抽象出來。新建com.joyin.volleydemo.view.dialog.BaseDialog.java

package com.joyin.volleydemo.view.dialog;

import android.app.Dialog;
import android.content.Context;
import android.view.View;

import com.joyin.volleydemo.R;

/**
 * Created by joyin on 16-4-4.
 */
public abstract class BaseDialog {
    public Dialog mDialog;

    public BaseDialog(Context context) {
        View view = getDefaultView(context);
        mDialog = createDialog(context, view);
    }

    /**
     * 子類重寫該方法,即可創(chuàng)建樣式相同的對話框。
     * @param context
     * @return
     */
    protected abstract View getDefaultView(Context context);

    private static Dialog createDialog(Context context, View v) {
        Dialog dialog = new Dialog(context, R.style.default_dialog);
        dialog.setCancelable(false);
        dialog.setContentView(v);
        return dialog;
    }

    public void show() {
        if (mDialog != null) {
            mDialog.show();
        }
    }

    public void dismiss() {
        if (mDialog != null) {
            mDialog.dismiss();
        }
    }
}

新建com.joyin.volleydemo.view.dialog.LoadingDialog.java

package com.joyin.volleydemo.view.dialog;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;

import com.joyin.volleydemo.R;

/**
 * Created by joyin on 16-4-4.
 */
public class LoadingDialog extends BaseDialog {

    public LoadingDialog(Context context) {
        super(context);
    }

    @Override
    protected View getDefaultView(Context context) {
        LayoutInflater inflater = LayoutInflater.from(context);
        View v = inflater.inflate(R.layout.dialog_loading, null);

        ImageView icon = (ImageView) v.findViewById(R.id.icon_loading);
        Animation animation = AnimationUtils.loadAnimation(context, R.anim.loading_rotate);
        icon.startAnimation(animation);
        return v;
    }
}

至此,自定義Dialog已經(jīng)完,代碼非常簡單,這里不多解釋,其中具體代碼有疑問的應該都可以百度到,或者也可以直接問我。
接下來,要將我們自定義的對話框用到前面的網(wǎng)絡流程中,只需將RequestHandler中ProgressDialog改為LoadingDialog

修改前
修改后

測試一下,效果已經(jīng)有了,我貼一張截圖,不是動態(tài)的。

效果圖

好了,本章到此結束。下一章的內容是:使目前的框架支持HTTPS安全請求,附帶利用現(xiàn)在的對話框模板,創(chuàng)建一個消息框。
另,這些文章是邊整理邊寫,如果有混亂的,歡迎指正,后續(xù)會優(yōu)化改進,最后會將代碼上傳至github,有興趣的可以去逛逛。捂臉,逃~


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

推薦閱讀更多精彩內容