乐者为王

Do one thing, and do it well.

实现带Icon的ListView(改进版)

带Icon的ListView工作起来还不错,但是如果手机里安装的软件比较多的话就有问题了。在程序启动时会黑屏一段时间,就好像程序挂起了一样。所以这时需要显示一个对话框,告诉用户正在加载数据,最好还能准确告诉用户加载的进度。

显示对话框用ProgressDialog,它有两种样式:HORIZONTAL和SPINNER。这里暂时选择SPINNER,等以后有机会再改成HORIZONTAL。

我们在加载数据前显示ProgressDialog,在之后关闭对话框。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
ProgressDialog pd = ProgressDialog.show(this, "Wait", "loading...");

ListView lv = (ListView)findViewById(R.id.list_view);
adapter = new SimpleIconAdapter(this,
        getInstalledApps(false),
        R.layout.list_item,
        new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
        new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
lv.setAdapter(adapter);

pd.dismiss();

但结果ProgressDialog没有显示出来,因为加载数据的代码也执行在UI线程中,所以对话框的显示被阻塞了。怎么办呢?可以试着创建一个新线程,然后把加载数据这个耗时的操作放到这个非UI线程中去执行,这样不就可以了么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ProgressDialog pd = ProgressDialog.show(this, "Wait", "loading...");

new Thread() {
    @Override
    public void run() {
        ListView lv = (ListView)findViewById(R.id.list_view);
        adapter = new SimpleIconAdapter(Main.this,
                getInstalledApps(false),
                R.layout.list_item,
                new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
                new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
        lv.setAdapter(adapter);
    }
}.start();

pd.dismiss();

这下更惨了,直接报RuntimeException,连界面都打不开了。

深入研究一下Android的线程模型,看看Android到底是怎么处理UI线程和其它线程的交互的,为什么上面的代码会报异常?

When an application is launched, the system creates a thread called "main" for the application. The main thread, also called the UI thread, is very important because it is in charge of dispatching the events to the appropriate widgets, including drawing events. It is also the thread where your application interacts with running components of the Android UI toolkit.

For instance, if you touch the a button on screen, the UI thread dispatches the touch event to the widget, which in turn sets its pressed state and posts an invalidate request to the event queue. The UI thread dequeues the request and notifies the widget to redraw itself.

This single-thread model can yield poor performance unless your application is implemented properly. Specifically, if everything is happening in a single thread, performing long operations such as network access or database queries on the UI thread will block the whole user interface. No event can be dispatched, including drawing events, while the long operation is underway. From the user's perspective, the application appears hung. Even worse, if the UI thread is blocked for more than a few seconds (about 5 seconds currently) the user is presented with the infamous "application not responding" (ANR) dialog.

看了上面这段话可以知道为啥程序一打开就黑屏,像挂掉了一样。

那为什么用新建线程来执行耗时操作的代码也有问题呢?这是因为它虽然没有阻塞UI线程,但它违背了单线程模型,Android的UI操作并不是线程安全的,并且这些操作必须在UI线程中执行。在这段代码片段中,在另一个线程中执行数据加载绑定,这会引起一些古怪的问题。

Andriod提供了几种在其它线程中访问UI线程的方法:

上面的任何一个类或方法都可以修复我们前面代码中遇到的问题,很不幸的是这些类或方法同样会使你的代码变的很复杂很难理解。而且当你需要实现一些很复杂的操作并需要频繁地更新UI时会变得更糟糕。为了解决这个问题,Android 1.5提供了一个工具类AsyncTask,它使创建需要与用户界面交互的长时间运行的任务变得更简单。在Android 1.0和1.1中具有与AsyncTask相同功能的类UserTask,它提供了完全一样的API,你需要做的只是把它的代码拷贝的你的程序中。

其实这些方案背后采用的都是Handler。所以这里仍然使用旧有的,复杂的Handler来修复上述问题。

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
50
51
52
53
54
55
56
57
58
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ListView;

public class MainActivity extends Activity {
    private static final String APP_ICON = "app_icon";
    private static final String APP_TITLE = "app_title";
    private static final String APP_PACKAGE = "app_package";

    private SimpleIconAdapter adapter;
    private List<HashMap<String, Object>> listData;
    private ProgressDialog pd;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ListView lv = (ListView)findViewById(R.id.list_view);
        listData = new ArrayList<HashMap<String, Object>>();
        adapter = new SimpleIconAdapter(this,
                listData,
                R.layout.list_item,
                new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
                new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
        lv.setAdapter(adapter);

        pd = ProgressDialog.show(this, "Wait", "loading...");

        new Thread() {
            @Override
            public void run() {
                // 耗时操作,加载数据
                listData.addAll(getInstalledApps(false));
                handler.sendEmptyMessage(0);
            }
        }.start();
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // 通知UI更新。必须要放在这里
            adapter.notifyDataSetChanged();

            pd.dismiss();
        }
    };

Comments