ListView的缓存机制

1、RecycleBin缓存机制

RecycleBin是ListView的父类AbsListView的内部类,它主要用于ListView的缓存。有了它ListView才可以做到在数据量很大的情况下都不会OOM。RecycleBin中的代码并不多,下面看下其中主要的代码。

/**
 * The RecycleBin facilitates reuse of views across layouts. The RecycleBin
 * has two levels of storage: ActiveViews and ScrapViews. ActiveViews are
 * those views which were onscreen at the start of a layout. By
 * construction, they are displaying current information. At the end of
 * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews
 * are old views that could potentially be used by the adapter to avoid
 * allocating views unnecessarily.
 * 
 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
 * @see android.widget.AbsListView.RecyclerListener
 */
class RecycleBin {
    private RecyclerListener mRecyclerListener;
 
    /**
     * The position of the first view stored in mActiveViews.
     */
    private int mFirstActivePosition;
 
    /**
     * Views that were on screen at the start of layout. This array is
     * populated at the start of layout, and at the end of layout all view
     * in mActiveViews are moved to mScrapViews. Views in mActiveViews
     * represent a contiguous range of Views, with position of the first
     * view store in mFirstActivePosition.
     */
    private View[] mActiveViews = new View[0];
 
    /**
     * Unsorted views that can be used by the adapter as a convert view.
     */
    private ArrayList<View>[] mScrapViews;
 
    private int mViewTypeCount;
 
    private ArrayList<View> mCurrentScrap;
 
    /**
     * Fill ActiveViews with all of the children of the AbsListView.
     * 
     * @param childCount
     *            The minimum number of views mActiveViews should hold
     * @param firstActivePosition
     *            The position of the first view that will be stored in
     *            mActiveViews
     */
    void fillActiveViews(int childCount, int firstActivePosition) {
        if (mActiveViews.length < childCount) {
            mActiveViews = new View[childCount];
        }
        mFirstActivePosition = firstActivePosition;
        final View[] activeViews = mActiveViews;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
            // Don't put header or footer views into the scrap heap
            if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
                // active views.
                // However, we will NOT place them into scrap views.
                activeViews[i] = child;
            }
        }
    }
 
    /**
     * Get the view corresponding to the specified position. The view will
     * be removed from mActiveViews if it is found.
     * 
     * @param position
     *            The position to look up in mActiveViews
     * @return The view if it is found, null otherwise
     */
    View getActiveView(int position) {
        int index = position - mFirstActivePosition;
        final View[] activeViews = mActiveViews;
        if (index >= 0 && index < activeViews.length) {
            final View match = activeViews[index];
            activeViews[index] = null;
            return match;
        }
        return null;
    }
 
    /**
     * Put a view into the ScapViews list. These views are unordered.
     * 
     * @param scrap
     *            The view to add
     */
    void addScrapView(View scrap) {
        AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
        if (lp == null) {
            return;
        }
        // Don't put header or footer views or views that should be ignored
        // into the scrap heap
        int viewType = lp.viewType;
        if (!shouldRecycleViewType(viewType)) {
            if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
                removeDetachedView(scrap, false);
            }
            return;
        }
        if (mViewTypeCount == 1) {
            dispatchFinishTemporaryDetach(scrap);
            mCurrentScrap.add(scrap);
        } else {
            dispatchFinishTemporaryDetach(scrap);
            mScrapViews[viewType].add(scrap);
        }
 
        if (mRecyclerListener != null) {
            mRecyclerListener.onMovedToScrapHeap(scrap);
        }
    }
 
    /**
     * @return A view from the ScrapViews collection. These are unordered.
     */
    View getScrapView(int position) {
        ArrayList<View> scrapViews;
        if (mViewTypeCount == 1) {
            scrapViews = mCurrentScrap;
            int size = scrapViews.size();
            if (size > 0) {
                return scrapViews.remove(size - 1);
            } else {
                return null;
            }
        } else {
            int whichScrap = mAdapter.getItemViewType(position);
            if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
                scrapViews = mScrapViews[whichScrap];
                int size = scrapViews.size();
                if (size > 0) {
                    return scrapViews.remove(size - 1);
                }
            }
        }
        return null;
    }
 
    public void setViewTypeCount(int viewTypeCount) {
        if (viewTypeCount < 1) {
            throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
        }
        // noinspection unchecked
        ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
        for (int i = 0; i < viewTypeCount; i++) {
            scrapViews[i] = new ArrayList<View>();
        }
        mViewTypeCount = viewTypeCount;
        mCurrentScrap = scrapViews[0];
        mScrapViews = scrapViews;
    }
 
}

