Android面试---ListView原理及fling分析

Android面试---ListView原理及fling分析_第1张图片

为了节约你的时间,上面的图是整个文章的最终图,包括了以下部分问题的答案。

目录

1.为什么需要使用Adapter?

RecycleBin是什么? 

2.ListView的绘制过程是怎样的?

3.ListView的回收机制是怎么样的?为什么可以显示100个View而不崩呢?

4.ListView里面的观察者模式是什么?

5.惯性滑动(fling)是怎么实现的?

0.基本使用

获取ListView视图对象,设置adapter。

ListView lv = new ListView(context);
lv.setAdapter(new SimpleAdapter);

1.为什么需要使用Adapter?

ListView集成自absListView,同级的还有GridView。实际上我们可以发现,ListView是一个ViewGroup,提供把每个子View按照纵向布局在一起,然后提供滑动和fling功能。

从下面的图我们可以看到,左边是ListView,右边是data,中间是adapter,在这里ListView并不关心数据(data),是从数据库的cursor获取还是是从数组里面获取,我们只需要通过适配器Adapter统一一个协议就行。

试想一下不管是GridView还是ListView又或者是RecyclerView,无一不是需要知道总共几个子View,获取每个子View的视图,如果有多种类型的子View,还需要ItemViewType。所以我们其实就可以定义几个方法来获取这几个值,你看getView(获取子视图)、getCount(总共有几个子视图)、getItemViewType(每个子视图的type),这几个方法熟悉吗??我们实现这几个接口就能用adapter适配数据了。

那么有同学说,既然adapter隔离了数据,那么数据变动刷新ListView怎么操作呢?

你看1.1 register先注册观察者、1.2 notifyDataSetChanged 1.3 notifyChanged 不是典型的观察者模式吗? 这样就实现了adatper通知ListView刷新的功能了。

Android面试---ListView原理及fling分析_第2张图片

RecycleBin是什么? 

RecycleBin是用于ListView子元素的缓存。里面有一个View[] mActiviesViews和ArrayList[] mScrapViews。mActiviesViews用于存放ListView在屏幕内的对象,mScrapViews用于存放ListView移出屏幕的对象。注意mScrapViews是一个数组,索引是ItemType,元素是ArrayList,所以说视图是按照ItemType为维度回收的,所以复用也是ItemType维度复用的

图片左边是RecycleBin,右边是ListView,ListView中的所有元素,都是存在mActiviesViews里面一份的,所以当ListView刷新时,可以很快速的从内存中取出View来布局。当ListView上滑并且元素0完全移出屏幕时,会回收到RecycleBin的mScrapViews里面的ItemType类型为索引的ArrayList里面。

Android面试---ListView原理及fling分析_第3张图片

2.ListView的绘制过程是怎样的?

ListView也是ViewGroup,所以它的流程也是measure、layout、draw。ViewGroup的measure里面其实就是调用子布局的mesure,没什么特殊的。draw的话,其实也是调用子布局的绘制方法。所以我们分析layout过程,layout过程分为2种情况。

  1. 第一次layout。第一次layout,在RecycleBin获取不到mActiviesViews缓存,所以调用getView()直接朝adapter要View,要来了View后,然后逐个layout布局。
  2. 第N次layout。在RecycleBin获取到mActiviesViews缓存,然后逐个layout布局

3.ListView的回收机制是怎么样的?为什么可以显示100个View而不崩呢?

看图!!!

Android面试---ListView原理及fling分析_第4张图片

当我们向上滑动时,ListView的onTouchEvent中的move判断条件调用,判断元素0的getBottom()小于滑动距离,也就是元素0即将移出屏幕,这个时候ListView认为元素0没有用了,然后就把元素0放入RecycleBin的mScrapViews里面。然后判断元素6要进入屏幕,就从RecycleBin的mScrapViews的mScrapViews里面取出元素0刚刚的视图元素,作为元素6布局出来。

所以我们能够得出结论:

  1. 因为存在复用,所以如果元素0的图片会莫名出现在元素6的图片里面。元素0的图片还没有下载完,元素6出来之后,可能会设置成元素0的图片。所以我们一般通过设置tag来排除不对应的图片下载和默认占位图来解决图片错位的问题。
  2. 由于我们重复的模板是从RecycleBin缓存里面取出来,所以就算要加载100个视图,其实我只要有一屏幕卡片的缓存就行了。所以就算显示100个数据,ListView也不会崩。

