Android: What to do if performance of ListView is still not enough?
Asked Answered
F

2

9

Well this topic was and still is debated really a lot and I already read many tutorials, hints and saw talks about it. But I still have problems with my implementation of a custom BaseAdapter for a ListView whenever I reach a certain complexity of my rows. So what I basically have are some entities I'm getting by parsing xml coming from the network. In addition I fetch some Images, etc. and all this is done in an AsyncTask. I use the performance optimizing ViewHandler approach within my getView() method and reuse convertView as suggested by everyone. I.e. I hope that I'm using ListView as it's supposed to be and it really works fine when I'm just displaying a single ImageView and two TextViews, which are styled with a SpannableStringBuilder (I don't use any HTML.fromHTML whatsoever).

And now here it comes. Whenever I extend my row layout with multiple small ImageViews, a Button and some more TextViews all differently styled with SpannableStringBuilder, I get a ceasing scroll performance. The row consists of a RelativeLayout as a parent and all other elements are arranged with layout parameters, so I can't get the row to be more simple in its layout. I must admit that I never saw any example of a ListView implementation with rows containing that many UI elements.

However, when I'm using a TableLayout within a ScrollView and filling it by hand with an AsyncTask (new rows added steadily by onProgressUpdate() ), it behaves perfectly smooth even with hundreds of rows in it. It just stumbles a little bit when new rows are added if scrolled to the end of the list. Otherwise it's much smoother than with the ListView, where it's always stumbling when scrolled.

Are there any suggestions what to do when a ListView just doesn't want to perform well? Should I stay with the TableLayout approach or is it advised to fiddle with a ListView to optimize the performance a bit?

Here is the implementation of my adapter:

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

This is the xml for the row (no button, still stumbling):

<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>

********** UPDATE *************

As it turned out the problem was simply solved by using ArrayAdapter instead of a BaseAdapter. I used the exact same code with an ArrayAdapter and the performance difference is GIGANTIC! It runs just as smooth as with a TableLayout.

So whenever I'm using ListView, I will definitely avoid using BaseAdapter as it is significantly slower and less optimized for complex layouts. This is a rather interesting conclusion because I hadn't read a word about it in examples and tutorials. Or perhaps I wasn't reading it accurately. ;-)

Well however this is the code that is working smoothly (as you can see my solution is using seperators to group the list):

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

+++++++++++++++++ SECOND EDIT WITH TRACES +++++++++++++++++++++ Just to show that BaseAdapter DOES something different than the ArrayAdapter. This is just the whole trace coming from the getView() method with the EXACT same code in both adapters.

First the amount of calls http://img845.imageshack.us/img845/5463/tracearrayadaptercalls.png

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

Exclusive time consumption http://img823.imageshack.us/img823/6541/tracearrayadapterexclus.png

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

Inclusive time consumption http://img13.imageshack.us/img13/4403/tracearrayadapterinclus.png

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

As you can see there is a HUGE difference (ArrayAdapter is four times faster in the getView() method) between those two adapters. And I really don't have any idea why this is so dramatic. I can only assume that ArrayAdapter has some sort of better caching or further optimizations.

++++++++++++++++++++++++++JUST ANOTHER UPDATE+++++++++++++++++ To show you how my current UIBlog class is built:

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

Just to make it clear, I'm using this for BOTH adapters.

Floatstone answered 30/8, 2011 at 17:29 Comment(6)
And what is you problem with tables ? You found a solution performing well but why would you prefer a list ?Phosphaturia
I certainly don't have any problems with tables, it really is just a question of "Are there any limits of using ListView? Or can you always use ListView (with all it's advantages) for a list?" Perhaps I'm using ListView wrong in a certain way when it comes to optimize its performance. :-)Floatstone
@user535762: Use Traceview to figure out where your time is being spent.Unworldly
An important difference between your two approaches is that you call get(position) only once in the ArrayAdapter version. If your List<UIBlog> is expensive to randomly access (LinkedList) it could be an important difference. Also posting screenshots of traceview is not very useful to us. Your screenshots don't show what's going on in getView() and make it difficult to verify your claims.Cropland
This is late to the party, but the original adapter based on BaseAdapter is clearly wrong. getItem(position) returns position? The reason it worked better with ArrayAdapter is probably because he was converting his objects from whatever he was using (probably parsing them every time) to a proper backing array.Swivel
I don't know what the hell I copied back then, but I'll definitely check it. This could have been the cause of the problem and truly a dumb mistake that I have to be ashamed of. Thank you for pointing it out to me.Floatstone
C
7

You should use DDMS' profiler to see exactly where time is spent. I suspect that what you are doing inside getView() is expensive. For instance, does viewUtility.setUserIcon(holder.usericon, blogs.get(position).getUid(), 30); create a new icon each time? Decoding images all the time would create hiccups.

