Volley 之 Request篇

与Request相关的类

  • Request:实现一个请求的基类
  • HttpHeaderParse:实现对响应头的解析
  • StringRequest:实现String类型的请求类
  • JsonRequest:实现Json类型的请求基类
  • JsonObjectRequest:实现JsonObject类型的请求类
  • JsonArrayRequest:实现JsonArray类型的请求类

1.Request类的实现

  • 常用变量的定义
//定义默认的字符编码
private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";
//定义请求的方式
public interface Method {
    int DEPRECATED_GET_OR_POST = -1;
    int GET = 0;
    int POST = 1;
    int PUT = 2;
    int DELETE = 3;
    int HEAD = 4;
    int OPTIONS = 5;
    int TRACE = 6;
    int PATCH = 7;
}

//定义一个日志事件,用于记录请求
private final VolleyLog.MarkerLog mEventLog = VolleyLog.MarkerLog.ENABLED ? new VolleyLog.MarkerLog() : null;

private final int mMethod;

private final String mUrl;
//流量统计的默认标志
private final int mDefaultTrafficStatsTag;

/** Listener interface for errors. */
private final Response.ErrorListener mErrorListener;

//请求的序列号,在优先级相同的情况下,用于保证FIFO
private Integer mSequence;

//请求队列
private RequestQueue mRequestQueue;

//是否需要缓存
private boolean mShouldCache = true;

//该请求是否存在缓存
private boolean mCanceled = false;

//该请求事件是否被分发
private boolean mResponseDelivered = false;

/** Whether the request should be retried in the event of an HTTP 5xx (server) error. */
private boolean mShouldRetryServerErrors = false;

//重试策略
private RetryPolicy mRetryPolicy;
//缓存内容
private Cache.Entry mCacheEntry = null;
//标志该请求,用于批量取消
private Object mTag;

public Request(String url, Response.ErrorListener listener){
    this(Method.DEPRECATED_GET_OR_POST, url, listener);
}

public Request(int method, String url, Response.ErrorListener listener){
    mMethod = method;
    mUrl = url;
    mErrorListener = listener;
    mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
    setRetryPolicy(new DefaultRetryPolicy());
}
  • 针对POST类型,对请求体数据的解析:
//获取提交内容
public byte[] getPostBody() throws AuthFailureError{
    Map<String,String> postParams = getParams();
    if (postParams != null && postParams.size() > 0){
        return encodeParameter(postParams, getPostParamsEncoding());
    }
    return null;
}

//将提交内容进行解析,返回为byte数组
private byte[] encodeParameter(Map<String,String> params, String paramEncoding){
    StringBuilder encodedParams = new StringBuilder();
    try {
        for (Map.Entry<String,String> entry : params.entrySet()){
            encodedParams.append(URLEncoder.encode(entry.getKey(),paramEncoding));
            encodedParams.append("=");
            encodedParams.append(URLEncoder.encode(entry.getValue(), paramEncoding));
            encodedParams.append("&");
        }
        return encodedParams.toString().getBytes(paramEncoding);
    } catch (UnsupportedEncodingException uee){
        throw new RuntimeException("Encoding not supported: " + paramEncoding, uee);
    }
}
  • 实现请求的优先级
public enum Priority{
    LOW,
    NORMAL,
    HIGH,
    IMMEDIATE
}

//默认为普通优先级
public Priority getPriority(){
    return Priority.NORMAL;
}

//定义请求的“大小”:优先级优先,若优先级相同,则比较序列数
@Override
public int compareTo(Request<T> o) {
    //获取其对应的优先级
    Priority left = this.getPriority();
    Priority right = o.getPriority();

    // High-priority requests are "lesser" so they are sorted to the front.
    // Equal priorities are sorted by sequence number to provide FIFO ordering.
   // 注意序列数和优先级的前后顺序
    return left == right ?
            this.mSequence - o.mSequence :
            right.ordinal() - left.ordinal();
}

要注意的是这里对优先级高的线程对应的数值为较小。
关于ordinal函数的使用,若没有给对应的变量赋值,会默认第一个为0,并逐个递增。如下:

