AndroidUI系列—源码分析View的绘制

欢迎关注作者的github<a> https://github.com/BudSpore</a>
本系列文章都有点长,首先AndroidUI显示的大致流程,并结合项目对视图,流畅度进行性能优化,最后整合一个大项目,认真读完,您一定有所收获。

Paste_Image.png

Android视图显示整体流程是先绘制(CPU的工作)再渲染(GPU的工作),本篇文章先讲CPU做的部分事情,即View绘制,该文章源码都在Java层。
文章目录
<li>案例实战</li>
<li>绘制的起点——测量</li>
<li>绘制第二步——布局</li>
<li>绘制第三步——绘制</li>

下面是Android屏幕组成架构
<h3>Android屏幕架构</h3>


Paste_Image.png
下面对图中以及Android视图里的概念进行详细的介绍
DecorView

DecorView是一个应用窗口的根容器,它是整个页面的根View,也是一个FrameLayout布局,DecorView有2个子View,第一个子View就是屏幕这个部分,看图:

Paste_Image.png

第二个子View是一个垂直LinearLayout,包含两个子元素,一个是TitleView(ActionBar的容器),它就是我们看到的标题栏,另一个是ContentView(窗口内容的容器)。
ContentView是一个FrameLayout(android.R.id.content),
我们在Activity的生命周期的起点——onCreate回调方法里(如下图)使用setContentView就是成了它的子View。每个Activity都与一个PhoneWindow相关联,用户界面则由PhoneWindow所承载。注意,setContentView方法最后会调用到绘制的起点函数,后面将详细讲解哦~

Paste_Image.png
Canvas(给View用的画布)

Canvas位于Java层,ViewGroup会把自己的Canvas拆分给子View。View会在onDraw方法里将图形数据绘制在它获得的Canvas上,在View类里通过建立这个Canvas类来构建绘画的基础,把Canvas想象成一个画布,笔(Paint)把View画在画布上,然后再把所有数据传递给Surface(下篇文章会讲到),Canvas是View树和Surface的连接点,android中的UI元素通过调用Canvas类来构建的。
Canvas类处理“draw“的调用,当绘制(draw)内容时需要4个基本组件,保持像素的Bitmap,处理绘制调用 的canvas(写入Bitmap,bitmap用于存储surface内容),绘制 的内容(如,Rect,Path,text,Bitmap)和一个笔paint(描述颜色和样式)。

Window

应用通过 Window Manager创建一个window,Window Manager 为每一个window创建一个surface,并把该surface传递给应用以便应用在上面绘制,一个应用有很多Window。
Window是屏幕上用于绘制各种UI元素(比如Button,TextView)及响应用户输入事件(键盘事件)的一个矩形区域,它独立绘制,不与其他界面产生影。在Android系统中,每个Window是独占一个Surface实例的显示区域,每个窗口的Surface由WindowManagerService分配。
每个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系,对于Activity来说,ViewRootImpl是连接WindowManager和DecorView的纽带,绘制的入口是由ViewRootImpl的performTraversals方法来发起Measure,Layout,Draw等流程的,当然performTraversals函数调用之前也有很多方法要做,这里不仔细分析。

ViewRoot

从上一段得知,WindowManagerGlobal里面有一个mRoots,也就是ViewRoot,它主要用来管理窗口的根 view,并跟 WindowManagerService 交互。

WindowManager

管理窗口的一些状态属性(view 的增加,删除,窗口位置,更新等等),它管理window和WindowManagerService交互,由ViewRoot完成。主要的方法有:addview(),updateViewLayout();removeView()。
WindowManager是一个接口,WindowManagerImpl 是它的实现类。
WindowManager 里面维护了三个变量:mViews(根view),mRoots(viewroot),mParams(一些相关变量),由 WindowManagerGlobal 维护它们。

WindowManagerService

对系统中的所有窗口进行管理,它和WindowManager区别是WindowManager管理的是单独的窗口,而WindowManagerService需要管理所有的窗口。
Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时创建 ViewRootImpl 对象并将ViewRootImpl和 DecorView 建立关联。

PhoneWindow

它是Activity和整个View系统交互的接口,是Window的具体实现。