1.1、变量

  • private View[] mActiveViews :缓存屏幕上可见的View,比如你的设备一屏可展示8个item,那么mActiveViews的length就是8。
  • private int mViewTypeCount:ListView中item类型的个数。可通过复写BaseAdapter的getViewTypeCount()方法指定,会在ListView的setAdapter中调用RecycleBin的setViewTypeCount进行设置。
  • ArrayList<View>[] mScrapViews :不同类型的item有自己独立的缓存,存储在ArrayList链表中。mScrapViews的类型是ArrayList<View>[],其中存储的就是所有item类型的离屏缓存,其长度等于ViewTypeCount。
  • ArrayList<View> mCurrentScrap:mCurrentScrap=mScrapViews[0],用来存储离屏的View。

1.2、方法

  • fillActiveViews() :根据传入的参数将ListView指定的View添加到mActiveViews数组当中去。
  • getActiveView():这个方法和fillActiveViews()是对应的,用于从mActiveViews中获取缓存的View。一旦获取之后会从mActiveViews中移除。
  • addScrapView():将一个废弃的View(比如滑出了屏幕)进行缓存,如果ViewTypeCount=1,则直接存入mCurrentScrap链表中,否则会存入mScrapViews数组中对应的链表中。
  • getScrapView():和addScrapView()方法对应,从缓存中取出View,同样的取出后会从缓存中移除。
  • setViewTypeCount():为每种类型的item启用一个RecycleBin缓存。

2、源码解析

2.1、第一次Layout

现在我们对RecyleBin已经有了大概的了解,下面具体分析下ListView如何进行缓存的。
ListView也是一个View,它的绘制流程也就是三步:measure、layout、draw,这里只看下和缓存相关的layout过程。
如果你到ListView源码中去找一找,你会发现ListView中是没有onLayout()这个方法的,这是因为这个方法是在ListView的父类AbsListView中实现的,代码如下所示:

/**
 * Subclasses should NOT override this method but {@link #layoutChildren()}
 * instead.
 */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    mInLayout = true;
    if (changed) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            getChildAt(i).forceLayout();
        }
        mRecycler.markChildrenDirty();
    }
    layoutChildren();
    mInLayout = false;
}

可以看到,onLayout中并没有什么特殊的操作,就是一个判断,如果ListView的位置或大小发生了变化,那么change为true,此时会要求所有的子View都强制重绘。紧接着调用了layoutChildren()进行子元素布局,该方法是一个空方法,具体实现在ListView中。

@Override
protected void layoutChildren() {
        //....
        //1
        int childCount = getChildCount();
        //....
        //2
        if (dataChanged) {
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i));
                if (ViewDebug.TRACE_RECYCLER) {
                    ViewDebug.trace(getChildAt(i),
                            ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
                }
            }
        } else {
            recycleBin.fillActiveViews(childCount, firstPosition);
        }
        //...
        // Clear out old views
        //3
        detachAllViewsFromParent();
        //4
        switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            if (newSel != null) {
                sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
            } else {
                sel = fillFromMiddle(childrenTop, childrenBottom);
            }
            break;
        case LAYOUT_SYNC:
            sel = fillSpecific(mSyncPosition, mSpecificTop);
            break;
        case LAYOUT_FORCE_BOTTOM:
            sel = fillUp(mItemCount - 1, childrenBottom);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_FORCE_TOP:
            mFirstPosition = 0;
            sel = fillFromTop(childrenTop);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_SPECIFIC:
            sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
            break;
        case LAYOUT_MOVE_SELECTION:
            sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
            break;
        default:
        //5
            if (childCount == 0) {
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    //6
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - 1, false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - 1, childrenBottom);
                }
            } else {
                if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                    sel = fillSpecific(mSelectedPosition,
                            oldSel == null ? childrenTop : oldSel.getTop());
                } else if (mFirstPosition < mItemCount) {
                    sel = fillSpecific(mFirstPosition,
                            oldFirst == null ? childrenTop : oldFirst.getTop());
                } else {
                    sel = fillSpecific(0, childrenTop);
                }
            }
            break;
        }
       //...
    } finally {
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}

layoutChildren中代码较多,这里只展示我们关心的。
首先可以确定的是注释1处的getChildCount()=0,因为此时ListView中还未添加任何子View。注释2处的dataChanged只有在数据源发生变化时才会等于true,其他情况都为false,因此会走else的逻辑调用RecyleBin的fillActiveViews()将ListView的子View进行缓存,而此时LIstView中并没有任何子View,故暂时不起任何作用。

接下来会走注释4处的代码,会根据mLayoutMode的值来决定布局模式,默认情况下都是普通模式LAYOUT_NORMAL。故进入注释5处执行default的逻辑。由于此时childCount=0并且默认的布局顺序是从上到下的,故执行注释6处的fillFromTop()方法。

/**
 * Fills the list from top to bottom, starting with mFirstPosition
 *
 * @param nextTop The location where the top of the first item should be
 *        drawn
 *
 * @return The view that is currently selected
 */
private View fillFromTop(int nextTop) {
    mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
    mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
    if (mFirstPosition < 0) {
        mFirstPosition = 0;
    }
    return fillDown(mFirstPosition, nextTop);
}

