Android: What to do if performance of ListView is still not enough?
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.
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.
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
精彩评论