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

来源:互联网 发布:大数据技术 原理 编辑:程序博客网 时间:2024/06/04 09:33

第三部分 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,并postUI线程执行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>     */    @CallSuperpublic void requestLayout() {......}

注释中并没有说得很确定,且ListView距离View已经有多层的继承关系,requestLayout()可能被重写。从ViewListView的继承链为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()

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

 

回到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的发生并不会影响到mBlockLayoutRequestsmInLayout的正常逻辑。

 

下面用一个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 exceptioncheckFields()是一个工具方法,在requestLayout()和onMeasure()执行之前检查一下mBlockLayoutRequestsmInLayout的值是否正确(为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一个工作线程调用5000addItemSelf()。测试结果:

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

(2)恢复TestListViewlayoutChildren(),结果UI正确,无crash,查看logexception被成功catch处理,checkFields()得到的值正确。








1 0
原创粉丝点击