这个方法本身并没有什么逻辑,就是判断了一下mFirstPosition值的合法性,然后调用fillDown()方法,那么我们就有理由可以猜测,填充ListView的操作是在fillDown()方法中完成的。继续向下看fillDown()

/**
 * Fills the list from pos down to the end of the list view.
 *
 * @param pos The first position to put in the list
 *
 * @param nextTop The location where the top of the item associated with pos
 *        should be drawn
 *
 * @return The view that is currently selected, if it happens to be in the
 *         range that we draw.
 */
private View fillDown(int pos, int nextTop) {
    View selectedView = null;
    int end = (getBottom() - getTop()) - mListPadding.bottom;
    while (nextTop < end && pos < mItemCount) {
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
        nextTop = child.getBottom() + mDividerHeight;
        if (selected) {
            selectedView = child;
        }
        pos++;
    }
    return selectedView;
}

上面代码逻辑也很简单,主要就是通过循环创建并添加View,这里会进行边界判断,保证最多添加一屏的View。创建添加View的逻辑主要在makeAndAddView()方法中。

/**
 * Obtain the view and add it to our list of children. The view can be made
 * fresh, converted from an unused view, or used as is if it was in the
 * recycle bin.
 *
 * @param position Logical position in the list
 * @param y Top or bottom edge of the view to add
 * @param flow If flow is true, align top edge to y. If false, align bottom
 *        edge to y.
 * @param childrenLeft Left edge where children should be positioned
 * @param selected Is this position selected?
 * @return View that was added
 */
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    View child;
    if (!mDataChanged) {
        // Try to use an exsiting view for this position
        child = mRecycler.getActiveView(position);
        if (child != null) {
            // Found it -- we're using an existing child
            // This just needs to be positioned
            setupChild(child, position, y, flow, childrenLeft, selected, true);
            return child;
        }
    }
    // Make a new view for this position, or convert an unused view if possible
    child = obtainView(position, mIsScrap);
    // This needs to be positioned and measured
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    return child;
}

在该方法中,先从缓存中取出指定位置的View,很遗憾此时并没有缓存任何View,所以会执行obtainView()方法来再次尝试获取一个View,obtainView()方法是可以保证一定返回一个View的,下面将获取到的View传递给setupChild()方法。下面看下obtainView()是如何工作的。

/**
 * Get a view and have it show the data associated with the specified
 * position. This is called when we have already discovered that the view is
 * not available for reuse in the recycle bin. The only choices left are
 * converting an old view or making a new one.
 * 
 * @param position
 *            The position to display
 * @param isScrap
 *            Array of at least 1 boolean, the first entry will become true
 *            if the returned view was taken from the scrap heap, false if
 *            otherwise.
 * 
 * @return A view displaying the data associated with the specified position
 */
View obtainView(int position, boolean[] isScrap) {
    isScrap[0] = false;
    View scrapView;
    scrapView = mRecycler.getScrapView(position);
    View child;
    if (scrapView != null) {
        child = mAdapter.getView(position, scrapView, this);
        if (child != scrapView) {
            mRecycler.addScrapView(scrapView);
            if (mCacheColorHint != 0) {
                child.setDrawingCacheBackgroundColor(mCacheColorHint);
            }
        } else {
            isScrap[0] = true;
            dispatchFinishTemporaryDetach(child);
        }
    } else {
        child = mAdapter.getView(position, null, this);
        if (mCacheColorHint != 0) {
            child.setDrawingCacheBackgroundColor(mCacheColorHint);
        }
    }
    return child;
}

首先调用RecyleBin的getScrapView()获取废弃的View,此时由于ListView中还未添加任何View,故调用getScrapView()返回null,故会调用mAdapter.getView(position, null, this)去创建View。这个mAdapter就是ListView关联的适配器,我们在创建适配器时会重写getView()方法,一般写法如下

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    Fruit fruit = getItem(position);
    View view;
    if (convertView == null) {
        view = LayoutInflater.from(getContext()).inflate(resourceId, null);
    } else {
        view = convertView;
    }
    ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
    TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
    fruitImage.setImageResource(fruit.getImageId());
    fruitName.setText(fruit.getName());
    return view;
}

此时传入getView()的convertView为null,所以会通过LayoutInflater加载布局,然后返回这个view。

那么这个View会作为obtainView()的结果进行返回,并传入到setupChild()方法。

从上面的分析可知:在第一次layout时,所有的子View都是通过LayoutInflater.inflate()方法加载进来的,这样加载时比较耗时的,但以后不会出现这种情况了。接着进入setupChild()方法,看下具体的执行。

/**
 * Add a view as a child and make sure it is measured (if necessary) and
 * positioned properly.
 *
 * @param child The view to add
 * @param position The position of this child
 * @param y The y position relative to which this view will be positioned
 * @param flowDown If true, align top edge to y. If false, align bottom
 *        edge to y.
 * @param childrenLeft Left edge where children should be positioned
 * @param selected Is this position selected?
 * @param recycled Has this view been pulled from the recycle bin? If so it
 *        does not need to be remeasured.
 */
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean recycled) {
    final boolean isSelected = selected && shouldShowSelector();
    final boolean updateChildSelected = isSelected != child.isSelected();
    final int mode = mTouchMode;
    final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
            mMotionPosition == position;
    final boolean updateChildPressed = isPressed != child.isPressed();
    final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
    // Respect layout params that are already in the view. Otherwise make some up...
    // noinspection unchecked
    AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
    if (p == null) {
        p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT, 0);
    }
    p.viewType = mAdapter.getItemViewType(position);
    if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
            p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        attachViewToParent(child, flowDown ? -1 : 0, p);
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
    }
    if (updateChildSelected) {
        child.setSelected(isSelected);
    }
    if (updateChildPressed) {
        child.setPressed(isPressed);
    }
    if (needToMeasure) {
        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                mListPadding.left + mListPadding.right, p.width);
        int lpHeight = p.height;
        int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);
    } else {
        cleanupLayoutState(child);
    }
    final int w = child.getMeasuredWidth();
    final int h = child.getMeasuredHeight();
    final int childTop = flowDown ? y : y - h;
    if (needToMeasure) {
        final int childRight = childrenLeft + w;
        final int childBottom = childTop + h;
        child.layout(childrenLeft, childTop, childRight, childBottom);
    } else {
        child.offsetLeftAndRight(childrenLeft - child.getLeft());
        child.offsetTopAndBottom(childTop - child.getTop());
    }
    if (mCachingStarted && !child.isDrawingCacheEnabled()) {
        child.setDrawingCacheEnabled(true);
    }
}

该方法中代码比较多,这里我们只看核心代码addViewInLayout(child, flowDown ? -1 : 0, p, true),其中的child就是刚才调用obtainView()获取的View。再结合fillDown() 中的while循环,会让子元素填满整个ListView控件后退出。

到此为止第一次Layout就结束了。


第一次layout.png

2.2、第二次layout

ListView在展示到界面之前会执行两次onLayout,这意味着layoutChildren()方法会执行两次,前面我们分析了,在layoutChildren()中会添加子元素到ListView中,如果执行两次,那么会不会重复添加子元素呢,其实并没有,下面再次看下layoutChildren的逻辑。

@Override
protected void layoutChildren() {
        //....
        //1
        int childCount = getChildCount();
        //....
        //2
        if (dataChanged) {
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i));
                if (ViewDebug.TRACE_RECYCLER) {
                    ViewDebug.trace(getChildAt(i),
                            ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
                }
            }
        } else {
            recycleBin.fillActiveViews(childCount, firstPosition);
        }
        //...
        // Clear out old views
        //3
        detachAllViewsFromParent();
        //4
        switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            if (newSel != null) {
                sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
            } else {
                sel = fillFromMiddle(childrenTop, childrenBottom);
            }
            break;
        case LAYOUT_SYNC:
            sel = fillSpecific(mSyncPosition, mSpecificTop);
            break;
        case LAYOUT_FORCE_BOTTOM:
            sel = fillUp(mItemCount - 1, childrenBottom);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_FORCE_TOP:
            mFirstPosition = 0;
            sel = fillFromTop(childrenTop);
            adjustViewsUpOrDown();
            break;
        case LAYOUT_SPECIFIC:
            sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
            break;
        case LAYOUT_MOVE_SELECTION:
            sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
            break;
        default:
        //5
            if (childCount == 0) {
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    //6
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - 1, false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - 1, childrenBottom);
                }
            } else {
                if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                    sel = fillSpecific(mSelectedPosition,
                            oldSel == null ? childrenTop : oldSel.getTop());
                } else if (mFirstPosition < mItemCount) {
                    sel = fillSpecific(mFirstPosition,
                            oldFirst == null ? childrenTop : oldFirst.getTop());
                } else {
                    sel = fillSpecific(0, childrenTop);
                }
            }
            break;
        }
       //...
    } finally {
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }
}

由于经过第一次layout后,ListView中已经添加了一屏子元素,所以注释1处的childcount!=0,而且调用RecyleBin.fillActiveViews()会将屏幕上的View添加到缓存中去。

紧接着在注释3处调用detachAllViewsFromParent()方法,将ListView中的所有子View都清空了。这样做的目的是为了防止在第二次layout时重复添加子元素。后面在添加子元素时,直接从缓存中取出即可(前面已经调用RecyleBin.fillActiveViews()进行了缓存),不需要再通过LayoutInflater加载布局了。

紧接着执行注释5处代码,此时childCount!=0,故执行else的逻辑,调用fillSpecific()方法

/**
 * Put a specific item at a specific location on the screen and then build
 * up and down from there.
 *
 * @param position The reference view to use as the starting point
 * @param top Pixel offset from the top of this view to the top of the
 *        reference view.
 *
 * @return The selected view, or null if the selected view is outside the
 *         visible area.
 */
private View fillSpecific(int position, int top) {
    boolean tempIsSelected = position == mSelectedPosition;
    View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
    // Possibly changed again in fillUp if we add rows above this one.
    mFirstPosition = position;
    View above;
    View below;
    final int dividerHeight = mDividerHeight;
    if (!mStackFromBottom) {
        above = fillUp(position - 1, temp.getTop() - dividerHeight);
        // This will correct for the top of the first view not touching the top of the list
        adjustViewsUpOrDown();
        below = fillDown(position + 1, temp.getBottom() + dividerHeight);
        int childCount = getChildCount();
        if (childCount > 0) {
            correctTooHigh(childCount);
        }
    } else {
        below = fillDown(position + 1, temp.getBottom() + dividerHeight);
        // This will correct for the bottom of the last view not touching the bottom of the list
        adjustViewsUpOrDown();
        above = fillUp(position - 1, temp.getTop() - dividerHeight);
        int childCount = getChildCount();
        if (childCount > 0) {
             correctTooLow(childCount);
        }
    }
    if (tempIsSelected) {
        return temp;
    } else if (above != null) {
        return above;
    } else {
        return below;
    }
}

该方法和fillUp()、fillDown()方法功能也是差不多的,我们主要看下makeAndAddView()这个方法

/**
 * Obtain the view and add it to our list of children. The view can be made
 * fresh, converted from an unused view, or used as is if it was in the
 * recycle bin.
 *
 * @param position Logical position in the list
 * @param y Top or bottom edge of the view to add
 * @param flow If flow is true, align top edge to y. If false, align bottom
 *        edge to y.
 * @param childrenLeft Left edge where children should be positioned
 * @param selected Is this position selected?
 * @return View that was added
 */
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    View child;
    if (!mDataChanged) {
        // Try to use an exsiting view for this position
        child = mRecycler.getActiveView(position);
        if (child != null) {
            // Found it -- we're using an existing child
            // This just needs to be positioned
            setupChild(child, position, y, flow, childrenLeft, selected, true);
            return child;
        }
    }
    // Make a new view for this position, or convert an unused view if possible
    child = obtainView(position, mIsScrap);
    // This needs to be positioned and measured
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    return child;
}

这里仍然先调用RecycleBin.getActiveView()获取缓存的View,此时肯定可以得到View,得到View后直接调用setupChild()方法并返回。不再执行obtainView()去inflate布局,这样就省去了很多时间。

注意调用setupChild()方法时最后一个参数传入的是true,表示这个View是之前被回收过的。

/**
 * Add a view as a child and make sure it is measured (if necessary) and
 * positioned properly.
 *
 * @param child The view to add
 * @param position The position of this child
 * @param y The y position relative to which this view will be positioned
 * @param flowDown If true, align top edge to y. If false, align bottom
 *        edge to y.
 * @param childrenLeft Left edge where children should be positioned
 * @param selected Is this position selected?
 * @param recycled Has this view been pulled from the recycle bin? If so it
 *        does not need to be remeasured.
 */
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
        boolean selected, boolean recycled) {
    final boolean isSelected = selected && shouldShowSelector();
    final boolean updateChildSelected = isSelected != child.isSelected();
    final int mode = mTouchMode;
    final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
            mMotionPosition == position;
    final boolean updateChildPressed = isPressed != child.isPressed();
    final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
    // Respect layout params that are already in the view. Otherwise make some up...
    // noinspection unchecked
    AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
    if (p == null) {
        p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT, 0);
    }
    p.viewType = mAdapter.getItemViewType(position);
    if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
            p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
        attachViewToParent(child, flowDown ? -1 : 0, p);
    } else {
        p.forceAdd = false;
        if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            p.recycledHeaderFooter = true;
        }
        addViewInLayout(child, flowDown ? -1 : 0, p, true);
    }
    if (updateChildSelected) {
        child.setSelected(isSelected);
    }
    if (updateChildPressed) {
        child.setPressed(isPressed);
    }
    if (needToMeasure) {
        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                mListPadding.left + mListPadding.right, p.width);
        int lpHeight = p.height;
        int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);
    } else {
        cleanupLayoutState(child);
    }
    final int w = child.getMeasuredWidth();
    final int h = child.getMeasuredHeight();
    final int childTop = flowDown ? y : y - h;
    if (needToMeasure) {
        final int childRight = childrenLeft + w;
        final int childBottom = childTop + h;
        child.layout(childrenLeft, childTop, childRight, childBottom);
    } else {
        child.offsetLeftAndRight(childrenLeft - child.getLeft());
        child.offsetTopAndBottom(childTop - child.getTop());
    }
    if (mCachingStarted && !child.isDrawingCacheEnabled()) {
        child.setDrawingCacheEnabled(true);
    }
}

