出于性能考虑,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类就是这些工作的桥梁。
下面例子中,通过一个计划任务子线程,循环向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更进一步,为我们提供了这个方便的工具。
注:有时Message不一定非要带有什么数据,此时可以发送空消息。
下面代码在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的引用,此时不认为这是内存泄漏。
虽然名字里有个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>
publishProgress(Progress... values)
时时发送进度参数。我们可能需要重写AsyncTask的如下几个方法: