Android源码-深入理解Window和WindowManager

前言

Window 表示一个窗口的概念,Android中所有的视图都是通过Window来呈现的,不管是ActivityDialog、还是Toast,它们的视图实际上都是附加在Window上的,因此,Window实际是View的管理者。Window是一个非常重要的子系统,这也是我们常说的WMS(WindowManagerService)。下面我们就分析一下WindowWMSView建立关联以及交互的一个基本过程。

Window体系相关UML类图

Window.png
  • Session :是一个Binder对象,代表一个活跃的客户端会话,在每个进程中都有一个
    SessionWindowManager交互的对象。
  • WindowManagerService :也是一个Binder对象,负责对窗口的管理。
  • Window :应用程序用来与窗口管理器交谈的界面。
  • PhoneWindowWindow 的具体实现。
  • WindowManagerImpl : 负责与系统窗口管理器通信、绑定到上下文、显示的操作。
  • ViewRootImpl :负责View的(测量、摆放、绘制)三大流程。
  • WindowManagerGlobalWindowManager 的具体实现。

WindowManager

WindowManager联系上的第一步就是通过ContextgetSystemService()方法,在分析文章Android源码中单例模式 中我们知道,各种系统服务会注册到ContextImpl的一个map容器中,然后通过该服务的字符串键获取,WindowManager也是ContextImpl中注册的众多服务之一,我们看下下面这段程序:

        //窗口服务
        registerService(Context.WINDOW_SERVICE, WindowManager.class,
                new CachedServiceFetcher<WindowManager>() {
            @Override
            public WindowManager createService(ContextImpl ctx) {
                return new WindowManagerImpl(ctx);
            }});

最后一行代码中,我们看到了WindowManager在Java层的具体实现,也就是WindowManagerImpl。那Activity或者Dialog又是如何获取到WindowManager对象呢?我们从上述代码知道,WindowManager是注册到ContextImpl中的,而getSystemService也是Context定义的接口,因此,我们就从Dialog的构造函数和Activity入手,因为Context是传到Dialog构造函数的。

Dialog构造函数


    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        //获取WindowManager
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        //设置Window回调
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        //设置Window的WindowManager对象
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
    }

Activity的attch方法

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
        ...
         //创建Window并设置window的监听
        mWindow = new PhoneWindow(this, window);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        ...
        //Window设置WindowManager对象
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;
    }

无论是Dialog还是Activity都是通过Window对象的setWindowManager方法将WindowManagerWindow关联。该函数是在Window中,看看实现:

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        ...
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

最后调用的是WindowManagerImplcreateLocalWindowManager方法,这里与ContextImpl注册的WindowManagerImpl不同的是,这里多了一个parentWindow参数,也就是说,此时构建的WindowManagerImpl对象是与具体的Window关联的,而ContextImpl注册的并没有此参数。这是Window已经和WindowManager建立了初步联系。为什么这么说呢?我们看下WindowManagerImpl 的具体实现:

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Context mContext;
    private final Window mParentWindow;
    ...
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

    @Override
    public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.updateViewLayout(view, params);
    }

    @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

    @Override
    public void removeViewImmediate(View view) {
        mGlobal.removeView(view, true);
    }

    @Override
    public Display getDefaultDisplay() {
        return mContext.getDisplay();
    }
}

显然WindowManagerImpl还没有实现对视图的操作,Dialog是在show()方法里添加的,添加、更新、删除都交给了WindowManagerGlobal这个类,通过以上分析对VIew的操作实际上是调用的是WindowManagerGlobal的方法,继续跟踪:

WindowManagerGlobal对View的操作

看上面UML类图我看到WindowManagerGlobal有几个重要的属性,

    private final ArrayList<View> mViews = new ArrayList<View>();
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
    private final ArraySet<View> mDyingViews = new ArraySet<View>();

在上面声明中,没mViews存储的是所有Window所对应的ViewmRoots存储的是所有Window多对用的ContextImpl,mParams存储的是所有Window的布局参数,而mDyingViews则存储了那些整被删除的View对象,或者说那些已经调用removeView方法但是删除操作还未完成的Window对象,在addView中将Window一系列对象添加到容器中。

