Android不允许在子线程中进行UI操作。但是有些时候,我们必须在子线程里去执行一些耗时任务,然后根据任务的执行结果来更新相应的UI。对于这种情况,Android提供了一套异步消息处理的使用方法。
我们以在子线程中进行myCount++操作,并将结果作为textview中的值来显示为例,来学习android的异步消息处理机制。
代码如下:
package com.example.myandroidthread;
import .AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
Button btn_exectask;
TextView tv_changeText;
int myCount;
public static final int UPDATE_TEXT = 1;
public class UpdateCountThread implements Runnable{
Handler mainHandler;
@Override
public void run() {
for(int i=0;i<1000;i++){
for(int j=0;j<1000;j++){
myCount++;
Message message = new Message();
message.what = UPDATE_TEXT;
message.arg1 = myCount;
handler.sendMessage(message);
}
}
Log.d("MainActivity","任务结束");
}
UpdateCountThread(Handler handeler){
this.mainHandler = handler;
}
}
Handler handler = new Handler(){
public void handleMessage(Message msg){
//消息id
switch (msg.what){
case UPDATE_TEXT:
String show = "myCount="+msg.arg1;
tv_changeText.setText(show);
break;
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myCount = 0;
btn_exectask = findViewById(.btn_exectask);
tv_changeText = findViewById(.tv_changeText);
btn_exectask.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d("MainActivity","开始任务线程");
new Thread( new UpdateCountThread(handler)).start();
Log.d("MainActivity","主线程继续。。。。");
Toast.makeText(MainActivity.this,"结束任务", Toast.LENGTH_SHORT).show();
}
});
}
}
解释一下:
先定义了一个整型变量UODATE_TEXT,用于表示更新TextView的这个动作。然后新增一个Handle对象,并重写父类的handelMessage()方法,在这里对具体的Message进行处理。若发现Message中的what字段的值等于UPDATE_TEXT,就将TextView显示的内容改成myCount的值。
下面再来看一下change text按钮的点击事件中的代码。这次我们并没有在子线程里直接进行UI操作,而是创建了一个Message(android.os.Message)对象,并将它的what字段的值指定为UPDATE_TEXT,然后调用Handler的sendMessage()方法将这条Meaasge发送出去。很快,Handler就会收到这条Meaage,并在handleMessage()方法中对它进行处理。注意此时handleMessage()方法中的代码就是在主线程当中运行的了,所以我们可以放心的进行UI操作。然后对Message携带的what字段的值进行判断,如果等于UPDATE_TEXT,就将TextView显示的内容改成myCount的值。
解释一下它的原理:
每一个线程都可以有一套消息队列、looper。每一个Activity会默认创建一个消息队列和一个looper。主线程中创建一个Handler,Looper一直等待取出消息队列中的消息。子线程对myCount进行++操作,并将myCount的值放进消息队列,主线程中的looper发现消息队列中有消息,将其取出,传递给Handler,在handleMessage方法中对消息进行处理,并将数据显示在UI上。
使用AsyncTask
为了更方便的在子线程中对UI进行操作,android封装了一个asyncTask来实现。
AsyncTask是一个抽象类,我们使用时,要创建一个子类来继承它,继承时我们可以为AsyncTask类指定三个泛型参数。
Params:在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。
Progress:后台执行任务时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
Result:当任务执行完毕后,需要对结果进行返回时,这里指定的泛型作为返回值类型。
比如这样写:
第一个参数:指定为void,表示在执行AsyncTask的时候不需要传入参数给后台任务。
第二个参数:指定为Integer,表示用整型数据来作为进度显示单位。
第三个参数:指定为Boolean,表示用布尔类型来反馈结果。
为了完成对任务的定制,我们还需要重写4个方法。这里以模拟下载任务,显示进度条,下载完成后,关闭进度条并提示下载完成为例。
代码如下:
在这个DownloadTask中,我们在doInBackground()方法里去执行具体的下载任务,这个方法的代码都是在子线程中运行的,因而不会影响主线程的运行。我们这里虚构了一个download()方法,用于计算当前的下载进度并返回,我们假设这个方法已经存在。在得到当前下载进度后,我们调用publishProgress()方法并将当前的下载进度传进来,很快,onProgress()方法就会被调用,就可以进行UI操作,显示下载进度了。
当下载完成后,doBackground()方法会返回一个布尔型变量,很快,onPostExecute()方法就会被调用,这个方法也是在主线程中运行的。然后我们会根据下载结果弹出相应Toast提示,从而完成DownloadTask任务。
启动方式如下:
总结一下:
doInBackground()方法在子线程中运行,因此具体的下载任务在这个方法里写。
onPreExecute方法在主线程中运行,在执行后台任务前调用,用于进行一些界面的初始化操作,例如显示一个进度条的对话框。
onProgressUpdate方法在主线程中运行,用于接受后台传来的进度值,利用这个进度值对界面元素进行相应更新。
onPostExecute方法在主线程中运行,用于接收后台任务执行完毕的返回结果,利用返回结果进行一些UI操作,比如提示任务执行结果、关闭进度条对话框等。
注:runOnUiThread()是一个异步消息处理机制的接口封装,表面看起来用法更为简单,但其实背后实现原理是一样的。