复现ANR的场景
先定义一个适配器继承自PagerAdapter
public class ViewPageAdapter extends PagerAdapter {
private static final String TAG = "ViewPageAdapter";
//轮播图片链接
private List imgUrls;
//图片数量
private int count;
private Context context;
//用来加载图片
private ImageLoader imageLoader;
public ViewPageAdapter(Context context, ImageLoader imageLoader, List imgUrls) {
this.context = context;
this.imageLoader = imageLoader;
this.imgUrls = imgUrls;
count = imgUrls.size();
}
@Override
public int getCount() {
//注释1处,返回Integer.MAX_VALUE
return Integer.MAX_VALUE;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
return view == o;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
Log.d(TAG, "instantiateItem: position = " + position);
//注释2处 取余获取正确的图片链接
position %= count;
ImageView imageView = new ImageView(context);
imageView.setLayoutParams(new RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
//使用Glide加载图片
imageLoader.displayImage(context, imgUrls.get(position), imageView);
//这里的container就是ViewPager,调用ViewPager的addView方法,添加子View
container.addView(imageView);
return imageView;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View) object);
}
}
在注释1处,getCount方法返回Integer.MAX_VALUE。
@Override
public int getCount() {
//注释1处,返回Integer.MAX_VALUE
return Integer.MAX_VALUE;
}
注释2处,取余获取正确的图片链接
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
Log.d(TAG, "instantiateItem: position = " + position);
//注释2处 取余获取正确的图片链接
position %= count;
//...
}
使用适配器
private void initMultiBanner() {
List<String> multiImgs = new ArrayList<>();
multiImgs.add(Images.imageUrls[0]);
multiImgs.add(Images.imageUrls[1]);
multiImgs.add(Images.imageUrls[2]);
ViewPageAdapter adapter = new ViewPageAdapter(this, new GlideImageLoader(), multiImgs);
binding.viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
currentPosition = position;
Log.d(TAG, "onPageSelected: position = " + position);
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
//注释1处
binding.viewPager.setAdapter(adapter);
int middle = Integer.MAX_VALUE >> 1;
//取divisiblePosition为可以整除multiImgs的size的值
int divisiblePosition = middle - (middle % multiImgs.size());
binding.viewPager.setCurrentItem(divisiblePosition);
}
我们重点看一下上面方法的注释1处
binding.viewPager.setAdapter(adapter);
int middle = Integer.MAX_VALUE >> 1;
//取divisiblePosition为可以整除multiImgs的size的值
int divisiblePosition = middle - (middle % multiImgs.size());
binding.viewPager.setCurrentItem(divisiblePosition);
我们这样做的目的是为了让ViewPager可以左右无限切换实现轮播图。(大约可以切换Integer.MAX_VALUE 一半的次数),并且显示第一张轮播图。
举个例子
- 假设轮播图有3张,Integer.MAX_VALUE为100
- middle = Integer.MAX_VALUE>>1=50
- divisiblePosition = 50 - (50 % 3)= 50 - 2 =48,此时显示的正好是第一张图。
到现在还是没有问题的,下面我们手动切换一下ViewPager显示的item。
binding.btnChangeCurrentItem.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
//注释1处,设置ViewPager当前item为0
binding.viewPager.setCurrentItem(0);
}
}
);
在上面注释1处,我们调用ViewPager的setCurrentItem方法,显示第一个item。然后我们点击几次返回键,这时候我们发现应用就ANR了。
其实我们猜也猜得到为什么ANR,肯定是因为从divisiblePosition到0之间有一个for循环执行次数太多导致的。(Integer.MAX_VALUE >> 1 = 1073741823,十亿多,这可是10个小目标啊,哈哈)。
接下来我们分析一下ANR的具体原因。首先我们先根据Android ANR 定位与分析这篇文章中的方法找到ANR所在的具体类和行数。

我们打开代码一看,在ViewPager的populate方法中果然有一个for循环。

在这个例子中,我们是让ViewPager的item减小了。我们再试试增大ViewPager的item。
binding.btnChangeCurrentItem.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
//注释1处,设置ViewPager当前item为Integer.MAX_VALUE-1
binding.viewPager.setCurrentItem(Integer.MAX_VALUE-1);
}
}
);
同样也会ANR,如下图所示

这次是在1171行,我们打开代码看一看,ViewPager的populate方法中也是一个for循环。

