Несколько push-сообщений: содержимое адаптера изменилось, но ListView не получил уведомление

Когда я получаю много push-сообщений (скажем 50) из GCM в течение 1 секунды, я получаю следующее исключение:

Java.lang.IllegalStateException: содержимое адаптера изменилось, но ListView не получил уведомление. Убедитесь, что содержимое вашего адаптера не изменено из фонового потока, но только из потока пользовательского интерфейса. [В ListView (2131427434, класс android.widget.ListView) с адаптером (класс an)] в файле android.widget.ListView.layoutChildren (ListView.java:1544) в android.widget.AbsListView.onLayout (AbsListView.java:2045) На android.view.View.layout (View.java:14255) на android.view.ViewGroup.layout (ViewGroup.java:4413) на android.widget.LinearLayout.setChildFrame (LinearLayout.java:1670) на android.widget. LinearLayout.layoutVertical (LinearLayout.java:1528) в файле android.widget.LinearLayout.onLayout (LinearLayout.java:1441) в android.view.View.layout (View.java:14255) в android.view.ViewGroup.layout (ViewGroup .java: 4413) в android.support.v4.view.ViewPager.onLayout (Неизвестный источник) в android.view.View.layout (View.java:14255) в android.view.ViewGroup.layout (ViewGroup.java:4413 ) На android.widget.LinearLayout.setChildFrame (LinearLayout.java:1670) на android.widget.LinearLayout.layoutVertical (LinearLayout.java:1528) на android.widget.LinearLayout.onLayout (LinearLayout.java:1441) на android.view .View.layout (Vie W.java:14255) на android.view.ViewGroup.layout (ViewGroup.java:4413) на android.support.v4.widget.DrawerLayout.onLayout (Неизвестный источник) на android.view.View.layout (View.java: 14255) на android.view.ViewGroup.layout (ViewGroup.java:4413) на android.widget.FrameLayout.onLayout (FrameLayout.java:446) на android.view.View.layout (View.java:14255) на android. View.ViewGroup.layout (ViewGroup.java:4413) в файле android.support.v7.internal.widget.ActionBarOverlayLayout.onLayout (Неизвестный источник) в android.view.View.layout (View.java:14255) в android.view. ViewGroup.layout (ViewGroup.java:4413) в android.widget.FrameLayout.onLayout (FrameLayout.java:446) в android.view.View.layout (View.java:14255) в android.view.ViewGroup.layout (ViewGroup .java: 4413) на android.widget.LinearLayout.setChildFrame (LinearLayout.java:1670) на android.widget.LinearLayout.layoutVertical (LinearLayout.java:1528) на android.widget.LinearLayout.onLayout (LinearLayout.java:1441) На android.view.View.layout (View.java:14255) на android.vi Ew.ViewGroup.layout (ViewGroup.java:4413) в файле android.widget.FrameLayout.onLayout (FrameLayout.java:446) в android.view.View.layout (View.java:14255) в android.view.ViewGroup.layout (ViewGroup.java:4413) в android.view.ViewRootImpl.performLayout (ViewRootImpl.java:1998) в android.view.ViewRootImpl.performTraversals (ViewRootImpl.java:1812) в android.view.ViewRootImpl.doTraversal (ViewRootImpl.java: 1050) на android.view.ViewRootImpl $ TraversalRunnable.run (ViewRootImpl.java:4560) на android.view.Choreographer $ CallbackRecord.run (Хореограф.java:749) на android.view.Choreographer.doCallbacks (Хореограф.java:562 ) На android.view.Choreographer.doFrame (Хореограф.java:532) на android.view.Choreographer $ FrameDisplayEventReceiver.run (Хореограф.java:735) на android.os.Handler.handleCallback (Handler.java:725) на android .os.Handler.dispatchMessage (Handler.java:92) на android.os.Looper.loop (Looper.java:137) в android.app.ActivityThread.main (ActivityThread.java:5171) в java.lang.reflect. M Thisod.invokeNative (Native Method) в java.lang.reflect.Method.invoke (Method.java:511) в com.android.internal.os.ZygoteInit $ MethodAndArgsCaller.run (ZygoteInit.java:797) в com.android. Internal.os.ZygoteInit.main (ZygoteInit.java:564) в dalvik.system.NativeStart.main (собственный метод)

Я уже пытался исправить это, поставив ОБОИХ messages.add() и notifyDataSetChanged() внутри runOnUIThread . Я предполагаю, что это происходит, потому что onUpdate() моего слушателя вызывается для каждого push-сообщения. Но не должна ли эта проблема решаться 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(); } }); } }); 

EDIT: Я, вероятно, пропустил очень важную часть кода, который я забыл сам:

 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 или есть лучший способ?

EDIT 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: Полный источник фрагмента, который содержит код: https://gist.github.com/ChristopherWalz/89a071b1606460e18ce7

Solutions Collecting From Web of "Несколько push-сообщений: содержимое адаптера изменилось, но ListView не получил уведомление"