Paste_Image.png
MeasureSpac(规格测量)

下面的测量过程用到了MeasureSpac,MeasureSpec是一个int类型的值,由高2位的规格模式和低30位的具体尺寸,由父View的MeasureSpec和子View的LayoutParams(LayoutParams就是我们在xml写的时候设置的layout_width和layout_height 转化而来的)通过计算得出一个针对子View的测量规格。

好了,枯燥的概念讲完了,下面结合一个实例来详细讲解Android视图绘制的过程。
<h2 id="example">案例实战</h2>
看下面的activity_main的XML代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="50dp"
    android:background="@android:color/holo_blue_dark"
    android:paddingBottom="70dp"
    android:orientation="vertical">
    <TextView
        android:id="@+id/textview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/material_green"
        android:text="TextView"
        android:textColor="@android:color/white"
        android:textSize="20dp" />
    <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        />
</LinearLayout>

效果如图所示

Paste_Image.png

分析上面的视图:
view1.png

View树的整体结构:
Paste_Image.png

为了方便,我把header设置为Gone,去掉ActionBar的测量过程
View的绘制分为measure、layout、draw 过程,绘制的View节点从根节点——DecorView开始。绘制的起点就是上面讲的performTraversals函数:
函数做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小(measure)、是否重新放置视图的位置(layout)、以及是否重绘 (draw),其核心也就是通过判断来选择顺序执行这三个方法中的哪个.

Paste_Image.png

<h1>绘制的起点</h1>

<a name="measure"> <h4>绘制的起点——测量</h4> </a>

private void performTraversals() {
        ......
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        //mWidth和mHeight代表屏幕的宽高,lp是WindowManager.LayoutParams,它的lp.width和lp.height的默认值是MATCH_PARENT,
        ......
        // Ask host how big it wants to be
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
      
        ......
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ......
        performDraw();       
        ......
        =—=  这个函数太长了
    }
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

            case ViewGroup.LayoutParams.MATCH_PARENT:
                // Window can't resize. Force root view to be windowSize.
                measureSpec = View.MeasureSpec.makeMeasureSpec(windowSize, View.MeasureSpec.EXACTLY);
                break;
            case ViewGroup.LayoutParams.WRAP_CONTENT:
                // Window can resize. Set max size for root view.
                measureSpec = View.MeasureSpec.makeMeasureSpec(windowSize, View.MeasureSpec.AT_MOST);
                break;
            default:
                // Window wants to be an exact size. Force root view to be that size.
                measureSpec = View.MeasureSpec.makeMeasureSpec(rootDimension, View.MeasureSpec.EXACTLY);
                break;
        }
        return measureSpec;
    }

performTraversals传入参数后,getRootMeasureSpec函数里的switch case(张老师反复强调switch---case是一条语句)走的是MATCH_PARENT,使用MeasureSpec.makeMeasureSpec方法组装一个MeasureSpec,MeasureSpec的specMode等于EXACTLY,specSize等于windowSize,屏幕的宽高,也就是为何根视图总是全屏的原因。

Paste_Image.png
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        //Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
       // try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      //  } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
       // }
    }
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
       ...
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);           
       ...       
    }

因为DecorView 是一个FrameLayout 那么接下来会进入FrameLayout 的measure方法

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ....
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                //基本思想就是父View把自己的MeasureSpec 
                // 传给子View结合子View自己的LayoutParams 算出子View 的MeasureSpec,然后继续往下传,
                // 传递叶子节点,叶子节点没有子View,根据传下来的这个MeasureSpec测量自己就好了。
//注意DecorView的子节点(View)有2个。
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth() +  lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                ....
                ....
            }
        }
        .....
        .....
//所有的孩子测量之后,经过一系类的计算之后通过setMeasuredDimension设置自己的宽高,
//对于FrameLayout 可能用最大的字View的大小,对于LinearLayout,可能是高度的累加,
//具体测量的原理去看看源码。总的来说,父View是等所有的子View测量结束之后,再来测量自己。
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
        ....
    }

DecorView的第一个子节点是ViewRoot,它继承自LinearLayout,LinearLayout继承自ViewGroup,(跟你在XML里写的LinearLayout布局没关系)所以后面的measure函数调用的是LinearLayout类里的
再次附上View树


