Android,ListView IllegalStateException:“适配器的内容已更改,但ListView未收到通知”

时间:2010-06-28 11:44:27

标签: android exception listview adapter

我想做什么:运行后台线程,计算ListView内容并部分更新ListView,同时计算结果。

我知道我必须避免:我无法从后台线程中删除ListAdapter内容,因此我继承了AsyncTask并从onProgressUpdate发布结果(向适配器添加条目)。我的适配器使用结果对象的ArrayList,这些arraylists上的所有操作都是同步的。

其他人的研究:有非常有价值的数据here。对于500名用户来说,我几乎每天都会遇到崩溃,当我在onProgressUpdate中添加list.setVisibility(GONE)/trackList.setVisibility(VISIBLE)块时,崩溃率降低了10倍但没有消失。 (在answer

中有人建议

我有时会得到什么:请注意,它很少发生(每周一次为3.5k用户之一)。但我想完全摆脱这个错误。这是部分堆栈跟踪:

`java.lang.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. [in ListView(2131296334, class android.widget.ListView) with Adapter(class com.transportoid.Tracks.TrackListAdapter)]
at android.widget.ListView.layoutChildren(ListView.java:1432)
at android.widget.AbsListView.onTouchEvent(AbsListView.java:2062)
at android.widget.ListView.onTouchEvent(ListView.java:3234)
at android.view.View.dispatchTouchEvent(View.java:3709)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:852)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
[...]

帮助?不再需要了,请参阅下文

最终答案:事实证明,我每隔5次插入就会调用notifyDataSetChanged,以避免闪烁和突然更改列表。它不能这样做,总是在基本列表更改时通知适配器。这个错误现在已经消失了。

25 个答案:

答案 0 :(得分:114)

我遇到了同样的问题。

我在UI线程之外的ArrayList添加了项目。

解决方案:我在UI线程中同时完成了adding the itemsnotifyDataSetChanged()调用。

答案 1 :(得分:26)

我有同样的问题,但我使用方法

修复了它
requestLayout();

来自班级ListView

答案 2 :(得分:20)

这是 MultiThreading 问题并正确使用同步块可以防止这种情况发生。 不在UI线程上添加额外的东西,导致app的响应能力丧失。

我也面临同样的问题。而且最常见的答案表明,从UI Thread更改适配器数据可以解决问题。这将是有效的,但是一个快速简单的解决方案,但不是最好的解决方案。

正如你可以看到正常情况。从后台线程更新数据适配器并在UI线程中调用notifyDataSetChanged。

当ui线程正在更新视图而另一个后台线程再次更改数据时,会出现此illegalStateException。那一刻导致了这个问题。

因此,如果您将同步所有正在更改适配器数据的代码并进行notifydatasetchange调用。这个问题应该消失了。对我来说,我仍然在后台线程中更新数据。

这是我的案例特定代码供其他人参考。

主屏幕上的我的加载程序会将电话簿联系人加载到后台的数据源中。

    @Override
    public Void loadInBackground() {
        Log.v(TAG, "Init loadings contacts");
        synchronized (SingleTonProvider.getInstance()) {
            PhoneBookManager.preparePhoneBookContacts(getContext());
        }
    }

此PhoneBookManager.getPhoneBookContacts从电话簿中读取联系人并将其填入哈希映射。哪个可以直接用于列表适配器绘制列表。

我的屏幕上有一个按钮。这将打开列出这些电话号码的活动。 如果我在上一个线程完成其工作之前直接在列表上设置了Adapter,那么快速导航的情况就会发生。它弹出了例外。这个问题的标题是什么。所以我必须在第二项活动中做这样的事情。

我在第二个活动中的加载程序等待第一个线程完成。直到它显示进度条。检查两个加载器的loadInBackground。

然后它创建适配器并将其传递给ui线程我调用setAdapter的活动。

这解决了我的问题。

此代码仅为代码段。你需要改变它以便为你编译好。

@Override
public Loader<PhoneBookContactAdapter> onCreateLoader(int arg0, Bundle arg1) {
    return new PhoneBookContactLoader(this);
}

@Override
public void onLoadFinished(Loader<PhoneBookContactAdapter> arg0, PhoneBookContactAdapter arg1) {
    contactList.setAdapter(adapter = arg1);
}

/*
 * AsyncLoader to load phonebook and notify the list once done.
 */
private static class PhoneBookContactLoader extends AsyncTaskLoader<PhoneBookContactAdapter> {

    private PhoneBookContactAdapter adapter;

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

    @Override
    public PhoneBookContactAdapter loadInBackground() {
        synchronized (SingleTonProvider.getInstance()) {
            return adapter = new PhoneBookContactAdapter(getContext());    
        }
    }

}

希望这有帮助

答案 3 :(得分:15)

我通过2个列表解决了这个问题。我只用于适配器的一个列表,我在另一个列表上进行所有数据更改/更新。这允许我在后台线程中的一个列表上进行更新,然后更新主/ UI线程中的“适配器”列表:

List<> data = new ArrayList<>();
List<> adapterData = new ArrayList();

...
adapter = new Adapter(adapterData);
listView.setAdapter(adapter);

// Whenever data needs to be updated, it can be done in a separate thread
void updateDataAsync()
{
    new Thread(new Runnable()
    {
        @Override
        public void run()
        {
            // Make updates the "data" list.
            ...

            // Update your adapter.
            refreshList();
        }
    }).start();
}

void refreshList()
{
    runOnUiThread(new Runnable()
    {
        @Override
        public void run()
        {
            adapterData.clear();
            adapterData.addAll(data);
            adapter.notifyDataSetChanged();
            listView.invalidateViews();
        }
    });
}

答案 4 :(得分:7)

我编写了这段代码并让它在2.1模拟器映像中运行了大约12个小时,但没有得到IllegalStateException。我将给予android框架这个问题的好处,并说它很可能是你的代码中的一个错误。我希望这有帮助。也许你可以根据你的列表和数据进行调整。

public class ListViewStressTest extends ListActivity {
    ArrayAdapter<String> adapter;
    ListView list;
    AsyncTask<Void, String, Void> task;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        this.adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
        this.list = this.getListView();

        this.list.setAdapter(this.adapter);

        this.task = new AsyncTask<Void, String, Void>() {
            Random r = new Random();
            int[] delete;
            volatile boolean scroll = false;

            @Override
            protected void onProgressUpdate(String... values) {
                if(scroll) {
                    scroll = false;
                    doScroll();
                    return;
                }

                if(values == null) {
                    doDelete();
                    return;
                }

                doUpdate(values);

                if(ListViewStressTest.this.adapter.getCount() > 5000) {
                    ListViewStressTest.this.adapter.clear();
                }
            }

            private void doScroll() {
                if(ListViewStressTest.this.adapter.getCount() == 0) {
                    return;
                }

                int n = r.nextInt(ListViewStressTest.this.adapter.getCount());
                ListViewStressTest.this.list.setSelection(n);
            }

            private void doDelete() {
                int[] d;
                synchronized(this) {
                    d = this.delete;
                }
                if(d == null) {
                    return;
                }
                for(int i = 0 ; i < d.length ; i++) {
                    int index = d[i];
                    if(index >= 0 && index < ListViewStressTest.this.adapter.getCount()) {
                        ListViewStressTest.this.adapter.remove(ListViewStressTest.this.adapter.getItem(index));
                    }
                }
            }

            private void doUpdate(String... values) {
                for(int i = 0 ; i < values.length ; i++) {
                    ListViewStressTest.this.adapter.add(values[i]);
                }
            }

            private void updateList() {
                int number = r.nextInt(30) + 1;
                String[] strings = new String[number];

                for(int i = 0 ; i < number ; i++) {
                    strings[i] = Long.toString(r.nextLong());
                }

                this.publishProgress(strings);
            }

            private void deleteFromList() {
                int number = r.nextInt(20) + 1;
                int[] toDelete = new int[number];

                for(int i = 0 ; i < number ; i++) {
                    int num = ListViewStressTest.this.adapter.getCount();
                    if(num < 2) {
                        break;
                    }
                    toDelete[i] = r.nextInt(num);
                }

                synchronized(this) {
                    this.delete = toDelete;
                }

                this.publishProgress(null);
            }

            private void scrollSomewhere() {
                this.scroll = true;
                this.publishProgress(null);
            }

            @Override
            protected Void doInBackground(Void... params) {
                while(true) {
                    int what = r.nextInt(3);

                    switch(what) {
                        case 0:
                            updateList();
                            break;
                        case 1:
                            deleteFromList();
                            break;
                        case 2:
                            scrollSomewhere();
                            break;
                    }

                    try {
                        Thread.sleep(0);
                    } catch(InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }

        };

        this.task.execute(null);
    }
}

答案 5 :(得分:3)

几天前我遇到了同样的问题并导致每天数千次崩溃,大约0.1%的用户遇到这种情况。我尝试过setVisibility(GONE/VISIBLE)requestLayout(),但崩溃次数只会减少一点。

我终于解决了它。 setVisibility(GONE/VISIBLE)没有任何内容。 requestLayout()没有任何内容。

最后我发现原因是我在更新数据后使用Handler来调用notifyDataSetChanged(),这可能导致某种情况:

  1. 将数据更新到模型对象(我称之为DataSource)
  2. 用户触摸列表视图(可以调用checkForTap() / onTouchEvent(),最后调用layoutChildren()
  3. 适配器从模型对象获取数据并调用notifyDataSetChanged()并更新视图
  4. 我又犯了一个错误:getCount()getItem()getView(),我直接使用DataSource中的字段,而不是将它们复制到适配器。所以最后它崩溃了:

    1. 适配器更新上次响应提供的数据
    2. 当下一个回复时,DataSource会更新导致项目数更改的数据
    3. 用户触摸列表视图,可以是点击,移动或翻转
    4. 调用
    5. getCount()getView(),listview查找数据不一致,并抛出java.lang.IllegalStateException: The content of the adapter has changed but...之类的异常。如果您在IndexOutOfBoundException中使用页眉/页脚,则另一个常见的例外是ListView
    6. 所以解决方案很简单,当我的Handler触发适配器获取数据并调用notifyDataSetChanged()时,我只是从我的DataSource将数据复制到适配器。崩溃现在再也不会发生了。

答案 6 :(得分:3)

如果Feed对象有一个List。 它是从非UI线程追加和截断的。 它适用于下面的适配器。 无论如何我在UI线程中调用FeedAdapter.notifyDataSetChanged但稍后。 我喜欢这样,因为我的Feed对象在本地服务中保留在内存中,即使UI已经死了。

public class FeedAdapter extends BaseAdapter {
    private int size = 0;
    private final List<Feed> objects;

    public FeedAdapter(Activity context, List<Feed> objects) {
        this.context = context;
        this.objects = objects;
        size = objects.size();
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        ...
    }

    @Override
    public void notifyDataSetChanged() {
        size = objects.size();

        super.notifyDataSetChanged();
    }

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

    @Override
    public Object getItem(int position) {
        try {
            return objects.get(position);
        } catch (Error e) {
            return Feed.emptyFeed;
        }
    }

    @Override
    public long getItemId(int position) {
        return position;
    }
}

答案 7 :(得分:3)

我的问题与 Filter 和ListView一起使用有关。

在设置或更新ListView的基础数据模型时,我做了类似的事情:

public void updateUnderlyingContacts(List<Contact> newContacts, String filter)
{
    this.allContacts = newContacts;
    this.filteredContacts = newContacts;
    getFilter().filter(filter);
}

在最后一行中调用filter()将(并且必须)导致在过滤器的notifyDataSetChanged()方法中调用publishResults()。这有时可以正常工作,特别是在我的快速Nexus 5中。但实际上,它隐藏了一个错误,你会注意到设备速度较慢或资源密集的情况。

问题是过滤是异步完成的,因此在filter()语句的结尾和对publishResults()的调用之间,在UI线程中,一些其他UI线程代码可以执行,更改适配器的内容。

实际修复很简单,只需在请求执行过滤之前调用notifyDataSetChanged()

public void updateUnderlyingContacts(List<Contact> newContacts, String filter)
{
    this.allContacts = newContacts;
    this.filteredContacts = newContacts;
    notifyDataSetChanged(); // Fix
    getFilter().filter(filter);
}

答案 8 :(得分:3)

如果间歇性地发生这种情况,事实证明我在单击“加载更多”最后一项后滚动列表时只有这个问题。如果列表没有滚动,一切正常。

经过多次调试,这是我的错误,但Android代码也存在不一致。

验证发生时,此代码在ListView中执行

        } 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 "

但是当onChange发生时,它会在AdapterView(ListView的父级)

中触发此代码
    @Override
    public void onChanged() {
        mDataChanged = true;
        mOldItemCount = mItemCount;
        mItemCount = getAdapter().getCount();

请注意不能保证适配器的相同方式!

在我的情况下,因为它是'LoadMoreAdapter',我在getAdapter调用中返回WrappedAdapter(用于访问底层对象)。由于额外的“加载更多”项和抛出异常,这导致计数不同。

我之所以这样做是因为文档似乎可以做到

ListView.getAdapter javadoc

  

返回此ListView中当前使用的适配器。归来了   适配器可能与传递给的适配器不同   setAdapter(ListAdapter)但可能是WrapperListAdapter。

答案 9 :(得分:2)

即使我在XMPP通知应用程序中遇到同样的问题,也需要将接收者消息添加回列表视图(使用ArrayList实现)。当我尝试通过MessageListener(单独的线程)添加接收者内容时,应用程序退出时出现上述错误。我通过将内容添加到arraylist&amp; setListviewadapaterrunOnUiThread方法,它是Activity类的一部分。这解决了我的问题。

答案 10 :(得分:2)

这是Android 4到4.4(KitKat)中的已知错误,已在“&gt; 4.4”中解决

见这里:https://code.google.com/p/android/issues/detail?id=71936

答案 11 :(得分:2)

我遇到了完全相同的错误日志同样的问题。 在我的情况下,AsyncTask的onProgress()使用mAdapter.add(newEntry)将值添加到适配器。为了避免用户界面响应变慢,我设置了mAdapter.setNotifyOnChange(false)并在第二次调用mAdapter.notifyDataSetChanged() 4次。每秒一次,数组被排序。

这项工作做得很好,看起来很容易上瘾,但遗憾的是,通过触摸显示的列表项目可能会使其崩溃。

但似乎我找到了一个可接受的解决方法。 我的猜测即使你只是在ui线程上工作,适配器也不会接受对它的数据的多次更改而不调用notifyDataSetChanged(),因此我创建了一个存储所有新项目的队列,直到提到的300ms为止。过度。如果达到这个时刻,我会一次性添加所有存储的项目并致电notifyDataSetChanged()。 到现在为止,我无法再崩溃

答案 12 :(得分:1)

在我的情况下,我在主Activity上的GetFilter()方法上调用了适配器上的方法TextWatcher(),并在GetFilter()上添加了For循环数据。 解决方案是在主Activity上将For循环更改为AfterTextChanged() sub方法,并删除对GetFilter()的调用

答案 13 :(得分:1)

此崩溃的一个原因是ArrayList对象无法完全更改。 所以,当我删除一个项目时,我必须这样做:

mList.clear();
mList.addAll(newDataList);

这解决了我的崩溃。

答案 14 :(得分:1)

我遇到了类似的问题,这是我在案件中的解决方法。我验证task是否已RUNNINGFINISHED,因为任务只能运行一次。您将在下面看到我的解决方案中的部分代码和改编代码。

public class MyActivity... {
    private MyTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
       // your code
       task = new MyTask();
       setList();
    }

    private void setList() {
    if (task != null)
        if (task.getStatus().equals(AsyncTask.Status.RUNNING)){
            task.cancel(true);
            task = new MyTask();
            task.execute();         
        } else if (task.getStatus().equals(AsyncTask.Status.FINISHED)) {
            task = new MyTask();
            task.execute();
        } else 
            task.execute();
    }

    class MyTask extends AsyncTask<Void, Item, Void>{
       List<Item> Itens;

       @Override
       protected void onPreExecute() {

        //your code

        list.setVisibility(View.GONE);
        adapterItem= new MyListAdapter(MyActivity.this, R.layout.item, new ArrayList<Item>());
        list.setAdapter(adapterItem);

        adapterItem.notifyDataSetChanged();
    }

    @Override
    protected Void doInBackground(Void... params) {

        Itens = getItens();
        for (Item item : Itens) {
            publishProgress(item );
        }

        return null;
    }

    @Override
    protected void onProgressUpdate(Item ... item ) {           
        adapterItem.add(item[0]);
    }

    @Override
    protected void onPostExecute(Void result) {
        //your code
        adapterItem.notifyDataSetChanged();     
        list.setVisibility(View.VISIBLE);
    }

}

}

答案 15 :(得分:1)

我有同样的问题,我解决了。我的问题是我使用的是listview,带有数组适配器和过滤器。在方法performFiltering上我弄乱了有数据的数组,这是问题,因为这个方法没有在UI线程上运行,并且偶然会引起一些问题。

答案 16 :(得分:0)

请尝试以下解决方案之一:

  1. 有时,如果将新对象添加到线程(或doInBackground方法)中的数据列表中,则会发生此错误。解决方案是:创建一个临时列表并在线程(或doInBackground)中将数据添加到此列表,然后将所有数据从临时列表复制到UI线程中的适配器列表(或onPostExcute

  2. 确保在UI线程中调用所有UI更新。

答案 17 :(得分:0)

我在延迟图像加载器中添加新数据时遇到同样的问题 我只是把

         adapter.notifyDataSetChanged();

in

       protected void onPostExecute(Void args) {
        adapter.notifyDataSetChanged();
        // Close the progressdialog
        mProgressDialog.dismiss();
         }

希望它可以帮到你

答案 18 :(得分:0)

就像@Mullins所说&#34;
我在UI线程中添加了项目并调用了notifyDataSetChanged(),我解决了这个问题。 - Mullins&#34;。

在我的情况下,我有asynctask我在notifyDataSetChanged()方法中调用了doInBackground(),问题解决了,当我从onPostExecute()来电时,我收到了异常。< / p>

答案 19 :(得分:0)

我有一个自定义ListAdapter并且在方法的开头而不是方法结束时调用super.notifyDataSetChanged()

@Override
public void notifyDataSetChanged() {
    recalculate();
    super.notifyDataSetChanged();
}

答案 20 :(得分:0)

我也得到完全相同的错误并使用AsyncTask:

`java.lang.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. [in ListView(2131296334, class android.widget.ListView) with Adapter... etc

我通过将adapter.notifyDataSetChanged();放在我的UI线程的底部来解决它,这是我的AsyncTask onPostExecute方法。像这样:

 protected void onPostExecute(Void aVoid) {

 all my other stuff etc...
    all my other stuff etc...

           adapter.notifyDataSetChanged();

                }

            });
        }

