探索ViewPager使用Integer.MAX_VALUE实现无限轮播引发的问题

探索ViewPager使用Integer.MAX_VALUE实现无限轮播引发的问题

问题阐释

在项目的bugly记录中一直存在首页viewpager的ANR错误,每个版本至少1k+的次数。项目首页轮播viewpager的实现方式发现是将viewpagerAdapter的getCount返回为Integer.MAX_VALUE,初次加载设置index为5,也就是说还是一个只能向前无限(小于Integer.MAX_VALUE)滑动的viewpager,不过如果一开始就将初始位置设置为index为Integer.MAX_VALUE>>1来实现左右方向均可大规模的滑动,就会导致。现在来看一下将getCount设置为Integer.MAX_VALUE为什么会造成ANR的情况。

第一种情况

先分析第一种情况,将getCount设置为Integer.MAX_VALUE,当初始位置为Integer.MAX_VALUE>>1的时候,此时刷新页面将导致ANR的问题。
适配器代码如下:

public class CustomWrongBannerAdapter extends PagerAdapter {
    private Context mContext;
    private List<Drawable> mDrawableList;

    public CustomWrongBannerAdapter(Context context, List<Drawable> drawableList) {
        mContext = context;
        mDrawableList = drawableList;
    }

    @Override
    public int getCount() {
        return Integer.MAX_VALUE;
    }

    @Override
    public void destroyItem(ViewGroup container, int position,@NonNull Object object) {
        container.removeView((View) object);
    }

    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        View view = View.inflate(mContext, R.layout.vp_item, null);
        ImageView iv = view.findViewById(R.id.iv_bg);
        iv.setImageDrawable(mDrawableList.get(position % mDrawableList.size()));
        container.addView(view);
        iv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext, position % mDrawableList.size() + "", Toast.LENGTH_SHORT).show();
            }
        });
        return view;
    }

    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
        return view == object;
    }
}

Activity内的viewpager设置如下:

		vp = findViewById(R.id.vp);
        mDrawableList = new ArrayList<>();
        mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_1));
        mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_2));
        mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_3));
        mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_4));
        mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_3));
        mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_2));
        mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_1));
        mCustomBannerAdapter = new CustomWrongBannerAdapter(this, mDrawableList);
        vp.setAdapter(mCustomBannerAdapter);
        vp.setCurrentItem(Integer.MAX_VALUE >> 1);

极为简单的一个viewpager,轮播的实现代码就不赘述了,Handler实现。
viewpager的内容赋值是在adapter中的instantiateItem方法内进行数据源的处理,因此如果此时刷新数据,其实是不会影响到当前展示在屏幕里的内容的,同样还有左右两个缓存的viewpager页面,这三个页面都不会因为notifyDataSetChange刷新数据而发生改变。因此最简单的实现方式就是重新setAdapter来实现页面的刷新。

    private final Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            if (!isInit) {
                mDrawableList.clear();
                mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_2));
                mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_3));
                mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_4));
                mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_3));
                mDrawableList.add(ContextCompat.getDrawable(WrongViewPagerActivity.this, R.drawable.pic_2));
                int currentItem = vp.getCurrentItem();
                vp.setAdapter(mCustomBannerAdapter);
                vp.setCurrentItem(currentItem);
                isInit = true;
            }
        }
    };

通过Handler延迟几秒模拟刷新操作,此时就会发现页面因为ANR导致不响应,现在分析原因。
viewpager的绘制过程主要是在populate这个方法中进行,其中与ANR最相关的一段代码如下:

			float extraWidthLeft = 0.f;
            int itemIndex = curIndex - 1;
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            final int clientWidth = getClientWidth();
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
            	//1.销毁
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    if (ii == null) {
                        break;
                    }
                    if (pos == ii.position && !ii.scrolling) {
                        mItems.remove(itemIndex);
                        mAdapter.destroyItem(this, pos, ii.object);
                        if (DEBUG) {
                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                    + " view: " + ((View) ii.object));
                        }
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                //2.找到左侧的ii
                } else if (ii != null && pos == ii.position) {
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                //3.创建ii
                } else {
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }

    上面这段代码是为当前页面的左右两个页面设置缓存的,第一个if判断extraWidthLeft >= leftWidthNeeded && pos < startPos是为左边超出缓存范围的元素进行销毁操作,startPos的赋值为Math.max(0, mCurItem - pageLimit);也就是从当前item的左侧pageLimit个位置开始,pageLimit默认为1。超出这个范围内的元素会被销毁,mAdapter.destroyItem函数名也说明了这个分支的主要功能。
    第二个判断它的作用就是判断从mitems中得到的ii是否满足pos == ii.position,是则继续取上一个ii继续for循环(itemIndex–),直到满足pos < startPos。代码中找不到左边的页面,也不满足pos == ii.position,就会进入第三个分支为mItems添加新的元素,也就是为当前页面添加左侧缓存的页面。整个循环过程从mCurItem - 1开始,每次递减一开始,因此初次进入该方法会先调用第三个if再调用第一个if,因为一开始除了当前展示在页面中的元素外没有其他元素,进入第三个if设置左侧的页面,之后pos–进入pos < startPos,去查找需要被销毁的元素。之后再次刷新会先调用第二个if在当前页的左侧找到元素,如果存在,则进入第一个if判断是否满足pos == ii.position进行销毁操作,如果不存在则进入第三个if判断创建左侧的缓存对象。
    右侧的缓存机制与左侧相同。
解释完populate的主要实现,回到viewpager.setAdapter。

    public void setAdapter(@Nullable PagerAdapter adapter) {
    	//1.
        if (mAdapter != null) {
            mAdapter.setViewPagerObserver(null);
            mAdapter.startUpdate(this);
            for (int i = 0; i < mItems.size(); i++) {
                final ItemInfo ii = mItems.get(i);
                mAdapter.destroyItem(this, ii.position, ii.object);
            }
            mAdapter.finishUpdate(this);
            mItems.clear();
            removeNonDecorViews();
            mCurItem = 0;
            scrollTo(0, 0);
        }

        final PagerAdapter oldAdapter = mAdapter;
        mAdapter = adapter;
        mExpectedAdapterCount = 0;

        if (mAdapter != null) {
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            mAdapter.setViewPagerObserver(mObserver);
            mPopulatePending = false;
            final boolean wasFirstLayout = mFirstLayout;
            mFirstLayout = true;
            mExpectedAdapterCount = mAdapter.getCount();
            if (mRestoredCurItem >= 0) {
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                setCurrentItemInternal(mRestoredCurItem, false, true);
                mRestoredCurItem = -1;
                mRestoredAdapterState = null;
                mRestoredClassLoader = null;
            } else if (!wasFirstLayout) {
                populate();
            } else {
                requestLayout();
            }
        }

        // Dispatch the change to any listeners
        if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {
            for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {
                mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);
            }
        }
    }

    初次调用setAdapter不会进入第一个if判断,正常的设置流程,进入requestLayout进行绘制工作。
    问题出现在第二次调用setAdapter,会进入第一个if分支,进行mItems的清空,然后将mCurItem设置为0,因为不是初次绘制,所以之后进入if (!wasFirstLayout) {populate();}进行初始化mItems的元素对象,mCurItem为0,也就是说这个流程结束,mItems中会存有两个元素且这两个元素的position字段的内容分别为0和1。
    setAdapter完毕之后,我们调用vp.setCurrentItem(currentItem);将viewpager定位到我们刷新之前的位置。setCurrentItem方法最终调用populate对我们设置的currentItem进行新建mItems中的元素对象。currentItem的值为Integer.MAX_VALUE >> 1,根据我们之前对populate的分析,在进入销毁判断前,我们除了设置currentItem的对象,还创建了currentItem左侧的一个缓存对象。因此此时mCurItem存在四个对象,他们的position的内容分别为0,1,Integer.MAX_VALUE >> 1,Integer.MAX_VALUE >> 1 -1。那么接下来要销毁的话,自然是要把0,1这两个多余对象销毁掉,于是ANR就发生了,我们进入populate的第一个判断分支,我们发现销毁元素的一个先决条件是ii != null并且if (pos == ii.position && !ii.scrolling)。需要让遍历的pos等于我们要销毁对象的position的值,已知pos的起点数值为currentItem-1也就是Integer.MAX_VALUE >> 1 -1。所以我们为了销毁0,1这两个元素,需要从Integer.MAX_VALUE >> 1 -1开始遍历,一次递减1,经过2147483646次遍历直到pos==1才可以把ii给销毁掉,这个过程无疑是十分耗时的,也就造成了我们应用ANR的情况。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值