面试官:Glide 是如何加载 GIF 动图的?

前言

最近在一个群里看到有人说面试遇到一个问题是 “Glide 是如何加载 GIF 动图的?”,他说没看过源码回答不出来...

好家伙!现在面试都问的这么细了?我相信很多人即使看过源码也很难回答出来,包括我自己。比如之前自己虽然写了两篇 Glide 源码的文章,但是只分析了整个加载流程和缓存机制,关于 GIF 那里只是粗略的看了一下,想要回答的好还是有难度的。那么这篇文章就好好分析一下吧,这篇依然采用 4.11.0 版本来分析。

系列文章:

更多干货请关注 AndroidNotes

一、区分图片类型

我们知道使用 Glide 只需要下面一行简单代码就可以将静态图和 GIF 动图加载出来。

Glide.with(this).load(url).into(imageView);

加载静态图与 GIF 动图原理肯定是不同的,所以在加载之前需要先区分出图片类型。我们先看下源码是怎么区分的。

Glide 的执行流程源码解析 这篇文章中,我们知道网络请求拿到 InputStream 后会执行一个解码操作,也就是调用 DecodePath#decode() 进行解码。我们看一下这个方法:

  /*DecodePath*/
  public Resource<Transcode> decode(
      DataRewinder<DataType> rewinder,
      int width,
      int height,
      @NonNull Options options,
      DecodeCallback<ResourceType> callback)
      throws GlideException {
    Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options);

    ...
  }

这里又调用了 decodeResource 方法,继续跟踪:

  /*DecodePath*/
  private Resource<ResourceType> decodeResource(
      DataRewinder<DataType> rewinder, int width, int height, @NonNull Options options)
      throws GlideException {
    List<Throwable> exceptions = Preconditions.checkNotNull(listPool.acquire());
    try {
      return decodeResourceWithList(rewinder, width, height, options, exceptions);
    } finally {
      listPool.release(exceptions);
    }
  }

  /*DecodePath*/
  private Resource<ResourceType> decodeResourceWithList(
      DataRewinder<DataType> rewinder,
      int width,
      int height,
      @NonNull Options options,
      List<Throwable> exceptions)
      throws GlideException {
    Resource<ResourceType> result = null;
    //noinspection ForLoopReplaceableByForEach to improve perf
    for (int i = 0, size = decoders.size(); i < size; i++) {
      ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);
      try {
        DataType data = rewinder.rewindAndGet();
        //(1)
        if (decoder.handles(data, options)) {
          data = rewinder.rewindAndGet();
          //(2)
          result = decoder.decode(data, width, height, options);
        }
      } catch (IOException | RuntimeException | OutOfMemoryError e) {

        ...

      }

      if (result != null) {
        break;
      }
    }

 ...

    return result;
  }

可以看到,这里还不知道图片是什么类型,所以会遍历 decoders 集合找到合适的资源解码器(ResourceDecoder)进行解码。decoders 集合可能包含 ByteBufferGifDecoder,也可能包含 ByteBufferBitmapDecoder 与 VideoDecoder 等。解码后 result 不为空,说明解码成功,则跳出循环。

那么怎样才算是找到了合适的资源解码器呢?看一下上面的关注点(1),这里有个判断,只有满足这个判断才能进行解码,所以满足这个判断时的解码器就是合适的解码器。当加载 GIF 动图的时候,这里遍历首先拿到的资源解码器是 ByteBufferGifDecoder,所以我们看下 ByteBufferGifDecoder 的 handles 方法是怎么判断的:

  /*ByteBufferGifDecoder*/
  @Override
  public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException {
    return !options.get(GifOptions.DISABLE_ANIMATION)
        && ImageHeaderParserUtils.getType(parsers, source) == ImageType.GIF;
  }

第一个条件是满足的,我们主要看下第二个条件。没错,这个就是用来区分图片是不是 GIF 动图的。

ImageType 是一个枚举,里面有多种图片格式:

  enum ImageType {
    GIF(true),
    JPEG(false),
    RAW(false),
    /** PNG type with alpha. */
    PNG_A(true),
    /** PNG type without alpha. */
    PNG(false),
    /** WebP type with alpha. */
    WEBP_A(true),
    /** WebP type without alpha. */
    WEBP(false),
    /** Unrecognized type. */
    UNKNOWN(false);

    private final boolean hasAlpha;

    ImageType(boolean hasAlpha) {
      this.hasAlpha = hasAlpha;
    }

    public boolean hasAlpha() {
      return hasAlpha;
    }
  }

