Glide遇坑记---Glide与CircleImageView (二)

Glide遇坑记之决战

上述三种解决方法都避免了与真正BOSS---TransitionDrawable的正面交锋。TransitionDrawable继承自LayerDrawableLayerDrawable是一个特殊的Drawable,它内部保持着一个Drawable数组,其中每一个Drawable都是视图中的一层。通过多个Drawable的叠加、渐变、旋转等组合显示出与单一Drawable不同的效果。

@Override
public boolean animate(T current, ViewAdapter adapter) {
    Drawable previous = adapter.getCurrentDrawable();
    if (previous != null) {
        TransitionDrawable transitionDrawable = new TransitionDrawable(new Drawable[] { previous, current });
        transitionDrawable.setCrossFadeEnabled(true);
        transitionDrawable.startTransition(duration);
        adapter.setDrawable(transitionDrawable);
        return true;
    } else {
        defaultAnimation.animate(current, adapter);
        return false;
    }
}
public void startTransition(int durationMillis) {
    mFrom = 0;
    mTo = 255;
    mAlpha = 0;
    mDuration = mOriginalDuration = durationMillis;
    mReverse = false;
    mTransitionState = TRANSITION_STARTING;
    invalidateSelf();
}

DrawableCrossFadeViewAnimation.animate()方法先是获取TransitionDrawable对象实例,接着调用setCrossFadeEnabled()startTransition()方法对TransitionDrawable的成员变量进行设置。并在startTransition()方法的最后一行,调用invalidateSelf()方法尝试进行视图重绘。

public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

invalidateSelf()方法先是获取TransitionDrawable注册的Callback实例,如果无则返回null。通过Callback接口,一个Drawable实例可以回调其客户端来执行动画。为了动画可以被执行,所有的客户端都应该支持这个Callback接口。 View类正是实现了Callback接口,所以callback.invalidateDrawable()其实调用的就是View中的invalidateDrawable()方法。 但此时TransitionDrawable实例未注册任何Callback接口,invalidateSelf()方法直接返回。


紧接着animation()中执行adapter.setDrawable()方法,方法内部通过view.setImageDrawable(drawable)来更新Drawable

public void setImageDrawable(@Nullable Drawable drawable) {
    if (mDrawable != drawable) {
        mResource = 0;
        mUri = null;

        final int oldWidth = mDrawableWidth;
        final int oldHeight = mDrawableHeight;

        updateDrawable(drawable);

        if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
            requestLayout();
        }
        invalidate();
    }
}

view.setImageDrawable(drawable)方法内部先是通过updateDrawable(drawable)更新成员变量mDrawable,同时修改其属性。 接着调用invalidate()方法正式开始视图重绘。

private void updateDrawable(Drawable d) {
    if (mDrawable != null) {
        sameDrawable = mDrawable == d;
        mDrawable.setCallback(null);
        unscheduleDrawable(mDrawable);
        if (!sCompatDrawableVisibilityDispatch && !sameDrawable && isAttachedToWindow()) {
                mDrawable.setVisible(false, false);
        }
    }

    mDrawable = d;

    if (d != null) {
        d.setCallback(this);
        d.setLayoutDirection(getLayoutDirection());
        d.setLevel(mLevel);
        configureBounds();
    } else {
        mDrawableWidth = mDrawableHeight = -1;
    }
}

updateDrawble()首先对mDrawable做了一些检查,并将与ImageView关联的Drawable实例mDrawableCallback置空。接着把传进来的Drawable对象赋给成员变量mDrawable。如果参数d不为空的话,那么设置dCallbackImageView实例。通过d.getIntrinsicWidth()获取drawablewidth赋值全局变量mDrawableWidth

Android视图重绘机制

View的源码中会有数个invalidate()方法的重载和一个invalidateDrawable()方法,最终都是通过invalidateInternal()方法来实现视图重制。

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
    if (mGhostView != null) {
        mGhostView.invalidate(true);
        return;
    }

    if (skipInvalidate()) {
        return;
    }

    if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
        if (fullInvalidate) {
            mLastIsOpaque = isOpaque();
            mPrivateFlags &= ~PFLAG_DRAWN;
        }

        mPrivateFlags |= PFLAG_DIRTY;

        if (invalidateCache) {
            mPrivateFlags |= PFLAG_INVALIDATED;
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }

        final AttachInfo ai = mAttachInfo;
        final ViewParent p = mParent;
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            p.invalidateChild(this, damage);
        }
    }
}

