您的位置:首页 > 其它

安卓探究ListView+Adapter数据在工作线程中更新引发的crash以及解决方法(三)

2016-04-19 17:28 567 查看
第三部分 solution及测试

 

从上面的分析可以看出安卓希望的ListView+Adapter使用方式是更新数据,然后调用notifyDataSetChanged()触发重绘,整个过程在UI线程串行执行,框架逻辑会保证健壮可用。所以exception的描述也是说Make
sure the content of your adapter is not modified from a background thread, but only from the UI thread。所以在UI线程更新数据显然是一种solution。但这就失去了工作线程的并行优势,容易引起UI卡顿。所以需要一种保持工作线程执行数据更新的解决此crash的方案。

 

这里设想一种方案:catch这个exception,并post给UI线程执行notifyDataSetChanged(),触发重绘。

 

这个方案的疑问在于,catch这个exception,不让应用进程挂掉,继续活下去,是否足够健壮能够恢复正确的UI绘制?下面从框架源代码分析和app
demo测试两个角度验证一下这个方案。

 

先源码分析。前文分析可知触发重绘的关键在于requestLayout()。这里先简单看看requestLayout()。这个方法源自View,先看看View中对于这个方法的注释:

/**
* Call this when something has changed which has invalidated the
* layout of this view. This will schedule a layout pass of the view
* tree. This should not be called while the view hierarchy is currently in a layout
* pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
* end of the current layout pass (and then layout will run again) or after the current
* frame is drawn and the next layout occurs.
*
* <p>Subclasses which override this method should call the superclass method to
* handle possible request-during-layout errors correctly.</p>
*/
@CallSuper
public void requestLayout() {
......
}


注释中并没有说得很确定,且ListView距离View已经有多层的继承关系,requestLayout()可能被重写。从View到ListView的继承链为View->ViewGroup->AdapterView->AbsListView->ListView,对于requestLayout()方法检查结果如下:

ViewGroup.requestLayout() 未重写,等价于 View.requestLayout()

AdapterView.requestLayout() 未重写,等价于 View.requestLayout()

AbsListView.requestLayout() 重写如下:

@Override
public void requestLayout() {
if (!mBlockLayoutRequests && !mInLayout) {
super.requestLayout();
}
}

ListView.requestLayout()未重写,等价于AbsListView.requestLayout()

担心的事情发生了,子类重写了View的requestLayout(),使得其不确定性增加。可以看到,增加了真正调用requestLayout的条件限制。需要重点研究一下mBlockLayoutRequests和mInLayout。

 

回到exception发生的现场,首先,AbsListView.onLayout(),mInLayout被赋值为true,但执行完layoutChildren()之后,会恢复为false。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);

mInLayout = true;

final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}

layoutChildren();
mInLayout = false;

mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
}

再看mBlockLayoutRequests ,ListView.layoutChildren()中mBlockLayoutRequests被赋值为true。之后是一个try...finally,代码很长,可以看到,在try中抛出这个exception,同时,finally中把mBlockLayoutRequests赋值false。虽然抛出exception,但不影响finally代码段的执行。

@Override
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (blockLayoutRequests) {
return;
}

mBlockLayoutRequests = true;