接下来我们就一第一种ANR进行分析。
首先看一下ViewPager的setAdapter方法精简版
public void setAdapter(@Nullable PagerAdapter adapter) {
//...
final PagerAdapter oldAdapter = mAdapter;
mAdapter = adapter;
mExpectedAdapterCount = 0;
if (mAdapter != null) {
if (mObserver == null) {
mObserver = new PagerObserver();
}
mAdapter.setViewPagerObserver(mObserver);
mPopulatePending = false;
//默认mFirstLayout为true
final boolean wasFirstLayout = mFirstLayout;
mFirstLayout = true;
mExpectedAdapterCount = mAdapter.getCount();
if (!wasFirstLayout) {
populate();
} else {
//注释1处,调用requestLayout方法
requestLayout();
}
}
//...
}
注释1处,当我们第一次调用ViewPager的setAdapter方法时,mFirstLayout为true,会调用requestLayout方法。调用requestLayout方法会导致View重新走measure,layout,draw等方法。
我们调用完ViewPager的setAdapter方法,又调用了ViewPager的setCurrentItem方法。
//在这个例子中,我们传入的方法参数是1073741823
public void setCurrentItem(int item) {
mPopulatePending = false;
setCurrentItemInternal(item, !mFirstLayout, false);
}
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
//smoothScroll为false
setCurrentItemInternal(item, smoothScroll, always, 0);
}
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
//...
//adapter的getCount方法返回值会影响item
if (item < 0) {
item = 0;
} else if (item >= mAdapter.getCount()) {
item = mAdapter.getCount() - 1;
}
//mOffscreenPageLimit默认为1
final int pageLimit = mOffscreenPageLimit;
//条件满足
if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
//注释1处,此时mItems还是空的
for (int i = 0; i < mItems.size(); i++) {
mItems.get(i).scrolling = true;
}
}
//条件满足
final boolean dispatchSelected = mCurItem != item;
if (mFirstLayout) {
//将mCurItem赋值为我们要展示的item
mCurItem = item;
if (dispatchSelected) {//通知在下标为item的page被选中
dispatchOnPageSelected(item);
}
//调用requestLayout
requestLayout();
} else {
populate(item);
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
}
综上所述
binding.viewPager.setAdapter(adapter);
int middle = Integer.MAX_VALUE >> 1;
//取divisiblePosition为可以整除multiImgs的size的值
int divisiblePosition = middle - (middle % multiImgs.size());
binding.viewPager.setCurrentItem(divisiblePosition);
我们调用完ViewPager的setAdapter方法,又调用了ViewPager的setCurrentItem方法,内部都会调用requestLayout方法。导致ViewPager重新走measure,layout,draw等方法。
ViewPager的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//...
// Make sure we have created all fragments that we need to have shown.
mInLayout = true;
populate();
mInLayout = false;
//...
}
内部调用populate方法。
void populate() {
//mCurItem默认是0
populate(mCurItem);
}
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
if (mCurItem != newCurrentItem) {
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
//...
//注释1处,
mAdapter.startUpdate(this);
//注释2处,pageLimit=1
final int pageLimit = mOffscreenPageLimit;
//startPos=1073741823-1=1073741822
final int startPos = Math.max(0, mCurItem - pageLimit);
//N=Integer.MAX_VALUE
final int N = mAdapter.getCount();
//endPos=1073741823+1=1073741824,
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
int curIndex = -1;
ItemInfo curItem = null;
// 此时mItems仍然为空
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
final ItemInfo ii = mItems.get(curIndex);
if (ii.position >= mCurItem) {
if (ii.position == mCurItem) curItem = ii;
break;
}
}
//注释3处,添加当前我们要展示的界面
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
// 添加完当前要展示的界面以后,分别在左边和右边添加pageLimit个界面,pageLimit最小为1。
if (curItem != null) {
//左边额外的宽度
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;
//注释4处,for循环,当前界面左边的界面
for (int pos = mCurItem - 1; pos >= 0; pos--) {
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;
}
} else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor;
itemIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
float extraWidthRight = curItem.widthFactor;
itemIndex = curIndex + 1;
if (extraWidthRight < 2.f) {
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
final float rightWidthNeeded = clientWidth <= 0 ? 0 :
(float) getPaddingRight() / (float) clientWidth + 2.f;
//注释5处,
for (int pos = mCurItem + 1; pos < N; pos++) {
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
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));
}
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthRight += ii.widthFactor;
itemIndex++;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
}
}
calculatePageOffsets(curItem, curIndex, oldCurInfo);
mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
}
//注释6处
mAdapter.finishUpdate(this);
// Check width measurement of current pages and drawing sort order.
// 注释7处,这时候我们总共添加了3个view,childCount=3
final int childCount = getChildCount();
//...
}
注释2处,计算总共要添加的界面数量
//注释2处,pageLimit=1
final int pageLimit = mOffscreenPageLimit;
//startPos=1073741823-1=1073741822
final int startPos = Math.max(0, mCurItem - pageLimit);
//N=Integer.MAX_VALUE
final int N = mAdapter.getCount();
//endPos=1073741823+1=1073741824,
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
我们根据pageLimit计算出我们总共要添加的页面的数量,pageLimit默认为1。我们只需要添加3个界面就行了。
//注释3处,添加当前我们要展示的界面
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
ViewPager的addNewItem方法
ItemInfo addNewItem(int position, int index) {
ItemInfo ii = new ItemInfo();
ii.position = position;
//调用adapter的instantiateItem方法
ii.object = mAdapter.instantiateItem(this, position);
ii.widthFactor = mAdapter.getPageWidth(position);
if (index < 0 || index >= mItems.size()) {
mItems.add(ii);
} else {
mItems.add(index, ii);
}
return ii;
}
adapter的instantiateItem方法
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
//...
//这里的container就是ViewPager,调用ViewPager的addView方法,添加子View
container.addView(imageView);
return imageView;
}
注意:这里的container就是ViewPager,调用ViewPager的addView方法,添加子View。
我们继续回到ViewPager的populate方法
注释4处,for循环,当前界面左边的界面,添加一个界面就会跳出for循环。
注释5处,for循环,当前界面右边的界面,添加一个界面就会跳出for循环。
添加完毕以后,mItems中有3个元素。
private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
mItems[0]:当前界面左边的界面,position=1073741822
mItems[1]:当前界面,position=1073741823
mItems[2]:当前界面右边的界面,position=1073741824
到此,ViewPager的addView方法执行完毕,回到onMeasure方法会做测量子View的操作我们忽略。继续往下看ViewPager的onLayout方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//...
if (mFirstLayout) {
//滚动到当前界面,不要动画
scrollToItem(mCurItem, false, 0, false);
}
//将mFirstLayout置为false
mFirstLayout = false;
}
内部会放置我们添加的view,然后滚动到当前界面,最后把mFirstLayout置为false。到现在一切还是很美好的。
binding.btnChangeCurrentItem.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
//注释1处,设置ViewPager当前item为0
binding.viewPager.setCurrentItem(0);
}
}
);
然后当我们手动调用ViewPager的setCurrentItem方法,设置ViewPager当前item为0的时候就ANR了。继续往下看。
//此时我们传入的方法参数是0
public void setCurrentItem(int item) {
mPopulatePending = false;
//!mFirstLayout为true
setCurrentItemInternal(item, !mFirstLayout, false);
}
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
//smoothScroll为true
setCurrentItemInternal(item, smoothScroll, always, 0);
}
此时传入的方法参数是0,并且smoothScroll为true
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
//...
//mOffscreenPageLimit默认为1
final int pageLimit = mOffscreenPageLimit;
////注释1处,此时mItems的size是3条件满足
if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
for (int i = 0; i < mItems.size(); i++) {
mItems.get(i).scrolling = true;
}
}
//条件满足
final boolean dispatchSelected = mCurItem != item;
if (mFirstLayout) {
//将mCurItem赋值为我们要展示的item
mCurItem = item;
if (dispatchSelected) {//通知在下标为item的page被选中
dispatchOnPageSelected(item);
}
//调用requestLayout
requestLayout();
} else {
//注释2处
populate(item);
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
}
注释1处, item < (mCurItem - pageLimit)
条件成立,此时mItems的size是3。所以会将mItems中的每个元素的scrolling属性置为true。
这时候已经不是第一次layout了。mFirstLayout为false。所以会执行注释2处的代码,调用populate(item)方法。
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
if (mCurItem != newCurrentItem) {
//条件满足
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
//...
//注释1处,
mAdapter.startUpdate(this);
//注释2处,pageLimit=1
final int pageLimit = mOffscreenPageLimit;
//startPos=0
final int startPos = Math.max(0, mCurItem - pageLimit);
//N=Integer.MAX_VALUE
final int N = mAdapter.getCount();
//endPos=1
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
int curIndex = -1;
ItemInfo curItem = null;
//注释3处,添加当前我们要展示的界面
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
// 添加完当前要展示的界面以后,分别在左边和右边添加pageLimit个界面,pageLimit最小为1。
if (curItem != null) {
//左边额外的宽度
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;
//注释4处,for循环,当前界面左边的界面
for (int pos = mCurItem - 1; pos >= 0; pos--) {
//...
}
//extraWidthRight=1.0
float extraWidthRight = curItem.widthFactor;
//itemIndex=1
itemIndex = curIndex + 1;
if (extraWidthRight < 2.f) {
//注释5处,
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
//paddingright默认为0,rightWidthNeeded=2.0
final float rightWidthNeeded = clientWidth <= 0 ? 0 :
(float) getPaddingRight() / (float) clientWidth + 2.f;
//注释6处,添加当前界面右边的界面
for (int pos = mCurItem + 1; pos < N; pos++) {
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
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));
}
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthRight += ii.widthFactor;
itemIndex++;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
}
}
calculatePageOffsets(curItem, curIndex, oldCurInfo);
mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
}
//注释6处
mAdapter.finishUpdate(this);
// Check width measurement of current pages and drawing sort order.
// 注释7处,这时候我们总共添加了3个view,childCount=3
final int childCount = getChildCount();
//...
}
注释2处,计算总共要添加的界面数量
//注释2处,pageLimit=1
final int pageLimit = mOffscreenPageLimit;
//startPos=0
final int startPos = Math.max(0, mCurItem - pageLimit);
//N=Integer.MAX_VALUE
final int N = mAdapter.getCount();
//endPos=1
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
我们根据pageLimit计算出我们总共要添加的页面的数量,pageLimit默认为1。因为此时startPo为0,我们不需要添加左边的界面了,我们只需要添加2个界面就行了。
//注释3处,添加当前我们要展示的界面
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
注释3处,添加当前我们要展示的界面,此时mCurItem==0。添加完该界面后
mItems中一共有4个元素了。
mItems[0]:当前界面,position=0
mItems[1]:(老的当前界面)左边的界面,position=1073741822
mItems[2]:(老的当前界面),position=1073741823
mItems[3]:(老的当前界面)右边的界面,position=1073741824
注释4处的for循环
//注释4处,for循环,当前界面左边的界面
for (int pos = mCurItem - 1; pos >= 0; pos--) {
//...
}
此时pos = mCurItem - 1=-1;所以for循环不会执行。
注释5处,注释6处
extraWidthRight=1.0
itemIndex=1
ii=mItems[1],position=1073741822
rightWidthNeeded=2.0
然后进入for循环开始添加当前界面右边的界面
for (int pos = mCurItem + 1; pos < N; pos++) {
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
//...
}
} else if (ii != null && pos == ii.position) {
extraWidthRight += ii.widthFactor;
itemIndex++;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
}
第一次循环会执行else条件
else {
//新添加一个item
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
添加一个右边的界面,添加完毕后此时有5各元素
mItems[0]:当前界面,position=0
mItems[1]:当前界面右边的界面,position=1
mItems[2]:(老的当前界面)左边的界面,position=1073741822
mItems[3]:(老的当前界面),position=1073741823
mItems[4]:(老的当前界面)右边的界面,position=1073741824
执行完毕后itemIndex=2,extraWidthRight=2.0,获取的ii是position=1073741822的元素,不为null。
第二次,以及以后的每次循环都会执行if条件
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
//注释1处,条件不满足
if (ii == null) {
break;
}
//注释2处,条件不满足
if (pos == ii.position && !ii.scrolling) {
//...
}
}
注释1处
if (ii == null) {
break;
}
ii是position=1073741822的元素,即(老的当前界面)左边的界面,不为null无法break出去。
注释2处的条件也是无法满足的,因为我们在setCurrentItemInternal方法内部将mItems中老的元素的scrolling都置为true了。
if (pos == ii.position && !ii.scrolling) {
}
也就是说只有完整执行完循环我们才能继续向下执行,那得执行多少次啊?
for (int pos = mCurItem + 1; pos < N; pos++) {
//...
}
在这个例子中,pos起始值是1,执行到Integer.MAX_VALUE(2147483647)结束。20多亿(20个小目标)啊,不ANR才怪。
我们分析了调用ViewPager的setCurrentItem(int item)方法,减小mCurItem值造成的ANR的原因。调用ViewPager的setCurrentItem(int item)方法,增加mCurItem值造成ANR的原因我们就不分析了,原理都是一样的就是for循环执行次数太多。
总结一下:
- 当我们调用ViewPager的setCurrentItem(int item)方法,改变当前显示的界面的时候,如果传入的item值和ViewPager的mCurItem值相差很大的话,在ViewPager的populate(int newCurrentItem)方法内部会导致多次循环,很容易引起ANR的。
- item的取值范围是[0,mAdapter.getCount() - 1],也就是说适配器的getCount方法返回值的大小决定了item值的取值范围。
参考链接:
网友评论