在这个方法中首先会调用skipInvalidate()方法来判断当前 View是否需要重绘,判断的逻辑也比较简单,如果View是不可见的且没有执行任何动画,就认为不需要重绘了。之后会进行透明度的判断,并给View添加一些标记位,然后调用ViewParent的invalidateChild()方法,这里的ViewParent其实就是当前视图的父视图,因此会调用到ViewGroupinvalidateChild()方法中。省略若干循环调用。最终经过多次辗转的调用,最终会走到视图绘制的入口方法performTraversals()中,然后重新执行绘制流程。

invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measurelayout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()

绘制流程始于ViewRootImplperformDraw()方法,里面又调用了ViewRootImpldraw()方法,经过一系列调用,然后实例化Canvas对象,锁定该canvas的区域并进行一系列的属性赋值,最后调用了mView.draw(canvas)方法,这个mView就是DecorView,也就是说从DecorView开始绘制。由于ViewGroup没有重写draw方法,因此所有的View都是通过调用Viewdraw()方法实现绘制。

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // we're done...
        return;
    }
    ...
}

draw过程比较复杂,但是逻辑十分清晰,一般是遵循下面几个步骤:

  • 绘制背景 -- drawBackground()
  • 绘制内容 -- onDraw()
  • 绘制孩子 -- dispatchDraw()
  • 绘制装饰 -- onDrawScrollbars()

View中的onDraw()方法是一个空实现,不同的View有着不同的内容,这需要我们自己去实现,即在自定义View中重写该方法来实现。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    if (mDrawable == null) {
        return; // couldn't resolve the URI
    }

    if (mDrawableWidth == 0 || mDrawableHeight == 0) {
        return;     // nothing to draw (empty bounds)
    }

    if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
        mDrawable.draw(canvas);
    } else {
        final int saveCount = canvas.getSaveCount();
        canvas.save();

        canvas.translate(mPaddingLeft, mPaddingTop);

        if (mDrawMatrix != null) {
            canvas.concat(mDrawMatrix);
        }
        mDrawable.draw(canvas);
        canvas.restoreToCount(saveCount);
    }
}

ImageViewonDraw()方法首先对mDrawable进行检查,mDrawable是否为空、宽高是否具有意义。在设置了mDrawMatrix的一系列方法后,onDraw()在绘制前会根据mDrawMatrix设置的值对图片资源进行相应的变换操作。无论Drawable缩放与否在满足mDrawable != null && mDrawableWidth != 0 && mDrawableHeight != 0的绘制条件下,最终都是通过mDrawable.draw(canvas)方法对mDrawable进行绘制。这里的mDrawableTransitionDrawable对象实例。

public void draw(Canvas canvas) {
    boolean done = true;

    switch (mTransitionState) {
        case TRANSITION_STARTING:
            mStartTimeMillis = SystemClock.uptimeMillis();
            done = false;
            mTransitionState = TRANSITION_RUNNING;
            break;

        case TRANSITION_RUNNING:
            if (mStartTimeMillis >= 0) {
                float normalized = (float)(SystemClock.uptimeMillis() - mStartTimeMillis) / mDuration;
                done = normalized >= 1.0f;
                normalized = Math.min(normalized, 1.0f);
                mAlpha = (int) (mFrom  + (mTo - mFrom) * normalized);                                                
            }
            break;
    }

    final int alpha = mAlpha;
    final boolean crossFade = mCrossFade;
    final ChildDrawable[] array = mLayerState.mChildren;

    if (done) {
        if (!crossFade || alpha == 0) {
            array[0].mDrawable.draw(canvas);
        }
        if (alpha == 0xFF) {
            array[1].mDrawable.draw(canvas);
        }
        return;
    }

    Drawable d;
    d = array[0].mDrawable;
    if (crossFade) {
        d.setAlpha(255 - alpha);
    }
    d.draw(canvas);
    if (crossFade) {
        d.setAlpha(0xFF);
    }

    if (alpha > 0) {
        d = array[1].mDrawable;
        d.setAlpha(alpha);
        d.draw(canvas);
        d.setAlpha(0xFF);
    }

    if (!done) {
        invalidateSelf();
    }
}

调用adapter.setDrawable(transitionDrawable),进行视图重绘的流程中,实质还是调用TransitionDrawable.draw()方法完成自身绘制。TransitionDrawable.draw()方法的逻辑也是简单明了,d.setAlpha(alpha)d.draw(canvas),在不同阶段为两张Drawable设置对应透明度以此实现两个Drawable之间的淡入淡出效果。Drawable.draw()本身是个抽象方法,绘制具体逻辑由其子类实现。这里的drawable分别为GlideBitmapDrawableBitmapDrawable对象实例,具体为什么,在了解Drawable源码之后你就清楚了。(GlideBitmapDrawable则隐藏在Glide网络请求部分的源码之中)

Drawable源码分析

