listview源码学习

前言

本文从源码角度出发学习listview,主要分析首次RecycleBin的组成,layout的过程,滑动过程,item的点击实现,如何支持Header,notifyDataSetChanged原理。

问题

用了好几年的listview,有几个问题却一直不清楚
1、如何让一个itemview不被回收,比如我的listview里有个viewpager比较复杂,不想让他被回收又重新创建。
2、head和foot的原理是怎么样的,会被回收吗?
3、scrap里的view会被进一步回收掉吗?

基础知识

读了3遍郭神的http://blog.csdn.net/sinyu890807/article/details/44996879 真是受益匪浅,原来listview是这么实现的。
listview的实现方法跟scrollview完全不同,scrollview是内部实例化了所有的view,在滚动的时候只是改变可见的部分,scrollview的高度可能是几千几万。如果item数很多的话,必然会oom。
而listview是首先画出listview的壳,然后去adapter里取数据,取到数据inflate为view,填到listview里面去,填满了就好了。即使adapter里有1万个数据,第一次layout的时候取的也是很少的数据(看当前屏幕需要,假设10个)。然后在上滑的过程中,首先用offsetTopAndBottom对所有child进行移动,此时顶部view就会滑出部分,那么底部会出现gap,再去adapter里面捞数据,填到底部;然后顶部的view逐渐的被完全移出屏幕,先detach,然后把这个view丢到scrap里面去,继续滑动底部又出现了gap,就去scrap里面拿现成的view。如此往复循环,这就是listview的原理。
和scrollview对比,listview的滑动过程中伴随着view的detach,attach,但是这些都不是耗时的东西,时间上没什么损失,但是空间上减少了大量的内存开销。先分析下layout过程和滑动过程。
listview内的缓存主要就是scrap,离屏就会进入scrap。scrap在layout的时候会进行裁剪,去调尾部的一些view,但是实际上这种情况发生的不多,后边会详细说。

layout过程

我测试了下layout的次数,郭神文章说的是2次,我这里会有3次。

第一次layout

onLayout -> layoutChildren -> fillFromTop-> fillDown-> while() makeAndAddView

makeAndAddView ->  
                    1、obtainView  -> getView -> inflate       
                    2、setupChild  -> addViewInLayout
                                   ->child.measure
                                   ->child.layout

第二次layout

onLayout -> layoutChildren -> 
1、fillActiveViews   
2、detachAllViewsFromParent  
3、fillSpecific-> fillDown->while() makeAndAddView
                     
        makeAndAddView -> getActiveView
                          ->setupChild -> attachViewToParent     

第三次layout

onLayout -> layoutChildren -> 
1、fillActiveViews   
2、detachAllViewsFromParent  
3、fillSpecific-> fillDown->while() makeAndAddView
                     
        makeAndAddView -> getActiveView
                          ->setupChild -> attachViewToParent    
                                       ->child.measure
                                       ->child.layout  

可以看到后2次基本差不多,区别在于在setupChild内是否要执行child的measure和layout。为什么第3次layout会调用measure和layout,而第二次不会呢?看下边的代码,差别就在于第三次child.isLayoutRequested()变为了true。

//setupChild
final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();

我在重新捋一下流程
1、ViewRootImpl#dispatchResized被调用,发出MSG_RESIZED_REPORT消息
2、第一次ListView:layoutChildren
3、第二次ListView:layoutChildren
4、收到发出MSG_RESIZED_REPORT消息,ViewRootImpl#forceLayout
5、第三次ListView:layoutChildren

所以第三次ListView:layoutChildren的时候会触发child.measure和child.layout。奇怪的是,每次都是第2次layout之后收到MSG_RESIZED_REPORT消息

滑动

对移除屏幕的view addScrapView、detachViewsFromParent
对屏幕内的view offsetChildrenTopAndBottom
对屏幕内空白的地方 fillGap -> fillDown->while() makeAndAddView
makeAndAddView -> obtainView、setupChild
obtainView-》getScrapView-》adapter.getView(convertview....)