try {
super.layoutChildren();

......

// Handle the empty set by removing all views that are visible
// and calling it a day
if (mItemCount == 0) {
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) {
throw new IllegalStateException("The content of the adapter has changed but "
+ "ListView did not receive a notification. Make sure the content of "
+ "your adapter is not modified from a background thread, but only from "
+ "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
+ "when its content changes. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}

......

} finally {
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}

所以这个exception的发生并不会影响到mBlockLayoutRequests和mInLayout的正常逻辑。

 

下面用一个demo来测试这个方案:

 

自己扩展一个ListView的子类TestListView:

package com.example.android.testlistview;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.AdapterView;
import android.widget.ListView;

import java.lang.reflect.Field;

public class TestListView extends ListView {

private final String TAG = "TestListView";
private final boolean NEED_CHECK_FIELDS = true;

public TestListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

public TestListView(Context context) {
super(context);
}

public TestListView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public TestListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
public void requestLayout() {
Log.i(TAG , "requestLayout() ... ");
if (NEED_CHECK_FIELDS) {
checkFields();
}
super.requestLayout();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.i(TAG, "onMeasure() ... ");
if (NEED_CHECK_FIELDS) {
checkFields();
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

@Override
protected void layoutChildren() {
Log.i("TestListView", "layoutChildren() ... ");
try {
super.layoutChildren();
} catch (IllegalStateException e) {
Log.e("TestListView", "layoutChildren() : e - " + e.getMessage());
e.printStackTrace();
}
}

private void checkFields() {
Log.i(TAG , "checkFields() begin ---------------------------------------------------------------");
try {
final Class<AdapterView> c =(Class<AdapterView>) Class.forName("android.widget.AdapterView");
Field field;
boolean accessible;

field = c.getDeclaredField("mBlockLayoutRequests");
accessible = field.isAccessible();
field.setAccessible(true);
Log.i("TestListView", "checkFields() : mBlockLayoutRequests - " + (Boolean) field.get(this));
field.setAccessible(accessible);

field = c.getDeclaredField("mInLayout");
accessible = field.isAccessible();
field.setAccessible(true);
Log.i("TestListView", "checkFields() : mInLayout - " + (Boolean) field.get(this));
field.setAccessible(accessible);
} catch (NoSuchFieldException e1) {
Log.i("TestListView", "checkFields() : e - " + e1.getMessage());
e1.printStackTrace();
} catch (IllegalAccessException e1) {
Log.i("TestListView", "checkFields() : e - " + e1.getMessage());
e1.printStackTrace();
} catch (ClassNotFoundException e1) {
Log.i("TestListView", "checkFields() : e - " + e1.getMessage());
e1.printStackTrace();
}
Log.i(TAG, "checkFields() end ---------------------------------------------------------------");
}

}

重写layoutChildren(),catch exception。checkFields()是一个工具方法,在requestLayout()和onMeasure()执行之前检查一下mBlockLayoutRequests和mInLayout的值是否正确(为false)。因为这两个值的数据可见性定义为package,所以这里用反射拿值。

写一个简单的TestAdapter:

package com.example.android.testlistview;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;

public class TestAdapter extends BaseAdapter {

private int mCount = 0;
private Activity mActivity;
private ListView mListView;

public TestAdapter(Activity activity) {
mActivity = activity;
}

public void addItemSelf () {
++mCount;
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
Log.e("TestListView" , "run TestAdapter.notifyDataSetChanged() ..." + mCount);
TestAdapter.this.notifyDataSetChanged();
}
});
}

public void setListView (ListView listView) {
mListView = listView;
}

@Override
public int getCount() {
return mCount;
}

@Override
public Object getItem(int position) {
if (position >= mCount) {
return null;
}
return String.valueOf(mCount).intern();
}

@Override
public long getItemId(int position) {
if (position >= mCount) {
return -1;
}
return (long)mCount;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (position >= mCount) {
return null;
}
View view;

if (null == convertView) {
view = mActivity.getLayoutInflater().inflate(R.layout.listview_item, null);
} else {
view = convertView;
}

((TextView)view.findViewById(R.id.text)).setText(String.valueOf(position).intern());
return view;
}

}


可以看到addItemSelf()做两件事:

(1)在调用线程中更新数据。

(2)在UI线程中notifyDataSetChanged()。

Layout文件:

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.android.testlistview.MainActivity">

<com.example.android.testlistview.TestListView
android:id="@+id/list"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>


listview_item.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
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" />

</RelativeLayout>

入口Activity:

package com.example.android.testlistview;

import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ListView;

public class MainActivity extends AppCompatActivity {

private TestListView mListView;
private TestAdapter mTestAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTestAdapter = new TestAdapter(this);
mListView = (TestListView) findViewById(R.id.list);
mListView.setAdapter(mTestAdapter);
mTestAdapter.setListView(mListView);

new Thread() {
public void run() {
for (int i = 0 ; i < 5000 ; ++i) {
Log.i("TestListView", "i - " + i);
mTestAdapter.addItemSelf();
}
}
}.start();
}
}


在onCreate()的时候,new一个工作线程调用5000次addItemSelf()。测试结果:

(1)先注释掉TestListView中的layoutChildren(),结果必现此crash。

(2)恢复TestListView的layoutChildren(),结果UI正确,无crash,查看log,exception被成功catch处理,checkFields()得到的值正确。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: