402 дня назад  —  12 Апрель 2011

Мобильные приложения

Android: ListView, Adapter и различные layout элементов списка в одном ListView

Разрабатывая один проект, я обнаружил проблему утечки памяти в таком графическом элементе как ListView, а именно в адаптере (Adapter), который с ним связан. После нескольких минут поиска в google.com и stackoverflow.com я понял, что создавать каждый раз объект(ы) для convertView в методе getView(int postion, View convertView, ViewGroup parent) неграмотно, поскольку этот метод вызывается каждый раз, когда появляется новый элемент списка. Для того, чтобы этого избежать, достаточно ввести новый класс, например ViewHolder, и хранить в нем экземпляры нужных нам виджетов. Т.е. вместо следующего кода:

 public View getView(int position, View convertView, ViewGroup parent) {
            TextView text;

            if (convertView == null) {
                text = (TextView)mInflater.inflate(android.R.layout.simple_list_item_1, parent, false);
            } else {
                text = (TextView)convertView;
            }

            text.setText("Some text.");

            return text;
        }

нужно использовать следующий:

public View getView(int position, View convertView, ViewGroup parent) {
            // ViewHolder содержит ссылки на виджеты для избежания ненужных вызовов метода findViewById()
            // в каждом новом видимом элементе списка
            ViewHolder holder;

            // Если convertView не null, мы используем его напрямую, нет необходимости повторно
            // вызывать inflate
            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.list_item_icon_text, null);

                holder = new ViewHolder();
                holder.text = (TextView) convertView.findViewById(R.id.text);
                holder.icon = (ImageView) convertView.findViewById(R.id.icon);

                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }

            holder.text.setText("Some text.");

            return convertView;
        }

        static class ViewHolder {
            TextView text;
        }

Но на этом я не остановился и решил понять механизм работы в принципе метода getView в частности и Adapter в общем. И моё любопытство было вознаграждено.

Итак, давайте подведём итоги, что мы уже уяснили:

  • ListView для каждого элемента списка вызывает метод getView у связанного с ним Adapter
  • Новый View создается и отображается

А что, если наших элементов списка будет не 10 и не 100, а скажем 1 000 000 000. Создавать новый View для каждого из элементов? Это было бы глупо, и я был уверен, что Google на это не способен. На самом деле Android кэширует графические виджеты для нас, т.е. объекты типа View. Есть компонент в Android, который называется «Recycler». На просторах я нашел картинку, иллюстрирующую его работу:

Если вы имеете 1 миллиард записей — только видимые элементы будут в памяти + виджеты в Recycler. ListView вызывает для виджета типа type1 метод getView столько раз, сколько элементов отображается при первой загрузке. createView — null в методе getView и поэтому создается новый виджет типа type1 и возвращает его. ListView запрашивает виджет типа type1, когда один из виджетов данного типа за пределами видимости, и новый элемент такого же типа появляется снизу. convertView уже не null и равняется item1. Вам достаточно установить новое значение и вернуть convertView. Уже не нужно создавать заново виджет.

Давайте посмотрим, что выведет в консоль данный код:

public class MultipleItemsList extends ListActivity {

    private MyCustomAdapter mAdapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new MyCustomAdapter();
        for (int i = 0; i < 50; i++) {
            mAdapter.addItem("item " + i);
        }
        setListAdapter(mAdapter);
    }

    private class MyCustomAdapter extends BaseAdapter {

        private ArrayList mData = new ArrayList();
        private LayoutInflater mInflater;

        public MyCustomAdapter() {
            mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

        public void addItem(final String item) {
            mData.add(item);
            notifyDataSetChanged();
        }

        @Override
        public int getCount() {
            return mData.size();
        }

        @Override
        public String getItem(int position) {
            return mData.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            System.out.println("getView " + position + " " + convertView);
            ViewHolder holder = null;
            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.item1, null);
                holder = new ViewHolder();
                holder.textView = (TextView)convertView.findViewById(R.id.text);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder)convertView.getTag();
            }
            holder.textView.setText(mData.get(position));
            return convertView;
        }

    }

    public static class ViewHolder {
        public TextView textView;
    }
}

Запустив программу, мы увидим следующее

Метод getView вызовется 9 раз и все эти разы convertView будет равен null.

INFO/System.out(947): getView 0 null
INFO/System.out(947): getView 1 null
INFO/System.out(947): getView 2 null
INFO/System.out(947): getView 3 null
INFO/System.out(947): getView 4 null
INFO/System.out(947): getView 5 null
INFO/System.out(947): getView 6 null
INFO/System.out(947): getView 7 null
INFO/System.out(947): getView 8 null

Прокрутим слегка вниз до тех пор, пока не появится item10:

convertView до сих пор null, потому что в Recycler нет виджета, т.к. граница item1 все ещё видна наверху.

INFO/System.out(947): getView 9 null

Давайте прокрутим список ещё:

Ура! convertView уже не null: item1 скрылся из области видимости прямиком в Recycler и item11 создалось на основе item1.

INFO/System.out(947): getView 10 android.widget.LinearLayout@437430f8

Давайте прокрутим ещё вниз, просто чтобы проверить, что случится:

INFO/System.out(947): getView 11 android.widget.LinearLayout@437447d0
INFO/System.out(947): getView 12 android.widget.LinearLayout@43744ff8
INFO/System.out(947): getView 13 android.widget.LinearLayout@43743fa8
INFO/System.out(947): getView 14 android.widget.LinearLayout@43745820
INFO/System.out(947): getView 15 android.widget.LinearLayout@43746048
INFO/System.out(947): getView 16 android.widget.LinearLayout@43746870
INFO/System.out(947): getView 17 android.widget.LinearLayout@43747098
INFO/System.out(947): getView 18 android.widget.LinearLayout@437478c0
INFO/System.out(947): getView 19 android.widget.LinearLayout@43748df0
INFO/System.out(947): getView 20 android.widget.LinearLayout@437430f8

convertView, как мы и ожидали, не null.

Теперь давайте посмотрим, что произойдет, если мы добавим сепаратор где-нибудь в списке. Вы должны сделать следующее

  • Переопределить метод getViewTypeCount() – возвращает сколько различных layout будет использоваться
  • Переопределить метод getItemViewType(int) – возвращает id типа по его позиции
  • Сослаться convertView на нужный виджет в методе getView в зависимости от типа

Вот пример кода:

public class MultipleItemsList extends ListActivity {

    private MyCustomAdapter mAdapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new MyCustomAdapter();
        for (int i = 1; i < 50; i++) {
            mAdapter.addItem("item " + i);
            if (i % 4 == 0) {
                mAdapter.addSeparatorItem("separator " + i);
            }
        }
        setListAdapter(mAdapter);
    }

    private class MyCustomAdapter extends BaseAdapter {

        private static final int TYPE_ITEM = 0;
        private static final int TYPE_SEPARATOR = 1;
        private static final int TYPE_MAX_COUNT = TYPE_SEPARATOR + 1;

        private ArrayList mData = new ArrayList();
        private LayoutInflater mInflater;

        private TreeSet mSeparatorsSet = new TreeSet();

        public MyCustomAdapter() {
            mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

        public void addItem(final String item) {
            mData.add(item);
            notifyDataSetChanged();
        }

        public void addSeparatorItem(final String item) {
            mData.add(item);
            mSeparatorsSet.add(mData.size() - 1);
            notifyDataSetChanged();
        }

        @Override
        public int getItemViewType(int position) {
            return mSeparatorsSet.contains(position) ? TYPE_SEPARATOR : TYPE_ITEM;
        }

        @Override
        public int getViewTypeCount() {
            return TYPE_MAX_COUNT;
        }

        @Override
        public int getCount() {
            return mData.size();
        }

        @Override
        public String getItem(int position) {
            return mData.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder = null;
            int type = getItemViewType(position);
            System.out.println("getView " + position + " " + convertView + " type = " + type);
            if (convertView == null) {
                holder = new ViewHolder();
                switch (type) {
                    case TYPE_ITEM:
                        convertView = mInflater.inflate(R.layout.item1, null);
                        holder.textView = (TextView)convertView.findViewById(R.id.text);
                        break;
                    case TYPE_SEPARATOR:
                        convertView = mInflater.inflate(R.layout.item2, null);
                        holder.textView = (TextView)convertView.findViewById(R.id.textSeparator);
                        break;
                }
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder)convertView.getTag();
            }
            holder.textView.setText(mData.get(position));
            return convertView;
        }

    }

    public static class ViewHolder {
        public TextView textView;
    }
}

Каждый 5-ый элемент будет сепаратором.

В логах мы увидим следующее:

INFO/System.out(1035): getView 0 null type = 0
INFO/System.out(1035): getView 1 null type = 0
INFO/System.out(1035): getView 2 null type = 0
INFO/System.out(1035): getView 3 null type = 0
INFO/System.out(1035): getView 4 null type = 1
INFO/System.out(1035): getView 5 null type = 0
INFO/System.out(1035): getView 6 null type = 0
INFO/System.out(1035): getView 7 null type = 0
INFO/System.out(1035): getView 8 null type = 0
INFO/System.out(1035): getView 9 null type = 1

Пролистнем список и посмотрим, что случится:

INFO/System.out(1035): getView 10 null type = 0
INFO/System.out(1035): getView 11 android.widget.LinearLayout@43744528 type = 0
INFO/System.out(1035): getView 12 android.widget.LinearLayout@43744eb0 type = 0
INFO/System.out(1035): getView 13 android.widget.LinearLayout@437456d8 type = 0
INFO/System.out(1035): getView 14 null type = 1
INFO/System.out(1035): getView 15 android.widget.LinearLayout@43745f00 type = 0
INFO/System.out(1035): getView 16 android.widget.LinearLayout@43747170 type = 0
INFO/System.out(1035): getView 17 android.widget.LinearLayout@43747998 type = 0
INFO/System.out(1035): getView 18 android.widget.LinearLayout@437481c0 type = 0
INFO/System.out(1035): getView 19 android.widget.LinearLayout@437468a0 type = 1
INFO/System.out(1035): getView 20 android.widget.LinearLayout@437489e8 type = 0
INFO/System.out(1035): getView 21 android.widget.LinearLayout@4374a8d8 type = 0

convertView не null до тех пор, пока первый сепаратор не скроется. Когда же он скрывается, то автоматически добавляется в Recycler и используется для дальнейших виджетов.