enum Priority{
    LOW,
    NORMAL,
    HIGH
}

public static void main(String[] args){
    System.out.print(Priority.LOW.ordinal());
    System.out.print(Priority.NORMAL.ordinal());
    System.out.println(Priority.HIGH.ordinal());
}
输出结果:
0 1 2 
  • 取消请求事件
//通知请求队列将该请求终止
void finish(final String tag){
    if (mRequestQueue != null){
        mRequestQueue.finish(this);
    }

    if (VolleyLog.MarkerLog.ENABLED){
        final long threadId = Thread.currentThread().getId();
        //使用主线程来记录该事件,保证事件的有序性
        if (Looper.getMainLooper() != Looper.myLooper()){
            Handler mainThread = new Handler(Looper.getMainLooper());
            mainThread.post(new Runnable() {
                @Override
                public void run() {
                    mEventLog.add(tag, threadId);
                    mEventLog.finish(this.toString());
                }
            });
            return;
        }
        mEventLog.add(tag, threadId);
        mEventLog.finish(this.toString());
    }
}

实际上,这里重点是要学习日志记录线程时间时,采用主线程提交方式,以保证事件的有序性。否则,若交给其他线程处理,无法确保事件的先后顺序。

2.HttpHeaderParse的实现

  • 获取响应头的字符编码
Content-Type: application/x-www-form-urlencoded;charset=utf-8;

具体实例如上,那么我们要做的就是获取到charset中对应的内容

//获取响应头的字符编码
public static String parseCharset(Map<String,String> headers, String defaultCharset){
    //获取到该行数据
    String contentType = headers.get("Content-Type");
    if (contentType != null){
        String[] params = contentType.split(";");
        for (int i = 0; i < params.length; i ++){
            //trim清除空白字符
            String[] pair = params[i].trim().split("=");
            if (pair.length == 2 && pair[0].equals("charset")){
                return pair[1];
            }
        }
    }
    return defaultCharset;
}

public static String parseCharset(Map<String,String> headers){
    return parseCharset(headers, HTTP.DEFAULT_CONTENT_CHARSET);
}
  • 对响应头进行解析:
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain
Cache-Control: no-cache

具体实例如上,我们要获取的信息主要是用于构造Entry对象

//解析响应头,并返回一个Entry对象
public static Cache.Entry parseCacheHeaders(NetworkResponse response){
    //获取当前时间
    long now = System.currentTimeMillis();
    Map<String,String> headers = response.headers;

    //服务器返回数据的时间
    long serverDate = 0;
    //上一次修改的时间
    long lastModified = 0;
    //服务器过期时间
    //三者之间的关系:第一个为服务器获取的事件,后两个对应为Entry对象中的softTtl和ttl
    long serverExpires = 0;
    long softExpire = 0;
    long finalExpire = 0;
    //缓存最大存活时间
    long maxAge = 0;
    //服务器对客户端重新验证的时间
    long staleWhileRevalidate = 0;
    //存在缓存信息
    boolean hasCacheControl = false;
    //必须马上重新验证
    boolean mustRevalidate = false;

    String serverETag = null;
    String headValue;

    headValue = headers.get("Date");
    if (headValue != null){
        serverDate = parseDateAsEpoch(headValue);
    }

    headValue = headers.get("Cache-Control");
    //获取服务器缓存有关信息
    if (headValue != null){
        hasCacheControl = true;
        String[] tokens = headValue.split(",");
        for (int i = 0; i < tokens.length; i ++){
            String token = tokens[i].trim();
            //不使用缓存,则直接返回null
            if (token.equals("no-cache") || token.equals("no-store")){
                return null;
            } else if (token.startsWith("max-age=")){
                try {
                    maxAge = Long.parseLong(token.substring(8));
                } catch (Exception e){

                }
            } else if (token.startsWith("stale-with-revalidate=")){
                try {
                    staleWhileRevalidate = Long.parseLong(token.substring(23));
                } catch (Exception e){

                }
            } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")){
                mustRevalidate = true;
            }
        }
    }

    headValue = headers.get("Expires");
    if (headValue != null){
        serverExpires = parseDateAsEpoch(headValue);
    }

    headValue = headers.get("last-Modified");
    if (headValue != null){
        lastModified = parseDateAsEpoch(headValue);
    }

    serverETag = headers.get("ETag");

    //计算获取Entry对象中的ttl和softTtl
    if (hasCacheControl){
        //注意:这里的单位为毫秒
        softExpire = now + maxAge * 1000;
        //若不需要立马重新验证,那么缓存还可以再存活一段时间
        finalExpire = mustRevalidate ?
                softExpire : softExpire + staleWhileRevalidate * 1000;
    } else if (serverDate > 0 && serverExpires >= serverDate){
        softExpire = now + (serverExpires - serverDate);
        finalExpire = softExpire;
    }
    //返回Entry对象
    Cache.Entry entry = new Cache.Entry();
    entry.data = response.data;
    entry.eTag = serverETag;
    entry.softTtl = softExpire;
    entry.ttl = finalExpire;
    entry.serverDate = serverDate;
    entry.lastModified = lastModified;
    entry.responseHeaders = headers;

    return entry;
}

