And I'm back! Reporting live on the glorious adventures in the exciting world of Android. This blog post is the second one in the Android series. This time with code samples! Yeah!
Inmy first blog post about AndroidI talked about setting up a project using
Android. This time I want to discuss a more "advanced" topic:ListViewperformance. A ListView is a view component which allows
you to display and scroll through a list of items. It can display simple text in each row, but is also able to display a more complicated structure. In the latter case you will need to make sure your ListView still performs well (read: renders fast and scrolls
smoothly). I am going to provide solutions to a few different performance problems when using aListView
The ListAdapter
If you want to use aListView, you will have to supply it with aListAdapterto allow it to display any content. A few simple implementations of that adapter are already available in the SDK:
- ArrayAdapter<T>(for displaying an array of objects, usingtoString()to display them)
- SimpleAdapter(for displaying a list ofMaps)
- SimpleCursorAdapter(for displaying rows fetched from a database table using aCursor)
These implementations are perfect for displaying very simple lists. But if your list is just a little more complicated than that, you will need to write your own customListAdapterimplementation. In most cases it's useful to subclassArrayAdapterwhich already takes care of managing a list of objects. Now you only have to tell it how to render each object in the list. Do this by overriding thegetView(int, View, ViewGroup)method of the ArrayAdapter class.
A simple example
To give you a simple example of a case in which you need to write your ownListAdapter: displaying a list of images with some text next to it.
Example of aListViewcontaining Youtube search results in the form of images and text
The images need to be on-the-fly downloaded from the internet. Let's create a class which represents items in the list:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public
class
ImageAndText {
private
String imageUrl;
private
String text;
public
ImageAndText(String imageUrl, String text) {
this .imageUrl
= imageUrl;
this .text
= text;
}
public
String getImageUrl() {
return
imageUrl;
}
public
String getText() {
return
text;
}
}
|
Now, let's create an implementation of aListAdapterthat is able to display a list of theseImageAndTexts.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
public
class
ImageAndTextListAdapter extends
ArrayAdapter<ImageAndText> {
public
ImageAndTextListAdapter(Activity activity, List<ImageAndText> imageAndTexts) {
super (activity,
0 ,
imageAndTexts);
}
@Override
public
View getView( int
position, View convertView, ViewGroup parent) {
Activity
activity = (Activity) getContext();
LayoutInflater
inflater = activity.getLayoutInflater();
//
Inflate the views from XML
View
rowView = inflater.inflate(R.layout.image_and_text_row, null );
ImageAndText
imageAndText = getItem(position);
//
Load the image and set it on the ImageView
ImageView
imageView = (ImageView) rowView.findViewById(R.id.image);
imageView.setImageDrawable(loadImageFromUrl(imageAndText.getImageUrl()));
//
Set the text on the TextView
TextView
textView = (TextView) rowView.findViewById(R.id.text);
textView.setText(imageAndText.getText());
return
rowView;
}
public
static
Drawable loadImageFromUrl(String url) {
InputStream
inputStream;
try
{
inputStream
= new
URL(url).openStream();
}
catch
(IOException e) {
throw
new
RuntimeException(e);
}
return
Drawable.createFromStream(inputStream, "src" );
}
}
|
The views are inflated from an XML file called "image_and_text_row.xml":
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<? xml
version = "1.0"
encoding = "utf-8" ?>
android:orientation = "horizontal"
android:layout_width = "fill_parent"
android:layout_height = "wrap_content" >
< ImageView
android:id = "@+id/image"
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
android:src = "@drawable/default_image" />
< TextView
android:id = "@+id/text"
android:layout_width = "wrap_content"
android:layout_height = "wrap_content" />
</ LinearLayout >
|
ThisListAdapterimplementation renders theImageAndTexts in theListViewlike you would expect. The only thing is that this only works for a very small list which doesn't require scrolling to see all items. If the list ofImageAndTexts gets bigger you will notice that scrolling isn't as smooth as it should be (in fact, it's far off!).
Improving performance
The biggest bottleneck in the above example is the fact that the images have to be downloaded from the internet. Because we execute all our code in the same thread as the UI, the UI will get stuck each time an image is being downloaded. If you run the same application using a 3G internet connection instead of WiFi, the performance will even be worse.
To avoid this we want the image to be loaded in a separate thread to not disturb the UI thread too much. To make this happen, we could use anAsyncTaskwhich is designed for cases like this. But in practice, you will notice that theAsyncTaskis limited to 10 threads. This number is hardcoded somewhere in the Android SDK so we cannot change this. In this case it's a limitation we cannot live with, because often more than 10 images are loaded at the same time.
AsyncImageLoader
An alternative is to manually spawn a newThreadfor each image. In addition we should useHandlers to deliver the downloaded images to the UI thread. We want to do this because only from the UI thread you are allowed to modify the UI (read: draw an image on the screen). I created a class calledAsyncImageLoaderwhich takes care of loading images usingThreadsandHandlerslike I just described. Also it caches images to avoid a single image to be downloaded multiple times.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public
class
AsyncImageLoader {
private
HashMap<String, SoftReference<Drawable>> imageCache;
public
AsyncImageLoader() {
drawableMap
= new
HashMap<String, SoftReference<Drawable>>();
}
public
Drawable loadDrawable( final
String imageUrl, final
ImageCallback imageCallback) {
if
(drawableMap.containsKey(imageUrl)) {
SoftReference<Drawable>
softReference = imageCache.get(imageUrl);
Drawable
drawable = softReference.get();
if
(drawable != null )
{
return
drawable;
}
}
final
Handler handler = new
Handler() {
@Override
public
void
handleMessage(Message message) {
imageCallback.imageLoaded((Drawable)
message.obj, imageUrl);
}
};
new
Thread() {
@Override
public
void
run() {
Drawable
drawable = loadImageFromUrl(imageUrl);
imageCache.put(imageUrl,
new
SoftReference<Drawable>(drawable));
Message
message = handler.obtainMessage( 0 ,
drawable);
handler.sendMessage(message);
}
}.start();
return
null ;
}
public
static
Drawable loadImageFromUrl(String url) {
//
...
}
public
interface
ImageCallback {
public
void
imageLoaded(Drawable imageDrawable, String imageUrl);
}
}
|
Notice that I used aSoftReferencefor caching images, to allow the garbage collector to clean the images from the cache when needed. How it works:
- CallloadDrawable(imageUrl, imageCallback)providing an anonymous implementation of theImageCallbackinterface
- If the image doesn't exist in the cache yet, the image is downloaded in a separate thread and theImageCallbackis called as soon as the download is complete.
- If the image DOES exist in the cache, it is immediately returned and theImageCallbackis never called.
Only one instance ofAsyncImageLoadershould exist in your application, or else the caching won't work. If we take the exampleImageAndTextListAdapterclass we can now replace:
1
2
|
ImageView
imageView = (ImageView) rowView.findViewById(R.id.image);
imageView.setImageDrawable(loadImageFromUrl(imageAndText.getImageUrl()));
|
with:
1
2
3
4
5
6
7
|
final
ImageView imageView = (ImageView) rowView.findViewById(R.id.image);
Drawable
cachedImage = asyncImageLoader.loadDrawable(imageAndText.getImageUrl(), new
ImageCallback() {
public
void
imageLoaded(Drawable imageDrawable, String imageUrl) {
imageView.setImageDrawable(imageDrawable);
}
});
imageView.setImageDrawable(cachedImage);
|
Using this approach, the ListView performs a lot better and feels much more smooth because the UI thread is no longer blocked by the loading of images.
Improve the performance even more
If you tried the solution described above you will notice that theListViewis still not a 100% smooth. You will still notice some little disruptions that make it a little less smooth than it could be. There are two things remaining that can be improved:
- the expensive call to findViewById()
- inflating the entire row from XML every time
The solution is obvious: we should cache/reuse these things!Mark Murphydid a very nice job on writing a few blog entries describing how this can be done. To reuse the views which are inflated from XML
read this blog entry:
http://www.androidguys.com/2008/07/17/fancy-listviews-part-two/
To cache the views returned byfindViewById()read this blog entry:
http://www.androidguys.com/2008/07/22/fancy-listviews-part-three/
If we apply the strategies described in Mark Murphy's blog entries ourImageAndTextListAdaptercould look like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
public
class
ImageAndTextListAdapter extends
ArrayAdapter<ImageAndText> {
private
ListView listView;
private
AsyncImageLoader asyncImageLoader;
public
ImageAndTextListAdapter(Activity activity, List<ImageAndText> imageAndTexts, ListView listView) {
super (activity,
0 ,
imageAndTexts);
this .listView
= listView;
asyncImageLoader
= new
AsyncImageLoader();
}
@Override
public
View getView( int
position, View convertView, ViewGroup parent) {
Activity
activity = (Activity) getContext();
//
Inflate the views from XML
View
rowView = convertView;
ViewCache
viewCache;
if
(rowView == null )
{
LayoutInflater
inflater = activity.getLayoutInflater();
rowView
= inflater.inflate(R.layout.image_and_text_row, null );
viewCache
= new
ViewCache(rowView);
rowView.setTag(viewCache);
}
else
{
viewCache
= (ViewCache) rowView.getTag();
}
ImageAndText
imageAndText = getItem(position);
//
Load the image and set it on the ImageView
String
imageUrl = imageAndText.getImageUrl();
ImageView
imageView = viewCache.getImageView();
imageView.setTag(imageUrl);
Drawable
cachedImage = asyncImageLoader.loadDrawable(imageUrl, new
ImageCallback() {
public
void
imageLoaded(Drawable imageDrawable, String imageUrl) {
ImageView
imageViewByTag = (ImageView) listView.findViewWithTag(imageUrl);
if
(imageViewByTag != null )
{
imageViewByTag.setImageDrawable(imageDrawable);
}
}
});
imageView.setImageDrawable(cachedImage);
//
Set the text on the TextView
TextView
textView = viewCache.getTextView();
textView.setText(imageAndText.getText());
return
rowView;
}
}
|
There are two things to notice. The first thing is that the drawable is not directly set to theImageViewanymore after loading. Instead, the rightImageViewis looked up through it's tag. This is done because we're now reusing views and the images might end up on the wrong rows. We need a reference to theListViewto lookupImageViewsby tag.
The other thing to notice, is that this implementation uses an object calledViewCache. This is what the class for that object looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public
class
ViewCache {
private
View baseView;
private
TextView textView;
private
ImageView imageView;
public
ViewCache(View baseView) {
this .baseView
= baseView;
}
public
TextView getTextView() {
if
(textView == null )
{
textView
= (TextView) baseView.findViewById(R.id.text);
}
return
titleView;
}
public
ImageView getImageView() {
if
(imageView == null )
{
imageView
= (ImageView) baseView.findViewById(R.id.image);
}
return
imageView;
}
}
|
ThisViewCacheis the same as what Mark Murphy calls a "ViewWrapper" and takes care of caching individual views which normally would have to be looked up every time using the expensive call tofindViewById().
To summarize
I've shown you how to improve performance of aListViewin three different ways:
- By loading images in a seperate thread
- By reusing rows in the list
- By caching views within a row
It took me quite some time to figure this stuff out, especially the image loading part. So I thought it is all worth mentioning to avoid you having to waste too much time on it.
Next time I will discuss other interesting challenges in the world of Android!
TO BE CONTINUED ...
相关推荐
This book is ideal for a one-semester course in statistics, offering a streamlined presentation of Introductory Statistics: Exploring the World through Data, by Gould/Ryan. Exploring the World through...
本书重新审查了理论计算机科学,提供了一种新的方法,该方法将资源折衷和复杂性分类的优先级放在机器的结构及其与语言的关系上。
J2ME Tutorial, Part 3: Exploring the Game API of MIDP 2.0
I: An Overview of the Android Platform 1 Introducing Android 11 A Brief History of Mobile Software Development 11 Way Back When 11 “The Brick” 13 Wireless Application Protocol (WAP) 15 Proprietary ...
Part 2: Custom Types – Project2: Fixed-point Numbers Part 3: Generic Programming – Function Templates Part 3: Generic Programming – Class Templates Part 3: Generic Programming – Template ...
文章目录Step 0. Create the Task Environment file from templateStep 1.... Fill in the virtual functions4.0 Set the initial position of the robot4.1 Initialize environment variables4.2 D
[Hsinchun_Chen]_Dark_Web_Exploring_and_Data_Mining the Dark Side of the Web
Exploring the Road to 6G: ABC-Foundation for Intelligent Mobile Networks
Title: Beginning iPhone Development with Swift 5: Exploring the iOS SDK, 5th Edition Author: Wallace Wang Length: 628 pages Edition: 5th ed. Language: English Publisher: Apress Publication Date: 2019-...
Al大模型,Exploring the Limits of Transfer Learning
Chapter 2: Appeasing the Tiki Gods Chapter 3: Handling Basic Interaction Chapter 4: More User Interface Fun Chapter 5: Autorotation and Autosizing Chapter 6: Multiview Applications Chapter 7: Tab Bars...
Exploring the Road to 6G: ABC-Foundation for Intelligent Mobile Networks.pdf
Exploring the Representation Capabilities of the HOG Descriptor
The book starts with the basics, walking you through the process of downloading and installing Apple's free iPhone SDK, then stepping you though the creation of your first simple iPhone application....
Beginning iPhone Development: Exploring the iPhone SDK by Dave Mark, Jeff LaMarche 一共两个压缩包
Exploring the history of social work as a human rights profession
ros2 性能测试结果
Beginning iPhone Development: Exploring the iPhone SDK by Dave Mark, Jeff LaMarche
Beginning iPhone 3 Development Exploring the iPhone SDK.part2.rar
This one of a kind short book walks any Android developer through the process of creating mobile games using the new Android Studio IDE. Android Studio offers a myriad of tools for developers such as ...