由于recycled=true,所以会调用attachViewToParent()方法,不同于第一次layout,第一次layout调用的是addViewInLayout()。如果是添加一个新View那么就需要调用addViewInLayout(),如果是将一个detach状态的View重新attach到ListView中,就需要调用attachViewToParent()方法,由于之前调用了detachAllViewsFromParent()方法,所以此时需要调用attachViewToParent()方法。

经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。


第二次layout.png

2.3、滑动加载更多数据

监听滑动部分的逻辑肯定是在onTouchEvent中进行的,下面看下AbsListView的onTouchEvent方法。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    if (!isEnabled()) {
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return isClickable() || isLongClickable();
    }
    final int action = ev.getAction();
    View v;
    int deltaY;
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);
    switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_DOWN: {
        mActivePointerId = ev.getPointerId(0);
        final int x = (int) ev.getX();
        final int y = (int) ev.getY();
        int motionPosition = pointToPosition(x, y);
        if (!mDataChanged) {
            if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
                    && (getAdapter().isEnabled(motionPosition))) {
                // User clicked on an actual view (and was not stopping a
                // fling). It might be a
                // click or a scroll. Assume it is a click until proven
                // otherwise
                mTouchMode = TOUCH_MODE_DOWN;
                // FIXME Debounce
                if (mPendingCheckForTap == null) {
                    mPendingCheckForTap = new CheckForTap();
                }
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
            } else {
                if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
                    // If we couldn't find a view to click on, but the down
                    // event was touching
                    // the edge, we will bail out and try again. This allows
                    // the edge correcting
                    // code in ViewRoot to try to find a nearby view to
                    // select
                    return false;
                }
 
                if (mTouchMode == TOUCH_MODE_FLING) {
                    // Stopped a fling. It is a scroll.
                    createScrollingCache();
                    mTouchMode = TOUCH_MODE_SCROLL;
                    mMotionCorrection = 0;
                    motionPosition = findMotionRow(y);
                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                }
            }
        }
        if (motionPosition >= 0) {
            // Remember where the motion event started
            v = getChildAt(motionPosition - mFirstPosition);
            mMotionViewOriginalTop = v.getTop();
        }
        mMotionX = x;
        mMotionY = y;
        mMotionPosition = motionPosition;
        mLastY = Integer.MIN_VALUE;
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        final int pointerIndex = ev.findPointerIndex(mActivePointerId);
        final int y = (int) ev.getY(pointerIndex);
        deltaY = y - mMotionY;
        switch (mTouchMode) {
        case TOUCH_MODE_DOWN:
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            // Check if we have moved far enough that it looks more like a
            // scroll than a tap
            startScrollIfNeeded(deltaY);
            break;
        case TOUCH_MODE_SCROLL:
            if (PROFILE_SCROLLING) {
                if (!mScrollProfilingStarted) {
                    Debug.startMethodTracing("AbsListViewScroll");
                    mScrollProfilingStarted = true;
                }
            }
            if (y != mLastY) {
                deltaY -= mMotionCorrection;
                int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
                // No need to do all this work if we're not going to move
                // anyway
                boolean atEdge = false;
                if (incrementalDeltaY != 0) {
                    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
                }
                // Check to see if we have bumped into the scroll limit
                if (atEdge && getChildCount() > 0) {
                    // Treat this like we're starting a new scroll from the
                    // current
                    // position. This will let the user start scrolling back
                    // into
                    // content immediately rather than needing to scroll
                    // back to the
                    // point where they hit the limit first.
                    int motionPosition = findMotionRow(y);
                    if (motionPosition >= 0) {
                        final View motionView = getChildAt(motionPosition - mFirstPosition);
                        mMotionViewOriginalTop = motionView.getTop();
                    }
                    mMotionY = y;
                    mMotionPosition = motionPosition;
                    invalidate();
                }
                mLastY = y;
            }
            break;
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        switch (mTouchMode) {
        case TOUCH_MODE_DOWN:
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            final int motionPosition = mMotionPosition;
            final View child = getChildAt(motionPosition - mFirstPosition);
            if (child != null && !child.hasFocusable()) {
                if (mTouchMode != TOUCH_MODE_DOWN) {
                    child.setPressed(false);
                }
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                final AbsListView.PerformClick performClick = mPerformClick;
                performClick.mChild = child;
                performClick.mClickMotionPosition = motionPosition;
                performClick.rememberWindowAttachCount();
                mResurrectToPosition = motionPosition;
                if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
                    final Handler handler = getHandler();
                    if (handler != null) {
                        handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
                                : mPendingCheckForLongPress);
                    }
                    mLayoutMode = LAYOUT_NORMAL;
                    if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                        mTouchMode = TOUCH_MODE_TAP;
                        setSelectedPositionInt(mMotionPosition);
                        layoutChildren();
                        child.setPressed(true);
                        positionSelector(child);
                        setPressed(true);
                        if (mSelector != null) {
                            Drawable d = mSelector.getCurrent();
                            if (d != null && d instanceof TransitionDrawable) {
                                ((TransitionDrawable) d).resetTransition();
                            }
                        }
                        postDelayed(new Runnable() {
                            public void run() {
                                child.setPressed(false);
                                setPressed(false);
                                if (!mDataChanged) {
                                    post(performClick);
                                }
                                mTouchMode = TOUCH_MODE_REST;
                            }
                        }, ViewConfiguration.getPressedStateDuration());
                    } else {
                        mTouchMode = TOUCH_MODE_REST;
                    }
                    return true;
                } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                    post(performClick);
                }
            }
            mTouchMode = TOUCH_MODE_REST;
            break;
        case TOUCH_MODE_SCROLL:
            final int childCount = getChildCount();
            if (childCount > 0) {
                if (mFirstPosition == 0
                        && getChildAt(0).getTop() >= mListPadding.top
                        && mFirstPosition + childCount < mItemCount
                        && getChildAt(childCount - 1).getBottom() <= getHeight()
                                - mListPadding.bottom) {
                    mTouchMode = TOUCH_MODE_REST;
                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                } else {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    final int initialVelocity = (int) velocityTracker
                            .getYVelocity(mActivePointerId);
                    if (Math.abs(initialVelocity) > mMinimumVelocity) {
                        if (mFlingRunnable == null) {
                            mFlingRunnable = new FlingRunnable();
                        }
                        reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                        mFlingRunnable.start(-initialVelocity);
                    } else {
                        mTouchMode = TOUCH_MODE_REST;
                        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                    }
                }
            } else {
                mTouchMode = TOUCH_MODE_REST;
                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
            }
            break;
        }
        setPressed(false);
        // Need to redraw since we probably aren't drawing the selector
        // anymore
        invalidate();
        final Handler handler = getHandler();
        if (handler != null) {
            handler.removeCallbacks(mPendingCheckForLongPress);
        }
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
        mActivePointerId = INVALID_POINTER;
        if (PROFILE_SCROLLING) {
            if (mScrollProfilingStarted) {
                Debug.stopMethodTracing();
                mScrollProfilingStarted = false;
            }
        }
        break;
    }
    case MotionEvent.ACTION_CANCEL: {
        mTouchMode = TOUCH_MODE_REST;
        setPressed(false);
        View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
        if (motionView != null) {
            motionView.setPressed(false);
        }
        clearScrollingCache();
        final Handler handler = getHandler();
        if (handler != null) {
            handler.removeCallbacks(mPendingCheckForLongPress);
        }
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
        mActivePointerId = INVALID_POINTER;
        break;
    }
    case MotionEvent.ACTION_POINTER_UP: {
        onSecondaryPointerUp(ev);
        final int x = mMotionX;
        final int y = mMotionY;
        final int motionPosition = pointToPosition(x, y);
        if (motionPosition >= 0) {
            // Remember where the motion event started
            v = getChildAt(motionPosition - mFirstPosition);
            mMotionViewOriginalTop = v.getTop();
            mMotionPosition = motionPosition;
        }
        mLastY = y;
        break;
    }
    }
    return true;
}

这个方法中的代码就非常多了,但是我们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是ACTION_MOVE这个动作。
可以看到ACTION_MOVE中有个switch语句,当手指在屏幕上滑动时的mTouchMode=TOUCH_MODE_SCROLL,在其中会调用trackMotionScroll()方法。

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
    final int childCount = getChildCount();
    if (childCount == 0) {
        return true;
    }
    final int firstTop = getChildAt(0).getTop();
    final int lastBottom = getChildAt(childCount - 1).getBottom();
    final Rect listPadding = mListPadding;
    final int spaceAbove = listPadding.top - firstTop;
    final int end = getHeight() - listPadding.bottom;
    final int spaceBelow = lastBottom - end;
    final int height = getHeight() - getPaddingBottom() - getPaddingTop();
    if (deltaY < 0) {
        deltaY = Math.max(-(height - 1), deltaY);
    } else {
        deltaY = Math.min(height - 1, deltaY);
    }
    if (incrementalDeltaY < 0) {
        incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
    } else {
        incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
    }
    final int firstPosition = mFirstPosition;
    if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
        // Don't need to move views down if the top of the first position
        // is already visible
        return true;
    }
    if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
        // Don't need to move views up if the bottom of the last position
        // is already visible
        return true;
    }
    final boolean down = incrementalDeltaY < 0;
    final boolean inTouchMode = isInTouchMode();
    if (inTouchMode) {
        hideSelector();
    }
    final int headerViewsCount = getHeaderViewsCount();
    final int footerViewsStart = mItemCount - getFooterViewsCount();
    int start = 0;
    int count = 0;
    if (down) {
        final int top = listPadding.top - incrementalDeltaY;
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getBottom() >= top) {
                break;
            } else {
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    mRecycler.addScrapView(child);
                }
            }
        }
    } else {
        final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getTop() <= bottom) {
                break;
            } else {
                start = i;
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    mRecycler.addScrapView(child);
                }
            }
        }
    }
    mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
    mBlockLayoutRequests = true;
    if (count > 0) {
        detachViewsFromParent(start, count);
    }
    offsetChildrenTopAndBottom(incrementalDeltaY);
    if (down) {
        mFirstPosition += count;
    }
    invalidate();
    final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down);
    }
    if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
        final int childIndex = mSelectedPosition - mFirstPosition;
        if (childIndex >= 0 && childIndex < getChildCount()) {
            positionSelector(getChildAt(childIndex));
        }
    }
    mBlockLayoutRequests = false;
    invokeOnItemScrollListener();
    awakenScrollBars();
    return false;
}