4.ListView里面的观察者模式是什么?

试想一下,我们刷新ListView是做的?没有直接调用ListView的方法吧,而是调用adapter.notifyDataSetChanged,那么adapter又是为什么能够刷新ListView的呢?

想想一下,如果adapter是被观察对象,它的数据变动回去通知观察者,那么只要把ListView作为adapter的观察者不就行了吗?

看图!!!

Android面试---ListView原理及fling分析_第5张图片

实际上可以看到adapter里面有一个dataSetObservable,然后第一步ListView在adapter上面注册一个回调,之后数据变化的时候,adapter调用notifyDataSetChanged,实际上就会调用回调的notifyChanged方法,然后就实现了adapter数据变化通知ListView刷新的功能。下面我们来具体分析下代码是怎么实现的?

首先我们从mDataSetObserver对象开始看,可以看到它是在absListView在onAttachedToWindow中注册的,我们看下mDataSetObserver对象的代码。

 protected void onAttachedToWindow() {
        ...
        if (mAdapter != null && mDataSetObserver == null) {
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);
            ...  
        }
    }

 可以看到他是调用他父类AdapterView.AdapterDataSetObserver的方法,我们再追进去一下。

    class AdapterDataSetObserver extends AdapterView.AdapterDataSetObserver {
        @Override
        public void onChanged() {
            super.onChanged();
            ...    
        }
        ...
    }

可以看到,最后是调用ListView的requestLayout(),那么requestLayout又是怎么实现刷新的呢?

@Override
public void onChanged() {
   ...
   requestLayout();
}

我们看到requestLayout里面去调用mParent的requestLayout,那么父布局又会去查找他的mParent,然后执行requestLayout ,最终调用到DecorView的requestLayout。根布局为什么叫DecorView,我们在5分钟告诉你,Activity的视图绘制流程(onMeasure、onLayout、onDraw的调用和参数解释)讲过。在这个过程中,请注意第6、7行代码, 我们把关键字设置为PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED,这是为了测量和布局的时候不走缓存。然后DecorView的mParent又是什么呢?其实是ViewRootImpl。

这篇文章关于View中mParent的来龙去脉介绍了

  1. DecorView中的mParent来源于ViewRootImpl#setView()。
  2. 普通View的mParent来源于ViewGroup的addViewInnner()。

所以就其实requestLayout最后就成了调用ViewRootImpl的requestLayout方法。我们去看看这个方法。 

public void requestLayout() {
        //清除测量缓存
        if (mMeasureCache != null) mMeasureCache.clear();
       ...省略
        //修改关键字
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            //很重要。。
            mParent.requestLayout();
        }
    }

可以看到在 5分钟告诉你,Activity的视图绘制流程(onMeasure、onLayout、onDraw的调用和参数解释中,第6行ViewRootImpl.scheduleTraversals()实际会调用performTraversals,然后调用performMeasure、performLayout、performDraw,然后执行刷新操作。 

 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

5.惯性滑动(fling)是怎么实现的?

我们思考一下惯性滑动是怎么操作的?我们手指先按住ListView,然后快速的滑动,然后手指离开ListView,这个时候ListView就会自动滑动,直到停止或者触碰到边界,触发边界的水波纹效果。所以我们可以看到,其实是在手指抬起时触发fling操作的,那么我们就去onTouchEvent里面的Action_UP看一下。

可以看到事件处理里面处理了ACTION_UP和ACTION_POINTER_UP,ACTION_POINTER_UP是第二个手指的处理,这里我们只分析ACTION_UP一个手指的情况。再跟进onTouchUp(ev)看下。

public boolean onTouchEvent(MotionEvent ev) {
       ....省略
        switch (actionMasked) {
           ....
            case MotionEvent.ACTION_UP: {
                onTouchUp(ev);
                break;
            }
            case MotionEvent.ACTION_POINTER_UP: {
                onSecondaryPointerUp(ev);
                ....
                break;
            }

            }
        }
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(vtev);
        }
        return true;
    }

我们可以看到代码比较多,我们简单分析下。