Paste_Image.png
Paste_Image.png
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
  
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //根据父View的测量规格和父View自己的Padding,
        // 还有子View的Margin和已经用掉的空间大小(widthUsed),就能算出子View的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);

        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin  + heightUsed, lp.height);
                //通过父View的MeasureSpec和子View的自己LayoutParams的计算,算出子View的MeasureSpec,然后父容器传递给子容器的
                // 然后让子View用这个MeasureSpec(一个测量要求,比如不能超过多大)去测量自己,如果子View是ViewGroup 那还会递归往下测量。
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

    }

大家注意下这个:

Paste_Image.png

Paste_Image.png

父节点得到自己的MeasureSpac后,调用 child.measure 方法(因为它是 FrameLayout 只有一个直接子类),传入自己的MeasureSpac,开始测量子节点的MeasureSpac。
如果child 是 viewgroup ,测量流程基本上如下:得到父 view 的MeasureSpac,算出自己的MeasureSpac,调用 measureChildren,多次调用 measureChild,进而调用 child.measure
如果child 是 view,测量流程基本上如下:得到父 view 的 MeasureSpec,算出自己的MeasureSpac,设置自己的宽高(setMeasureDimension),并结束测量。
这样就讲清楚了是如何得到MeasureSpac的了。

而怎么得到子View的MeasureSpac,请看下面的图(省略了UPSPECIFIED模式: 父容器对于子容器没有任何限制,子容器想要多大就多大):

Paste_Image.png
// spec参数   表示父View的MeasureSpec 
// padding参数    父View的Padding+子View的Margin,父View的大小减去这些边距,才能精确算出
//               子View的MeasureSpec的size
// childDimension参数  表示该子View内部LayoutParams属性的值(lp.width或者lp.height)
//                    可以是wrap_content、match_parent、一个精确指(an exactly size),  
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);  //获得父View的mode  
        int specSize = MeasureSpec.getSize(spec);  //获得父View的大小  

        //父View的大小-自己的Padding-子View的margin,得到值才是子View的大小。
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;    //初始化值,最后通过这个两个值生成子View的MeasureSpec
        int resultMode = 0;    //初始化值,最后通过这个两个值生成子View的MeasureSpec

        switch (specMode) {
            // Parent has imposed an exact size on us  
            //1、父View是EXACTLY的 !  
            case MeasureSpec.EXACTLY:
                //1.1、子View的width或height是个精确值 (an exactly size)  
/*
子view是确定值,那么控件最后展示就是那个具体的值,与父View无关
*/
                if (childDimension >= 0) {
                    resultSize = childDimension;         //size为精确值  
                    resultMode = MeasureSpec.EXACTLY;    //mode为 EXACTLY 。  
                }
                //1.2、子View的width或height为 MATCH_PARENT/FILL_PARENT   
                //如果一个View的MeasureSpec 是EXACTLY,那么它的size 是多大,最后展示到屏幕就一定是那么大
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.  
                    resultSize = size;                   //size为父视图大小  
                    resultMode = MeasureSpec.EXACTLY;    //mode为 EXACTLY 。  
                }
                //1.3、子View的width或height为 WRAP_CONTENT  
                /* 
                 我们还不知道具体子View的大小是多少,
                要等到child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 调用的时候
                才去真正测量子View 自己content的大小
                */
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be  
                    // bigger than us.  
                    resultSize = size;                   //size为父视图大小  
                    resultMode = MeasureSpec.AT_MOST;    //mode为AT_MOST 。  
                }
                break;

            // Parent has imposed a maximum size on us  
            //2、父View是AT_MOST的 !      
            case MeasureSpec.AT_MOST:
                //2.1、子View的width或height是个精确值 (an exactly size)  
                if (childDimension >= 0) {
                    // Child wants a specific size... so be it  
                    resultSize = childDimension;        //size为精确值  
                    resultMode = MeasureSpec.EXACTLY;   //mode为 EXACTLY 。  
                }
                //2.2、子View的width或height为 MATCH_PARENT/FILL_PARENT  
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size, but our size is not fixed.  
                    // Constrain child to not be bigger than us.  
                    resultSize = size;                  //size为父视图大小  
                    resultMode = MeasureSpec.AT_MOST;   //mode为AT_MOST  
                }
                //2.3、子View的width或height为 WRAP_CONTENT  
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be  
                    // bigger than us.  
                    resultSize = size;                  //size为父视图大小  
                    resultMode = MeasureSpec.AT_MOST;   //mode为AT_MOST  
                }
                break;

            // Parent asked to see how big we want to be  
            //3、父View是UNSPECIFIED的 !  
            case MeasureSpec.UNSPECIFIED:
                //3.1、子View的width或height是个精确值 (an exactly size)  
                if (childDimension >= 0) {
                    // Child wants a specific size... let him have it  
                    resultSize = childDimension;        //size为精确值  
                    resultMode = MeasureSpec.EXACTLY;   //mode为 EXACTLY  
                }
                //3.2、子View的width或height为 MATCH_PARENT/FILL_PARENT  
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should  
                    // be  
                    resultSize = 0;                        //size为0! ,其值未定  
                    resultMode = MeasureSpec.UNSPECIFIED;  //mode为 UNSPECIFIED  
                }
                //3.3、子View的width或height为 WRAP_CONTENT  
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size.... find out how  
                    // big it should be  
                    resultSize = 0;                        //size为0! ,其值未定  
                    resultMode = MeasureSpec.UNSPECIFIED;  //mode为 UNSPECIFIED  
                }
                break;
        }
        //根据上面逻辑条件获取的mode和size构建MeasureSpec对象。  
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

