Android: что делать, если производительность ListView все еще недостаточна? - PullRequest
9 голосов
/ 30 августа 2011

Что ж, эта тема была и остается дискуссионной, и я уже прочитал много уроков, подсказок и видел разговоры по этому поводу. Но у меня все еще есть проблемы с моей реализацией пользовательского BaseAdapter для ListView всякий раз, когда я достигаю определенной сложности моих строк. Так что у меня есть некоторые сущности, которые я получаю, анализируя xml, поступающий из сети. Кроме того, я получаю некоторые изображения и т. Д., И все это делается в AsyncTask. Я использую подход ViewHandler, оптимизирующий производительность, в своем методе getView () и повторно использую convertView, как это предлагают все. То есть Я надеюсь, что я использую ListView, как это должно быть, и он действительно работает нормально, когда я просто отображаю один ImageView и два TextView, которые стилизованы с помощью SpannableStringBuilder (я вообще не использую HTML.fromHTML).

А теперь вот оно. Всякий раз, когда я расширяю свой макет строки несколькими небольшими ImageViews, Button и еще несколькими TextViews, все по-разному стилизованные с помощью SpannableStringBuilder, я получаю превосходную производительность прокрутки. Строка состоит из RelativeLayout в качестве родителя, а все остальные элементы располагаются с параметрами макета, поэтому я не могу сделать строку более простой в своем макете. Я должен признать, что я никогда не видел ни одного примера реализации ListView со строками, содержащими столько элементов пользовательского интерфейса.

Однако, когда я использую TableLayout в ScrollView и заполняю его вручную с помощью AsyncTask (новые строки стабильно добавляются с помощью onProgressUpdate ()), он ведет себя идеально гладко даже с сотнями строк в нем. Просто немного спотыкается при добавлении новых строк, если прокрутить до конца списка. В противном случае это намного плавнее, чем в ListView, где он всегда спотыкается при прокрутке.

Есть ли какие-либо предложения, что делать, когда ListView просто не хочет работать хорошо? Должен ли я придерживаться подхода TableLayout или рекомендуется возиться с ListView, чтобы немного оптимизировать производительность?

Вот реализация моего адаптера:

protected class BlogsSeparatorAdapter extends BaseAdapter {

        private LayoutInflater inflater;
        private final int SEPERATOR = 0;
        private final int BLOGELEMENT = 1;

        public BlogsSeparatorAdapter(Context context) {
                inflater = LayoutInflater.from(context);
        }

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

        @Override
        public Object getItem(int position) {
                return position;
        }

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

        @Override
        public int getItemViewType(int position) {
                int type = BLOGELEMENT;
                if (position == 0) {
                        type = SEPERATOR;
                } else if (isSeparator(position)) {
                        type = SEPERATOR;
                }
                return type;
        }

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

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
                UIBlog blog = getItem(position);
            ViewHolder holder;
            if (convertView == null) {
            holder = new ViewHolder();

            convertView = inflater.inflate(R.layout.blogs_row_layout, null);
            holder.usericon = (ImageView) convertView.findViewById(R.id.blogs_row_user_icon);
            holder.title = (TextView) convertView.findViewById(R.id.blogs_row_title);
            holder.date = (TextView) convertView.findViewById(R.id.blogs_row_date);
            holder.amount = (TextView) convertView.findViewById(R.id.blogs_row_cmmts_amount);
            holder.author = (TextView) convertView.findViewById(R.id.blogs_row_author);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }           
        holder.usericon.setImageBitmap(blog.icon);
        holder.title.setText(blog.titleTxt);
        holder.date.setText(blog.dateTxt);
        holder.amount.setText(blog.amountTxt);
        holder.author.setText(blog.authorTxt);          

                    return convertView;
        }

        class ViewHolder {
                TextView separator;
                ImageView usericon;
                TextView title;
                TextView date;
                TextView amount;
                TextView author;
        }

        /**
         * Check if the blog on the given position must be separated from the last blogs.
         * 
         * @param position
         * @return
         */
        private boolean isSeparator(int position) {
                boolean separator = false;
                // check if the last blog was created on the same date as the current blog
                if (DateUtility.getDay(
                                DateUtility.createCalendarFromUnixtime(blogs.get(position - 1).getUnixtime() * 1000L), 0)
                                .getTimeInMillis() > blogs.get(position).getUnixtime() * 1000L) {
                        // current blog was not created on the same date as the last blog --> separator necessary
                        separator = true;
                }
                return separator;
        }
}