private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        ....
        case TOUCH_MODE_SCROLL:
            if (childCount > 0) {
            ....
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    final int initialVelocity = (int)
                            (velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale);
                    //是否满足惯性滑动的速度并且没有滑动边界上
                    boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity;
                    if (flingVelocity &&
                            !((mFirstPosition == 0 &&
                                    firstChildTop == contentTop - mOverscrollDistance) ||
                              (mFirstPosition + childCount == mItemCount &&
                                    lastChildBottom == contentBottom + mOverscrollDistance))) {
                        //不分发嵌套fling
                        if (!dispatchNestedPreFling(0, -initialVelocity)) {
                            //很关键
                            if (mFlingRunnable == null) {
                                mFlingRunnable = new FlingRunnable();
                            }                            
          reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                            mFlingRunnable.start(-initialVelocity);
                            dispatchNestedFling(0, -initialVelocity, true);
                        } else {
                            mTouchMode = TOUCH_MODE_REST;
                       reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                        }
                    } else {
                        //已经在边界上了,直接当做重置状态
                        mTouchMode = TOUCH_MODE_REST;
                     reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                        if (mFlingRunnable != null) {
                            mFlingRunnable.endFling();
                        }
                        if (mPositionScroller != null) {
                            mPositionScroller.stop();
                        }
                        if (flingVelocity && !dispatchNestedPreFling(0, -initialVelocity)) {
                            dispatchNestedFling(0, -initialVelocity, false);
                        }
                    }
                }
            } else {
                //没有子元素了,设置到Reset状态
                mTouchMode = TOUCH_MODE_REST;
                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
            }
            break;

        case TOUCH_MODE_OVERSCROLL:
            .....
            //速度大于最小速度
            if (Math.abs(initialVelocity) > mMinimumVelocity) {
                mFlingRunnable.startOverfling(-initialVelocity);
            } else {
                mFlingRunnable.startSpringback();
            }

            break;
        }

第7行到 13行,主要是判断速度有没有达到惯性滑动的速度。

!((mFirstPosition == 0 && firstChildTop == contentTop - mOverscrollDistance) ||
 (mFirstPosition + childCount == mItemCount && lastChildBottom == contentBottom + mOverscrollDistance))

第14行到17行,其实是惯性滑动的条件,我们可以看到第一个条件 mFirstPosition==0代表现在ListView处于第一个位置,firstChildTop是第一个元素顶部距离ListView顶部的距离,那么contentTop又是mListPadding.top,那么mListPadding是什么?

final int firstChildTop = getChildAt(0).getTop(); //第一个元素顶部距离屏幕顶部的距离
final int contentTop = mListPadding.top; //0
mFirstPosition == 0 && firstChildTop == contentTop - mOverscrollDistance

可以看到是在onMeasure给mListPadding赋值的,跟到最后发现结果为0,所以可以看到就是判断ListView是否滑到顶了。同样,下面的条件是判断是否滑到底部了。

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        .....
        final Rect listPadding = mListPadding;
        listPadding.left = mSelectionLeftPadding + mPaddingLeft;
        listPadding.top = mSelectionTopPadding + mPaddingTop;
        listPadding.right = mSelectionRightPadding + mPaddingRight;
        listPadding.bottom = mSelectionBottomPadding + mPaddingBottom;
        .....
    }

22行到25行,是在不嵌套fling的情况下,直接调用mFlingRunnable去执行。我们看下mFlingRunnable.start(-initialVelocity)代码。 

void start(int initialVelocity) {
            int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
            mLastFlingY = initialY;
            mScroller.setInterpolator(null);
            mScroller.fling(0, initialY, 0, initialVelocity,
                    0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
            mTouchMode = TOUCH_MODE_FLING;
            mSuppressIdleStateChangeCall = false;
            postOnAnimation(this);
            .......
        }

可以看到第5行,mScroller.fling方法,mScroller又是什么类型呢?可以看到是OverScroller类型,它是Scroller的高级版,可以支持边界的绘制。所以说惯性滑动,其实就是OverScroller的fling方法来实现的。

 

完整Android学习路径 请戳我的Android学习之旅(持续更新中...)

从源码角度分析Activity的生命周期怎么触发的(onCreate onStart onResume onPause onStop onDestroy)(附测试代码)

基于AIDL的 Activity、Service跨进程观察者模式实现与源码解读

走进源码,Android面试最常见Handler、Looper、Message问题总结与解答

Android面试---ListView原理及fling分析

5分钟告诉你,Activity的视图绘制流程(onMeasure、onLayout、onDraw的调用和参数解释)

参考:

关于View中mParent的来龙去脉

Android ListView工作原理完全解析,带你从源码的角度彻底理解

你可能感兴趣的:(Android)