Android UI布局优化

也算是老生常谈的问题,最近正好有这方面的需求,查阅了很多官方文档和优秀的博客,加上自己的理解编写了这篇文章。

Android 渲染机制

大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。从设计师的角度,他们希望App能够有更多的动画,图片等时尚元素来实现流畅的用 户体验。但是Android系统很有可能无法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染, 如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。

04080416_dgEb.png

如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一帧画面。

04080416_cWwX.png

用户容易在UI执行动画或者滑动ListView的时候感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原 因可以导致丢帧,也许是因为你的layout太过复杂,无法在16ms内完成渲染,有可能是因为你的UI上有层叠太多的绘制单元,还有可能是因为动画执行 的次数过多。这些都会导致CPU或者GPU负载过重。

性能优化

影响Android渲染性能的无非以下几个方面:

  1. 过度绘制OverDraw

  2. 页面布局层级太深

  3. 频繁GC导致的页面卡顿

本篇我们主要探讨前两种原因导致的UI性能问题,可以从一下几个方面进行优化:

  • 避免过度绘制OverDraw
    • 移除不必要的背景
      • 透明背景不会被绘制
      • 移除不必要的背景可以有效的优化过度绘制
        • phonewindow背景
        • activity嵌套布局背景
        • ImageView设置的无效背景src和background
    • 自定义控件
      • 使用clipRect减少叠加处的重复绘制
    • google官方还提到了一点降低透明度,可以理解为尽量减少半透明对象的使用
      • 绘制半透明像素会带来额外的成本
  • 减少布局层级,布局尽量扁平化
    • 使用merge减少布局层级
    • 尽量使用ConstraintLayout
    • 嵌套过深使用RelativeLayout而不要使用LinearLayout
  • ViewSub和incluce
    • ViewSub只在需要时加载控件
    • incluce布局复用

过度绘制OverDraw

过度绘制是单帧内同一个像素被重复绘制了多次。为了追求复杂的UI效果,界面上同一位置通常叠加了多个控件,顶部控件遮盖了底部控件时,系统仍然需要绘制被遮盖的部分,从而导致了多度绘制问题。

移除不必要的背景

移除不必要控件的背景,可以有效的优化过度绘制问题。例如某个Activity有一个背景,其上的Layout也有一个背景,Layout中的N个子View也有背景,通过移除Activity或者Layout的背景,可以减少大量红色OverDraw区域。能够显著的提升程序性能。

常见的不必要的背景的场景有:

  • 父View和子View同时设置了background

    例如以下布局:

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#FFFFFF">
    
        <RelativeLayout
            android:id="@+id/content_rel"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#FFFFFF">
            <!-- 子View -->
        </RelativeLayout>
    
    </FrameLayout>
    

    很明显content_rel的backgroud是重复的,去掉background后可以有效降低其子View过度绘制的次数。

  • ImageView设置了background

    例如:

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#FFFFFF">
    
        <RelativeLayout
            android:id="@+id/content_rel"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#FFFFFF">
            
            <ImageView
                android:id="@+id/img_iv"
                android:layout_width="200dp"
                android:layout_height="200dp"
                android:background="#ededed"/>
        </RelativeLayout>
    
    </FrameLayout>
    

    定义了一个ImageView并设置了背景色#ededed,乍看可能没问题,但当我们带代码中去加载显示一个图片时。

    ImageView imgIv = new ImageView(this);
    //加载resource资源
    imgIv.setImageResource(R.mipmap.ic_launcher);
    
    //通过Glide加载url
    Glide.with(this).load(imageUrl).apply(
      new RequestOptions()
          .centerCrop()
          .placeholder(R.mipmap.ic_launcher))
      .into(imgIv);
    

    ImageView就会被绘制2次,可以通过以下两种方式进行优化:

    • 通过不设置background或者设置src
     <ImageView
        android:id="@+id/img_iv"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:src="#ededed"/>
    
    • 判断src加载之后取消background的方式都可以优化这个问题。

      Glide.with(this).load(imageUrl).apply(
              new RequestOptions()
                      .centerCrop()
                      .placeholder(R.mipmap.ic_launcher))
              .listener(new RequestListener<Drawable>() {
                  @Override
                  public boolean onLoadFailed(@Nullable GlideException e, Object model, 
                                              Target<Drawable> target, boolean isFirstResource) {
                      //加载失败
                      return false;
                  }
      
                  @Override
                  public boolean onResourceReady(Drawable resource, Object model, 
                                                 Target<Drawable> target, DataSource dataSource, 
                                                 boolean isFirstResource) {
                      //加载成功
                      imgIv.setBackgroundDrawable(null);
                      return false;
                  }
              })
              .into(imgIv);
      
  • PhoneWindow背景

    这种情况可能不太容易被注意到,当已经在Activity根布局设置了background时,window的背景是无效的,可以设置为null。

    getWindow().setBackgroundDrawable(null);
    

优化前后的对比,可以说十分明显了。

image

自定义View——clipRect

使用比较简单,直接引用官方说明了。

对于那些过于复杂的自定义的View(重写了onDraw方法),Android系统无法检测具体在onDraw里面会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来 帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠 组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执 行,那些部分内容在矩形区域内的组件,仍然会得到绘制。

for (int i = 0; i < mCards.length; i++) {
    canvas.translate(120, 0);
    canvas.save();
    if (i < mCards.length - 1) {
      //只绘制这个区域内
            canvas.clipRect(0, 0, 120, mCards[i].getHeight());
    }
    canvas.drawBitmap(mCards[i], 0, 0, null);
    canvas.restore();
}
  • 避免使用透明度

对于解决过度绘制问题,Google官方文档还提到了一种方式,减少透明元素的使用。

在屏幕上渲染透明像素,即所谓的透明度渲染,是导致过度绘制的重要因素。在普通的过度绘制中,系统会在已绘制的现有像素上绘制不透明的像素,从而将其完全遮盖,与此不同的是,透明对象需要先绘制现有的像素,以便达到正确的混合效果。诸如透明动画、淡出和阴影之类的视觉效果都会涉及某种透明度,因此有可能导致严重的过度绘制。您可以通过减少要渲染的透明对象的数量,来改善这些情况下的过度绘制。例如,如需获得灰色文本,您可以在 TextView 中绘制黑色文本,再为其设置半透明的透明度值。但是,您可以简单地通过用灰色绘制文本来获得同样的效果,而且能够大幅提升性能。

减少布局层级

  • 使用merge减少布局层级
  • 使用ViewSub只在需要时加载控件
  • 使用incluce标签实现布局复用
  • 尽量使用ConstraintLayout
  • 嵌套过深使用RelativeLayout而不要使用LinearLayout

merge

  • merge既不是View也不是ViewGroup,只是一种标记。
  • merge必须在布局的根节点。
  • 当merge所在布局被添加到容器中时,merge节点被合并不占用布局,merge下面的所有视图转移到容器中。

通过一种比较常用的场景来比较下使用merge和不使用的区别。

不使用merge

Activity布局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <RelativeLayout
        android:id="@+id/title_rel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <include layout="@layout/layout_merge"/>
        
    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/content_rel"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/title_rel"/>
</RelativeLayout>

ToolBar布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:tools="http://schemas.android.com/tools" >

    <ImageView
        android:id="@+id/home_iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/title_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_below="@id/home_iv"
        android:gravity="center_vertical"
        android:textSize="25sp"
        android:textColor="#000000"
        tools:text="测试标题"/>
</RelativeLayout>

实际Activity布局层级:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <RelativeLayout
        android:id="@+id/title_rel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <RelativeLayout 
            android:layout_width="match_parent"
            android:layout_height="wrap_content" >

            <ImageView
                android:id="@+id/home_iv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:src="@mipmap/ic_launcher"/>

            <TextView
                android:id="@+id/title_tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_below="@id/home_iv"
                android:gravity="center_vertical"
                android:textSize="25sp"
                android:textColor="#000000"
                tools:text="测试标题"/>
        </RelativeLayout>

    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/content_rel"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/title_rel"/>
</RelativeLayout>

使用merge进行优化:

优化后的ToolBar布局:

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:tools="http://schemas.android.com/tools"
    tools:parentTag="android.widget.RelativeLayout">

    <ImageView
        android:id="@+id/home_iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/title_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_below="@id/home_iv"
        android:gravity="center_vertical"
        android:textSize="25sp"
        android:textColor="#000000"
        tools:text="测试标题"/>
</merge>

使用tools:parentTag属性可以指定父布局类型,方便在Android Studio中编写布局时进行预览。

实际Activity布局层级,可以通过Layout Inspector来查看具体布局层级:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <RelativeLayout
        android:id="@+id/title_rel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/home_iv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:src="@mipmap/ic_launcher"/>

        <TextView
            android:id="@+id/title_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_below="@id/home_iv"
            android:gravity="center_vertical"
            android:textSize="25sp"
            android:textColor="#000000"
            tools:text="测试标题"/>

    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/content_rel"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/title_rel"/>
</RelativeLayout>

可以看到使用merge之后布局层级减少了一层。

使用场景