Это xml для строки (без кнопки, все еще спотыкаясь):

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="fill_parent"
    android:background="@drawable/listview_selector">
    <ImageView
        android:id="@+id/blogs_row_user_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:paddingTop="@dimen/blogs_row_icon_padding_top"
        android:paddingLeft="@dimen/blogs_row_icon_padding_left"/>
    <TextView
        android:id="@+id/blogs_row_title"
        android:layout_toRightOf="@id/blogs_row_user_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="@dimen/blogs_row_title_padding"
        android:textColor="@color/blogs_table_text_title"/>
    <TextView
        android:id="@+id/blogs_row_date"
        android:layout_below="@id/blogs_row_title"
        android:layout_toRightOf="@id/blogs_row_user_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="@dimen/blogs_row_date_padding_left"
        android:textColor="@color/blogs_table_text_date"/>
    <ImageView
        android:id="@+id/blogs_row_cmmts_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/blogs_row_title"
        android:layout_toRightOf="@id/blogs_row_date"
        android:layout_margin="@dimen/blogs_row_cmmts_icon_margin"
        android:src="@drawable/comments"/>
    <TextView
        android:id="@+id/blogs_row_cmmts_amount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/blogs_row_title"
        android:layout_toRightOf="@id/blogs_row_cmmts_icon"
        android:layout_margin="@dimen/blogs_row_author_margin"/>
    <TextView
        android:id="@+id/blogs_row_author"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/blogs_row_title"
        android:layout_toRightOf="@id/blogs_row_cmmts_amount"
        android:marqueeRepeatLimit="marquee_forever"
        android:singleLine="true"
        android:ellipsize="marquee"
        android:layout_margin="@dimen/blogs_row_author_margin"/>
</RelativeLayout>

********** ОБНОВЛЕНИЕ *************

Как оказалось, проблема была просто решена с помощью ArrayAdapter вместо BaseAdapter. Я использовал точно такой же код с ArrayAdapter, и разница в производительности огромна! Он работает так же гладко, как и с TableLayout.

Таким образом, всякий раз, когда я использую ListView, я определенно буду избегать использования BaseAdapter, поскольку он значительно медленнее и менее оптимизирован для сложных макетов. Это довольно интересный вывод, потому что я не читал ни слова об этом в примерах и учебных пособиях. Или, возможно, я не читал это точно. ; -)

Однако этот код работает без сбоев (как вы видите, мое решение использует разделители для группировки списка):

protected class BlogsSeparatorAdapter extends ArrayAdapter<UIBlog> {

    private LayoutInflater inflater;

    private final int SEPERATOR = 0;
    private final int BLOGELEMENT = 1;

    public BlogsSeparatorAdapter(Context context, List<UIBlog> rows) {
        super(context, R.layout.blogs_row_layout, rows);
        inflater = LayoutInflater.from(context);
    }

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