void fillGap(boolean down) {  
    final int count = getChildCount();  
    if (down) {  
        final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :  
                getListPaddingTop();  
        //手指向上滑动,所以需要填充底部        
        fillDown(mFirstPosition + count, startOffset);  
        correctTooHigh(getChildCount());  
    } else {  
        final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :  
                getHeight() - getListPaddingBottom();  
        fillUp(mFirstPosition - 1, startOffset);  
        correctTooLow(getChildCount());  
    }  
}  

滑动过程中不会调用onMeasure或者onLayout

RecycleBin基本成员与方法

view的回收复用主要就依靠RecycleBin,所以重点分析下RecycleBin

mScrapViews

RecycleBin内有个垃圾箱,mScrapViews用来存放移除屏幕的view。

 private ArrayList<View>[] mScrapViews;
 private ArrayList<View> mCurrentScrap = mScrapViews[0];;

为什么是个数组呢?数组的每一项都是个ArrayList<View>,代表着某个type的垃圾view集合.如果只有一种type,那么垃圾都存在mScrapViews[0]内,mCurrentScrap = scrapViews[0];如果只有一个类型,我们直接操作mCurrentScrap即可

addScrapView

addScrapView就是把一个view加入到垃圾箱内,一般在view离开屏幕的时候调用。如果数据未变,adapter有stable IDs,有暂态,那就不会被收到垃圾箱里,会存着备用。。如果是header、footer那么就放入mSkippedScrap内,不放入mScrapViews。如果是暂态而且有有stable IDs,就丢到mTransientStateViewsById里面去。如果不需要stable IDs,数据未变可以丢到mTransientStateViews

    /**
         * Puts a view into the list of scrap views.
         * <p>
         * If the list data hasn't changed or the adapter has stable IDs, views
         * with transient state will be preserved for later retrieval.
         *
         * @param scrap The view to add
         * @param position The view's position within its parent
         */
        void addScrapView(View scrap, int position) {
            final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
            if (lp == null) {
                // Can't recycle, but we don't know anything about the view.
                // Ignore it completely.
                return;
            }

            lp.scrappedFromPosition = position;

            // Remove but don't scrap header or footer views, or views that
            // should otherwise not be recycled.
            final int viewType = lp.viewType;
            if (!shouldRecycleViewType(viewType)) {
                // Can't recycle. If it's not a header or footer, which have
                // special handling and should be ignored, then skip the scrap
                // heap and we'll fully detach the view later.
                if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                    getSkippedScrap().add(scrap);
                }
                return;
            }

            scrap.dispatchStartTemporaryDetach();

            // The the accessibility state of the view may change while temporary
            // detached and we do not allow detached views to fire accessibility
            // events. So we are announcing that the subtree changed giving a chance
            // to clients holding on to a view in this subtree to refresh it.
            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);

            // Don't scrap views that have transient state.
            final boolean scrapHasTransientState = scrap.hasTransientState();
            if (scrapHasTransientState) {
                if (mAdapter != null && mAdapterHasStableIds) {
                    // If the adapter has stable IDs, we can reuse the view for
                    // the same data.
                    //是暂态view,并且需要stable ID就丢到mTransientStateViewsById里面去
                    if (mTransientStateViewsById == null) {
                        mTransientStateViewsById = new LongSparseArray<>();
                    }
                    mTransientStateViewsById.put(lp.itemId, scrap);
                } else if (!mDataChanged) {
                    // If the data hasn't changed, we can reuse the views at
                    // their old positions.
                    if (mTransientStateViews == null) {
                        mTransientStateViews = new SparseArray<>();
                    }
                    //数据未变可以丢到mTransientStateViews
                    mTransientStateViews.put(position, scrap);
                } else {
                    // Otherwise, we'll have to remove the view and start over.
                    getSkippedScrap().add(scrap);
                }
            } else {
                if (mViewTypeCount == 1) {
                    mCurrentScrap.add(scrap);
                } else {
                    mScrapViews[viewType].add(scrap);
                }

                if (mRecyclerListener != null) {
                    mRecyclerListener.onMovedToScrapHeap(scrap);
                }
            }
        }