现在我的应用程序正常运作。

编辑:事实上,我的应用程序仍然每10次崩溃一次,造成同样的错误。

最后我在上一篇文章中遇到了runOnUiThread,我觉得这可能很有用。所以我将它放在我的doInBackground方法中,如下所示:

@Override
protected Void doInBackground(Void... voids) {

    runOnUiThread(new Runnable() {
                      public void run() { etc... etc...

我删除了adapter.notifyDataSetChanged();方法。现在,我的应用程序永远不会崩溃。

答案 21 :(得分:0)

我有同样的情绪,我有许多buttongroup在listview上我的项目,我正在改变我的项目中的一些布尔值,如holder.rbVar.setOnclik ......

我的问题发生了,因为我在getView()中调用了一个方法;并将一个对象保存在sharepreference中,所以我上面有同样的错误

我是如何解决的;我将getView()中的方法移除到notifyDataSetInvalidated()并且问题消失了

   @Override
    public void notifyDataSetChanged() {
        saveCurrentTalebeOnShare(currentTalebe);
        super.notifyDataSetChanged();
    }

答案 22 :(得分:0)

我有同样的问题。最后我得到了解决方案

在更新列表视图之前,如果软键盘存在,请先关闭它。之后设置数据源并调用notifydatasetchanged()。

在内部关闭键盘时listview将更新其ui。它一直打电话直到关闭键盘。那个时候如果数据源改变它会抛出这个异常。 如果onActivityResult中的数据正在更新,则可能会出现相同的错误。

 InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.hideSoftInputFromWindow(v.getWindowToken(), 0);

        view.postDelayed(new Runnable() {
            @Override
            public void run() {
                refreshList();
            }
        },100L);

答案 23 :(得分:0)

我的解决方案:

1)创建temp ArrayList

2)在doInBackground方法中执行繁重的工作(sqlite row fetch,...)并将项添加到temp arraylist。

3)使用onPostExecute方法将临时araylist中的所有项目添加到listview的arraylist中。

note:你可能想要从listview中删除一些项目,也可以从sqlite数据库中删除,也许从sdcard删除一些与项目相关的文件,只需从数据库中删除项目并删除它们的相关文件并将它们添加到temp arraylist在background thread。然后在UI thread中从listview的arraylist中删除临时arraylist中存在的项目。

希望这有帮助。

答案 24 :(得分:0)

adapter.notifyDataSetChanged()