Drawable是一个用于处理各种可绘制资源的抽象类。我们使用Drawable最常见的情况就是将获取到的资源绘制到屏幕上。

Drawable实例可能存在以下多种形式

  • Bitmap:最简单的Drawable形式,PNG或者JPEG图片。
  • .9图PNG的一个扩展,可以支持设置其如何填充内容,如何被拉伸。
  • Shape:包含简单的绘制指令,用于替代Bitmap,某些情况下对大小调整有更好表现。
  • Layers:一个复合的Drawable,按照层级进行绘制,单个Drawable实例绘制于其下层Drawable实例集合之上。
  • States:一个复合的Drawable,根据它的state选择一个Drawable集合。
  • Levels:一个复合的Drawable,根据它的level选择一个Drawable集合。
  • Scale:一个复合的Drawable和单个Drawable实例构成,它的总体尺寸由它的当前level值决定。

Drawable常见使用步骤

  • 通过Resource获取Drawable实例
  • 将获取的Drawable实例当做背景设置给View或者作为ImageViewsrc进行显示:

getResources().getDrawable()方法经过多次辗转的调用最终会通过ResourcesImpl实例的drawableFromBitmap()方法加载资源图片。

private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np, Rect pad, Rect layoutBounds, String srcName) {

    if (np != null) {
        return new NinePatchDrawable(res, bm, np, pad, layoutBounds, srcName);
    }

    return new BitmapDrawable(res, bm);
}

drawableFromBitmap()方法对于.9图返回1个NinePatchDrawable实例,普通图片返回1个BitmapDrawable实例。


public boolean animate(T current, ViewAdapter adapter) {
    Drawable previous = adapter.getCurrentDrawable();
    if (previous != null) {
        TransitionDrawable transitionDrawable = new TransitionDrawable(new Drawable[] { previous, current });
        return true;
    } 
} 

TransitionDrawable在实例化时传入的previouscurrent分别来自adapter.getCurrentDrawable()方法和animate()方法传入的current参数。adapter.getCurrentDrawable()方法内部通过view.getDrawable()来获取与ImageView关联的Drawable实例。这个Drawable则来自getPlaceholderDrawable()方法。

private Drawable getPlaceholderDrawable() {
        if (placeholderDrawable == null && placeholderResourceId > 0) {
            placeholderDrawable = context.getResources().getDrawable(placeholderResourceId);
        }
        return placeholderDrawable;
}

getPlaceholderDrawable()方法,通过Resource实例加载占位符placeHolder图片资源。没错,getPlaceholderDrawable()方法就像👆Drawable常见使用步骤,最终会通过ResourcesImpl.drawableFromBitmap()加载资源图片,返回1个BitmapDrawable实例。通过target.onLoadStarted(getPlaceholderDrawable())将获取的Drawable实例当做背景设置给ImageView

LayerDrawable&Callback

Android LayerDrawable and Drawable.Callback

文章中之所以提到Callback是因为为ImageView设置占位符时ImageView的Callback指向当前的Drawable。
当使用占位符作为子图层创建LayerDrawable实例时

    LayerDrawable(@NonNull Drawable[] layers, @Nullable LayerState state) {
        this(state, null);

        final int length = layers.length;
        final ChildDrawable[] r = new ChildDrawable[length];
        for (int i = 0; i < length; i++) {
            r[i] = new ChildDrawable(mLayerState.mDensity);
            r[i].mDrawable = layers[i];
            layers[i].setCallback(this);
            mLayerState.mChildrenChangingConfigurations |= layers[i].getChangingConfigurations();
        }
    }
BitmapDrawable&GlideBitmapDrawable

Drawable.draw()本身是个抽象方法,绘制具体逻辑由其子类实现。TransitionDrawable.draw() 方法最终还是通过d.setAlpha(alpha)d.draw(canvas),在不同阶段为Drawable设置对应透明度以此实现两个Drawable之间的淡入淡出效果。

public void setAlpha(int alpha) {
    final int oldAlpha = mBitmapState.mPaint.getAlpha();
    if (alpha != oldAlpha) {
        mBitmapState.mPaint.setAlpha(alpha);
        invalidateSelf();
    }
}

LayerDrawable中,每层视图(Drawable)都会将LayerDrawable注册为它的Drawable.Callback。从而允许Drawable在需要重绘自己的时候能够告知LayerDrawable重绘它。LayerDrawable最终调用到View中的invalidateDrawable()方法,之后就会按照我们前面分析的流程执行重绘逻辑,以此改变视图背景。

