android - 多条推送消息 : The content of the adapter has changed but ListView did not receive a notification

标签 android android-listview google-cloud-messaging

当我在 1 秒内从 GCM 收到大量推送消息(比如说 50 条)时,我收到以下异常:

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(2131427434, class android.widget.ListView) with Adapter(class a.n)] at android.widget.ListView.layoutChildren(ListView.java:1544) at android.widget.AbsListView.onLayout(AbsListView.java:2045) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1670) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1528) at android.widget.LinearLayout.onLayout(LinearLayout.java:1441) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.support.v4.view.ViewPager.onLayout(Unknown Source) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1670) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1528) at android.widget.LinearLayout.onLayout(LinearLayout.java:1441) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.support.v4.widget.DrawerLayout.onLayout(Unknown Source) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.FrameLayout.onLayout(FrameLayout.java:446) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.support.v7.internal.widget.ActionBarOverlayLayout.onLayout(Unknown Source) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.FrameLayout.onLayout(FrameLayout.java:446) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1670) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1528) at android.widget.LinearLayout.onLayout(LinearLayout.java:1441) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.FrameLayout.onLayout(FrameLayout.java:446) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:1998) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1812) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1050) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:4560) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:749) at android.view.Choreographer.doCallbacks(Choreographer.java:562) at android.view.Choreographer.doFrame(Choreographer.java:532) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:735) at android.os.Handler.handleCallback(Handler.java:725) at android.os.Handler.dispatchMessage(Handler.java:92) at android.os.Looper.loop(Looper.java:137) at android.app.ActivityThread.main(ActivityThread.java:5171) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:511) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:797) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:564) at dalvik.system.NativeStart.main(Native Method)



我已经尝试通过将 messages.add()notifyDataSetChanged() 都放在 runOnUIThread 中来解决这个问题。我想这是因为每条推送消息都会调用我的听众的 onUpdate()。但是这个问题不应该由 runOnUIThread() 解决吗,因为一切都是连续执行的?
    MainApplication app = (MainApplication) context.getApplicationContext();
    app.setOnRoomMessageUpdateListener(new OnRoomMessageUpdateListener() {
        @Override
        public void onUpdate() {
            // save message with highest time, so we can only query the new
            // messages
            long highestTime = getHighestMessageTime();

            messageDatabase.getConditionBuilder().add(
                    DatabaseHelper.KEY_MESSAGE_ROOM_ID + " = ? AND " + DatabaseHelper.KEY_MESSAGE_LOCAL_TIME
                            + " > ? AND " + DatabaseHelper.MESSAGE_TABLE_NAME + "."
                            + DatabaseHelper.KEY_MESSAGE_USER_ID + " <> ?",
                    new String[] { String.valueOf(roomID), String.valueOf(highestTime),
                            String.valueOf(user.getUserID()) });
            messageDatabase.getConditionBuilder().setSortOrder(DatabaseHelper.KEY_MESSAGE_LOCAL_TIME + " DESC");
            final ArrayList<Message> newMessages = messageDatabase.getList();

            ((Activity) context).runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    messages.addAll(newMessages);
                    messageAdapter.notifyDataSetChanged();
                }
            });
        }
    });

编辑: 我可能错过了代码的一个非常重要的部分,我自己忽略了它:
app.setOnRoomUserUpdateListener(new OnRoomUserUpdateListener() {
    @Override
    public void onUpdate(final User user, final int roomID, final int joinStatus) {
        final String message;
        if (joinStatus == OnRoomUserUpdateListener.USER_JOINED) {
            message = context.getString(R.string.join_room_message, user.getUsername());
        } else {
            message = context.getString(R.string.leave_room_message, user.getUsername());
        }

        ((Activity) context).runOnUiThread(new Runnable() {
            @Override
            public void run() {
                messages.add(new Message(-1, user, message, System.currentTimeMillis(), System
                        .currentTimeMillis(), false, roomID, 0, true, Message.TYPE_JOINLEAVE));
                messageAdapter.notifyDataSetChanged();
            }
        });
    }
});

该代码块位于上面的代码下方,显然它也在修改适配器的内容。当他们两个同时运行时,可能会出现问题,对吧?这可以通过使用 synchronized 解决还是有更好的方法?