//将时间格式化为Long类型
private static long parseDateAsEpoch(String headValue) {
    try {
        return DateUtils.parseDate(headValue).getTime();
    } catch (DateParseException e){
        return 0;
    }
}

要注意的一点是对日期的处理,转换为Long类型保存

3. 实现JsonRequest

public abstract class JsonRequest<T> extends Request<T>{
    //定义默认字符编码
    protected static final String PROTOCOL_CHARSET = "utf-8";
    //定义默认请求类型
    private static final String PROTOCOL_CONTENT_TYPE =
            String.format("application/json;charset=%s",PROTOCOL_CHARSET);

    private final Response.Listener<T> mListener;
    //提交的请求体
    private final String mRequestBody;

    public JsonRequest(String url, String requestBody, Response.Listener<T> listener,
                       Response.ErrorListener errorListener){
        this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener);
    }

    public JsonRequest(int method, String url, String requestBody, Response.Listener<T> listener,
                       Response.ErrorListener errorListener) {
        super(method, url, errorListener);
        mListener = listener;
        mRequestBody = requestBody;
    }

    @Override
    protected abstract Response<T> parseNetworkResponse(NetworkResponse response);

    @Override
    protected void deliverResponse(T response) {
        mListener.onResponse(response);
    }

    @Override
    public String getPostBodyContentType() {
        return getBodyContentType();
    }

    //重新定义了请求体的Content-Type
    @Override
    public String getBodyContentType() {
        return PROTOCOL_CONTENT_TYPE;
    }

    @Override
    public byte[] getPostBody() throws AuthFailureError {
        return getBody();
    }

    //若请求体不为空,则将请求体转换为byte数组返回
    @Override
    public byte[] getBody() throws AuthFailureError {
        try {
            return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET);
        } catch (UnsupportedEncodingException e){
            VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s",
                    mRequestBody, PROTOCOL_CHARSET);
            return null;
        }
    }
}

要注意的几点:

  1. 重新定义了其请求体的Content-Type类型,因为Request基类中定义的Content-Type并不包含json数据:
public String getBodyContentType(){
    return "application/x-www-form-urlencoded;charset=" + getParamsEncoding();
}
  1. 重新了请求体的getBody内容,保证是在Json定义下的字符编码下进行转换为byte数组类型

4.实现JsonObjectRequest

public class JsonObjectRequest extends JsonRequest<JSONObject> {