retrieveFromScrap

从scrap里取view,核心代码如下,如果是固定id的,那就根据adapter的id来找,否则就根据scrappedFromPosition 来找,比如第7个item被回收到scrap里了,记下这个view的scrappedFromPosition为7, 那下次滑回第7个item,就尽量给scrappedFromPosition为7的view给他,简单的说就是从哪里回收来的,还回哪里去。如果根据scrappedFromPosition找不到,那就直接取scrap的最后一个

          if (mAdapterHasStableIds) {
                        final long id = mAdapter.getItemId(position);
                        if (id == params.itemId) {
                            return scrapViews.remove(i);
                        }
                    } else if (params.scrappedFromPosition == position) {
                        final View scrap = scrapViews.remove(i);
                        clearAccessibilityFromScrap(scrap);
                        return scrap;
                    }
        final View scrap = scrapViews.remove(size - 1);            
        return scrap;

activeViews

这个有什么意义,没看懂。根据上面的分析,在第二次layout的过程中,首先会把当前屏幕的itemview给detach掉,扔到activeViews内,然后又把他们抓出来,给attach上,此时activeViews必定为空,如果不为空,把残余的view丢到mScrapViews内(scrapActiveViews) 我实在不明白这么搞有什么意义。

shouldRecycleViewType

根据type类型来确定这个view是否能回收,type类型一般可以在adapter里指定,但是系统默认提供了2个类型,一个是ITEM_VIEW_TYPE_IGNORE=-1,一个是ITEM_VIEW_TYPE_HEADER_OR_FOOTER=-2。第二个很明显就是listview的头和尾。第一个是什么呢?如果我们希望某个view不被回收的话,可以设置ITEM_VIEW_TYPE_IGNORE,这样就可以了。(recyclerView有类似的吗?)

        public boolean shouldRecycleViewType(int viewType) {
            return viewType >= 0;
        }

mRecyclerListener

当发生View回收时,mRecyclerListener若有注册,则会通知给注册者.RecyclerListener接口只有一个函数onMovedToScrapHeap,指明某个view被回收到了scrap heap.可以在这个接口回调里进行昂贵资源的回收(比如bitmap)。可以直接用listview来注册监听者.

        listview.setRecyclerListener(new AbsListView.RecyclerListener() {
            @Override
            public void onMovedToScrapHeap(View view) {
            }
        });

点击item

点击一个item,是和第二次layout类似的,会调用layoutChildren,然后把界面上的view抓起来丢到activeViews内,然后又重新填充,setupChild内不会调用measure和layout

onTouchUp -> layoutChildren -> 
1、fillActiveViews   
2、detachAllViewsFromParent  
3、fillSpecific-> fillDown->while() makeAndAddView
        makeAndAddView -> getActiveView
                          ->setupChild -> attachViewToParent    
                                      

跟第二次layout的区别就是没有调用child的onMeasure和onLayout,关键代码如下,这里needToMeasure为false

final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();

Header

我们可以轻易的给使用addHeaderView一个listview加上header。
我知道addHeaderView必须在setAdapter之前,可以add多个head
那么问题来了,为什么addHeaderView必须在setAdapter之前?
看setAdapter的部分代码可以明白,如果之前设置了header,那mAdapter将会被包装起来HeaderViewListAdapter

//setAdapter
        if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
        } else {
            mAdapter = adapter;
        }

再看看HeaderViewListAdapter,看下边的代码可以看到实际上HeaderViewListAdapter实现了Adapter的各种接口,比如getCount,getItem,getItemViewType,getView,这就是把原来的adapter进行包装,然后实现对应接口,把Header作为一种特殊类型AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER,在ListView看来,他就是一个普通的adapter。

//HeaderViewListAdapter
public class HeaderViewListAdapter implements WrapperListAdapter, Filterable {

    public int getCount() {
        if (mAdapter != null) {
            return getFootersCount() + getHeadersCount() + mAdapter.getCount();
        } else {
            return getFootersCount() + getHeadersCount();
        }
    }
    
    public Object getItem(int position) {
        // Header (negative positions will throw an IndexOutOfBoundsException)
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return mHeaderViewInfos.get(position).data;
        }

        // Adapter
        final int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getItem(adjPosition);
            }
        }

        // Footer (off-limits positions will throw an IndexOutOfBoundsException)
        return mFooterViewInfos.get(adjPosition - adapterCount).data;
    }
    public View getView(int position, View convertView, ViewGroup parent) {
        // Header (negative positions will throw an IndexOutOfBoundsException)
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return mHeaderViewInfos.get(position).view;
        }

        // Adapter
        final int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getView(adjPosition, convertView, parent);
            }
        }

        // Footer (off-limits positions will throw an IndexOutOfBoundsException)
        return mFooterViewInfos.get(adjPosition - adapterCount).view;
    }

    public int getItemViewType(int position) {
        int numHeaders = getHeadersCount();
        if (mAdapter != null && position >= numHeaders) {
            int adjPosition = position - numHeaders;
            int adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getItemViewType(adjPosition);
            }
        }

        return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
    }
}

我们在分析下header是否会被多次创建,是否会被丢到scrap里去

首先看,滑动的时候会不会回收header,这里明显可以看到position的限制,header和footer是不会被回收的。既然不会回收,那下次再滑到header的时候还是找adapter要,看上边的adapte的getView代码,mHeaderViewInfos.get(position).view,只是从mHeaderViewInfos.get内取,所以header是不会被回收的,永远存在mHeaderViewInfos里面。这里可以得到启发,Recyclerview是不支持header,footer的,那我们是不是可以针对Recyclerview来一次类似的包装,让他支持header,footer

//trackMotionScroll
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        // The view will be rebound to new data, clear any
                        // system-managed transient state.
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);
                    }

notifyDataSetChanged

之前一直没有说过,数据发生变化的情况会怎么样,我们都知道,数据发生变化调用adpater的notifyDataSetChanged就会刷新界面。这里面的原理是什么? 这里面有个观察者模式,BaseAdapter内有个mDataSetObservable,AbsListView在onAttachedToWindow的时候会注册观察者,代码如下,这样就注册了一个观察者mDataSetObserver

//AbsListView#onAttachedToWindow
   if (mAdapter != null && mDataSetObserver == null) {
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);

            // Data may have changed while we were detached. Refresh.
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = mAdapter.getCount();
        }

notifyDataSetChanged会调用mDataSetObserver.onChanged,里面更新了mItemCount,然后调用了rememberSyncState和requestLayout。

        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();

            // Detect the case where a cursor that was previously invalidated has
            // been repopulated with new data.
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
                rememberSyncState();
            }
            checkFocus();
            requestLayout();
        }

这里rememberSyncState比较陌生,实际上他做的事情也很少,主要就2行代码,设置mSyncMode和mSyncPosition。重新布局的时候默认有个原则,之前谁在第一个,那么这次谁还是在第一个.

      mSyncPosition = mFirstPosition;
      mSyncMode = SYNC_FIRST_POSITION;

然后我们看又一次layout的过程
首先,handleDataChanged会定下mSyncPosition,然后把
mLayoutMode = LAYOUT_SYNC;,这个mLayoutMode后边会用到
第二步,因为dataChanged所以这里直接把所有的界面上的view丢到scrap里,不像以前放在activeViews里
第三步,detachAllViewsFromParent
第四步,fillSpecific是因为mLayoutMode是LAYOUT_SYNC所以直接调用fillSpecific。
里面的getView是adapter的getView一般在这里设置实际view的内容(比如文本图片)。所以view一般都会设置为PFLAG_FORCE_LAYOUT,所以会重新measure、layout。(这里可以再思考下,其实大部分情况下,重用view,并不用重新measure,而layout的时候只要把item往listview的框里丢就可以了,item内部也不需要layout,这样应该能够提供效率,但是看了代码后发现setupChild内是根据needToMeasure来决定是否measure、layout的,不能分别对待,哎。)

