学而实习之 不亦乐乎

Android:多线程编程

2022-07-05 11:45:05

一、基本用法

与Java中的多线程编程类似,定义一个线程,只需要创建一个类,并且这个类继承自Thread类,然后重写run()方法。如:

class MyThread extends Thread{
    @override
    Public void run(){
    //Todo something.
    }
}

调用

new MyThread().start();

使用继承的方式耦合性有点高,大多数情况下我们使用Runnable接口来定义一个线程。如:

class MyThread extends Runnable{
    @override
    pblic void run(){
    //Todo something.
    }
}

调用:

MyThread myThread = new MyThread();
new Thread(myThread).start();

也可以使用匿名方法类的方式。如:

new Thread(new Runnable() {
        @Override
        public void run(){
        //TODO
        }
    }
).start();

二、如何在子线程中更新UI

1.问题:在子线程中更新UI时出现异常

虽然Android中的多线程的创建和启动与Java中方式相同,但其使用方式还是不完全相同的。因为Android的UI也是线程不安全的,如果想要更新应用程序中的UI元素,必须在主线程中进行,否则会出现异常。

实例如下:
1.activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/change_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Change Text" />

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Hello world"
        android:textSize="20sp" />

</RelativeLayout>

2.MainActivity

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private TextView text;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView) findViewById(R.id.text);
        Button changeText = (Button) findViewById(R.id.change_text);
        changeText.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.change_text:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        text.setText(“Hello!”);
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

运行程序出错!
logcat中显示错误如下:

android view.ViewRootImpl$ … … :Only the original thread that created a view hierarchy can touch its views.

处理这个问题需要用异步消息处理(如下面的介绍)。

2.使用异步消息处理

Android 中的异步消息处理主要由 Message、Handler、MessageQueue 和 Looper。其关系如下图

(1)Message
线程之间传递消息,它可以在内部携带少量信息,用于在不同线程之间交换数据。如:
message.what,message.arg1,message.arg2,message.obj等。

(2)Handler
用于发送(sendMessage())和处理消息(handleMessage())。

(3)MessageQueue
存放所有通过Handler发送的消息,每个线程中只有一个MessageQueue对象。

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

实例:
MainActivity

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    public static final int UPDATE_TEXT = 1;
    private TextView text;
    private Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case UPDATE_TEXT:
                    // 在这里可以进行UI操作
                    text.setText("Hello!");
                    break;
                default:
                    break;
            }
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView) findViewById(R.id.text);
        Button changeText = (Button) findViewById(R.id.change_text);
        changeText.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.change_text:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Message message = new Message();
                        message.what = UPDATE_TEXT;
                        handler.sendMessage(message); // 将Message对象发送出去
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

当然你可以直接使用runOnUiThread()方法直接实现对UI的更新,其实这个方法也是异步消息处理机制的封装,其实现原理与这里展示的方法是一样的。

三、使用AsyncTask

AsyncTask 的实现也是基于异步消息处理机制。其基本用法如下:

class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
    … … … …
}

其中三个参数分别如下:
1.Params:执行AsyncTask时需要传入的参数,用于在后台任务中使用
2.Progress:后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位
3.Result:当任务执行完,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

如上面的DownloadTask还是一个空的任务,不能执行任何任务,所以我们需要重写AsyncTask中的几个方法。常用的如下:

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

如下面代码:

public class DownloadTask extends AsyncTask<String, Integer, Integer> {
    @Override
    protected Void doInBackground(String... params) {
        progressDialog.show();//显示进度对话框
    }

    @Override
    protected Boolean doInBackground(String... params) {
        //方法在子线程中运行,不会影响主线程运行,所以不能进行UI操作
        try {
            while (true) {
                int downloadPercent = doDownload();    //伪代码
                publishProgress(downloadPercent);
                if (downloadPercent >= 100) {
                    break;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        //这里更新进度条,进行UI操作
        progressDialog.setMessage("Download " + value[0] + "%");
    }

    @Override
    protected void onPostExecute(Boolean result) {
        //下载完成时,doInBackground()方法返回布尔变量,此方法很快被调用。
        //此方法是在主线程中运行的。
        //此方法多执行一些收尾的工作。
        if (result) {
            Toast.makeText(context,"Download succeeded", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(context,"Download failed", Toast.LENGTH_SHORT).show();
        }
    }
}

如果要启动这个任务,只需要使用下面的代码即可:
new DownloadTask().execute();