    //传入JsonObject数据
    public JsonObjectRequest(int method, String url, JSONObject jsonRequest, Response.Listener<JSONObject> listener, Response.ErrorListener errorListener) {
        super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(),
                listener, errorListener);
    }

    //根据JsonObject数据判断请求方式
    public JsonObjectRequest(String url, JSONObject jsonRequest, Response.Listener<JSONObject> listener, Response.ErrorListener errorListener) {
        this(jsonRequest == null ? Method.GET : Method.POST, url, jsonRequest,
                listener, errorListener);
    }

    @Override
    protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
        try {
            //转换为String类型
            String jsonString = new String(response.data,
                    HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET));
            //返回为JsonObject类型
            return Response.success(new JSONObject(jsonString),
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e){
            return Response.error(new ParseError(e));
        } catch (JSONException je){
            return Response.error(new ParseError(je));
        }
    }
}

实际上,基于JsonRequest实现,所需添加的内容非常简单。不过要注意的是其构造函数的使用,根据传入的JSONObject类型参数判断是否为空,来决定其请求方式。

5. 实现JsonArrayRequest

public class JsonArrayRequest extends JsonRequest<JSONArray> {

    public JsonArrayRequest(String url,  Response.Listener<JSONArray> listener,
                            Response.ErrorListener errorListener){
        this(Method.GET, url, null, listener, errorListener);
    }

    public JsonArrayRequest(int method, String url, JSONArray jsonRequest, Response.Listener<JSONArray> listener, Response.ErrorListener errorListener) {
        super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(), listener, errorListener);
    }

    @Override
    protected Response<JSONArray> parseNetworkResponse(NetworkResponse response) {
        try {
            String jsonString = new String(response.data,
                    HttpHeaderParser.parseCharset(response.headers));
            //返回JsonArray类型的结果
            return Response.success(new JSONArray(jsonString),
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (JSONException je){
            return Response.error(new ParseError(je));
        }
    }
}

实际上,该实现和JsonObjectRequest类似,只不过就是在解析响应时,返回的数据类型不同。

6. 实现StringRequest

public class StringRequest extends Request<String> {
    private final Response.Listener<String> mListener;

    public StringRequest(String url, Response.Listener<String> listener,
                         Response.ErrorListener errorListener) {
        super(url, errorListener);
        mListener = listener;
    }

    public StringRequest(int method, String url, Response.Listener<String> listener,
                         Response.ErrorListener errorListener){
        super(method, url, errorListener);
        mListener = listener;
    }

    //对数据进行解析,并返回
    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e){
            parsed = new String(response.data);
        }
        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response ));
    }

    @Override
    protected void deliverResponse(String response) {
        mListener.onResponse(response);
    }
}

实际上,就做了两步,一步是用listener去分发结果,一步是将响应解析为String类型

7. 实现ImageRequest

  • 对ImageRequest中常见变量的定义
//默认获取图片时间
public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000;
//默认尝试次数
public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;

public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f;

private final Response.Listener<Bitmap> mListener;
//图片保存格式,比如ARGB_8888
private final Bitmap.Config mDecodeConfig;
//传入的所需图片的宽高
private final int mMaxWidth;
private final int mMaxHeight;
//图片的缩放类型
private final ImageView.ScaleType mScaleType;

//图片请求的锁,保证图片在解析过程中的线程安全性,避免多个线程对图片进行解析
private static final Object sDecodeLock = new Object();

public ImageRequest(String url, Response.Listener listener, int maxWidth, int maxHeight,
                    ImageView.ScaleType scaleType, Bitmap.Config decodeConfig, Response.ErrorListener errorListener) {
    super(Method.GET, url, errorListener);
    setRetryPolicy(new DefaultRetryPolicy(DEFAULT_IMAGE_TIMEOUT_MS,DEFAULT_IMAGE_MAX_RETRIES
            , DEFAULT_IMAGE_BACKOFF_MULT));
    mListener = listener;
    mDecodeConfig = decodeConfig;
    mMaxHeight = maxHeight;
    mMaxWidth = maxWidth;
    mScaleType = scaleType;
}

public ImageRequest(String url, Response.Listener listener, int maxWidth, int maxHeight,
                    Bitmap.Config decodeConfig, Response.ErrorListener errorListener) {
    this(url, listener, maxWidth, maxHeight, ImageView.ScaleType.CENTER_INSIDE,
            decodeConfig, errorListener);
}
  • 解析响应,并返回Bitmap对象