Прежде всего, давайте посмотрим на код в 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 – сообщение было более информативным в сентябре `13]

Давайте посмотрим, где mItemCount переменная-член mItemCount … Хм, эта переменная кажется наследуемой полностью от класса AdapterView . Итак, давайте найдем все назначения во всех классах:

Введите описание изображения здесь

В принципе, за исключением одного присваивания 0 (который находится в onIvalidated() и может быть проигнорирован), эта переменная всегда назначается значению getCount() Adapter .

Мы можем заключить, что вы получаете исключение, потому что есть некоторый параллельный код, который изменяет данные вашего Adapter , когда он используется для рисования содержимого ListView .

Теперь, исходя из вашего вопроса, похоже, что вы подозреваете, что существует какая-то «перегрузка» обновлений в потоке пользовательского интерфейса, потому что сообщений слишком много … Однако давайте иметь в виду, что код Runnable.run() который Вы отправляете в поток пользовательского интерфейса для выполнения, выполняет атомарно – каждый Runnable выгружается из очереди событий потока пользовательского интерфейса и запускается до завершения, прежде чем какое-либо другое событие получит возможность обработки.

Вышеизложенное означает, что messages будут обновляться новыми данными, а messageAdapter будет обрабатывать изменения немедленно, и никакое другое событие не может помешать этому потоку (до тех пор, пока messages.add() и messages.addAll() в вашем коде являются синхронными вызовами ). Итог: код, который отправляет Runnables в поток пользовательского интерфейса, выглядит нормально, и маловероятно, что он является источником проблемы. Кроме того, трассировка стека исключений не содержит ссылок на Adapter .

До сих пор мы обобщали факты. Давайте начнем догадки.

Я думаю, что проблема не в коде, который вы опубликовали. Я думаю, вы делаете одно (или более) из следующих действий, каждое из которых может привести к тому, что вы получили:

  1. Я предполагаю, что messages – это те же структуры данных, которые используются messageAdapter внутренне. Возможно, вы модифицируете messages в другой части кода, которая не работает в потоке пользовательского интерфейса. В этом случае эта модификация может произойти, когда ListView обновляется в потоке пользовательского интерфейса и приводит к исключению [обычно это плохая практика «утечки» структуры данных Adapter вне объекта Adapter ].
  2. Аналогичным образом, вы можете случайно манипулировать messageAdapter в других частях кода, который не работает в потоке пользовательского интерфейса.
  3. Возможно, вы отправляете события в поток пользовательского интерфейса, пока ListView еще не завершил свою инициализацию. Я почти не верю, что это так, но для того, чтобы быть в безопасности, я предлагаю вам убедиться, что вы зарегистрируете своих слушателей в onResume() и onPause() регистрацию в onPause()

РЕДАКТИРОВАТЬ:

Основываясь на коде вашего Fragment я вижу две возможные причины исключения:

  1. Если методы CustomResponseHandler вы передаете ServerUtil.post() будут вызваны из фоновых потоков, тогда может произойти тот факт, что вы удаляете объект из messages в onFailure() .
  2. Как я уже сказал, – регистрировать слушателей в onResume() и onPause() регистрацию в onPause() – это может быть не важно для прослушивателей кнопок, но это вызывает утечку памяти при передаче этих слушателей объекту Application . У вас есть утечки памяти в коде .

Если ни одно из вышеперечисленных действий не помогает, MessageAdapter код MessageAdapter и MessageDatabase

  1. Создайте экземпляр адаптера один раз при запуске действия или фрагментируйте что-то вроде этого:

MessageAdapter = новый MessageAdapter (getActivity (), R.layout.list_message_item, сообщения); listView.setAdapter (messageAdapter);

Затем напишите свой класс адаптера сообщений

Класс адаптера сообщения:

 private ArrayList<Message> mMessageList; // constructor MessageAdapter(Activity activity,int layoutId, ArrayList<Message> messageList){ .. updateMessageList(messageList); } public void updateMessageList(ArrayList<Message> newMessageList){ // if First time this method is called then if(mMessageList==null){ mMessageList=new ArrayList<Message>(); } //Add new messages to message list if(null!=newMessageList && newMessageList.size()>0){ //add new messages mMessageList.addAll(newMessageList); //Sort you message list Collections.sort(mMessageList); } //update list notifyDataSetChanged(); } ..... end adapter class 
  1. Теперь из класса Activity Class или класса Fragment вы можете использовать обработчик, поскольку он действует как мост между двумя потоками (например, пользовательский интерфейс и фоновый поток).
  //local message list declaration messages=new ArrayList<Message>(); //Declare handler as a Class member variable Handler globalhandler=new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == "Some identifier string") { // after 50 messages are collected it sends one update //request after one second to list adapter messageAdapter.updateMessageList(messages); // you can also fetch messages from db and update list by calling messageAdapter.updateMessageList(messages); method } } } 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()); } messages.add(new Message(-1, user, message, System.currentTimeMillis(), System .currentTimeMillis(), false, roomID, 0, true, Message.TYPE_JOINLEAVE)); //post via handler you can also send a delayed update(like 1 second) since you are receiving 50 messages in a second. globalhandler.removeMessages("Some identifier string"); globalhandler.sendEmptyMessageDelayed("Some identifier string", 1000); }); 

Надеюсь, это ответит на ваш вопрос.