我们看下 ImageHeaderParserUtils#getType() 是怎么获取图片类型的:

   /**ImageHeaderParserUtils**/
  @NonNull
  public static ImageType getType(
      @NonNull List<ImageHeaderParser> parsers, @Nullable final ByteBuffer buffer)
      throws IOException {
    if (buffer == null) {
      return ImageType.UNKNOWN;
    }

    return getTypeInternal(
        parsers,
        new TypeReader() {
          @Override
          public ImageType getType(ImageHeaderParser parser) throws IOException {
            // 调用 DefaultImageHeaderParser#getType()
            return parser.getType(buffer);
          }
        });
  }

  /*DefaultImageHeaderParser*/
  @NonNull
  @Override
  public ImageType getType(@NonNull ByteBuffer byteBuffer) throws IOException {
    return getType(new ByteBufferReader(Preconditions.checkNotNull(byteBuffer)));
  }

  /*DefaultImageHeaderParser*/
  private static final int GIF_HEADER = 0x474946;

  @NonNull
  private ImageType getType(Reader reader) throws IOException {
    try {
      final int firstTwoBytes = reader.getUInt16();
      // JPEG.
      if (firstTwoBytes == EXIF_MAGIC_NUMBER) {
        return JPEG;
      }

      // 关注点
      final int firstThreeBytes = (firstTwoBytes << 8) | reader.getUInt8();
      if (firstThreeBytes == GIF_HEADER) {
        return GIF;
      }

      ...

  }

可以看到,这里是从流里读取前 3 个字节进行判断的,若为 GIF 文件头,则返回图片类型为 GIF。这样第二个条件 ImageHeaderParserUtils.getType(parsers, source) == ImageType.GIF 也是满足的,所以这里找到的合适的资源解码器就是 ByteBufferGifDecoder。找到后就会跳出循环,不会继续寻找其他解码器。

GIF 文件头为 0x474946

到这里,我们就已经区分出图片类型了,接下来就分析下是加载 GIF 动图的原理。

二、加载原理

前面已经找到合适的资源解码器了,即 ByteBufferGifDecoder,那么下一步就是解码,我们看下 DecodePath#decodeResourceWithList() 中标记的关注点(2)。贴一下之前的代码吧:

  /*DecodePath*/
  private Resource<ResourceType> decodeResourceWithList(
      DataRewinder<DataType> rewinder,
      int width,
      int height,
      @NonNull Options options,
      List<Throwable> exceptions)
      throws GlideException {
    Resource<ResourceType> result = null;
    //noinspection ForLoopReplaceableByForEach to improve perf
    for (int i = 0, size = decoders.size(); i < size; i++) {
      ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);
      try {
        DataType data = rewinder.rewindAndGet();
        if (decoder.handles(data, options)) {
          data = rewinder.rewindAndGet();
          // 关注点
          result = decoder.decode(data, width, height, options);
        }
      } catch (IOException | RuntimeException | OutOfMemoryError e) {

        ...

      }

      if (result != null) {
        break;
      }
    }

    ...

    return result;
  }

进入 ByteBufferGifDecoder#decode() 看看:

  /*ByteBufferGifDecoder*/
  @Override
  public GifDrawableResource decode(
      @NonNull ByteBuffer source, int width, int height, @NonNull Options options) {
    final GifHeaderParser parser = parserPool.obtain(source);
    try {
      // 关注点
      return decode(source, width, height, parser, options);
    } finally {
      parserPool.release(parser);
    }
  }

调用了 decode() 的另一个重载方法:

  /*ByteBufferGifDecoder*/
  @Nullable
  private GifDrawableResource decode(
      ByteBuffer byteBuffer, int width, int height, GifHeaderParser parser, Options options) {
    long startTime = LogTime.getLogTime();
    try {
      // 获取 GIF 头部信息
      final GifHeader header = parser.parseHeader();
      if (header.getNumFrames() <= 0 || header.getStatus() != GifDecoder.STATUS_OK) {
        // If we couldn't decode the GIF, we will end up with a frame count of 0.
        return null;
      }

      // 根据 GIF 背景是否有透明通道来确定 Bitmap 的类型
      Bitmap.Config config =
          options.get(GifOptions.DECODE_FORMAT) == DecodeFormat.PREFER_RGB_565
              ? Bitmap.Config.RGB_565
              : Bitmap.Config.ARGB_8888;

      // 获取 Bitmap 的采样率
      int sampleSize = getSampleSize(header, width, height);
      //(1)
      GifDecoder gifDecoder = gifDecoderFactory.build(provider, header, byteBuffer, sampleSize);
      gifDecoder.setDefaultBitmapConfig(config);
      gifDecoder.advance();
      //(2)
      Bitmap firstFrame = gifDecoder.getNextFrame();
      if (firstFrame == null) {
        return null;
      }

      Transformation<Bitmap> unitTransformation = UnitTransformation.get();
      //(3)
      GifDrawable gifDrawable =
          new GifDrawable(context, gifDecoder, unitTransformation, width, height, firstFrame);
      //(4)
      return new GifDrawableResource(gifDrawable);
    } finally {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Decoded GIF from stream in " + LogTime.getElapsedMillis(startTime));
      }
    }
  }

