Android中的多线程支持

出于性能考虑,Android的UI操作不是线程安全的,因此如果允许多个线程同时更新UI,就会发生问题。因此Android规定,只能在主线程(也叫UI线程)更新UI。但是我们的程序中可能包含耗时操作,比如网络下载,大量的IO操作等。如果把耗时操作放在主线程,主线程失去响应超过5秒,程序就会抛出ANR异常(此时系统会弹出对话框,XXX程序没有响应,询问用户是否强制关闭),这样用户体验是极差的。

要做到主线程更新UI,子线程执行耗时操作,这免不了给编程上造成一定困难,因此Android提供了封装好的消息传递机制和异步任务机制供程序员使用。如果非UI线程想要修改UI组件,就可以通过Android提供的这些机制完成。

注:ProgressBar可以在非UI线程修改当前进度值,这是因为几乎所有情况下,修改进度条的操作都不在UI线程,Android框架的设计者为了用户方便,使ProgressBar内部对修改进度的线程进行了判断,如果不在UI线程就使用消息传递机制通知UI线程进行修改。

有关Java的多线程知识,可以参考Java/Java并发程序设计章节。

Handler 消息传递机制

消息传递机制使用起来非常简单:

  1. 子线程执行耗时操作并发送Message
  2. 主线程接收Message并更新UI

Handler类就是这些工作的桥梁。

Handler类

下面例子中,通过一个计划任务子线程,循环向Handler发送请求,不断改变TextView的值。

package com.gacfox.handlerdemo;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity
{

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final TextHandler textHandler = new TextHandler((TextView) findViewById(R.id.tv));

        new Timer().schedule(new TimerTask() {

            private boolean state = false;

            @Override
            public void run()
            {
                Message message = new Message();
                message.what = 0x1;
                if(state)
                {
                    Bundle bundle = new Bundle();
                    bundle.putString("text", "aaa");
                    message.setData(bundle);

                    state = false;
                }
                else
                {
                    Bundle bundle = new Bundle();
                    bundle.putString("text", "bbb");
                    message.setData(bundle);

                    state = true;
                }
                textHandler.sendMessage(message);
            }
        }, 0, 1000);
    }
}

class TextHandler extends Handler
{

    private TextView textView;

    TextHandler(TextView textView)
    {
        this.textView = textView;
    }

    @Override
    public void handleMessage(Message msg)
    {
        if(msg.what == 0x1)
        {
            String text = msg.getData().getString("text");
            textView.setText(text);
        }
    }
}

代码非常好理解,子线程获得Handler对象引用,就可以利用它发送Message对象,message.what相当于一个消息的类型标识,值我们可以任意指定。主线程中,Handler通过一个回调函数接收消息,消息中可以带有bundle对象,因此可以传递任何类型的数据。除此之外,我们要知道Handler内部有一个消息队列,我们发送的消息会存储于这个队列中等待主线程响应(这是典型的生产者消费者问题)。

实际上,我们不使用Handler或是自己实现一个Handler也是可行的,只不过Android更进一步,为我们提供了这个方便的工具。

Handler 的其他常用方法

  • boolean hasMessage(int what):返回消息队列中是否有指定类型的消息
  • send EmptyMessage(int what):发送空消息
  • boolean sendEmptyMessageDelayed(Message message, long delayMillis):指定延迟后发送消息

注:有时Message不一定非要带有什么数据,此时可以发送空消息。

Handler造成的内存泄漏问题

下面代码在Android Studio中,静态分析工具会指出一个警告:

package com.gacfox.handlerdemo;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class HandlerDemoActivity extends AppCompatActivity
{

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_demo);

        Handler handler = new Handler()
        {
            @Override
            public void handleMessage(Message msg)
            {
            }
        };
    }
}

警告信息:

This Handler class should be static or leaks might occur...

要实现一个Handler,需要一个轮询线程和一个线程安全的消息队列。因此Handler内部会初始化一个Looper对象实例。Java中非静态的内部类会隐式持有外部类的引用,而Looper是一个应用全局都存在的对象,它一直存在就会导致最外面的Activity无法被垃圾回收。

解决这个问题最简单的方法是把自己的Handler单独定义一个类,而不是匿名内部类,另一种方式是将Handler定义为静态内部类,静态内部类不会持有外部类的引用。但如果我们需要在Handler中调用Activity的方法或属性,那么还是不得不把Activity传入自定义的Handler类,内存泄漏还是无法解决,IDE提示可以使用弱引用,但前提是你要保证Activity退出后,Handler就不会再使用外部Activity的属性或方法,否则就会引发空指针。实际上,如果Handler必须调用外部Activity的属性或方法,就必须维持外部Activity的引用,此时不认为这是内存泄漏。

AsyncTask 异步任务

虽然名字里有个async,但Android提供的这个AsyncTask写起来似乎并不是很简洁(想想JavaScript),不过既然提供了这种机制,我们就来看一看如何使用。

AsyncTask解决的问题和Handler大致相同,但完全是两种实现风格。使用时,我们需要继承AsyncTask抽象类,并重写doInBackground()方法。该方法在异步任务执行时,Android会从线程池中开辟一条新线程,供耗时操作执行。下面例子中,实现了使用AsyncTask在子线程中通知UI线程改变TextView值,效果和上面使用Handler相同。

package com.gacfox.handlerdemo;

import android.os.AsyncTask;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class Main2Activity extends AppCompatActivity
{

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);

        new TextAsyncTask((TextView) findViewById(R.id.tv)).execute();
    }
}

class TextAsyncTask extends AsyncTask<Void, String, Void>
{

    private TextView textView;

    TextAsyncTask(TextView textView)
    {
        this.textView = textView;
    }

    @Override
    protected Void doInBackground(Void... voids)
    {
        boolean state = false;
        while(true)
        {
            if(state)
            {
                publishProgress("aaa");
                state = false;
            }
            else
            {
                publishProgress("bbb");
                state = true;
            }
            try
            {
                Thread.sleep(1000);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onProgressUpdate(String... values)
    {
        textView.setText(values[0]);
    }
}

下面讲解一下这些代码:

AsyncTask <Params, Progress, Result>
  • Params 异步任务的参数类型,比如:下载操作,参数可能就是下载URL。
  • Progress 进度参数,上面由于我们需要改变TextView的值,因此进度参数就是String类型。AsyncTask中,可以使用publishProgress(Progress... values)时时发送进度参数。
  • Result 异步任务结果参数类型。
  • 如果某个参数类型不需要使用,比如上面代码,我们不需要使用Params和Result,那么传Void即可。

我们可能需要重写AsyncTask的如下几个方法:

  • doInBackground(Params... params):该方法会在新线程中执行,其中代码是异步任务的主要耗时操作
  • onPreExecute():执行异步任务前的初始化操作,它在UI线程中执行
  • onPostExecute(Result... result):异步任务结束后,会将结果返回给这个方法,它在UI线程中执行
  • onProgressUpdate(Progress... progress):异步任务更新进度时,会将更新结果返回给这个函数,它在UI线程中执行
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap