在Android中,UI线程是程序内处理UI事件的主线程,应该避免在UI线程中进行耗时比较长的操作,比如通过网络接口存取数据。 如果在UI线程内执行耗时操作,则程序会被阻塞且界面无法响应用户操作。 对于耗时较长的操作,比如从网络上下载数据,可以新开一个线程,在这个线程里面下载数据。
使用Thread
我们先介绍使用Thread来实现多线程。 对于最简单的Thread使用,主要涉及到两个函数
构造函数需要传递一个实现了Runnable
接口的对象,Runnable接口非常简单,只有一个void run()函数,当启动线程的时候,线程就会执行这个run()函数。
在HelloThreadThread
项目中,我们在run函数中,只是简单的创建了一个局部变量i
,并且创建了一个无限循环体,每隔3秒钟将i加一,并输出log,在这个无限循环体中如果发现用户想终止该线程,则break跳出这个无限循环体,run()函数执行完毕,从而终止这个线程。
HelloThread.java:
28-50行定义了一个实现了Runnable接口的对象,该对象只有一个函数run()。
47行会检测是否需要终止当前线程,如果需要中止,则通过50行的break来跳出无限循环。
68-69行创建了一个新线程、将其启动。
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
59
60
61
62
63
64
65
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hello_thread);
Button startThreadButton = (Button)findViewById(R.id.startThread);
startThreadButton.setOnClickListener(startThread);
Button stopThreadButton = (Button)findViewById(R.id.stopThread);
stopThreadButton.setOnClickListener(stopThread);
runnable = new Runnable(){
@Override
public void run() {
int i = 0;
while(true) {
i++;
Log.i("HelloThread", "i is " + i);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (shouldCancel) {
Log.i("HelloThread", "thread is canceled");
thread = null;
break;
}
}
}
};
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.hello_thread, menu);
return true;
}
private OnClickListener startThread = new OnClickListener() {
@Override
public synchronized void onClick(View arg0) {
if (thread != null) {
return;
}
shouldCancel = false;
thread = new Thread(runnable);
thread.start();
}
};
private OnClickListener stopThread = new OnClickListener() {
@Override
public synchronized void onClick(View v) {
shouldCancel = true;
}
};
在线程中刷新UI
有一个最普通不过的场景:程序需要从网络下载一个比较大的文件,在下载的过程中把下载进度显示在界面上,这样子用户就可以实时查看当前下载的状态。
这个要求貌似简单,但是具体实施起来要比较小心。也许你会觉得对于上一个例子,我们只需在33-46行这个while循环中,根据下载的具体进度,将UI的某个控件(比如说TextView)的显示文字设置为``当前已经下载了xx%’‘。非常遗憾的是,这样做是行不通的,因为UI线程跟下载线程是两个不同的线程,对于该做什么、什么时候做什么,都是由UI线程自己控制的,我们不能在其他线程中直接指手画脚、粗暴干涉,如果我们在非UI线程中,调用UI控件的函数,直接后果就是使程序挂掉。
虽然不能直接在非UI线程中对UI进行操作,但是Android提供了一个办法,使得我们可以把想要做的事情发给UI线程,由UI线程择机处理这些事情。
Android给我们提供了一个类Handler
,我们可以把想要做的事情放在一个Runnable对象中(在run函数中写下要做的事情),然后调用Handler的public final boolean post (Runnable r)
函数,Handler就会将这个Runnable发送至某个线程。
请注意:是某个线程
,那具体是哪个线程呢?答案是创建Handler的线程!也就是说Handler someHandler = new Handler()
这行代码在哪个线程中执行的,那这个Handler就属于这个线程,post函数所提交的Runnable最后都交由这个线程来处理。
具体到我们这个例子,我们需要在UI线程中创建一个Handler,创建一个Runnable,在Runnable的run函数中,对界面的控件进行设置。见项目HelloThreadThreadUI
。
下面是HelloThread.java文件:
35行我们创建了一个Handler;
37-42行我们创建了一个Runnable,在其中我们设置TextView显示的文字;
51行,需要更新UI时,我们调用Handler的post方法,参数为37-42行创建的Runnable;
读者可以试试注释掉第51行,改用第50行代码,看看会发生什么情况。
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class HelloThread extends Activity {
private Runnable runnable;
private Thread thread;
private boolean shouldCancel;
private Handler handler;
private Runnable updateUIRunnable;
private int i;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hello_thread);
Button startThreadButton = (Button) findViewById(R.id.startThread);
startThreadButton.setOnClickListener(startThread);
Button stopThreadButton = (Button) findViewById(R.id.stopThread);
stopThreadButton.setOnClickListener(stopThread);
final TextView valueOfI = (TextView) findViewById(R.id.valueOfI);
handler = new Handler();
updateUIRunnable = new Runnable() {
@Override
public void run() {
valueOfI.setText("value of i is " + Integer.toString(i));
}
};
runnable = new Runnable() {
@Override
public void run() {
while (true) {
i++;
Log.i("HelloThread", "i is " + i);
// valueOfI.setText("value of i is " + Integer.toString(i));
handler.post(updateUIRunnable);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (shouldCancel) {
Log.i("HelloThread", "thread is canceled");
thread = null;
break;
}
}
}
};
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.hello_thread, menu);
return true;
}
private OnClickListener startThread = new OnClickListener() {
@Override
public synchronized void onClick(View arg0) {
if (thread != null) {
return;
}
shouldCancel = false;
thread = new Thread(runnable);
thread.start();
}
};
private OnClickListener stopThread = new OnClickListener() {
@Override
public synchronized void onClick(View v) {
shouldCancel = true;
}
};
}
使用AsyncTask
AsyncTask
也同样可以实现多线程,
并且AsyncTask内建了对UI操作的机制。
在使用AsyncTask时,需要创建一个继承自AsyncTask的类,形如:
正如代码展示的,创建继承自AsyncTask的类的时候,需要指定三个泛型(分别是参数、进度、执行结果对应的类型)。
这三个函数不会由用户自己调用,当需要启动AsyncTask,使其需要在新线程中执行函数donInBackground内的代码时,用户需要调用public final AsyncTask<Params, Progress, Result> execute (Params... params)
函数(其中参数Params... var
的意思是:这个函数接受零个或多个Params类型的变量作为参数,而在在这个函数体内,var为一个包含了这若干个变量的数组),系统再以execute的参数为参数调用函数doInBackground。
当用户需要在doInBackground函数内刷新UI时,用户可以调用函数protected final void publishProgress (Progress... values)
,之后系统会在UI线程中调用onProgressUpdate函数。
当doInBackground执行完成后,系统会在UI线程中调用onPostExecute函数,如果AsyncTask被cancel掉,则系统不会调用这个函数。
参考项目HelloThreadAsyncTask
,HelloThread.java:
59-95行定义了一个AsyncTask。
70行使用publishProgress()来刷新界面。
66-68行为当i大于10时,正常的结束AsyncTask,这时候界面的TextView就会显示async task finished-1
;如果在i小于10的时候,按下了Stop Thread
按钮,则onPostExecute不会被执行,TextView显示value of i is
。
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class HelloThread extends Activity {
private boolean runningAsyncTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hello_thread);
Button startThreadButton = (Button) findViewById(R.id.startThread);
startThreadButton.setOnClickListener(startThread);
Button stopThreadButton = (Button) findViewById(R.id.stopThread);
stopThreadButton.setOnClickListener(stopThread);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.hello_thread, menu);
return true;
}
private OnClickListener startThread = new OnClickListener() {
@Override
public synchronized void onClick(View arg0) {
if (runningAsyncTask) {
return;
}
runningAsyncTask = true;
asyncTask.execute((Object) null);
}
};
private OnClickListener stopThread = new OnClickListener() {
@Override
public synchronized void onClick(View v) {
asyncTask.cancel(false);
}
};
private MyAsyncTask asyncTask = new MyAsyncTask();
private class MyAsyncTask extends AsyncTask<Object, Integer, Long> {
@Override
protected Long doInBackground(Object... arg0) {
int i = 0;
while (true) {
i++;
if (i > 10) {
break;
}
Log.i("HelloThread", "i is " + i);
publishProgress(i);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (isCancelled()) {
Log.i("HelloThread", "thread is canceled");
break;
}
}
return (long) -1;
}
protected void onProgressUpdate(Integer... progress) {
TextView valueOfI = (TextView) findViewById(R.id.valueOfI);
if (progress.length > 0) {
valueOfI.setText("value of i is " + progress[0]);
}
}
protected void onPostExecute(Long result) {
TextView valueOfI = (TextView) findViewById(R.id.valueOfI);
valueOfI.setText("async task finished" + result);
}
};
}