Несколько push-сообщений: содержимое адаптера изменилось, но ListView не получил уведомление
Когда я получаю много push-сообщений (пусть говорят 50) из GCM в течение 1 секунды, я получаю следующее исключение:
java.lang.IllegalStateException: содержимое адаптера имеет изменено, но ListView не получил уведомление. Убедитесь, что содержимое вашего адаптера не изменяется из фонового потока, но только из потока пользовательского интерфейса. [в ListView (2131427434, класс android.widget.ListView) с адаптером (класс a.n)] на 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(View.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(Неизвестно Source) в 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.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.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(Choreographer.java:749) на android.view.Choreographer.doCallbacks(Хореограф .java:562) на android.view.Choreographer.doFrame(Хореограф .java:532) at android.view.Choreographer $FrameDisplayEventReceiver.run(Choreographer.java:735) на android.os.Handler.handleCallback(Handler.java:725) на android.os.Handler.dispatchMessage(Handler.java:92) at android.os.Looper.loop(Looper.java:137) в android.app.ActivityThread.main(ActivityThread.java:5171) на java.lang.reflect.Method.invokeNative(собственный метод) в 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) at dalvik.system.NativeStart.main(собственный метод)
Я уже пытался это исправить, положив BOTH 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();
}
});
}
});
РЕДАКТИРОВАТЬ: Я, вероятно, пропустил очень важную часть кода, который я забыл сам:
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: Полный источник фрагмента, который содержит код: https://gist.github.com/ChristopherWalz/89a071b1606460e18ce7
Ответы
Ответ 1
Прежде всего, рассмотрим код в 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
... Хм, эта переменная кажется наследуемой полностью из класса AdapterView
. Хорошо, давайте найдем все назначения во всех классах:
![enter image description here]()
В принципе, за исключением одного присваивания 0
(который находится в методе onIvalidated()
и может быть проигнорирован), эта переменная всегда назначается значению Adapter
getCount()
.
Мы можем заключить, что вы получаете исключение, потому что есть некоторый параллельный код, который изменяет ваши данные Adapter
, пока он используется для рисования содержимого ListView
.
Теперь, из вашего вопроса, похоже, что вы подозреваете, что существует какая-то "перегрузка" обновлений в потоке пользовательского интерфейса, потому что слишком много сообщений... Однако имейте в виду, что код Runnable.run()
который вы отправляете в поток пользовательского интерфейса для выполнения, выполняется атомарно - каждый Runnable
выталкивается из очереди событий потока пользовательского интерфейса и запускается до завершения, прежде чем какое-либо другое событие получит возможность обработки.
Вышеизложенное означает, что messages
будет обновляться новыми данными, а messageAdapter
будет обрабатывать изменения немедленно, и никакое другое событие не может помешать этому потоку (пока messages.add()
и messages.addAll()
в вашем коде являются синхронными вызовами). Итог: код, который отправляет Runnables
в поток пользовательского интерфейса, выглядит отлично, и маловероятно, что он является источником проблемы. Кроме того, трассировка стека исключения не содержит ссылок на Adapter
.
До сих пор мы обобщали факты. Начните догадки.
Я думаю, что проблема не в том коде, который вы опубликовали. Я думаю, вы делаете одно (или более) из следующих действий, каждое из которых может привести к тому, что вы получили:
- Я предполагаю, что
messages
является той же структурой данных, что и messageAdapter
. Возможно, вы изменили messages
в какой-то другой части кода, который не работает в потоке пользовательского интерфейса. В этом случае эта модификация может произойти, когда ListView
обновляется в потоке пользовательского интерфейса и приводит к исключению [обычно это плохая практика для "утечки" Adapter
структуры данных за пределами объекта Adapter
].
- Аналогичным образом, вы можете случайно манипулировать
messageAdapter
в других частях кода, который не работает в потоке пользовательского интерфейса.
- Возможно, вы отправляете события в поток пользовательского интерфейса, а
ListView
еще не завершили свою инициализацию. Я почти не верю, что это так, но для того, чтобы быть в безопасности, я предлагаю вам убедиться, что вы зарегистрируете своих слушателей в onResume()
и отмените регистрацию в onPause()
EDIT:
На основе кода вашего Fragment
я вижу две возможные причины исключения:
- Если методы из
CustomResponseHandler
вы перейдете к ServerUtil.post()
, будут вызваны из фоновых потоков, тогда может произойти тот факт, что вы удаляете объект из messages
в onFailure()
.
- Как я и предложил - зарегистрировать слушателей в
onResume()
и отменить регистрацию в onPause()
- это может быть не важно для прослушивателей кнопок, но это вызывает утечку памяти при передаче этих слушателей объекту Application
. У вас есть утечка памяти в коде.
Если ни одно из вышеперечисленных действий не помогает, отправьте код messageAdapter
и MessageDatabase
Ответ 2
- Создайте экземпляр адаптера один раз при запуске действия или фрагментируйте что-то вроде этого:
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
- Теперь из класса "Класс активности" или "Фрагмент" Вы можете использовать обработчик, поскольку он действует как мост между двумя потоками (пользовательский интерфейс к фону thread также) что-то вроде этого.
//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);
});
Надеюсь, это ответит на ваш вопрос.