child.measure(childWidthMeasureSpec, childHeightMeasureSpec)会调用到LinearLayout的onMeasure函数,我们来看源码

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);//垂直测量
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);//水平测量
        }
    }
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
         ...
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);

            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

            ....
                 //这个函数会调用measureChildWithMargins,然后调用child.measure,因为我去掉了ActionBar,
               //所以ViewRoot的子View是content(FrameLayout布局),
                //child.measure进入FrameLayout的onMeasure函数
                measureChildBeforeLayout(  
                       child, i, widthMeasureSpec, 0, heightMeasureSpec,
                       totalWeight == 0 ? mTotalLength : 0);
                   ...
//所有的孩子测量之后,经过一系类的计算之后通过setMeasuredDimension设置自己的宽高,
//对于LinearLayout自己的测量,可能是高度的累加
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
    }

ViewRoot 是系统的View,它的LayoutParams默认都是match_parent,根据我们文章最开始MeasureSpec 的计算规则,ViewRoot 的MeasureSpec mode应该等于EXACTLY(DecorView MeasureSpec 的mode是EXACTLY,ViewRoot的layoutparams 是match_parent),size 也等于DecorView的size
对于content的lp.width lp.height都是系统设定的match_parent

Paste_Image.png
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
.....
for (int i = 0; i < count; i++) {
        ...关键代码
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
           ...   
        }
}
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
..... 
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, 
lp.height); 
....
}
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
...
            int specSize = MeasureSpec.getSize(spec); //获得父View的大小  
            int size = Math.max(0, specSize - padding); 
...
            resultSize = size;//父View的大小
            resultMode = MeasureSpec.EXACTLY;
...
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

由于ViewRoot 的mPaddingBottom=100px(这个可能和状态栏的高度有关,我们测量的最后会发现id/statusBarBackground的View的高度刚好等于100px,ViewRoot 是系统的View的它的Padding 我们没法改变,所以通过 Math.max(0, specSize - padding)计算出来Content(android.R.id.content) 的MeasureSpec 的高度用屏幕高度减去100px (假设结果是2460),它的宽高的mode 根据算出来也是EXACTLY(ViewRoot 是EXACTLY和android.R.id.content 是match_parent)。所以Content(android.R.id.content)的MeasureSpec高度少了100px。


Paste_Image.png

接下来测量content的子节点,即XML里的LinearLayout
同理,父View(content)的MeasureSpec+子View(linear)的LayoutParams就得出了子View的宽高,这里注意的是XML里设置了margintop=200px,所以通过 Math.max(0, specSize - padding)让linear的高度用减少200px

Paste_Image.png

1、id/linear的heightMeasureSpec 的mode=AT_MOST,因为id/linear 的LayoutParams 的layout_height="wrap_content"
2、id/linear的heightMeasureSpec 的size 少了200px, 由上面的代码
padding=mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed;
int size = Math.max(0, specSize - padding);
由于id/linear 的 android:layout_marginTop="50dp" 使得lp.topMargin=200px (本设备的density=4,px=4*pd),在计算后id/linear的heightMeasureSpec 的size 少了200px。(布局代码前面已给出,可自行查看id/linear 控件xml中设置的属性)
同理,对于TextView来说,linear的paddingButtom=70dp,70×4=280px,对于TextView来说,paddingTop=280,看图:


Paste_Image.png

然后遍历到WebView,高度是一个确定值,所以最后的高度就是其对应值

Paste_Image.png

附整体的MeasureSpec传递过程

Paste_Image.png

当遍历到叶子节点的时候,就测量叶子节点自身,TextView继承自View,就调用View类的onMeasure,WebView继承自AbsoluteLayout,就调用AbsoluteLayout的onMeasure
id/linear 的子View的高度都计算完毕了,接下来id/linear就通过所有子View的测量结果计算自己的高宽,id/linear是LinearLayout,它的高度是子View的高度总和+自身padding.
最终算出id/linear出来后,id/content 就要根据它唯一的子View id/linear 的测量结果和自己的之前算出的MeasureSpec一起来测量自己的结果,
在FrameLayout的for循环退出后,测量自己

Paste_Image.png

而对于ViewRoot来讲,它是LinearLayout,for循环也结束了
接下来测量ViewRoot,刚才讲到,它的测量就是子view的累加和和自己的padding,过程在onMeasure函数里
然后再测量id/statusBarBackground,虽然不知道id/statusBarBackground 是什么,但是调试的过程中,测出的它的高度=100px, 和 id/content 的paddingTop 刚好相等。在最后DecorView退出for循环,测量自己 的高宽最终整个测量过程结束。所有的View的大小测量完毕。所有的getMeasureWidth 和 getMeasureWidth 都已经有值了。

DecorView添加到窗口的过程:(感兴趣可以了解一下,这不是重点)


Paste_Image.png

<a name="layout"><h4>绘制第二步——布局</h4></a>

布局的基本思想也是由根View开始,递归地完成整个视图树的布局工作。
接下来进入的是:

performLayout(lp, desiredWindowWidth, desiredWindowHeight);

别忘了View树


Paste_Image.png
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
       ...
        final View host = mView;
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
       ...
    }
public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }

我们把对decorView的layout()方法的调用作为布局整个控件树的起点,实际上调用的是View类的layout()方法,源码如下:

/**
* l为本View左边缘与父View左边缘的距离 
  t为本View上边缘与父View上边缘的距离 
  r为本View右边缘与父View左边缘的距离 
  b为本View下边缘与父View上边缘的距离
*/
public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
        ...
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                  onLayout(changed, l, t, r, b);
               ...
           }
    }

Paste_Image.png

setFrame(l, t, r, b) 为mLeft 、mTop、mRight、mBottom赋值,然后基本就能确定View自己在父视图的位置了,这几个值构成的矩形区域就是该View显示的位置,当初始化完毕后,ViewGroup的布局流程也就完成了,这里的具体位置都是相对与父视图的位置,然后在setFrame()方法中会判断View的位置是否发生了改变,若发生了改变,则需要对子View调用onLayout()方法进行重新布局,由于普通View( 非ViewGroup)不含子View,View类的onLayout()方法为空。ViewGroup的onLayout函数是抽象方法,每个子类实现它的方式都不相同。

View类的onLayout函数

Paste_Image.png

