Android, ListView IllegalStateException :“어댑터의 내용이 변경되었지만 ListView가 알림을받지 못했습니다” 거의 매일 충돌이 발생 list.setVisibility(GONE)/trackList.setVisibility(VISIBLE)했으며

내가하고 싶은 일 : 결과가 계산되는 동안 ListView 내용을 계산하고 ListView를 부분적으로 업데이트하는 백그라운드 스레드를 실행하십시오.

내가 피해야 할 것 : 백그라운드 스레드에서 ListAdapter 내용을 망칠 수 없으므로 AsyncTask를 상속하고 onProgressUpdate에서 결과를 어댑터에 추가합니다. 내 어댑터는 결과 객체의 ArrayList를 사용하며 해당 어레이 목록의 모든 작업이 동기화됩니다.

다른 사람들의 연구 : 여기에는 매우 귀중한 자료가 있습니다 . 또한 ~ 500 명의 사용자 그룹에 대해 거의 매일 충돌이 발생 list.setVisibility(GONE)/trackList.setVisibility(VISIBLE)했으며 onProgressUpdate 에 블록을 추가 하면 충돌이 10 배 감소했지만 사라지지 않았습니다. ( 답변 으로 제안되었습니다 )

내가 때때로 얻는 것 : 실제로는 거의 발생하지 않습니다 (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)
[...]

도움? 더 이상 필요하지 않습니다. 아래를 참조하십시오.

최종 답변 : 결과적으로 notifyDataSetChanged깜박임과 급격한 목록 변경을 피하기 위해 5 회 삽입 할 때마다 전화 를 걸었습니다. 기본 목록이 변경되면 항상 어댑터에 알리십시오. 이 버그는 오래 전에 사라졌습니다.



답변

나는 같은 문제가 있었다.

ArrayListUI 스레드 외부에 항목을 추가하고있었습니다 .

솔루션 : 두 가지를 모두 수행 했으며 UI 스레드에서 adding the items호출 notifyDataSetChanged()했습니다.


답변

같은 문제가 있었지만 방법을 사용하여 문제를 해결했습니다.

requestLayout();

수업에서 ListView


답변

이것은 멀티 스레딩 문제이며 제대로 동기화 된 블록 사용 방지 할 수 있습니다. UI 스레드에 추가 항목을 넣지 않고 앱의 응답 성을 잃지 않습니다.

나도 똑같이 직면했다. 가장 널리 인정되는 답변에서 UI Thread에서 어댑터 데이터를 변경하면 문제를 해결할 수 있습니다. 그것은 작동하지만 빠르고 쉬운 해결책이지만 최선의 해결책은 아닙니다.

보시다시피 정상적인 경우입니다. 백그라운드 스레드에서 데이터 어댑터를 업데이트하고 UI 스레드에서 notifyDataSetChanged를 호출하면 작동합니다.

이 illegalStateException은 UI 스레드가보기를 업데이트하고 다른 백그라운드 스레드가 데이터를 다시 변경하는 경우 발생합니다. 그 순간이 문제가 발생합니다.

따라서 어댑터 데이터를 변경하고 notifydatasetchange를 호출하는 모든 코드를 동기화하는 경우. 이 문제는 사라져야합니다. 나를 위해 사라졌고 여전히 백그라운드 스레드에서 데이터를 업데이트하고 있습니다.

다른 사람들이 참조 할 수있는 사례 별 코드는 다음과 같습니다.

메인 화면의 내 로더는 전화 번호부 연락처를 백그라운드의 내 데이터 소스에로드합니다.

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

이 PhoneBookManager.getPhoneBookContacts는 전화 번호부에서 연락처를 읽고 해시 맵에 채 웁니다. 목록 어댑터가 목록을 그리는 데 직접 사용할 수 있습니다.

화면에 버튼이 있습니다. 전화 번호가 나열된 활동이 열립니다. 이전 스레드가 작업을 완료하기 전에 목록에 어댑터를 직접 설정하면 빠른 탐색 사례가 덜 자주 발생합니다. 이 SO 질문의 제목은 예외입니다. 두 번째 활동에서 이와 같은 작업을 수행해야합니다.

두 번째 활동의 내 로더는 첫 번째 스레드가 완료되기를 기다립니다. 진행률 표시 줄이 표시 될 때까지 두 로더의 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());
        }
    }

}

도움이 되었기를 바랍니다


답변

나는 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();
        }
    });
}

답변

이 코드를 작성하여 ~ 12 시간 동안 2.1 에뮬레이터 이미지에서 실행했으며 IllegalStateException을 얻지 못했습니다. 나는 안드로이드 프레임 워크에 이것에 대한 의심의 이점을 줄 것이고 그것이 코드에 오류가 있다고 말할 것입니다. 이게 도움이 되길 바란다. 목록과 데이터에 적용 할 수 있습니다.

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);
    }
}

답변

며칠 전에 동일한 문제가 발생하여 하루에 수천 건의 충돌이 발생했으며 약 0.1 %의 사용자가이 상황을 충족합니다. 나는 시도 setVisibility(GONE/VISIBLE)하고 requestLayout()있지만, 충돌 횟수는 약간 감소합니다.

그리고 마침내 그것을 해결했습니다. 와 아무것도 없습니다 setVisibility(GONE/VISIBLE). 와 아무것도 없습니다 requestLayout().

마지막으로 데이터를 업데이트 한 후 Handler호출 하는 데 사용 된 이유는 notifyDataSetChanged()다음과 같습니다.

  1. 데이터를 모델 객체로 업데이트합니다 (데이터 소스라고 함).
  2. 사용자가 listview를 터치합니다 ( checkForTap()/ onTouchEvent()를 호출하고 마지막으로 호출 할 수 있음 layoutChildren())
  3. 어댑터는 모델 오브젝트에서 데이터를 가져오고 notifyDataSetChanged()뷰를 호출 하고 업데이트합니다.

그리고 나는 또 다른 실수를 getCount(), getItem()그리고 getView()내가 직접 오히려 어댑터 사본 그들을보다, 데이터 소스의 필드를 사용합니다. 따라서 다음과 같은 경우에 충돌이 발생합니다.

  1. 어댑터는 마지막 응답이 제공하는 데이터를 업데이트합니다
  2. 다음에 응답 할 때 DataSource는 데이터를 업데이트하여 항목 수를 변경합니다.
  3. 사용자가 탭 또는 이동 또는 뒤집기 일 수있는 목록보기를 터치합니다.
  4. getCount()그리고 getView()라고하며, 목록보기 발견 데이터가 일치하지 않습니다, 그리고 같은 예외가 발생합니다 java.lang.IllegalStateException: The content of the adapter has changed but.... 다른 일반적인 예외는에서 IndexOutOfBoundException헤더 / 바닥 글을 사용 하는 경우입니다 ListView.

그래서 해결책은 쉽습니다. 내 핸들러가 데이터를 가져오고 호출하기 위해 어댑터를 트리거 할 때 데이터 소스에서 어댑터로 데이터를 복사합니다 notifyDataSetChanged(). 이제 충돌이 다시 발생하지 않습니다.


답변

이 문제가 간헐적으로 발생했다면 마지막 항목을 클릭 한 후 목록을 스크롤했을 때만이 문제가 발생했습니다. 목록이 스크롤되지 않으면 모든 것이 잘 작동합니다.

MUCH 디버깅 후 버그가 발생했지만 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 일 수 있습니다.