    @Override
    public int getItemViewType(int position) {
        int type = BLOGELEMENT;
        if (position == 0) {
            type = SEPERATOR;
        } else if (isSeparator(position)) {
            type = SEPERATOR;
        }
        return type;
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final UIBlog blog = uiblogs.get(position);
        int type = getItemViewType(position);

        ViewHolder holder;
        if (convertView == null) {
            holder = new ViewHolder();
            if (type == SEPERATOR) {
                convertView = inflater.inflate(R.layout.blogs_row_day_separator_item_layout, null);
                View separator = convertView.findViewById(R.id.blogs_separator);
                separator.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // do nothing
                    }
                });
                holder.separator = (TextView) separator.findViewById(R.id.blogs_row_day_separator_text);
            } else {
                convertView = inflater.inflate(R.layout.blogs_row_layout, null);
            }
            holder.usericon = (ImageView) convertView.findViewById(R.id.blogs_row_user_icon);
            holder.title = (TextView) convertView.findViewById(R.id.blogs_row_title);
            holder.date = (TextView) convertView.findViewById(R.id.blogs_row_date);
            holder.amount = (TextView) convertView.findViewById(R.id.blogs_row_author);
            holder.author = (TextView) convertView.findViewById(R.id.blogs_row_author);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        if (holder.separator != null) {
            holder.separator
                    .setText(DateUtility.createDate(blog.blog.getUnixtime() * 1000L, "EEEE, dd. MMMMM yyyy"));
        }
        holder.usericon.setImageBitmap(blog.icon);
        holder.title.setText(createTitle(blog.blog.getTitle()));
        holder.date.setText(DateUtility.createDate(blog.blog.getUnixtime() * 1000L, "'um' HH:mm'Uhr'"));
        holder.amount.setText(createCommentsAmount(blog.blog.getComments()));
        holder.author.setText(createAuthor(blog.blog.getAuthor()));
        return convertView;
    }

    class ViewHolder {
        TextView separator;
        ImageView usericon;
        TextView title;
        TextView date;
        TextView amount;
        TextView author;
    }

    /**
     * Check if the blog on the given position must be separated from the last blogs.
     * 
     * @param position
     * @return
     */
    private boolean isSeparator(int position) {
        boolean separator = false;
        // check if the last blog was created on the same date as the current blog
        if (DateUtility.getDay(
                DateUtility.createCalendarFromUnixtime(blogs.get(position - 1).getUnixtime() * 1000L), 0)
                .getTimeInMillis() > blogs.get(position).getUnixtime() * 1000L) {
            // current blog was not created on the same date as the last blog --> separator necessary
            separator = true;
        }
        return separator;
    }
}

+++++++++++++++++ ВТОРОЕ РЕДАКТИРОВАНИЕ С ТРАССАМИ +++++++++++++++++++++ Просто чтобы показать, что BaseAdapter ДЕЛАЕТ что-то отличное от ArrayAdapter. Это всего лишь трассировка от метода getView () с ТОЧНЫМ кодом в обоих адаптерах.

Первая сумма звонков http://img845.imageshack.us/img845/5463/tracearrayadaptercalls.png

http://img847.imageshack.us/img847/7955/tracebaseadaptercalls.png

Эксклюзивное потребление времени http://img823.imageshack.us/img823/6541/tracearrayadapterexclus.png

http://img695.imageshack.us/img695/3613/tracebaseadapterexclusi.png

Включено потребление времени http://img13.imageshack.us/img13/4403/tracearrayadapterinclus.png

http://img831.imageshack.us/img831/1383/tracebaseadapterinclusi.png

Как вы можете видеть, существует ОГРОМНАЯ разница (ArrayAdapter в четыре раза быстрее в методе getView ()) между этими двумя адаптерами. И я действительно понятия не имею, почему это так драматично. Я могу только предположить, что ArrayAdapter имеет некоторое улучшение кэширования или дальнейшую оптимизацию.

++++++++++++++++++++++++++ ПРОСТО ДРУГОЕ ОБНОВЛЕНИЕ +++++++++++++++++ Чтобы показать вам, как построен мой текущий класс UIBlog:

private class UIBlog {
    Blog blog;
    CharSequence seperatorTxt;
    Bitmap icon;
    CharSequence titleTxt;
    CharSequence dateTxt;
    CharSequence amountTxt;
    CharSequence authorTxt;
}

Просто чтобы прояснить, я использую это для ОБА адаптеров.

Ответы [ 2 ]

7 голосов
/ 30 августа 2011

Вы должны использовать профилировщик DDMS, чтобы точно знать, где тратится время.Я подозреваю, что то, что вы делаете внутри getView (), стоит дорого.Например, делает viewUtility.setUserIcon (holder.usericon, blogs.get (position) .getUid (), 30);каждый раз создавать новую иконку?Постоянное декодирование изображений может привести к сбоям в работе.

2 голосов
/ 31 августа 2011

Довольно много почитать;)

Я не вижу ничего плохого в вашем макете.Вы могли бы оптимизировать - ваш первый, если с ||- кэшируйте blog.get (положение) в переменной - объявите вас постоянной статической.- Почему вы используете календарь и заканчиваете его обратно в MS?Кажется, у тебя уже есть мс?

Но, боюсь, этого будет недостаточно.

С уважением, Стефан

...