源码中我标记了 4 个关注点,分别如下:

  • (1):进入 GifDecoderFactory#build() 看看:
  /*ByteBufferGifDecoder*/
  @VisibleForTesting
  static class GifDecoderFactory {
    GifDecoder build(
        GifDecoder.BitmapProvider provider, GifHeader header, ByteBuffer data, int sampleSize) {
      return new StandardGifDecoder(provider, header, data, sampleSize);
    }
  }

这里创建了一个 StandardGifDecoder 的实例,所以关注点(1)的 gifDecoder 实际是一个 StandardGifDecoder。它的作用是从 GIF 图像源读取帧数据,并将其解码为单独的帧用在动画中。

  • (2):获取下一帧。这里获取的是第一帧的 Bitmap,内部就是将 GIF 中第一帧的数据转成 Bitmap 返回。

  • (3):创建 GifDrawable 的实例,看一下创建的时候做了什么:

public class GifDrawable extends Drawable
    implements GifFrameLoader.FrameCallback, Animatable, Animatable2Compat {
  public GifDrawable(
      Context context,
      GifDecoder gifDecoder,
      Transformation<Bitmap> frameTransformation,
      int targetFrameWidth,
      int targetFrameHeight,
      Bitmap firstFrame) {
    this(
        new GifState(
            // 关注点
            new GifFrameLoader(
                Glide.get(context),
                gifDecoder,
                targetFrameWidth,
                targetFrameHeight,
                frameTransformation,
                firstFrame)));
  }
}

  /*GifFrameLoader*/
  GifFrameLoader(
      Glide glide,
      GifDecoder gifDecoder,
      int width,
      int height,
      Transformation<Bitmap> transformation,
      Bitmap firstFrame) {
    this(
        glide.getBitmapPool(),
        Glide.with(glide.getContext()),
        gifDecoder,
        null /*handler*/,
        getRequestBuilder(Glide.with(glide.getContext()), width, height),
        transformation,
        firstFrame);
  }

  /*GifFrameLoader*/
  @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
  GifFrameLoader(
      BitmapPool bitmapPool,
      RequestManager requestManager,
      GifDecoder gifDecoder,
      Handler handler,
      RequestBuilder<Bitmap> requestBuilder,
      Transformation<Bitmap> transformation,
      Bitmap firstFrame) {
    this.requestManager = requestManager;
    if (handler == null) {
      // 关注点
      handler = new Handler(Looper.getMainLooper(), new FrameLoaderCallback());
    }
    this.bitmapPool = bitmapPool;
    this.handler = handler;
    this.requestBuilder = requestBuilder;

    this.gifDecoder = gifDecoder;

    setFrameTransformation(transformation, firstFrame);
  }

可以看到,GifDrawable 是一个实现了 Animatable 的 Drawable,所以 GifDrawable 可以播放 GIF 动图。
创建 GifDrawable 的时候还创建了 GifFrameLoader 的实例,它的作用是帮助 GifDrawable 实现 GIF 动图播放的调度。GifFrameLoader 的构造函数中还创建了一个主线程的 Handler,这个后面会用到。

  • (4):将 GifDrawable 包装成 GifDrawableResource 进行返回,GifDrawableResource 主要用来停止 GifDrawable 的播放,以及 Bitmap 的回收等。

接下来分析下 GifDrawable 是怎么播放 GIF 动图的。我们都知道 Animatable 播放动画的方法是 start 方法,那么 GifDrawable 肯定是重写了这个方法:

  /*GifDrawable*/
  @Override
  public void start() {
    isStarted = true;
    resetLoopCount();
    if (isVisible) {
      startRunning();
    }
  }