编辑 2:
初始化:
// get all messages
    messages = new ArrayList<Message>();
    messageDatabase.getConditionBuilder().add(DatabaseHelper.KEY_MESSAGE_ROOM_ID + " = ?",
            new String[] { String.valueOf(roomID) });
    messageDatabase.getConditionBuilder().setSortOrder(DatabaseHelper.KEY_MESSAGE_LOCAL_TIME + " DESC");
    messageDatabase.getConditionBuilder().setSqlLimit(100);
    messages.addAll(messageDatabase.getList());

    // get "user joined/left" messages
    UserDatabase userDatabase = UserDatabase.getInstance(context);
    messages.addAll(userDatabase.getJoinLeaveMessages(roomID));

    Collections.sort(messages);
    messageAdapter = new MessageAdapter(getActivity(), R.layout.list_message_item, messages);
    listView.setAdapter(messageAdapter);

编辑 3: fragment 的完整源代码,其中包含代码:https://gist.github.com/ChristopherWalz/89a071b1606460e18ce7

最佳答案

首先我们看一下ListView中的代码引发此异常:

@Override
protected void layoutChildren() {
    // ... code omitted...

        // 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() + ")]");
        }
    // ... code omitted...
}

[您可能会注意到消息有所不同,但这只是因为您使用的是旧版 Android - 该消息已在 9 月 `13 中提供了更多信息]

让我们看看mItemCount声明了成员变量……嗯,这个变量好像是从AdapterView一路继承而来的类(class)。好的,让我们找到所有类(class)中的所有作业:

enter image description here

基本上,除了对 0 的单个分配(在 onIvalidated() 方法中,可以忽略),这个变量总是赋值给 AdaptergetCount()值(value)。

我们可以得出结论,您得到了异常,因为有一些并发代码会改变您的 Adapter。的数据,而它用于绘制 ListView 的内容.

现在,从您的问题来看,您似乎怀疑 UI 线程上存在某种更新“拥塞”,因为消息太多......但是,请记住 Runnable.run() 的代码您发布到 UI 线程以执行的自动执行 - 每个 Runnable从 UI 线程的事件队列中弹出,并在任何其他事件有机会处理之前运行完成。

以上表示messages将使用新数据和 messageAdapter 进行更新将立即处理更改,并且没有其他事件可以干扰此流程(只要您的代码中的 messages.add()messages.addAll() 是同步调用)。底线:调度 Runnables 的代码到 UI 线程看起来很好,它不太可能是问题的根源。此外,异常的堆栈跟踪不包含对 Adapter 的任何引用。 .

到目前为止,我们总结了事实。让我们开始猜测。

我认为问题不在您发布的代码中。我猜你做了以下一个(或多个)操作,每一个都可能导致你得到的异常:
  • 我猜messagesmessageAdapter 使用的数据结构相同内部。可能是您修改了messages在代码的其他部分中,它不在 UI 线程上运行。在这种情况下,此修改可能发生在 ListView在 UI 线程上刷新并导致异常 [“泄漏”Adapter 通常是不好的做法的 Adapter 之外的数据结构目的]。
  • 同样,您可能不小心操纵了 messageAdapter在代码的其他部分未在 UI 线程上运行。
  • 可能是您在 ListView 时将事件发布到 UI 线程。尚未完成初始化。我几乎不相信是这种情况,但为了安全起见,我建议您确保在 onResume() 中注册您的听众。并在 onPause() 中取消注册

  • 编辑:

    基于您的 Fragment 的代码我可以看到异常的两个可能原因:
  • 如果CustomResponseHandler的方法你传给 ServerUtil.post()将从后台线程中调用,然后从 messages 中删除对象这一事实在 onFailure()可能是这样。
  • 正如我所建议的 - 在 onResume() 中注册听众并在 onPause() 中注销- 这对于按钮单击监听器可能并不重要,但是当您将这些监听器传递给 Application 时会导致内存泄漏目的。 您的代码中有内存泄漏 .

  • 如果上述方法都没有帮助,请发布 MessageAdapter 的代码和 MessageDatabase

    关于android - 多条推送消息 : The content of the adapter has changed but ListView did not receive a notification,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31191259/

    相关文章:

    android - 更改 Activity 堆栈行为

    android - ListView 选中位置总是重置

    android - 添加 onClick 监听器以在适配器内部查看时,ListView 的 listSelector 样式不起作用

    php - GCM curl 操作超时

    iOS - GCM 运行时错误

    android - Runnable 未在 Android 上的 float 操作栏上运行

    Java - 动态添加数据到 String[ ] 和 Composer[ ][ ]

    android - 为 react-native native android 方法编写单元测试

    java - ListView 的 onclicklistener

    android - 实现 GCM 时如何从 Android 应用程序调用服务器应用程序 servlet 来注册设备