上面方法中代码较多,主要功能如下:

  • 通过边界判断,将滑出屏幕的View通过调用RecyleBin.addScrapView()对废弃的View进行缓存。并将该View从ListView中移除。
  • 滑入屏幕的View通过调用fillGap()进行加载。fillGap()的具体实现在ListView中。
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());
    }
}

down=true表示向下滑动,反之为向上滑动。在向下滑动时调用fillDown(),向上滑动时调用fillUp()方法。这两个方法的内部是通过循环向ListView中添加View,主要逻辑仍然在makeAndAddView()方法中。

/**
 * Obtain the view and add it to our list of children. The view can be made
 * fresh, converted from an unused view, or used as is if it was in the
 * recycle bin.
 *
 * @param position Logical position in the list
 * @param y Top or bottom edge of the view to add
 * @param flow If flow is true, align top edge to y. If false, align bottom
 *        edge to y.
 * @param childrenLeft Left edge where children should be positioned
 * @param selected Is this position selected?
 * @return View that was added
 */
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    View child;
    if (!mDataChanged) {
        // Try to use an exsiting view for this position
        child = mRecycler.getActiveView(position);
        if (child != null) {
            // Found it -- we're using an existing child
            // This just needs to be positioned
            setupChild(child, position, y, flow, childrenLeft, selected, true);
            return child;
        }
    }
    // Make a new view for this position, or convert an unused view if possible
    child = obtainView(position, mIsScrap);
    // This needs to be positioned and measured
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    return child;
}

这里仍然先调用RecyleBin.getActiveView()从缓存中获取View,由于在第二次Layout时已经从mActiveViews中获取过View,而mActiveViews中的数据是不能复用的,故此时返回null。

由于getActiveView()返回null,此时会调用obtainView()。

/**
 * Get a view and have it show the data associated with the specified
 * position. This is called when we have already discovered that the view is
 * not available for reuse in the recycle bin. The only choices left are
 * converting an old view or making a new one.
 * 
 * @param position
 *            The position to display
 * @param isScrap
 *            Array of at least 1 boolean, the first entry will become true
 *            if the returned view was taken from the scrap heap, false if
 *            otherwise.
 * 
 * @return A view displaying the data associated with the specified position
 */
View obtainView(int position, boolean[] isScrap) {
    isScrap[0] = false;
    View scrapView;
    scrapView = mRecycler.getScrapView(position);
    View child;
    if (scrapView != null) {
        child = mAdapter.getView(position, scrapView, this);
        if (child != scrapView) {
            mRecycler.addScrapView(scrapView);
            if (mCacheColorHint != 0) {
                child.setDrawingCacheBackgroundColor(mCacheColorHint);
            }
        } else {
            isScrap[0] = true;
            dispatchFinishTemporaryDetach(child);
        }
    } else {
        child = mAdapter.getView(position, null, this);
        if (mCacheColorHint != 0) {
            child.setDrawingCacheBackgroundColor(mCacheColorHint);
        }
    }
    return child;
}

这里会调用RecyleBin.getScrapView()从废弃缓存中获取View,此时是可以得到的,因为在上面trackMotionScroll()方法中已经将滑出屏幕的View添加到废弃缓存中了。从obtainView()的逻辑可知,ListView中的子View来来回回就那么几个,滑出屏幕的废弃View很快就会被滑入屏幕的View复用。这样即使再多的数据,也不会导致OOM,而且内边占用几乎不变。

当viewTypeCount=1时,废弃缓存mCurrentScrap的大小最大只能为1,当被新滑入屏幕的View复用后它又会变成0。
而当viewTypeCount=2时,废弃缓存mCurrentScrap的大小最大为一屏显示的item数量,因为在不同的viewType有自己独立的缓存,不能互相复用的。

这里从缓存中获取了一个废弃的View,然后传递给adapter.getView()的第二个参数,即convertView,由于convertView不等于null,故无需inflate布局只更新数据即可。

紧接着将obtainView()得到的View,传递给setupChild(),在其中将View重新attach到ListView上。

2.4、缓存的获取

从上面的第一次layout、第二次layout、滑动加载更多数据的分析可知,最主要的方法就是makeAndAddView(),它主要是完成子View的填充,而这个子View可能是来自缓存,也可能是inflate加载进来的,具体的逻辑如下图。

image.png

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

推荐阅读更多精彩内容