那么这个方法是在哪里调用的呢?
其实在 Glide 的执行流程源码解析 这篇文章中,在最后显示图片之前那里调用了,即 ImageViewTarget#onResourceReady(),我再贴一下代码:

  /*ImageViewTarget*/
  @Override
  public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {
    if (transition == null || !transition.transition(resource, this)) {
      // 调用下面的 setResourceInternal 方法
      setResourceInternal(resource);
    } else {
      maybeUpdateAnimatable(resource);
    }
  }

  /*ImageViewTarget*/
  private void setResourceInternal(@Nullable Z resource) {
    setResource(resource);
    // 调用下面的 maybeUpdateAnimatable 方法
    maybeUpdateAnimatable(resource);
  }

  /*ImageViewTarget*/
  private void maybeUpdateAnimatable(@Nullable Z resource) {
    // 关注点
    if (resource instanceof Animatable) {
      animatable = (Animatable) resource;
      animatable.start();
    } else {
      animatable = null;
    }
  }

也就是如果加载的是 GIF 动图,那么关注点那里的 resource 其实就是 GifDrawable,然后调用了它的 start 方法开始播放动画。

那现在回去继续看 GifDrawable#start() 中的 startRunning 方法吧:

  /*GifDrawable*/
  private void startRunning() {

    ...

    if (state.frameLoader.getFrameCount() == 1) {
      invalidateSelf();
    } else if (!isRunning) {
      isRunning = true;
      state.frameLoader.subscribe(this);
      invalidateSelf();
    }
  }

可以看到,如果 GIF 只有一帧的时候会直接调用绘制方法,否则调用 GifFrameLoader#subscribe() 进行订阅,然后再调用绘制方法。

看一下 subscribe 方法:

  /*GifFrameLoader*/
  void subscribe(FrameCallback frameCallback) {

    ...

    boolean start = callbacks.isEmpty();
    // 将 FrameCallback 添加到集合中
    callbacks.add(frameCallback);
    if (start) {
      // 调用下面的 start 方法
      start();
    }
  }

  /*GifFrameLoader*/
  private void start() {
    if (isRunning) {
      return;
    }
    isRunning = true;
    isCleared = false;

    loadNextFrame();
  }

继续看 loadNextFrame 方法:

  /*GifFrameLoader*/
  private void loadNextFrame() {
    ...

    //(1)
    if (pendingTarget != null) {
      DelayTarget temp = pendingTarget;
      pendingTarget = null;
      onFrameReady(temp);
      return;
    }
    isLoadPending = true;
    // Get the delay before incrementing the pointer because the delay indicates the amount of time
    // we want to spend on the current frame.
    int delay = gifDecoder.getNextDelay();
    long targetTime = SystemClock.uptimeMillis() + delay;
    //(2)
    gifDecoder.advance();
    //(3)
    next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);
    //(4)
    requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);
  }

源码中我标记了 4 个关注点,分别如下:

  • (1):如果存在未绘制的帧数据(例如正在播放,然后熄屏再亮屏就会走这里),则调用 onFrameReady 方法,这个方法放到后面再分析。

  • (2):向前移动帧。

  • (3):创建了 DelayTarget 的实例,看一下这个类是干嘛的:

  /*GifFrameLoader*/
  @VisibleForTesting
  static class DelayTarget extends CustomTarget<Bitmap> {
    private final Handler handler;
    @Synthetic final int index;
    private final long targetTime;
    private Bitmap resource;

    DelayTarget(Handler handler, int index, long targetTime) {
      this.handler = handler;
      this.index = index;
      this.targetTime = targetTime;
    }

    Bitmap getResource() {
      return resource;
    }

    @Override
    public void onResourceReady(
        @NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
      this.resource = resource;
      Message msg = handler.obtainMessage(FrameLoaderCallback.MSG_DELAY, this);
      handler.sendMessageAtTime(msg, targetTime);
    }

    @Override
    public void onLoadCleared(@Nullable Drawable placeholder) {
      this.resource = null;
    }
  }

它继承了 CustomTarget,CustomTarget 的父类又是一个 Target,所以可以用在关注点(4)的 into 方法中。

在 “Glide 的执行流程源码解析” 这篇文章中已经知道当执行 into(imageView) 的时候会将传入的 imageView 转成 Target,所以这里直接传一个 Target 到 into 方法也是一样的。