WindowManagerGlobal的addView过程


    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...//省略参数检查代码
        ViewRootImpl root;
        View panelParentView = null;
            ...
            //创建ViewRootImpl
            root = new ViewRootImpl(view.getContext(), display);
            //设置参数
            view.setLayoutParams(wparams);
            //添加到容器列表中
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // 调用ViewRootImpl的setView方法将VIew显示到手机上
            root.setView(view, wparams, panelParentView);
    }

上面程序主要完成以下工作;

  • 构建ViewRootImpl
  • 将布局参数设置给View
  • 存储这些ViewRootImplViewLayoutParam到列表中;
  • 通过ViewRootImplsetViewView显示到窗口。

很多人对ViewRootImpl并不陌生,从UML类图可以看出这个类里面有一个我们熟知的performTraversals方法,ViewRootImpl收到系统绘制View的消息后performTraversals就会调用视图树的各个节点的meaturelayoutdraw方法来绘制整颗视图树。

从上述代码分析来看,第一个重要步骤就是创建了ViewRootImpl对象,我们看看它的构造方法:

    public ViewRootImpl(Context context, Display display) {
        mContext = context;
        //获取Window Session,也就是也WindowManagerService建立联系
        mWindowSession = WindowManagerGlobal.getWindowSession();
        //保存当前线程,,更新Ui的 线程只能是创建ViewRootImpl时的线程,
        //我们在开发中,如果在子线程更新UI会抛出异常,但并不是因为只有UI线程才能更新UI
        //而是因为ViewRootImpl是在UI线程中创建的
        mThread = Thread.currentThread();
        ...
    }

获取WindowManagerService

我们看下ViewRootImpl的构造函数中是如何获取到WindowManagerService的:

    public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                    InputMethodManager imm = InputMethodManager.getInstance();
                    //获取WindowManagerService
                    IWindowManager windowManager = getWindowManagerService();
                    sWindowSession = windowManager.openSession(
                            imm.getClient(), imm.getInputContext());
            }
            return sWindowSession;
        }
    }
    //获取WindowManagerService
    public static IWindowManager getWindowManagerService() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowManagerService == null) {
                //aidl
                sWindowManagerService = IWindowManager.Stub.asInterface(
                ServiceManager.getService("window"));
            }
            return sWindowManagerService;
        }
    }

getWindowSession方法中,FrameWork层首先通过getWindowManagerService方法获取IWindowManager对象,该函数中通过ServiceManager.getService方法获取WMS,并且将WMS转换为IWindowManager类型,我们先看看ServiceManager.getService方法:

    public static IBinder getService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                return Binder.allowBlocking(getIServiceManager().getService(name));
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }
        return null;
    }

从程序中可以看到ServiceManager.getService返回的是一个IBinder对象,也就是说Android Framework层与WMS之间也是通过Binder机制进行通讯。获取WMS之后,又调用IWindowManager.Stub类的asInterface方法,看到这里我们就会想起AIDL,详情请看这篇文章理解AIDL ,将获取到的WMSIBinder对象转换成WindowManager对象,最后,通过openSession函数来与WMS建立一个通信会话,相当于Framework层与native层建立了一个长期合作的”办事处“,双方有什么需求都通过这个Session来交换信息。

ViewRootImpl的setView方法

WMS建立Session后就到了ViewRootImplsetView方法了,该方法会向WMS发起显示Dialog或者Activity中的DecorView请求,具体代码:

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                //请求布局
                requestLayout();
                //向WMS发起请求
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);

            }
        }
    }

setView过程比较复杂,但我们只需要关注两步:

  • requestLayout();
  • WMS发起显示当前Window请求

ViewRootImpl的requestLayout过程

我们再来看下requestLayout方法

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            //发起绘制
            scheduleTraversals();
        }
    }

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //注意第二个参数,第一个第三个省略
            mChoreographer.postCallback(...  , mTraversalRunnable , ... );
            ...
        }
    }
    
    //创建子线程去绘制
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

    void doTraversal() {
        //绘制入口
        performTraversals();
    }

最终会执行performTraversals();方法,这是一个极其复杂有非常重要的函数。主要做了如下操作:

  • 获取Surface对象,同于图形绘制
  • 测量视图树中各个View的大小,performMeasure()
  • 摆放整个视图树,performLayout()
  • 绘制整棵视图树,performDraw()

代码如下:

    private void performTraversals() {
        //会调用View的onMeasure
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        //会调用View的onLayout
        performLayout(lp, mWidth, mHeight);
        //会调用View的Draw
        performDraw();
    }

performDraw方法中,Framework层获取到图形绘制表面的Surface对象,然后获取它的可绘制区域,也就是我们的Canvas对象,然后Framework在这个Canvas对象上绘制,具体代码如下;

    private void performDraw() {
      
            draw(fullRedrawNeeded);
    }

    private void draw(boolean fullRedrawNeeded) {
        //获取绘制表面
        Surface surface = mSurface;
        if (!surface.isValid()) {
            return;
        }
        ...
        if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
            //使用GPU绘制,也就是硬件加速
            if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
                ...
                mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
            } else {
                //使用CPU绘制图形
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
                }
            }
        }

    }

draw方法中会获取到需要绘制的区域,以及判断是否使用GPU进行绘制。通常情况下使用的是CPU绘制,也就是调用的是drawSoftware()。我们看看该函数的实现:

    //使用CPU绘制
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

        // Draw with software renderer.
        final Canvas canvas;
        try {
            //获取指定区域的获取指定区域的Canvas对象对象,用于绘制
            canvas = mSurface.lockCanvas(dirty);
        }
        ...
        try {
            ...
            //从DecorView开始绘制,也就是整个Window的根视图,整棵树都会绘制
            mView.draw(canvas);     
        } finally {
            try {
                //释放Canvas锁,然后通知Surface更新这块区域,与开头照应
                surface.unlockCanvasAndPost(canvas);
        }
        return true;
    }

综上所述,上述的视图树绘制代码主要分为下面几个步骤:

  • 判断是CPU还是GPU绘制
  • 获取绘制表面的Surface对象
  • 通过Surface对象获取并锁住Canvas绘图对象
  • DecorView开始发起整颗树的绘制流程
  • Surface对象解锁Canvas,并通知SurfaceFlinger更新视图

了解具体View的三大流程请看文章:

以上就是整个视图的绘制过程,但是此时Dialog或者ActivityView并不能显示在手机屏幕上,WMS只是负责管理手机上的View,也就是说WMS管理当前状态下那个View应该显示在最上层。其实WMS管理的并不是Window,而是View,只不过他管理的是属于某个WIndow下的View

ViewRootImpl请求WMS添加Window过程

我们上面只是分析了ViewRootImplrequestLayout过程,下面再回到ViewRootImplsetView方法,绘制完成接着会通过WindowSession最终来完成WIndow的添加过程,在下面的代码中mWindowSession的类型是IWindowSession,它也是一个Binder对象,真正的实现类是Session,也就是Window的添加过程是一次IPC调用。

    
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
             getHostVisibility(), mDisplay.getDisplayId(),
             mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
             mAttachInfo.mOutsets, mInputChannel);

在Session内部会通过WindowManagerService来实现Window的添加,代码如下:

frameworks\base\services\core\java\com\android\server\wm\Session.java

    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            Rect outOutsets, InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }

这样Window的添加过程就交给了WindowManagerService去处理了,在WMS内部会保留一个单独的Session。具体WindowWMS内部如何添加的,本篇不对分析,至此对于View的绘制以及视图如何添加到Window整个流程已经很明了了。关于WMSSurface系统的细节可以参考市面上关于源码的书籍。我们看下Window的删除过程

WindowManagerGlobal的removeView过程

Window的删除过程和添加过程一样,都是先通过WIndowManagerImpl后,在进一步通过WindowManagerGlobal来实现删除,下面是WindowManagerGlobalremoveView的实现:

    public void removeView(View view, boolean immediate) {
        ...
        synchronized (mLock) {
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            removeViewLocked(index, immediate);
        }
      ...
    }

removeView 的过程很清晰,首先通过findViewLoched来查找待删除View的索引,这个查找过程就是建立的数组遍历,然后再调用removeViewLocked来做进一步的删除,如下所示:

    private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();
        
        if (view != null) {
            InputMethodManager imm = InputMethodManager.getInstance();
            if (imm != null) {
                imm.windowDismissed(mViews.get(index).getWindowToken());
            }
        }
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }

removeViewLocked是通过ViewRootImpl来完成删除操作的。在WIndowManager中提供了两个接口removeViewremoveViewImmediate,分别表示异步删除和同步删除,一般不使用同步删除,以免发生意外的错误,这里主要说下异步删除的情况,具体的异步删除操作是由ViewRootImpldie方法完成,在异步删除的情况下,die方法只是发送了一个请求删除的消息后就立刻返回了,这个时候View并没有完成删除操作,所以最后会将其添加到WindowManagerGlobal的待删除列表mDyingViews中,看下ViewRootImpldie方法实现:

    boolean die(boolean immediate) {
        //同步删除  直接调用doDie,并返回
        if (immediate && !mIsInTraversal) {
            doDie();
            return false;
        }
        ...
        //发送handler消息
        mHandler.sendEmptyMessage(MSG_DIE);
        return true;
    }

在die方法内部只是做了简单的判断,如果是异步删除,那么就发送一个MSG_DIE 的消息,ViewRootImpl中的Handler会调用doDie方法,如果是同步删除,就不发送消息,直接调用doDie方法,这就是这两种方法的区别。在doDie方法中会调用dispatchDepatchedFromWindow方法,真正删除View的逻辑在dispatchDepatchedFromWindow内部实现,代码如下:

    void doDie() {
        dispatchDetachedFromWindow();
        //将WindowManagerGlobal的列表中移除保存的ViewRootImpl、View、Param
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }
    
   void dispatchDetachedFromWindow() {
        //调用VIew的dispatchDetachedFromWindow();
        mView.dispatchDetachedFromWindow();
        ...
        //Session中的remove
        mWindowSession.remove(mWindow);
        ...
    }
        
    public void remove(IWindow window) {
        //WMS移除Window
        mService.removeWindow(this, window);
    }
    //WindowManagerGlobal中移除保存的ViewRootImpl、View、Param
    void doRemoveView(ViewRootImpl root) {
        synchronized (mLock) {
            final int index = mRoots.indexOf(root);
            if (index >= 0) {
                mRoots.remove(index);
                mParams.remove(index);
                final View view = mViews.remove(index);
                mDyingViews.remove(view);
            }
        }
    }

以上代码主要做了一下事情:

  • 垃圾回收相关的工作,比如清除数据和消息,移除回调
  • 通过Sessionremove方法删除Window,同样也是一个IPC过程,最终会调用WMSremoveView方法
  • 调用ViewdispatchDetachedFromWindow();方法,对于ViewdispatchDetachedFromWindow()我们不陌生,当ViewWindow中移除时,这个方法就会被调用,可以在这个方法内部做一些资源回收的工作,比如终止动画、停止线程。
  • 调用WindowManagerGlobaldoRemoveView方法刷新数据,包括mViews、mRoots、mParams、mDyingViews,需要将当前Window所关联的这三类对象从列表中删除。

WindowManagerGlobal的updateViewLayout过程

Window的删除过程我们已经分析完了,下面看下WIndow的更新过程,还是要从WindowManagerGlobalupdateViewLayout说起,代码如下:

    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        ...
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

        view.setLayoutParams(wparams);

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            mParams.remove(index);
            mParams.add(index, wparams);
            root.setLayoutParams(wparams, false);
        }
    }

updateViewLayout方法做的事情就比较简单了,首先他需要更新View的LayoutParams并替换老的LayoutParams,接着再更新ViewRootImpl中的LayoutParams,这一步是通过ViewRootImplsetLayoutParams方法来实现的。在ViewRootImplsetLayoutParams中会通过scheduleTraversales方法来对View重新测量布局以及绘制这三个过程,在performTraversales会通过WindowSession来更新Window视图,这个过程最终是由WMSrelayoutWindow来具体实现的,同样也是一个IPC过程。

参考

  • 《Android开发艺术探索》
  • 《Android源码设计模式》
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容