@Override
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
//保证单一线程进行解析图片
synchronized (sDecodeLock){
    try {
        return doParse(response);
    } catch (OutOfMemoryError e){
        VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length);
        return Response.error(new ParseError(e));
    }
}

我们可以看到解析过程是保证单线程进行的,防止同一张图进行多次解析,带来的不必要的浪费。

//对图片进行解析
private Response<Bitmap> doParse(NetworkResponse response) {
    byte[] data = response.data;
    BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
    Bitmap bitmap = null;
    //若图片所需的显示宽高设置都为0,则显示原图即可
    if (mMaxWidth == 0 && mMaxHeight == 0){
        //设置图片格式
        decodeOptions.inPreferredConfig = mDecodeConfig;
        bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
    } else {
        //保证只解析图片的宽高
        decodeOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
        //获取到原图的宽高
        int actualWidth = decodeOptions.outWidth;
        int actualHeight = decodeOptions.outHeight;

        //获取图片预期的宽高
        int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, actualWidth,
                actualHeight, mScaleType);
        int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth, actualHeight,
                actualWidth, mScaleType);

        //对图片进行解析,并转换为Bitmap对象
        decodeOptions.inJustDecodeBounds = false;
        //获取图片的缩放比
        decodeOptions.inSampleSize = findBestSimpleSize(actualWidth, actualHeight,
                desiredWidth, desiredHeight);
        //???是否需要添加
        decodeOptions.inPreferredConfig = mDecodeConfig;
        //创建一个临时的Bitmap对象
        Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length,
                decodeOptions);
        //若该对象超过预期的宽高,则需要进一步缩小
        if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
                tempBitmap.getHeight() > desiredHeight)){
            bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true);
            //回收临时Bitmap对象
            tempBitmap.recycle();
        } else {
            bitmap = tempBitmap;
        }
    }
    //返回解析结果
    if (bitmap == null){
        return Response.error(new ParseError(response));
    } else {
        return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
    }
}

如上为图片的解析过程,主要流程为:
获取图片预期的宽高 —— 获取图片的缩放比 —— 获取到对应的Bitmap对象

获取图片预期的宽高

//获取预期宽高
private int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
                                int actualSecondary, ImageView.ScaleType scaleType) {
    //无需处理,直接返回原图尺寸
    if (maxPrimary == 0 && maxSecondary == 0){
        return actualPrimary;
    }

    //FIT_XY:不需要进行等比缩放,但要求图片铺满矩阵
    if (scaleType == ImageView.ScaleType.FIT_XY){
        if (maxPrimary == 0){
            return actualPrimary;
        }
        return maxPrimary;
    }

    // If primary is unspecified, scale primary to match secondary's scaling ratio.
    if (maxPrimary == 0) {
        double ratio = (double) maxSecondary / (double) actualSecondary;
        return (int) (actualPrimary * ratio);
    }

    if (maxSecondary == 0){
        return maxPrimary;
    }

    double ratio = (double) actualSecondary / (double) actualPrimary;
    int resized = maxPrimary;

    //CENTER_CROP:要求等比缩放,且不留空白,不要求图片完全显示
    if (scaleType == ImageView.ScaleType.CENTER_CROP){
        if ((resized * ratio) < maxSecondary){
            resized = (int)(maxSecondary / ratio);
        }
        return resized;
    }

    //保证图片完全显示,允许留空白
    if ((resized * ratio) > maxSecondary){
        resized = (int)(maxSecondary / ratio);
    }

    return resized;
}

注意FIT_XY和CENTER_CROP两种类型的不同处理

获取图片的最适压缩比:

static int findBestSimpleSize(int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
    double wr = (double) actualWidth / desiredWidth;
    double hr = (double) actualHeight / desiredHeight;
    double ratio = Math.min(wr, hr);
    float n = 1.0f;
    while ((n * 2) <= ratio){
        n *= 2;
    }
    return (int)n;
}

如上,保证了其压缩比为2的幂次方数

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

推荐阅读更多精彩内容