Volley简析

概述

Volley是一个HTTP库,它能够帮助Android应用更方便地执行网络操作,更重要的是,它更快速高效。
Volley有如下优点:

  • 自动调度网络请求
  • 高并发网络连接
  • 具有标准HTTP缓存一致性的透明磁盘和内存缓存
  • 支持请求优先级
  • 提供取消请求的API,可以取消单个或多个请求。
  • 易于定制,例如重试和回退功能
  • 强大的指令使得从异步网络加载的数据正确地显示到UI上的操作变得更加容易
  • 包含调试与追踪工具

Volley的特点:适合数据量小,通信频繁的网络操作。

请求过程

Volley的请求过程如官网提供的演示图所示,简单来说就几下几个步骤:

  1. 新建一个RequestQueue对象,并启动
  2. 新建Request对象,通过add()方法,先将请求添加到缓存队列中
  3. 缓存调度器从缓存队列中取出一个请求,并在缓存中检索该请求,如果缓存中已有该请求,且请求未过期,则解析请求结果,并通过ResponseDelivery将解析后的结果分发给主线程
  4. 如果缓存中找不到该请求或请求过期,则将请求添加到网络请求队列中
  5. 网络请求调度器从网络请求队列中取出一个请求,通过网络接口获取请求结果。如果获取成功,则解析请求结果,并将结果分发给主线程;如果发生异常,则发送请求失败的通知
volley-request.png

代码解析

RequestQueue

RequestQueue是一个带有调度线程池的请求调度队列,可以通过add()方法添加一个请求到队列中,然后通过缓存或者网络方式获取数据,再分发一个解析过的响应到主线程。
先来看下RequestQueue的一些主要参数:

  • mCurrentRequests:Set<Request<?>>,存储了所有待处理的请求。
  • mCacheQueue:PriorityBlockingQueue<Request<?>>,缓存队列
  • mNetworkQueue:PriorityBlockingQueue<Request<?>>,网络请求队列
  • mDispatchers:NetworksDispatcher[],网络请求调度器,是个数组
  • mCacheDispatcher:CacheDispatcher,缓存调度器
  • mDelivery:ResponseDelivery,用于分发请求结果
  • mNetwork:处理HTTP请求的网络接口
  • PriorityBlockingQueue:优先级队列,不支持null元素,存储对象必须实现Comparable接口,因为需要通过每个元素的compareTo()方法对元素进行排序。

我们都知道volley的可定制性很高,具体表现在mCachemNetworkmDelivery均可以自定义,只需要实现相应的接口即可,网络请求调度器的线程数量也可以自定义(默认为4)。

创建RequestQueue对象

首先,我们知道要使用volley来进行网络操作,需要创建一个RequestQueue对象,最简单的方法就是通过Volley工具类来new一个:

RequestQueue requestQueue = Volley.newRequestQueue(context);

看下Volley类的代码实现:

public static RequestQueue newRequestQueue(Context context) {
    return newRequestQueue(context, (BaseHttpStack) null);
}

public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {
    BasicNetwork netwokr;
    if (stack == null) {
          if (Build.VERSION.SDK_INT >= 9) {
              network = new BasicNetwork(new HurlStack());
          } else {
              ...
          }
    } else {
          network = new BasicNetwork(stack);
    }
    return newRequestQueue(context, network);
}

private static RequestQueue newRequestQueue(Context context, Network network) {
    File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
    RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
    queue.start();
    return queue;
}

可以看到,最终调用的是newRequestQueue(Context, Network)这个方法,这个方法,就做了两件事:

  1. new一个RequestQueue对象:初始化Cache, Network, NetworkDispatcher[], Delivery
  2. 启动RequestQueue: queue.start()

启动RequestQueue

通过RequestQueuestart()可以启动请求队列,看下启动都做了什么:

public void start() {
    stop();     // 停掉正在运行的调度器
    // 初始化缓存调度器线程,并启动
    mCacheDispatcher = new CachePatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
    mCacheDispatcher.start();
    // 根据指定的线程池大小(默认4),始化n个网络调度器线程,添加到网络调度器数组中,并启动它们
    for (int i = 0; i < mDispatcher.length; i++) {
          NetworkDispatcher networkDispatcher = 
                        new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
          mDispatcher[i] = networkDispatcher;
          networkDispatcher.start();
    }
}

