安卓探究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,并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> */ @CallSuperpublic 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()得到的值正确。
- 安卓探究ListView+Adapter数据在工作线程中更新引发的crash以及解决方法(三)
- 安卓探究ListView+Adapter数据在工作线程中更新引发的crash以及解决方法(一)
- 安卓探究ListView+Adapter数据在工作线程中更新引发的crash以及解决方法(二)
- adapter数据更新要放在ui线程中
- 安卓自定义Adapter,以及如何提高ListView的效率
- Android开发系列(十四):ListView用法、对ListView监听的三种方法以及ListView中Adapter的使用方法
- 【Android笔记-异常-4】定义一个临时的数组变量承接数据,ListView的数据以及通知数据更新要放到同一个线程(主线程)。避免出现异常"The content of the adapter
- 安卓listview adapter
- 安卓打造listview的万用adapter
- android 在其他线程中更新UI线程的解决方法
- Android 在其他线程中更新UI线程的解决方法
- Android 在其他线程中更新UI线程的解决方法
- Android 在其他线程中更新UI线程的解决方法
- Android开发中ListView数据更新显示的解决方法
- 安卓学习笔记(一)——线程的用法及怎样在子线程中更新UI
- ListView中使用自定义Adapter及时更新数据
- 关于ListView中使用自定义Adapter及时更新数据
- Android 中listView数据混乱的原因以及解决方法
- javascript基础三 (EVENT事件详解)
- Linux电源管理(11)_Runtime PM之功能描述
- iOS真机测试
- 常用语言和工具的中文翻译
- OpenCV学习笔记(四)—— OpenCV for Android移植到Android平台
- 安卓探究ListView+Adapter数据在工作线程中更新引发的crash以及解决方法(三)
- cocos2d 使用 C++开发游戏 出现#include "CardSprite.h" 无法引入源文件的情况。
- JFinal实现原理
- Codeforces--333C--The Two Routes(最短路弗洛伊德)(思维)
- ios开发多线程篇——多线程简单介绍
- android多点触控统一的原理(使用 event.getAction()&MotionEvent.ACTION_MASK的原因)
- ubuntu on power服务器上安装docker binary package
- TokuMX will not run with transparent huge pages enabled.
- GLFW Keyboard keys