安卓探究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中对于这个方法的注释:
注释中并没有说得很确定,且ListView距离View已经有多层的继承关系,requestLayout()可能被重写。从View到ListView的继承链为View->ViewGroup->AdapterView->AbsListView->ListView,对于requestLayout()方法检查结果如下:
ViewGroup.requestLayout() 未重写,等价于 View.requestLayout()
AdapterView.requestLayout() 未重写,等价于 View.requestLayout()
AbsListView.requestLayout() 重写如下:
ListView.requestLayout()未重写,等价于AbsListView.requestLayout()
担心的事情发生了,子类重写了View的requestLayout(),使得其不确定性增加。可以看到,增加了真正调用requestLayout的条件限制。需要重点研究一下mBlockLayoutRequests和mInLayout。
回到exception发生的现场,首先,AbsListView.onLayout(),mInLayout被赋值为true,但执行完layoutChildren()之后,会恢复为false。
再看mBlockLayoutRequests ,ListView.layoutChildren()中mBlockLayoutRequests被赋值为true。之后是一个try...finally,代码很长,可以看到,在try中抛出这个exception,同时,finally中把mBlockLayoutRequests赋值false。虽然抛出exception,但不影响finally代码段的执行。
所以这个exception的发生并不会影响到mBlockLayoutRequests和mInLayout的正常逻辑。
下面用一个demo来测试这个方案:
自己扩展一个ListView的子类TestListView:
重写layoutChildren(),catch exception。checkFields()是一个工具方法,在requestLayout()和onMeasure()执行之前检查一下mBlockLayoutRequests和mInLayout的值是否正确(为false)。因为这两个值的数据可见性定义为package,所以这里用反射拿值。
写一个简单的TestAdapter:
可以看到addItemSelf()做两件事:
(1)在调用线程中更新数据。
(2)在UI线程中notifyDataSetChanged()。
Layout文件:
activity_main.xml:
listview_item.xml:
入口Activity:
在onCreate()的时候,new一个工作线程调用5000次addItemSelf()。测试结果:
(1)先注释掉TestListView中的layoutChildren(),结果必现此crash。
(2)恢复TestListView的layoutChildren(),结果UI正确,无crash,查看log,exception被成功catch处理,checkFields()得到的值正确。
从上面的分析可以看出安卓希望的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()得到的值正确。
相关文章推荐
- EventBus3.0源码分析
- C#笔记一 .Net Framwork
- mybaits不能出现小于号
- foreach的实现原理
- sinatra - Ruby web application 轻量级框架
- studio 集成 Genymotion后打开模拟器出错、打开虚拟机VirtualBox出错
- ArrayList(一): JAVA中ArrayList类的用法
- Linux 服务器时间同步
- 懒人爱家务_添加广告
- delphi 错误信息
- Eclipse配置SQL Explorer插件和数据库
- 计算机网络学习笔记
- Happen-before
- 1-6-03:计算书费
- Qt QtableView使用
- Python Requests模块讲解4
- flask+mako+peewee(下)(解决了Error 2006: MySQL server has gone away)
- 线性内插和双线性内插
- Python自动化部署工具Fabric的简单上手指南
- winserver2012R2虚拟机安装密钥