通过以上代码,可以看出,启动RequestQueue,其实就是启动缓存调度器和网络调度器。其中,缓存调度器只有一个,而网络调度器有多个,根据创建RequestQueue对象时传入的threadPoolSize参数,未传入的话则使用默认值:

private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;

到这里,RequestQueue就开始工作了,可以通过调用add(Request)发送请求了。

public <T> Request<T> add(Request<T> request) {
    request.setRequestQueue(this);
    // 将request添加到mCurrentRequests集合中
    synchroinzed (mCurrentRequests) {
          mCurrentRequests.add(request);
    }
    // 按添加的顺序处理请求
    request.setSequence(getSequenceNumber());
    request.addMarker("add-to-queue");
    // 如果请求是不可缓存的,跳过缓存队列,直接发到网络请求队列
    if (!request.shouldCache()) {
          mNetworkQueue.add(request);
          return request;     
    }
    mCacheQueue.add(request);
    return request;
}

添加请求到队列中时,首先会添加到mCurrentRequests集合中,这个集合存放了所有未处理的请求。接着判断请求是否是可缓存的,如果不可缓存,直接添加到网络请求队列,否则会添加到缓存队列,其中请求的shouldCache默认为true,需显示设为false,否则请求都会先发到缓存队列中。

Request

Volley根据常用请求方式,实现了以下种Request

  • StringRequest
  • ImageRequest
  • JsonRequest:
    • JsonArrayRequest
    • JsonObjectRequest

用户也可根据自己的需求,自定义Request,必须实现以下两个方法:

  • parseNetworkResponse(NetworkResponse response):对请求结果进行解析,将NetworkResponse解析成合适的类型
  • deliverReponse(T response):将解析后的请求结果分发给监听器,请求结果不能为null,解析失败的请求也不能进行分发。

CacheDispatcher

在上一节中我们知道,默认情况下,一个请求会优先发送到缓存队列中,由缓存调度器处理,而缓存调度器就是一个Thread,我们来看下它的run()方法做了哪些事情:

@Overrid
public void run() {
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    mCache.initialize();
    // 开始死循环
    while (true) {
          try {
              // 处理请求
              processRequest();
          } catch (InterruptedException e) {
              // 通过mQuit标志判断是否中断线程
              if (mQuit) {
                   Thread.currentThread().interrupt();
                   return;
              }
          }
    }
}

真正处理请求的地方在processRequest()方法,接着往下看:

private void processRequest() throws InterruptedException {
    // 从缓存队列中取出一个请求
    final Request<?> request = mChacheQueue.take();
    processRequests(request);
}

void processRequest(final Request<?> request) throws InterruptedException {
    request.addMarker("cache-queue-take");
    // 如果请求已经取消,不做调度
    if (request.isCanceled()) {
          request.finish("cache-discard-canceled");
          return;
    }
    // 尝试从缓存中检索该请求
    Cache.Entry entry = mCache.get(request.getCacheKey());
    // 缓存中未找到该请求
    if (entry == null) {
          request.addMarker("cache-miss");
          if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
              mNetworkQueue.put(request);
          }
          return;
    }
    // 缓存中的请求过期
    if (entry.isExpired()) {
          request.addMarker("cache-hit-expired");
          request.setCacheEntry(entry);
          if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
              mNetworkQueue.put(request);
          }
          return;
    }
    request.addMarker("cache-hit");
    Response<?> response =
                   request.parseNetworkResponse(
                                   new NetworkResponse(entry.data, entry.responseHeaders));
    request.addMarker("cache-hit-parsed");
    if (!entry.refreshNeeded()) {
          // 请求结果是最新的,直接通过分发器post请求结果
          mDelivery.postResponse(request, response);
    } else {
          // 请求结果不新鲜,重新获取
          request.addMarker("cache-hit-refresh-needed");
          request.setCacheEntry(entry);
          response.intermediate = true;
          if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
              mDelivery.postResponse(request, response,
                             new Runnable() {
                                  @Override
                                  public void run() {
                                       try {
                                            mNetworkQueue.put(request);
                                       } catch (InterruptedException e) {
                                            Thread.currentThread().interrupt();
                                       }
                                  }
                             });
          } else {
              mDelivery.postResponse(request, response);
          } 
    }
}