这是一个空实现,主要作用是在我们的自定义View中重写该方法,实现自定义的布局逻辑。
我们以decorView,也就是FrameLayout的onLayout()方法为例,它继承自ViewGroup,要重写onLayout函数,那我们就根据它来分析ViewGroup的布局过程:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        //把父容器的位置参数传递进去
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }
Paste_Image.png
/**根据上面函数
*  传入的四个值分别是0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()
*DecorView的左上位置为0,然后宽高为它的测量宽高
*/
void layoutChildren(int left, int top, int right, int bottom,
                                  boolean forceLeftGravity) {
        final int count = getChildCount();
        /*
         parentLeft 表示当前View为其子View显示区域指定的一个左边界,
         即子View显示区域的左边缘到父View的左边缘的距离,它由父容器的padding和Foreground决定
         */
        final int parentLeft = getPaddingLeftWithForeground();
         /*
         parentRight 表示当前View为其子View显示区域指定的一个右边界,
         即子View显示区域的右边缘到父View的右边缘的距离
         */
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();
         //以上四个值确立子View的显示区域
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;//子View的左边缘距父View左边缘的距离
                int childTop;//子View的上边缘距父View上边缘的距离

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
               //当子View设置了水平方向的layout_gravity属性时,根据不同的属性设置不同的childLeft //childLeft表示子View的 左上角坐标X值
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                 /* 水平居中,由于子View要在水平中间的位置显示,因此,要先计算出以下:
                 * (parentRight - parentLeft -width)/2 此时得出的是父容器减去子View宽度后的
                 * 剩余空间的一半,那么再加上parentLeft后,就是子View初始左上角横坐标(此时正好位于中间位置),
                 * 假如子View还受到margin约束,由于leftMargin使子View右偏而rightMargin使子View左偏,所以最后
                 * 是 +leftMargin -rightMargin .
                 */

                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                      //水平居右,子View左上角横坐标等于 parentRight 减去子View的测量宽度 减去 margin
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                     //如果没设置水平方向的layout_gravity,那么它默认是水平居左 
                     //水平居左,子View的左上角横坐标等于 parentLeft 加上子View的magin值
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }
                       //当子View设置了竖直方向的layout_gravity时,根据不同的属性设置同的childTop 
                       //childTop表示子View的 左上角坐标的Y值 
                       //分析方法同上   
                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
                ////对子元素进行布局,左上角坐标为(childLeft,childTop),右下角坐标为(childLeft+width,childTop+height)
                //若子View是容器View,则会递归地对其子View进行布局。  
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

上面的源码涉及到android:layout_gravity属性,记住,你的orientation设置为horizontal,横向排列则只能竖向居中,当你设置为vertical竖向排列则只能横向居中。(LinearLayout如果设置 android:orientation="vertical",那么android:layout_gravity的设置只在水平方向生效,android:orientation="horizontal",那么android:layout_gravity属性只在垂直方向生效)
比如我举一个例子

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_blue_dark"
    android:orientation="vertical">
    <TextView
        android:id="@+id/text"
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:background="@color/material_green"
        android:text="TextView"
        android:gravity="bottom"
        android:layout_gravity="center_horizontal"
        android:textColor="@android:color/black"
        android:textSize="20sp"
        />

</LinearLayout>

显示效果如图

Paste_Image.png

回到我们的主话题中来,以上就是首先先获取父容器的padding值,然后遍历其每一个子View,根据子View的layout_gravity属性、子View的测量宽高、父容器的padding值、来确定子View的布局参数,然后调用child.layout方法,把布局流程从父容器传递到子元素。如果子View是一个ViewGroup,那么就会重复以上步骤,如果是一个View,那么会直接调用View的layout方法,根据以上分析,在该方法内部会设置view的四个布局参数,接着调用onLayout的空方法。

在for循环过程中,会布局到LinearLayout,child.layout函数会调用到LinearLayout类的onLayout函数,我给出源码

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
Paste_Image.png
void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;
        
        // Where right end of child should go
//获得子视图可以用的宽度,顺便也把子视图左边沿的位置计算出来。
        final int width = right - left;
        int childRight = width - mPaddingRight;
        
        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;
        
        final int count = getVirtualChildCount();
//根据父视图中的gravity属性,决定子视图的起始位置。
        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }
        //遍历所有子视图,为它们分配位置
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
                
                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                 //还会调用child.layout()为子视图设置布局位置.
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }
private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

布局到TextView,同样会调用到TextView类的onLayout方法

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (mDeferScroll >= 0) {
            int curs = mDeferScroll;
            mDeferScroll = -1;
            bringPointIntoView(Math.min(curs, mText.length()));
        }
    }