onLayout -> layoutChildren -> 
1、handleDataChanged:定个mSyncPosition、mLayoutMode = LAYOUT_SYNC;
2、for() addScrapView
3、detachAllViewsFromParent  
4、fillSpecific-> fillDown->while() makeAndAddView
                     
        makeAndAddView -> obtainView->getView
                          ->setupChild -> attachViewToParent    
                                       ->child.measure
                                       ->child.layout  

这次layout跟之前的区别主要是第二步和第四步。

listview动画错乱

listview的item如果在执行动画的同时,listview在滑动,我们知道listview滑动过程中,是会重用view的,所以可能本来针对position 为1的动画,跑到position为11的地方去了,所以我们得禁止这个view进入scrap,如何禁止?
setHasTransientState(true),让view进入暂态
setHasTransientState是API16引入的函数,在View里,下边是对他的介绍,主要是用于动画开始和结束,在开始的时候setHasTransientState(true),结束的时候setHasTransientState(false),在这之间就是暂态的。

常见用法如下,在动画开始的时候进入暂态,动画结束退出暂态。我们再对比listview的代码可以发现,进入暂态的view不会进入scrap,而是进入mTransientStateViewsById这个LongSparseArray内,这样就不会被重用而导致动画错乱了。

//Listview 的 OnItemClickListener 的内容
//本范例点了 item 后会淡出并删除该 item
public void onItemClick(AdapterView
 
  parent, final View view, int position, long id) {
    final String item = (String) parent.getItemAtPosition(position);
    ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.ALPHA, 0);
    anim.setDuration(500);
    view.setHasTransientState(true); //设为 true 宣告 item 要被追踪
    anim.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            myListview.remove(item);
            adapter.notifyDataSetChanged(); //重新整理 listview
            view.setAlpha(1);
            view.setHasTransientState(false); //完成后设定回 false
        }
    });
    anim.start();
}

不可否认这是一种解决方法,但并不是好的解决方法,因为item都滑出去了,还在搞动画,没啥意义,真正好的方法是什么,如下所示,在onMovedToScrapHeap里面停止动画,这才是最合适的。

listView.setRecyclerListener(new RecyclerListener() {
        @Override
        public void onMovedToScrapHeap(View view) {
            // Stop animation on this view
        }
});

hasStableIds

adapter有个接口叫hasStableIds,这个有什么意义,我查了资料和代码。发现hasStableIds一般情况下都是false,只有2个情况是true。
什么样的adapter是stable的,个人以为是里面的数据的id不变化,数据可以变化,但是id不能变化。
首先,如果要用到listview的选中功能时,只有hasStableIds返回true,才能通过getCheckedItemIds方法才能正常获取用户选中的选项的id(当然adapter内必须复写getItemId)。
还有个地方就是CursorAdapter,因为cursor是sql查询的结果,所以说是stable的无可厚非。CursorAdapter里面的hasStableIds就是返回true的。
总的来说hasStableIds没啥用,我也没看到改为true能优化什么。

问题

addViewInLayout和attachViewToParent有什么区别呢

addViewInLayout和attachViewToParent两者接收的参数是一样的,主要功能也相似,也就是往ViewGroup的view数组里添加View, 但是调用addViewInLayout会使被添加的View在界面上添加时会有动画效果呈现。两者的使用场景差别也很明显了:一般来说某一个View第一次添加进ViewGroup时比较适合调用addViewInLayout,而以后同一个View再次被添加时则适合使用attachViewToParent。因为一般情况想我们会希望进入的动画效果执行一次就够了,而不需要多次执行。
具体可参考http://www.itdadao.com/articles/c15a444236p0.html

scap有数量限制吗

在滑动过程中scrap是没限制的,但是在layout的过程中调用scrapActiveViews->pruneScrapViews,在这里会把mScrapViews内的每组缓存,都限制在mActiveViews.length大小。

scrap里的view会被去掉吗

为什么要考虑这个问题呢?因为有的view创建成本很高,我们不希望重复创建,什么情况下会重复创建呢?那就是view离屏,进scrap,scrap裁剪,被裁剪的view就没有了,下次必须重新inflate出来。
我看了下要想remove scrap里的view,只有pruneScrapViews和clear方法,clear在设置setAdapter(ListAdapter)和onDetachedFromWindow()时会被调用。而pruneScrapViews是在layout过程中被调的。所以主要看pruneScrapViews。这里可以看到其实处理scrap长度的方法是比较粗暴的,查一下mActiveViews.length,任何一个scrap堆都不准超过这个长度,否则直接截尾。于是我又看了下mActiveViews,每次显示在界面上的view都会丢到mActiveViews里,又发现mActiveViews只会变长不会变短。这就有意思了,比如当前页面有5个item,那mActiveViews.length就是5,待会当前页面有10个item了,那mActiveViews.length就是10,再过一块又只有3个item了,那mActiveViews.length还是10(只增不减)。要注意一点只有在layout的时候才会更新mActiveViews.length,如果只是滑来滑去是不会触发layout的。所以如果,mActiveViews.length值比较小,而scrap的item又很多的话,会进入到L10,进行裁剪(一般会发生在header高度比较大的情况下),这种裁剪方式其实是比较奇怪的,凭什么根据mActiveViews.length来裁剪。
所以scrap里的view是有可能被丢弃的,但是如果某个scap堆里只有一个view,那放心,他绝不会被丢弃。
另外如果我们希望缓存的view数量多一些的话,我们可以在view比较多的时候掉一遍requestLayout,这样让他更新mActiveViews.length

     final int maxViews = mActiveViews.length;
            final int viewTypeCount = mViewTypeCount;
            final ArrayList<View>[] scrapViews = mScrapViews;
            for (int i = 0; i < viewTypeCount; ++i) {
                final ArrayList<View> scrapPile = scrapViews[i];
                int size = scrapPile.size();
                final int extras = size - maxViews;
                size--;
                for (int j = 0; j < extras; j++) {
                    removeDetachedView(scrapPile.remove(size--), false);
                }
            }

其他

1、layoutChildren必定调用invalidate
2、initAbsListView内设置ListView本身可以点击即可以消耗父View分发的事件: setClickable(true);
3、我们常常用的convertView实际上来自scrapView

ref

http://blog.csdn.net/sinyu890807/article/details/44996879
https://github.com/CharonChui/AndroidNote/blob/master/Android%E5%8A%A0%E5%BC%BA/ListView%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md
http://www.itdadao.com/articles/c15a444236p0.html
http://www.cnblogs.com/qiengo/p/3628235.html

http://www.eoeandroid.com/thread-303373-1-1.html?_dsign=6a0c274f
http://edscb.blogspot.com/2013/09/animation-listview-animations.html

ListView单选和多选模式完全解析

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,179评论 25 707
  • RecyclerView是Android 5.0系统官方推出的一个代替listView的组件,那么究竟好在哪里呢?...
    niknowzcd阅读 6,881评论 5 24
  • 一、适用场景 ListViewListview是一个很重要的组件,它以列表的形式根据数据的长自适应展示具体内容,用...
    Geeks_Liu阅读 10,626评论 1 28
  • 最近时不时看看首页推荐的文章,发现没什么意思,真材实料不多,哗众取宠一堆,谩骂扭曲一窝,唉,一声长叹,不待了,走人!
    一声长叹唉阅读 279评论 0 0
  • 早上锻炼选择了条新路线,去滨海栈道走了一圈,真是不虚此行,咔咔的不停拍照片,心情也变得更好了,这几天什么都不用管,...
    令宜阅读 272评论 0 0