上面例子可能不太合适,这么写布局容易被打。

来看一种使用频率更高的应用场景——自定义View,大家应该都实现过,比如要定义一个通用的天气控件,通常是自定义一个WeatherView 继承自RelativeLayout,然后通过inflate动态引入布局,那么布局怎么写呢?不使用merge的情况下根布局肯定是RelativeLayout,引入WeatherView之后岂不是嵌套了一层RelativeLayout。这时候就可以在布局中使用merge进行优化。

还有一种应用场景,如果Activity的根布局是FrameLayout可以使用merge进行替换,使用之后可以使Activity的布局层级减少一层。为什么会这样呢?首先我们要了解Activity页面的布局层级,最外层是PhoneWindow其下是一个DecorView下面就是TitleView和ContentView,ContentView就是我们通过SetContentView设置的Activity的布局,没错ContentView是一个FrameLayout,所以在Activity布局中使用merge可以减少层级。

使用merge后可以有效的减少一层布局嵌套。

ViewSub

  • ViewStub是一种没有大小,不占用布局的View。
  • 直到当调用 inflate() 方法或者可见性变为VISIBLE时,才会将指定的布局加载到父布局中。
  • ViewStub加载完指定布局之后会被移除,不再占用空间。(所以 inflate() 方法只能调用一次 )

因为这些特性ViewStub可以用来懒加载布局,优化UI性能。

使用:

布局

在布局中添加ViewStub标签并通过layout属性指定要替换的布局。

        <ViewStub
            android:id="@+id/visible_view_stub"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout="@layout/layout_view_stub_content" />

代码

在需要展示布局的地方调用 inflate() 方法或者将ViewStub的可见性设置为VISIBLE。

private View viewStubContentView = null;

visibleViewStub.setVisibility(View.VISIBLE);

if(viewStubContentView == null){
    viewStubContentView = inflateViewStub.inflate();
}

注意inflate() 方法只能调用一次,重复调用被抛出IllegalStateException异常。

inflate() 方法会返回替换的布局的根View而设置VISIBLE不会返回,如果需要获取替换布局的实例,如:需要为替换的布局设置监听事件,这是需要使用inflate() 方法而不是VISIBLE。

ViewStub源码分析

针对我们前面说的ViewStub的几个特点,我们来分析下源码是如何实现的。分析源码可以学习别人优秀的代码设计,也可以为我们日后类似需求的实现提供借鉴。

  • ViewSutb没有大小,不占用布局

ViewStub在构造方法中设置了控件可见性为GONE并且指定不进行绘制。

public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context);
    final TypedArray a = context.obtainStyledAttributes(attrs,
            R.styleable.ViewStub, defStyleAttr, defStyleRes);
    mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
    mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
    mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
    a.recycle();
    //设置不可见
    setVisibility(GONE);
    //指定不进行绘制
    setWillNotDraw(true);
}

并且重写了onMeasure(widthMeasureSpec, heightMeasureSpec)设置尺寸为(0,0),并且重写了draw(canvas)dispatchDraw(canvas)方法,并且没有做任何绘制操作。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //指定尺寸为0,0
    setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
}
@Override
protected void dispatchDraw(Canvas canvas) {
}
  • setVisibility()inflate()方法
//定义了一个View的弱引用
private WeakReference<View> mInflatedViewRef;

@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {
        //如果弱引用不为空且View不为空,调用View的setVisibility方法
        View view = mInflatedViewRef.get();
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        super.setVisibility(visibility);
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            //弱引用为空且可见性设置为VISIBLE或者INVISIBLE,调用inflate()方法
            inflate();
        }
    }
}

到这里基本可以分析出弱引用持有的对象就是替换布局的View。继续往下看mInflatedViewRef是在哪里初始化的。

inflate()方法,核心方法执行具体的布局替换操作。

public View inflate() {
    //获取父布局
    final ViewParent viewParent = getParent();
    if (viewParent != null && viewParent instanceof ViewGroup) {
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            //获取要替换的View对象
            final View view = inflateViewNoAdd(parent);
            //执行替换操作
            replaceSelfWithView(view, parent);
            //初始化弱引用持有View对象
            mInflatedViewRef = new WeakReference<>(view);
            if (mInflateListener != null) {
                //触发监听
                mInflateListener.onInflate(this, view);
            }
            return view;
        } else {
            throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
        }
    } else {
        throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
    }
}

inflate()方法中获取要替换的View对象并执行了替换操作,mInflatedViewRef持有的确实是替换View对象的实例。

  • ViewStub加载完指定布局之后会被移除,不再占用空间