代码比较长,但逻辑很清晰.首先,会判断该请求是否已经取消了,如果是的话,直接返回,不再对该请求进行处理。
如果请求未取消,就去尝试从缓存中检索该请求,检索结果分为以下几种情况:

  • 检索失败:发送到网络请求队列
  • 检索到请求,但已过期:发送到网络请求队列
  • 检索到请求,且未过期,解析响应数据,这里又分为以下几种情况:
    • 响应已过期,需要更新数据:发送到网络请求队列更新数据,然后将缓存数据分发给线程
    • 响应未过期:分发给主线程

如果检索失败或过期,需要将请求添加到网络请求队列中,但并不是直接添加到mNetworkQueue中,而是调用mWaitingRequestManager.maybeAddToWaitingRequests(request)方法进行判断。
这个WaitingRequestManager的作用是防止重复请求的,来看下它的实现:

private final Map<String, List<Request<?>>> mWaitingRequests = new HashMap<>();

private synchronized boolean maybeAddToWaitingRequests(Request<?> request) {
    String cacheKey = request.getCacheKey();
    if (mWaitingRequests.containsKey(cacheKey)) {
          // 等待队列中包含该请求,即已经有请求在执行了,
          // 将其加入等待队列中,不再发送到网络请求队列中
          List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
          if (stagedRequests == null) {
               stagedRequests = new ArrayList<>();
          }
          request.addMarker("waiting-for-response");
          stagedRequests.add(request);
          mWaitingRequests.put(cacheKey, stagedRequests);
          return true;
    } else {
          // 当前没有同样cacheKey的请求在执行,可以发送到网络请求队列中
          mWaitingRequests.put(cacheKey, null);
          // 监听网络请求结束
          request.setNetworkRequestCompleteListener(this);
          return false;
    }
}

WaitingRequestManager维护了一个cacheKey到请求列表的映射表mWaitingRequests,如果请求不在等待队列中,就将其加入映射表,然后返回false,表示可以将这个请求直接添加到mNetworkQueue中。相反,如果映射表中存在该请求了,表示已经有相同cacheKey的请求已经在执行了,避免重复操作,将其加入等待队列,返回true,不再添加到mNetworkQueue

这里还注册了一个网络请求结束的监听器,接受到请求响应后,会将cacheKey对应的等待队列从映射表中移除,并分发响应结果。

NetworkDispatcher

接下来我们来看NetworkDispatcher的工作流程,它的run()方法与CacheDispatcher一样,真正的操作都是放在processRequest()方法中执行,所以我们直接看processRequest()方法。

// 与CacheDispatcher一样,从队列中取出请求,不过这里的队列是网络请求队列
private void processRequest() throws InterruptedException {
    final Request<?> request = mChacheQueue.take();
    processRequests(request);
}

private void processRequest(Request<?> request) throws InterruptedException {
    long startTimeMs = SystemClock.elapsedRealtime();
    try {
          request.addMarker("network-queue-take");
          if (request.isCanceled()) {
              // 请求已取消,不做处理
              request.finis("network-discard-cancelled");
              request.notifyListenerResponseNotUsable();
              return;
          }
          addTrafficStatsTag(request);
          // 调用网络接口处理请求
          NetworkResponse networkResponse = mNetwork.performRequest(request);
          request.addMarker("network-http-complete");
          // 如果服务器返回304且我们已经分发了一个响应,不再分发第二个相同的响应
          if (networkResponse.notModified && request.hasHadResponseDelivered()) {
               request.finis("not-modified");
               request.notifyListenerResponseNotUsable();
               return;
          }
          // 解析响应,自定义的Request必须实现该接口
          Response<?> response = request.parseNetworkResponse(networkResponse);
          request.addMarker("network-parse-complete");
          // 如果请求需要缓存,缓存响应
          if (request.shouldCache() && response.cacheEntry != null) {
              mCache.put(request.getCacheKey(), response.cacheEntry);
              request.addMarker("network-cache-written");
          }
          // 分发响应
          request.markDelivered();
          mDelivery.postResponse(request, response);
          request.notifyListenerResponseReceived(response);
    } catch (VolleyError volleyError) {
          volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
          parseAndDeliverNetworkError(request, volleyError);
          request.notifyListenerResponseNotUsable();
    } catch (Exception e) {
          VolleyError volleyError = new VolleyError(e);
          volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
          mDelivery.postError(request, volleyError);
          request.notifyListenerResponseNotUsable();
    }
}

CacheDispatcher一样,NetworkDispatcher也会先判断请求是否取消,如果是的话,就停止处理该请求。
如果请求未取消,就调用网络请求接口去处理请求并获取响应。
获取到响应之后,就将获取到的NetworkResponse对象解析成可用于分发的Response对象。
此时如果请求需要缓存,就将其存到缓存中。
之后就开始分发请求。
注意到这里的两个调用:

  • request.notifyListenerResponseNotUsable
  • request.notifyListenerResponseReceived

这两个调用是用来通知注册了NetworkRequestCompleteListener监听器的对象网络请求的结果,上面的WaitingRequestManager就实现了该监听器,并在将请求添加到等待队列中时将监听器注册到请求中,以便在网络请求结束时作出相应的响应。

到这里,请求调度的部分就讲完了,接下来我们来看看volley是怎么将请求结果分发到主线程的。

ResponseDelivery

ResponseDelivery是一个接口,定义了三个方法:

  • postResponse(Request<?> request, Response<?> response):分发请求结果
  • postResponse(Request<?> request, Response<?> response, Runnable runnable):分发请求结果,它提供了一个Runnable参数,将在分发后执行
  • postError(Request<?> request, VolleyError error):网络请求出现异常时

Volley实现了该接口:ExecutorDelivery,也可以自定义分发器,在创建RequestQueue的时候传入自定义分发器的对象。

ExecutorDelivery

ResponseDelivery接口的实现类。主要实现就是通过Handler机制来进行分发。
来看下它的实现:

private final Executor mResponsePoster;

public ExecutorDelivery(final Handler handler) {
    mResponsePoster =
                     new Executor() {
                             @Override
                             public void execute(Runnable command) {
                                  handler.post(command);
                             }
                     };
}

@Override
public void postResponse(Request<?> reqeust, Response<?> response, Runnable runnable) {
    request.markDelivered();
    request.addMarker("post-response");
    mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}

可以看到,ExecutorDelivery只是创建了一个Executor来封装Handler,分发的时候新建一个ResponseDeliveryRunnable对象,再通过Handler发送该Runnable到消息队列中。ResponseDeliveryRunnableExecutorDelivery的子类,它实现了Runnable接口,我们来看下它做了哪些事情。

private static class ResponseDeliveryRunnable implements Runnable {
    private final Request mRequest;
    private final Response mResponse;
    private final Runnable mRunnable;
    ...
    @Override
    public void run() {
          // 同样的,在分发过程中,如果请求被取消了,也将不会被分发
          if (mRequest.isCanceled()) {
              mRequest.finish("canceled-at-delivery");
              return;
          }
          // 请求成功,分发请求结果;请求失败则分发失败内容
          if (mResponse.isSuccess()) {
              mReqeust.deliveryResponse(mResponse.result);
          } else {
              mRequest.deliveryError(mResponse.error);
          }
          // 如果该请求结果只是中间产物,表示请求还未完成,为其添加标记;
          // 否则表示请求完成,调用finish()方法
          if (mResponse.intermediate) {
              mRequest.addMarker("intermediate-response");
          } else {
              mReqeust.finish("done");
          }
          // 如果Runnable对象不为空,执行它
          if (mRunnable != null) {
              mRunnable.run();
          }
    }
}

可以看出,ExecutorDelivery的分发过程如下:

  1. 判断请求是否已取消:
    • 已取消:停止分发
    • 未取消:继续下一步
  2. 请求未取消,判断请求是否成功
    • 请求成功:发送请求结果
    • 请求失败:发送失败相关内容
  3. 判断请求是否中间产物:
    • 是中间产物:表示请求还未结束,添加标记
    • 不是中间产物:该请求已结束,调用finish()方法
  4. 如果提供了Runnable对象,就执行它。

请求结束

我们来看下请求结束后,volley会做什么事情,先看下Requestfinish()方法

void finish(final String tag) {
    if (mRequestQueue != null) {
         mRequestQueue.finish(this);
    }
    ...    
}

回顾之前调用Requestadd()方法添加请求的时候,我们会将RequestQueue对象绑定到Request上,这里就会调用RequestQueuefinish()方法:

<T> void finish(Request<T> request) {
    // 从mCurrentRequests集合中移除已结束的请求
    synchronized (mCurrentRequests) {
          mCurrentRequests.remove(request);
    }
    // 通知所有RequestFinishedListener监听器请求结束
    synchronized (mFinishedListeners) {
          for (RequestFinishedListener<T> listener : mFinishedListeners) {
              listener.onRequestFinished(request);
          }
    }
}

到这里,一个请求的处理就结束了。

Volley对图片请求也做了一定支持,它提供了一个ImageLoader来负责图片的请求。接下来,我们看看ImageLoader是怎么工作的。

ImageLoader

首先看下简单用法

mImageLoade = new ImageLoader(mRequestQueue, mBitmapCache);
mImageLoader.get(IMG_URL, ImageLoader.getImageListener(mImageView, R.drawable.ic_default_img, 
                 R.drawable.ic_error_img));

首先,看下ImageLoader的构造函数:

public ImageLoader(RequestQueue queue, ImageCache imageCache) {
    mRequestQueue = queue;
    mCache = imageCache;
}

首先,需要传入ImageQueue的对象,由此可以推测ImageLoader也是基于ImageQueue来做请求分发的,只是针对图片请求做了一个封装。ImageCache是图像缓存接口,可以避免不断重复获取图片,需要开发者自己实现ImageCache接口,官方推荐使用LruCache来做图像缓存,此处不做赘述。
接着我们通过ImageLoaderget()方法,可以轻松获取网络图片,来看下它的参数:

  • requestUrl:网络图片的url
  • imageListener:网络图片获取的监听器
  • maxWidth:返回图片的最大宽度
  • maxHeight:返回图片的最大高度
  • scaleTypeImageView缩放方式

其中ImageListener是一个接口,包含两个方法:

public interface ImageListener extends ErrorListner {
    void onResponse(ImageContainer response, boolean isImmediate);
}

public interface ErrorListner {
     void onErrorResponse(VolleyError error);
}

其实就是处理两种情况:成功、失败,可以自己实现ImageListener接口,也可以简单地调用ImageLoader.getImageListener()方法获取实例。

接下来看下关键的get()方法

public ImageContainer get(String requestUrl, ImageListener imageListener,
              int maxWidth, int maxHeight, ScaleType scaleType) {
    // 只能处理从主线程发出的请求
    Threads.throwIfNotOnMainThread();
    // 构建缓存key,当且仅当requestUrl、maxWidth、maxHeight、scaleType均相等时,key才会相同
    final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
    // 优先查找缓存
    Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
    if (chachedBitmap != null) {
          // 如果找到缓存,直接响应,不再发送到网络请求队列
          ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
          imageListener.onResponse(container, true);
          return container;
    }

    ImageContainer = imageContainer =
                 new ImageContainer(null, requestUrl, cacheKey, imageListener);
    imageListener.onResponse(imageContainer, true);
    // 判断是否已经有一个请求在执行,如果是,则不再重复发送请求
    BatchedImageRequest request = mInFlightRequets.get(cacheKey);
    if (request != null) {
          request.addContainer(imageContainer);
          return imageContainer;
    }

    // 新建一个图片请求并发送
    Request<Bitmap> newRequest = 
                   makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey);
    mRequestQueue.add(newRequest);
    // 将新的图片请求加入到飞行队列中
    mInFlightRequests.put(cacheKey, new BachedImageRequest(newRequest, imageContainer));
    return imageContainer;
}

看完这部分代码,我们来捋一下get()方法的流程

  1. 首先,判断当前是否处于主线程,如果不是就抛出异常。即,ImageLoader的请求只允许在主线程发送,因为要处理图片返回的情况,不能在工作线程操作UI;
  2. 根据url、最大宽高、缩放方式构建缓存key,用以查找缓存中是否以包含有该请求,若有直接响应更新UI;否则下一步;
  3. 判断是否已经有一个同样的请求在执行,若是,则不再发送该请求;
  4. 新建一个ImageRequest,并添加到RequestQueue中进行分发

看到这里就可以发现我们在分析构造函数时的推测是正确的,ImageLoader实际上就是对ImageRequest的一个封装。

对Volley的解析到此结束。

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

推荐阅读更多精彩内容