最近开发的项目出现界面莫名其秒的卡住,直到发生`ANR`异常退出的问题。
问题排查许久,才发现是由于使用的`ViewPager`导致的。
在`ViewPager`中使用`setCurrentItem`长距离设置当前显示的位置的时候,比如`0->600`,`600->1000`,`1000->500` 这样的长距离跳转。会导致函数卡住在
void populate(int newCurrentItem)
函数中很长时间, 甚至一直不能跳出循环。具体卡住的的循环代码如下:
for(int pos = this.mCurItem + 1; pos < N; ++pos) {
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
this.mItems.remove(itemIndex);
this.mAdapter.destroyItem(this, pos, ii.object);
ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthRight += ii.widthFactor;
++itemIndex;
ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
} else {
ii = this.addNewItem(pos, itemIndex);
++itemIndex;
extraWidthRight += ii.widthFactor;
ii = itemIndex < this.mItems.size() ? (ViewPager.ItemInfo)this.mItems.get(itemIndex) : null;
}
}
上述代码中循环体会一直循环到`N`结束为止。而这个`N`是通过
int N = this.mAdapter.getCount();
赋值的。恰好,我们的代码中返回的是`Integer.MAX_VALUE`。咨询了同事,当时设置这个数值的目的是为了解决循环滚动展示才设置的。代码是直接抄的 Android无限广告轮播 自定义BannerView / 如何优雅的实现一个可复用的 PagerAdapter。
通过Google搜索关键词 `BannerAdapter extends PagerAdapter` 可以找到类似的代码。
具体的复现`BUG`的例子代码如下:
package com.mobibrw.viewpager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import java.util.Random;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ViewPager viewPager = findViewById(R.id.viewPager);
viewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return Integer.MAX_VALUE;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@Override
public Object instantiateItem(ViewGroup container, final int position) {
final View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.pager_view, null);
final TextView tv = view.findViewById(R.id.text);
tv.setText("" + position);
container.addView(view);
return view;
}
});
final Button btnCmd = findViewById(R.id.btnCmd);
btnCmd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Random rdm = new Random(System.currentTimeMillis());
int rd = Math.abs(rdm.nextInt())%700 + 1;
viewPager.setCurrentItem(rd);
}
});
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"/>
<Button
android:id="@+id/btnCmd"
android:text="@string/change_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
/>
</android.support.constraint.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.53" />
</android.support.constraint.ConstraintLayout>
完整的项目代码可以点击此处下载 ViewPager / ViewPagerAppCompatV7。项目编译完成之后,多点击几次 `Change Size` 按钮就可以复现界面长时间卡住的情况。第二个项目是支持`appcompat-v7:18.0.0`的版本,目的是观察各个版本的代码是否存在不同,结论是,都一样。
这个问题目前的解决方法就是保证`ViewPager`中的数据不要太多,比如底层分页显示,每页不要太多。另外就是不要大距离跳转,否则会有很多问题。
如果能把`android.support`升级到`androidx`的话,可以试试最新的`ViewPager2`,最新的`ViewPager2`是用`RecyclerView `实现的,应该不会有以前的问题了。
这个问题,我已经向Google提交了BUG,估计最后的答复也是要升级到`androidx`。
参考链接
Android无限广告轮播 - ViewPager源码分析