Android多线程基础知识详解(傻瓜教程)

线程的基本用法

Android中的多线程和Java中的多线程语法基本相同。定义一个新的线程首先需要新建一个类继承自Thread,然后重写父类方法run(),在run()方法中编写耗时逻辑即可。一般情况这个类为内部类。

class TestThread extends Thread{
    
    @Override
    public void run(){
        //处理具体的逻辑
    }
}

如何启动这个线程呢?只需要new一个TestThread的实例,然后调用它的start()方法。

new TestThread().start();

使用继承的方式实现多线程有一个弊端,耦合性有点高。所以一般情况下选择实现Runnable()接口的方式来定义一个线程。

class TestThread extends Runnable{
    
    @Override
    public void run(){
        //处理具体的逻辑
    }
}

使用实现Runnable()接口的方式,启动线程的方法也需要改变,如下:

TestThread testThread = new TestThread();
new Thread(testThread).start();

上面两行代码表示Thread的构造函数接收的是一个Runnable参数,而我们new出的TestThread正是一个实现了Runnable接口的对象,所以可以直接将它传入。最后调用start()方法即可运行。当使用线程熟练过后,可以使用匿名类的方式实现,这种方法更常见。

new Thread(new Runnable(){

    @Override
    public void run(){
        //处理具体的逻辑
    }
}).start();

在子线程中更新UI 

Android中的多线程和Java中的多线程虽然语法基本相同,但还是有不同的地方。首先Android中的UI线程是不安全的,也也就是说想要更新程序中的UI,就必须在主线程中进行,否则就会出现以下异常。

android.view.ViewRootImp$CalledFromWrongThreadException: Only thr original

thread that created a view hierarchy can touch its views.

但是在某些情况下,我们又不得不在子线程中更新UI,针对这种情况Android提供一套异步消息处理机制,完美的解决了在子线程中更新UI的问题。先看下面的代码示例:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    
    public static final int UPDATE_TEXT = 1;
    private TextView tv;
    private Handler handler = new Handler(){
        public void handleMessage(Message msg){
            switch(msg.what){
                case UPDATE_TEXT:
                    //在这里更新UI
                    tv.setText("hello world");
                    break;
                default:
                    break;
            }
        }
    };

    ...

    @Override
    public void onClick(View v){
        switch(v.getId()){
            case R.id.tv:
                new Thread(new Runnable(){
                    @Override
                    public void run(){
                        Message message = new Message();
                        message.what = UPDATE_TEXT;
                        handler.sendMessage(message);
                    }
                }).start();
                break;
            default;
                break;
        }
    }
}

首选定义了一个常量表示UPDATE_TEXT表示更新UI这个动作,然后创建一个Handler对象,并重写了父类的handlerMessage()方法,在此方法中对具体的Message进行了处理。如果发现message的what字段为UPDATE_TEXT,就进行UI更新。在点击事件中创建了一个Message(android.os.Message)对象,并将它的what字段指定为UPDATE_TEXT,然后调用Handler的sendMessage()方法发送。handler收到这个message后,就在handlerMessage中对这个message进行处理,判断what字段是否为UPDATE_TEXT,如果匹配就更新UI。

解析异步消息处理机制 

在Android中的异步消息处理主要由4个部分组成:Message、Handler、MessageQueue、Looper。其中Message和Handler在刚刚已经了解过了,而MessageQueue和Looper是接下来介绍的重点。

1.Message  

Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。除了what字段,还可以使用arg1和arg2字段来携带一些整型数据,使用obj字段来携带一个Object对象。

2.Handler 

Handler主要用于发送和处理消息,发送消息一般是使用Handler的sendMessage()方法,需要传递的消息最后到达handlerMessage()方法中。

3.MessageQueue 

MessageQueue是消息队列的意思,主要作用为存放所有通过Handler发送的消息。这部分消息会一直存在消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。

4.Looper

 Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入到一个无线循环中,然后每当发现MessageQueue中存在一条消息,就会将它取出来,并传递到Handler的handlerMessage()方法中。每个线程也只有一个Looper对象。

