Подтвердить что ты не робот

Несколько 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

4b9b3361

Ответ 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
  1. Теперь из класса "Класс активности" или "Фрагмент" Вы можете использовать обработчик, поскольку он действует как мост между двумя потоками (пользовательский интерфейс к фону 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);
});

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