同理WebView也布局完毕,上面的layoutChildren函数里的for循环结束,所有View就布局完毕。
<a name="draw"><h4>绘制第三步——绘制</h4></a>
到了View绘制的最后一步


Paste_Image.png
performDraw();
private void performDraw() {
    ...
           final boolean fullRedrawNeeded = mFullRedrawNeeded;
//fullRedrawNeeded参数判断是否需要重新绘制全部视图,
 //如果是第一次绘制视图,那么绘制所有的视图,
            mFullRedrawNeeded = false;
             //绘画逻辑都在这个函数里实现
            draw(fullRedrawNeeded);
....
   if (mSurfaceHolder != null && mSurface.isValid()) {
                mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder);
                SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
                if (callbacks != null) {
                    for (SurfaceHolder.Callback c : callbacks) {
                        if (c instanceof SurfaceHolder.Callback2) {
                            ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded(
                                    mSurfaceHolder);
                        }
                    }
                }
            }
....
    }
/*

*/
private void draw(boolean fullRedrawNeeded) {
        Surface surface = mSurface;
          ...
         final Rect dirty = mDirty;//表示重绘区域
        if (mSurfaceHolder != null) {
            // The app owns the surface, we won't draw.
            dirty.setEmpty();
            if (animating) {
                if (mScroller != null) {
                    mScroller.abortAnimation();
                }
                disposeResizeBuffer();
            }
            return;
        }

      ...
//如果fullRedrawNeeded为真,则把dirty区域置为整个屏幕,表示整个视图都需要绘制 
//第一次绘制流程,需要绘制所有视图
        if (fullRedrawNeeded) {
            mAttachInfo.mIgnoreDirtyState = true;
            dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
        }
...
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) 
                   {
                    return;
                   }
      ...
    }
 private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

        // Draw with software renderer.
        final Canvas canvas;
     ...
            //还记得文章开始讲的Surface的工作流程吗
           /*- create a bitmap
          - attach a canvas to it
          - do the rendering into that canvas
          - lockCanvas
          - draw your bitmap into the backbuffer
          - unlockAndPost
          */
            canvas = mSurface.lockCanvas(dirty);
...
            if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }

      ...
                canvas.translate(-xoff, -yoff);
               ...
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
                attachInfo.mSetIgnoreDirtyState = false;
                //decorView就是起点,也就是View.draw()方法
                mView.draw(canvas);
...
                surface.unlockCanvasAndPost(canvas);
          ...
    }

终于到了绘画的逻辑实现函数了

public void draw(Canvas canvas) {
/*我们首先来看一开始的标记位dirtyOpaque,该标记位的作用是判断当前View是否是透明的,
如果View是透明的,那么根据下面的逻辑可以看出,将不会执行一些步骤,
比如绘制背景、绘制内容等。这样很容易理解,
因为一个View既然是透明的,那就没必要绘制它了
*/
       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
      ...
            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;
        }

        /*
         * Here we do the full fledged routine...
         * (this is an uncommon case where speed matters less,
         * this is why we repeat some of the tests that have been
         * done above)
         */
       ...
    }

我对上面的步骤翻译一下
1.对View的背景进行绘制
2.保存当前的图层信息(可跳过)
3.绘制View自身的内容
4.对View的子View进行绘制(如果有子View)
5.绘制View的褪色的边缘,类似于阴影效果(可跳过)
6.绘制View的滚动条
<h6>第一步是绘画背景</h6>

private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;//mBackground是该View的背景参数,比如背景颜色
        if (background == null) {
            return;
        }
        //mRight - mLeft, mBottom - mTop layout确定的四个点来设置背景的绘制区域 
        setBackgroundBounds();

        // Attempt to use a display list if requested.
        if (canvas.isHardwareAccelerated() && mAttachInfo != null
                && mAttachInfo.mHardwareRenderer != null) {
            mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

            final RenderNode renderNode = mBackgroundRenderNode;
            if (renderNode != null && renderNode.isValid()) {
                setBackgroundRenderNodeProperties(renderNode);
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
                return;
            }
        }