而 onResourceReady 方法是资源加载完成的回调,这里首先进行了 Bitmap 的赋值,然后利用传进来的 Handler 发送了一个延迟消息。

  • (4):这句是不是很熟悉?其实他就相当于执行了我们熟悉的这句:
Glide.with(this).load(url).into(imageView);

这句执行后就会回调关注点(2)的 onResourceReady 方法。

刚刚发送了一个延迟消息,那么我们现在继续看下是怎么处理消息的:

  private class FrameLoaderCallback implements Handler.Callback {
    static final int MSG_DELAY = 1;
    static final int MSG_CLEAR = 2;

    @Synthetic
    FrameLoaderCallback() {}

    @Override
    public boolean handleMessage(Message msg) {
      if (msg.what == MSG_DELAY) {
        GifFrameLoader.DelayTarget target = (DelayTarget) msg.obj;
        // 关注点
        onFrameReady(target);
        return true;
      } else if (msg.what == MSG_CLEAR) {
        GifFrameLoader.DelayTarget target = (DelayTarget) msg.obj;
        requestManager.clear(target);
      }
      return false;
    }
  }

收到延迟消息后,调用了 onFrameReady 方法:

  /*GifFrameLoader*/
  @VisibleForTesting
  void onFrameReady(DelayTarget delayTarget) {

    ...

    if (delayTarget.getResource() != null) {
      recycleFirstFrame();
      DelayTarget previous = current;
      current = delayTarget;
      // 关注点
      for (int i = callbacks.size() - 1; i >= 0; i--) {
        FrameCallback cb = callbacks.get(i);
        cb.onFrameReady();
      }
      if (previous != null) {
        handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, previous).sendToTarget();
      }
    }
    // 继续加载下一帧
    loadNextFrame();
  }

可以看到,这里遍历 callbacks 集合拿到 FrameCallback,callbacks 集合是前面订阅的时候添加的数据。因为 GifDrawable 实现了 FrameCallback 接口,所以这里会回调到 GifDrawable#onFrameReady():

  /*GifDrawable*/
  @Override
  public void onFrameReady() {
    if (findCallback() == null) {
      stop();
      invalidateSelf();
      return;
    }

    // 关注点
    invalidateSelf();

    if (getFrameIndex() == getFrameCount() - 1) {
      loopCount++;
    }

    if (maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) {
      notifyAnimationEndToListeners();
      stop();
    }
  }

调用了绘制方法,所以会调用 draw 方法:

  /*GifDrawable*/
  @Override
  public void draw(@NonNull Canvas canvas) {
    if (isRecycled) {
      return;
    }

    if (applyGravity) {
      Gravity.apply(GRAVITY, getIntrinsicWidth(), getIntrinsicHeight(), getBounds(), getDestRect());
      applyGravity = false;
    }

    Bitmap currentFrame = state.frameLoader.getCurrentFrame();
    canvas.drawBitmap(currentFrame, null, getDestRect(), getPaint());
  }

使用 GifFrameLoader 获取到当前帧的 Bitmap,然后使用 Canvas 将 Bitmap 绘制到 ImageView 上。就这样循环将每一帧的 Bitmap 都通过 Canvas 绘制到 ImageView 上,就形成了 GIF 动图。

三、总结

面试官: Glide 是如何加载 GIF 动图的?

小明:
首先需要区分加载的图片类型,即网络请求拿到输入流后,获取输入流的前三个字节,若为 GIF 文件头,则返回图片类型为 GIF。

确认为 GIF 动图后,会构建一个 GIF 的解码器(StandardGifDecoder),它可以从 GIF 动图中读取每一帧的数据并转换成 Bitmap,然后使用 Canvas 将 Bitmap 绘制到 ImageView 上,下一帧则利用 Handler 发送一个延迟消息实现连续播放,所有 Bitmap 绘制完成后又会重新循环,所以就实现了加载 GIF 动图的效果。

关于我

我是 wildmaCSDN 认证博客专家简书程序员优秀作者,擅长屏幕适配
如果文章对你有帮助,点个赞就是对我最大的认可!

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

推荐阅读更多精彩内容

  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,543评论 0 11
  • 彩排完,天已黑
    刘凯书法阅读 4,175评论 1 3
  • 没事就多看看书,因为腹有诗书气自华,读书万卷始通神。没事就多出去旅游,别因为没钱而找借口,因为只要你省吃俭用,来...
    向阳之心阅读 4,768评论 3 11
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 123,620评论 2 7