了解了上面的四个概念后,我们再把异步消息处理的流程梳理一遍。首先在主线程中创建一个Handler对象,并重写handlerMessage()方法。然后在子线程中需要进行UI操作的时候,就创建一个Message对象,并通过Handler将这条消息发送出去,之后这条消息就会被添加到MessageQueue的队列中等待被处理,而Looper会一直尝试从MessageQueue中取出待处理的消息,最后消息被分发到Handler的handlerMessage()方法中。

使用AsyncTask 

AsyncTask实现的原理是基于异步消息处理机制的,只是Android已经做好了封装。AsyncTask是一个抽象类,要想使用它就需要创建一个子类去继承它。在继承的时候需要为AsyncTask类指定3个泛型参数

  • Params 在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。
  • Progress 在后台任务执行是,如果需要在界面上显示当前的进度,则使用这个指定的泛型作为进度单位。
  • Result 当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

因此,一个最简单的AsyncTask就可以写成如下方式:

class TestTask extends AsyncTask{
    ...
}

这里我们把第一次参数指定为Void,表示在执行TestTask的时候不需要传入参数给后台任务。第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位。第三个泛型参数指定为Boolean,则表示使用布尔数据来反馈执行结果。上面自定的Task还是一个空任务,需要重写AsyncTask其中的几个方法才能完成对任务的定制,经常重写的方法有以下四个:

  • onPreExecute() 这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
  • doInBackground(Params...) 这个方法中的所有代码都会在子线程中运行,应该在这里去处理所有耗时任务。任务一旦完成就可以通过return语句来将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是Void,就可以不返回任务执行结果。但是,在这个方法中是不可以执行UI操作的,如果要更新UI,比如反馈当前任务的执行进度,可以调用publishProgress(Progress...)方法来完成。
  • onProgressUpdate(Progress...) 当在后台任务中调用了publishProgress(Progress...)方法后,onProgressUpdate(Progress...)方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。
  • onPostExecute(Result) 当后台任务执行完毕并通过return语句进行返回时,这个方法就很快的被调用,返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些UI操作,比如说提醒任务执行完毕的结果,以及关掉进度对话框等。

下面是一个比较完成的AsyncTask:

class DownloadTask extends AsyncTask{
    
    @Override
    protected void onPreExecute(){
        progressDialog.show();//显示进度对话框
    }

    @Override
    protected Boolean doInBackground(Void... params){
        try{
            while(true){
                int downloadPercent = doDownload();//doDownload是一个虚构的方法
                publishProgress(downloadPercent);
                if(downloadPercent >= 100){
                    break;
                }
            }
        }catch(Exception e){
            return false;
        }
        return true;
    }

    @Override
    protected void onProgressUpdate(Integer... values){
        //在这里更新下载进度
        progressDialog.setMessage("Downloaded " + values[0] + "%");
    }

    @Override
    protected void onPostExecute(Boolean result){
        progressDialog.dismiss();
        //提示下载结果
        if(result){
            Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show();
        }else{
            Toast.makeText(context, "Download failed", Toast.LENGTH_SHORT).show();
        }
    }
}

在上面这个DownloadTask中,在doInBackground()方法中执行具体的下载任务,这个方法中所有代码都是子线程中运行的。其中虚构了一个doDownload()方法,用于计算当前下载进度,我们假设这个方法是可以用的。在得到进度后,就应该考虑,怎样把它显示在界面上,因为doInBackground()是在子线程中运行的,所以不能更新UI。可以在publishProgress()方法中架构当前的下载进度传递进来,这样inProgressUpdate()方法就会被调用,在这里就可以进行UI操作了。

当下载完成后,doInBackground()方法会返回一个布尔类型的变量,这样onPostExecute()方法就会被调用,这个方法也是在主线程中运行,所以我们可以使用Toast来进行提示。

简单来说,AsyncTask的使用方式就是,在doInBackground()中执行具体的耗时任务,在onProgressUpdate()中执行UI操作,在onPostExecute()中执行收尾工作。

箱套启动这个任务,只需要执行下面的代码即可:

new DownloadTask().execute();

发现错误,请指出,谢谢!

 

你可能感兴趣的:(android)