//获取当前View的mScrollX和mScrollY值
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
//如果scrollX和scrollY有值,则对canvas的坐标进行偏移,再绘制背景
            canvas.translate(scrollX, scrollY);
//调用Drawable的draw() 把背景图片画到画布上
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

跳过第2步
<h6>第3步,绘画自己View内容</h6>
onDraw(canvas) 方法是view用来draw 自己的,具体如何绘制,颜色线条什么样式就需要子View自己去实现,View类的onDraw(canvas) 是空实现,ViewGroup 也没有实现,每个View的内容是各不相同的,所以需要由子类去实现具体逻辑。
对于decorView来说,由于它是容器View,所以它本身并没有什么要绘制的,关键在下面.
<h6>第4步,绘画当前View的子View</h6>
对于View类来讲,它就是叶子节点,没有子节点,所以它的dispatchDraw(canvas)空
所以调用的是ViewGroup类的dispatchDraw(canvas)方法

protected void dispatchDraw(Canvas canvas) {
    ...
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                     //关键代码,依次调用drawChild()方法来绘制子View
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }
            int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)
                    ? children[childIndex] : preorderedList.get(childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
    ...
    }
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

drawChild()的核心过程就是为子视图分配合适的cavas剪切区,剪切区的大小正是由layout过程决定的,而剪切区的位置取决于滚动值以及子视图当前的动画。设置完剪切区后就会调用子视图的draw()函数进行具体的绘制了。
首先对canvas进行了一系列变换,以变换到将要被绘制的View的坐标系下。完成对canvas的变换后,便会调用View.draw(Canvas)方法进行实际的绘制工作,此时传入的canvas为经过变换的,在将被绘制View的坐标系下的canvas。

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
/*首先判断是否已经有缓存,即之前是否已经绘制过一次了,
如果没有,则会调用draw(canvas)方法,开始正常的绘制,即上面所说的六个步骤,
否则利用缓存来显示。
*/
if (!drawingWithDrawingCache) {
            if (drawingWithRenderNode) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            } else {
                // Fast path for layouts with no backgrounds
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    dispatchDraw(canvas);
                } else {
                    draw(canvas);
                }
            }
        } else if (cache != null) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            if (layerType == LAYER_TYPE_NONE) {
                // no layer paint, use temporary paint to draw bitmap
                Paint cachePaint = parent.mCachePaint;
                if (cachePaint == null) {
                    cachePaint = new Paint();
                    cachePaint.setDither(false);
                    parent.mCachePaint = cachePaint;
                }
                cachePaint.setAlpha((int) (alpha * 255));
                canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
            } else {
                // use layer paint to draw the bitmap, merging the two alphas, but also restore
                int layerPaintAlpha = mLayerPaint.getAlpha();
                mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
                canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
                mLayerPaint.setAlpha(layerPaintAlpha);
            }
        }
}

这一步就是ViewGroup绘制过程,它对子View进行了绘制,而子View又会调用自身的draw方法来绘制自身,这样不断遍历子View及子View的不断对自身的绘制,从而使得View树完成绘制。
跳过第5步
<h6>第六步,绘画View的滚动条</h6>

public void onDrawForeground(Canvas canvas) {
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);

        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        if (foreground != null) {
            if (mForegroundInfo.mBoundsChanged) {
                mForegroundInfo.mBoundsChanged = false;
                final Rect selfBounds = mForegroundInfo.mSelfBounds;
                final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

                if (mForegroundInfo.mInsidePadding) {
                    selfBounds.set(0, 0, getWidth(), getHeight());
                } else {
                    selfBounds.set(getPaddingLeft(), getPaddingTop(),
                            getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
                }

                final int ld = getLayoutDirection();
                Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                        foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
                foreground.setBounds(overlayBounds);
            }

            foreground.draw(canvas);
        }
    }

基本思想就是先设定绘制区域,然后利用canvas进行绘制,至此,View的绘制过程就完成了,
感谢UIT学长对文章的审阅,我对文章的细节再次进行了删减优化,特别感谢老姐对文章的纠错,作者将不断完善该篇文章,希望读者能提出宝贵意见。
下一篇文章我将会讲解AndroidUI渲染。

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

推荐阅读更多精彩内容