我们继续来看inflateViewNoAdd() 方法和replaceSelfWithView()方法。

private View inflateViewNoAdd(ViewGroup parent) {
    final LayoutInflater factory;
    if (mInflater != null) {
        factory = mInflater;
    } else {
        factory = LayoutInflater.from(mContext);
    }
    //动态加载View
    final View view = factory.inflate(mLayoutResource, parent, false);
    if (mInflatedId != NO_ID) {
        view.setId(mInflatedId);
    }
    return view;
}

inflateViewNoAdd() 方法比较简单,没什么好解释的。

private void replaceSelfWithView(View view, ViewGroup parent) {
    final int index = parent.indexOfChild(this);
    //从父布局中移除自己
    parent.removeViewInLayout(this);
    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    if (layoutParams != null) {
        //添加替换布局
        parent.addView(view, index, layoutParams);
    } else {
        //添加替换布局
        parent.addView(view, index);
    }
}

replaceSelfWithView()执行了移除和替换两步操作。这也解释了为什么inflate()方法只能执行一次,因为执行replaceSelfWithView()自身已经被移除,再次执行inflate()方法获取getParent()会为空,从而抛出IllegalStateException异常。

使用场景

app页面中总会有一些布局是不常显示的,如一些特殊提示和页面loading等,这时可以使用ViewStub来实现懒加载的功能,优化UI性能。

篇幅有限,其他几种方式相对简单,在此不详细展开了。

减少ContentView嵌套层级

每一个Activity都对应一个Window也就是PhoneWindow的实例,PhoneWindow对应布局是DecorView,也就是所有Activity的根布局都是DecorView,DecorView是一个FrameLayout,DecorView之下是一个竖向的LinearLayout,包含一个ActionBar和content,content也就是承载我们编写的Activity布局的控件是一个FrameLayout,也是我们调用setContentView所设置布局的父控件,整个结构如下:

image

通过Android Studio自带的Layout Inspector工具可以更清楚的看到整个Activity布局层级。

image

可以看到在加载自定义的Activity布局之前,DecorView中已经嵌套了三层布局了,而且action_bar在国内开发中几乎用不到了,那么我们直接把自定义的布局添加到DecorView中,可以至少减少2层嵌套,按照这个思路我们来实现一个基类。

public abstract class BaseDecorActivity extends FragmentActivity {
    protected final String TAG = getClass().getSimpleName();

    private Unbinder unbinder;

    private View rootView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        initLayoutView();
        unbinder = ButterKnife.bind(this);

        create();
    }

    /**
     * 添加布局
     */
    private void initLayoutView(){
        if(initLayout() != 0){
            if(getWindow().getDecorView() instanceof FrameLayout){
                //获取DecorView
                FrameLayout decorView = ((FrameLayout)getWindow().getDecorView());
                //移除DecorView的所有子View
                decorView.removeAllViews();
                //初始化子View,并attach到DecorView中
                rootView = LayoutInflater.from(this).inflate(initLayout(),decorView, true);
            } else{
                setContentView(initLayout());
            }
        }
    }

    /**
     * get rootView
     * @return
     */
    protected View getRootView(){
        return rootView;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if(unbinder != null){
            unbinder.unbind();
        }
    }

    /**
     * 初始化布局
     * @return
     */
    protected abstract int initLayout();
    protected abstract void create();
}

通过Layout Inspector来看一下修改后的效果:

image

可以看到效果很明显,优化后减少了两层布局嵌套。

根据上面的代码我们也知道DecorView是一个FrameLayout,既然..那么,如果我们使用merge对布局再次优化呢?

Activity布局如下:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    tools:parentTag="android.widget.FrameLayout">

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/main_view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#1E1E1E"/>

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/main_tab_layout"
        android:layout_width="match_parent"
        android:layout_height="@dimen/x120"
        android:paddingLeft="@dimen/x30"
        android:paddingRight="@dimen/x30"
        android:background="@drawable/main_tab_bg"/>

</merge>

优化后的结果:

[图片上传失败...(image-8c2570-1634107619063)]

可以看到布局层级已经很少了,基本达到了最优状态。但是此种方案,未做过多验证,在实际项目中谨慎使用。

写在最后

在进行UI布局优化时,注意配置检测工具使用,文中对这部分未做过多介绍,但是网上有很多关于功能的使用说明,也可以参考Google官方的文档。

检查 GPU 渲染速度和过度绘制

相关文档:

Google官方说明:

检查GPU过度绘制

减少过度绘制

Android性能优化典范(强烈推荐):

Android性能优化典范

其他博客:

Android UI性能实战

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

推荐阅读更多精彩内容