正确认识 Runnable

今天在写 Android 的时候,用到了 ProgressDialog 这个进度提示框,我把 progress 提前 new 出来,并在子线程开始之前把它 show 出来。这里我的子线程是 new 了一个 runnable 来实现的,在子线程做完耗时工作的最后,调用 progress 的 dismiss 方法。但是很奇怪,直到子线程结束,进度框都没有显示出来。开始我以为是子线程的耗时太短,根本来不及显示就隐藏掉了。我又让子线程里每循环一次睡上 0.1 秒,但是还是不能显示。我又注释了隐藏进度条的逻辑,这个时候才发现子线程执行完毕之后进度条才显示出来,所以之前其实是子线程执行完毕后才显示进度条,但是又立刻被关闭掉了,以至于我根本观察不到。但这样我还是不知道是什么导致了进度条在子线程执行完了才显示。下面是我开始用 runnable 的代码:

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
final ProgressDialog progressDialog = new ProgressDialog(ProToolsActovoty.this);
progressDialog.setCancelable(false);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setTitle("备份 SMS 信息");
progressDialog.setMessage("正在备份,请稍等。。。");
progressDialog.show();
new Runnable() {
@Override
public void run() {
List<SmsMessage> allSmsMessage = SMSEngine.getAllSmsMessage(
context, new SMSEngine.ProgressChangedListener() {
@Override
public void setMax(int max) {
progressDialog.setMax(max);
}
@Override
public void setProgress(int progressValue) {
progressDialog.setProgress(progressValue);
}
});
try {
//该路径为用户自插 sd 卡根目录,不会自动删除
String outerSdPath = SystemUtils.getOuterSDPath();
if (TextUtils.isEmpty(outerSdPath)) {
//该路径为 /mnt/sdcard/Android/data/packageName/files,会自动删除
outerSdPath = getExternalFilesDir(null).getPath();
}
outerSdPath += "/sms-backup.xml";
//backupSmsBySerializer(allSmsMessage, outerSdPath);
backupSmsByDOM(allSmsMessage, outerSdPath);
progressDialog.dismiss();
toast("备份成功!备份路径为:" + outerSdPath, Toast.LENGTH_LONG);
} catch (IOException e) {
e.printStackTrace();
}
}
}.run();

我也是知道 Thread 和 runnable 在某些条件下是可以通用的,不过脑海中总是隐约记得要优先使用 runnable 而不是 Thread 。所以 runnable 不对头,只好去试试 Thread 了。不过这一试还确实试对了,使用 Thread 来开启子线程的结果也是我所预期的。在子线程开始之前显示进度条,子线程结束之后隐藏,还是 Thread 大法好呀。但仅仅知道怎么做是正确的还不够,必须得知道为什么要这么做呢?只有了解了 runnable 和 Thread 的区别,才能真的算是会使用。

然而当我开始试图分清楚 runnable 和 Thread 的区别的时候,我发现我一开始就错了,因为 runnable 只是一个接口啊,即使我通过 runnable 创建了一个实例,它仍然只是个接口,并非一个真正意义上的子线程。为了验证我的想法,新建一个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
public class RunnableTest {
public static void main(String[] args) {
new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getId());
}
}.run();

System.out.println(Thread.currentThread().getId());
}
}

我用之前的办法创建了一个 runnable 的实例,并分别在这个实例中和主线程中打印了当前的线程 ID ,结果:

1
1

两个线程 ID 完全一致,所以 runnable 并没有在子线程中执行,只不过是相当于在主线程中调用了 runnable 的 run 方法而已。可即便如此,作为一个方法,它也无法阻止进度条的显示吧。这里我暂时还不能得出结论,先当做一个疑问留下来,如果有读者了解也欢迎留言一起探讨。

正确是的使用 runnable 接口的方法是在一个新的 Thread 中传入 runnable 接口,像这样:

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
new Thread(new Runnable() {
@Override
public void run() {
List<SmsMessage> allSmsMessage = SMSEngine.getAllSmsMessage(
context, new SMSEngine.ProgressChangedListener() {
@Override
public void setMax(int max) {
progressDialog.setMax(max);
}

@Override
public void setProgress(int progressValue) {
progressDialog.setProgress(progressValue);
}
});
try {
//该路径为用户自插 sd 卡根目录,不会自动删除
String outerSdPath = SystemUtils.getOuterSDPath();
if (TextUtils.isEmpty(outerSdPath)) {
//该路径为 /mnt/sdcard/Android/data/packageName/files,会自动删除
outerSdPath = getExternalFilesDir(null).getPath();
}
outerSdPath += "/sms-backup.xml";
//backupSmsBySerializer(allSmsMessage, outerSdPath);
backupSmsByDOM(allSmsMessage, outerSdPath);
progressDialog.dismiss();
toast("备份成功!备份路径为:" + outerSdPath, Toast.LENGTH_LONG);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();

尽管程序已经能够正确运行,但是我还是不知道我之前的错误是如何发生的,也不明白其中的原理,不过现在不明白,不代表将来也还是不明白,最近我会多多研究多线程以及 ProgressDialog 的深入解析,争取搞清楚其中的门道。