Cropland answered 30/8, 2011 at 18:40 Comment(13)
Ok I'll check it with the profiler. To your question: I'm just doing view.setImageBitmap(myBitmap); The Bitmap was already fetched and stored inside an ArrayList, so I can't imagine that this should be the cause. I'll post my results here, when I see something suspicious in the profiler. :)Floatstone
I'll mark your answer as correct, as you pointed out to have a more detailed look at all parts of the adapter. As it turned out it wasn't because of my time consuming operations that the list was stumbling, it's just the rather poor performance of the BaseAdapter. Do you have any explanation for this Romain Guy? It seems that you have a great knowledge about ListViews and ListAdapters. Thanks for your reply in advance. :)Floatstone
BaseAdapter doesn't do anything, it doesn't suffer from "poor performace." Where do you see the time being spent when you use the profiler?Cropland
Well it certainly does something different than the ArrayAdapter or else it wouldn't result in such a different performance. Just look at some of the traces:Floatstone
If you are seeing performance improvements with ArrayAdapter this means you were doing something very wrong before, maybe in the way you fetch data?Cropland
Well no Romain, I'm using the exact same code for both adapters.^^ The profiler pictures in my edit show the execution times for both adapters with the exact same code, I'm very serious. :-)Floatstone
And as I told you, I fetched every piece of network data in an AsyncTasks (also the pictures). I even get rid of any date format processing and it STILL showed a huge performance difference.Floatstone
Go look at the source code of BaseAdapter, you'll understand that it has to come from something you do in getView(). It doesn't do anything. Please don't advise other developers to not use BaseAdapter because your conclusions are plain wrong.Cropland
I don't advise other developers to not use it, I'm just speaking about MYSELF avoiding it whenever I can, because the ArrayAdapter runs much faster for this particular code I was providing in my post. I also provided profiling pictures to prove my standpoint and to show that it really DOES have a performance advantage other BaseAdapter. I don't say "Don't use the BaseAdapter!", I just came to the conclusion that for a list of data it is wise to choose an ArrayAdapter, because it boosts the performance by four (as shown in my pictures).Floatstone
And again: I have tried it with the same code for both adapters, how much prove do you want more?Floatstone
Your profiling pictures don't prove anything because the important data is missing. I maintain BaseAdapter and ArrayAdapter on the Android team and I have a good idea on how they work. Look for yourself at what BaseAdapter does: android.git.kernel.org/?p=platform/frameworks/… There must have been a difference in your code, for instance what is the uiBlogs list you are using?Cropland
let us continue this discussion in chatFloatstone
So.. Sorry for necroing this, but did you ever find out what caused your performance to go down with the BaseAdapter? Just out of interest.Apia
P
2

Quite a lot to read ;)

I couldn't see anything wrong in your layout. You could optimize - your first if with a || - cache blogs.get( position ) in a variable - déclare you constant static. - why do you use a Calendar and finery it back to ms ? You seem to already have your ms ?

But I fear it won't be enough.

Regards, Stéphane

Phosphaturia answered 30/8, 2011 at 23:10 Comment(4)
Merci beaucoup Stéphane, mais c'est ne pas la solution de ma problème. ;) I'll tell you why I use Calendar: For setting the given unixtime of a blog to a certain date time. The DateUtility.getDay(cal, 0) method sets a given calendar exactly to 00:00:00 o'clock of the date given at cal. So if cal is 2nd November 3:40 o'clock, it will get set to 2nd November 00:00:00 o'clock. This way I can check if a blog comes before or after this exact date. But as you said, this couldn't be the cause of the problem. Perhaps I'm somehow creating too much objects without freeing them. I'll check it out....Floatstone
I still think you could optimize more things. For instance, using variables as I said to cache the result of blogs.get( position ). A second thing to optimize would be to store the result of isSeparator. You could use null inside your list of UIBlog list (or a special UIBlog constant object) to mark separator and avoid to recalculate them everythime. Moreover, do you think your bitmaps are resized evrytime they are displayed or their size is already fit ?Phosphaturia
Moreover you should consider having a data structure, a model, that is less consice and close to logic but closer to UI to avoid recomputing the title, the author and dates, but having already everything transformed to be dsiaplyed quickly. Bonne chancePhosphaturia
I've already saved blogs.get(position) in a temp var and I don't do any resizing of images or any image related processing, I've already done that in an AsyncTask BEFORE adding them to the adapter. I could optimize the isSeperator()-method with your advice, but even WITHOUT (just display blogs, without any isSeperator()-checking) any seperators would the BaseAdapter still be laggy as hell. And yes I could change my model, but then I also have to change the third-party API with which I'm getting my model from. And as I already said, it's the same code with ArrayAdapter and it works perfectly!Floatstone

© 2022 - 2024 — McMap. All rights reserved.