public void draw(Canvas canvas) {
    final Bitmap bitmap = mBitmapState.mBitmap;
    if (bitmap == null) {
        return;
    }

    final BitmapState state = mBitmapState;
    final Paint paint = state.mPaint;

    final Shader shader = paint.getShader();
    if (shader == null) {
        if (needMirroring) {
            canvas.save();
            canvas.translate(mDstRect.right - mDstRect.left, 0);
            canvas.scale(-1.0f, 1.0f);
        }

        canvas.drawBitmap(bitmap, null, mDstRect, paint);

    } else {
        updateShaderMatrix(bitmap, paint, shader, needMirroring);
        canvas.drawRect(mDstRect, paint);
    }
}

BitmapDrawable.draw()方法先是对画笔Paint和画布Canvas进行相应设置,接着将Drawable实例中的Bitmap绘制到View实例关联的画布上。

GlideBitmapDrawable绘制逻辑与BitmapDrawable基本相同,便不再赘述。


至此,我们已经了解了TransitionDrawable实现渐变的原理,及与相关知识(Android视图重绘机制、Drawable及其实现类源码)。是不是觉得头昏脑胀,不知所云~~~ 不不不,应该是虽然学到了很多知识,并没有发现问题的存在~~~

还记得吗,这个坑只会在使用CircleImageView的情况下出现,对于ImageView,即便是在使用占位符和默认动画的情况下Glide仍可以正常工作。那CircleImageViewImageView两者之间又是存在何种差异导致了问题的出现?

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    if (mDrawable == null) {
        return; // couldn't resolve the URI
    }

    if (mDrawableWidth == 0 || mDrawableHeight == 0) {
        return;     // nothing to draw (empty bounds)
    }

    if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
        mDrawable.draw(canvas);
    } else {
        final int saveCount = canvas.getSaveCount();
        canvas.save();

        canvas.translate(mPaddingLeft, mPaddingTop);

        if (mDrawMatrix != null) {
            canvas.concat(mDrawMatrix);
        }
        mDrawable.draw(canvas);
        canvas.restoreToCount(saveCount);
    }
}

ImageView.onDraw()方法通过mDrawable.draw()方法对mDrawable进行绘制并实现淡入淡出效果。👆有对ImageView.onDraw()方法更为详细的分析过程,没错就是Android视图重绘机制哪里~

protected void onDraw(Canvas canvas) {
    if (mDisableCircularTransformation) {
        onDraw(canvas);
        return;
    }

    if (mBitmap == null) {
        return;
    }

    if (mFillColor != Color.TRANSPARENT) {
        canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mFillPaint);
    }
    canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
    if (mBorderWidth > 0) {
        canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
    }
}

通过上面的分析,我们终于可以得出只显示占位符placeHolder的原因。Glide在网络中获取图片并经过解码、逻辑操作(包括对图片的压缩,甚至还有旋转、圆角等逻辑处理)之后,会最终回调到GlideDrawableImageViewTarget.onResourceReady()方法来设置ImageView。一番处理之后,会调用父类ImageViewTargetonResourceReady()方法。

onResourceReady()方法通过对glideAnimation进行判空,对glideAnimation.animate()返回值进行分析,来决定是否执行setResource()方法。根据animationFactory引用工厂对象的不同,onResourceReady()方法可能传入DrawableCrossFadeViewAnimationNoAnimation对象实例。

DrawableCrossFadeViewAnimation.animate()方法内部先是获取先前通过placeHolder()设置占位符占位符previous。如previous不为空,则通过TransitionDrawable设置动画并添加图片至ImageView。否则通过defaultAnimation展示图片。

CircleImageView.onDraw()方法仅是通过canvas.drawCircle()方法将Drawable实例中的 Bitmap经过裁剪之后绘制到CircleImageView实例关联的画布上。没错,与ImageView.onDraw()方法相比缺少了对mDrawable.draw()方法的调用,而mDrawable.draw()方法则会不断调用invalidateSelf()方法获取其关联的View进行重复的视图重绘操作,通过不断调用TransitionDrawable.draw()方法,设置两个Drawable透明度从而实现渐入渐出效果的实现

除上述我个人的结论之外,网上也有一些分析的文章说:根本原因就是你的placeholder图片和你要加载显示的图片宽高比不一样,而Android的TransitionDrawable无法很好地处理不同宽高比的过渡问题,这的确是个Bug,是Android的也是Glide的。 文章实在是太长了😂,对此就先挖个坑,回头再填~

至此Glide遇坑记之Glide与CircleImageView的分析就告一段落~~
以上分析均是个人见解。如果错误或疏忽请及时指出,O(∩_∩)O谢谢!

参考文章

Glide v4快速高效的Android图片加载库

Android图片加载框架最全解析,从源码的角度理解Glide的执行流程

详谈高大上的图片加载框架Glide -源码篇

Android Drawable完全解析

Android LayerDrawable and Drawable.Callback

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

推荐阅读更多精彩内容