问题阐释
在项目的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的情况。