android的UI操作不是安全的,同时也只有主线程才能操作UI,同时主线程对UI操作有一定的时间限制(最长5秒)。为了能够作一些比较好使的操作(比如下载、打开大文件等),android提供了一些列机制。
线程安全:如果你的代码所在的进程中又多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也喝预期的是一样的,就是线程安全的,或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
当一个程序第一次启动的时候,android会启动一个LINUX进程和一个主线程。默认的情况下,所有该程序的组件都将在该进程和线程中运行。主线程(Main Thread)主要负责处理与UI相关的事件,如:用户的按键事件,用户接触屏幕的事件以及屏幕绘图事件并把相关的时间分发到对应的组件进行处理。所以主线程通常又被叫做UI线程。UI线程雨AndroidUI工具包中的组件进行交互,在考法Android应用时必须遵守单线程模型的原则--AndroidUI操作并不是线程安全的并且这些操作必须在UI线程中执行。
当主线程正在做一些比较耗时的操作的时候,如正从网络上下载一个大图片或者访问数据库,由于主线程悲这些好事的操作阻塞住,无法及时的响应用户的事件,从用户的角度看会觉得程序已经死掉。如果程序长时间不响应,用户还可能得重启系统。为了避免这样的情况,Android设置了一个5秒的超时时间,一旦用户的时间由于主线程阻塞而超过5秒钟没有响应,Android会弹出一个应许程序没有响应的对话框(ANR)。
Android的UI是单线程的,一些费时的操作交给独立的线程来执行UI对象。
handler引入
在Android开发中,通常会遇到:在UI基面上进行某项操作后要执行一段很好使的代码,比如我们在界面上点击下载按钮执行网络请求,这是一个好使操作,不知什么时候才能完成。为了保证不影响UI线程,我们会创建一个新的线程去执行耗时的代码。当我们的耗时操作完成时,我们需要更新UI界面告知用户操作完成了。简单代码如下:
public class MainActivity extends Activity implements Button.OnClickListener {
private TextView statusTextView = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
statusTextView = (TextView)findViewById(R.id.statusTextView);
Button btnDownload = (Button)findViewById(R.id.btnDownload);
btnDownload.setOnClickListener(this);
}
@Override
public void onClick(View v) {
DownloadThread downloadThread = new DownloadThread();
downloadThread.start();
}
class DownloadThread extends Thread{
@Override
public void run() {
try{
System.out.println("开始下载文件");
//此处让线程DownloadThread休眠5秒中,模拟文件的耗时过程
Thread.sleep(5000);
System.out.println("文件下载完成");
//文件下载完成后更新UI
MainActivity.this.statusTextView.setText("文件下载完成");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
上面代码点击“下载”按钮后会启动一个新的线程去执行世纪的下载操作,执行完毕后更新UI界面。但是在实际代码运行到MainActivity.this.statusTextView.setText("文件下载完成");
时,报错系统崩溃退出:报错如下:android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
错误的意思是只有创建View的原始线程才能更新View。出现这样错误的原因是Android中的View不是线程安全的,在Android应用启动时,会自动创建一个线程,即程序的主线程,主线程负责UI的展示、UI时间消息的处理等等,因此主线程也叫做UI线程,statusTextView
是在UI线程中创建的,当我们在DownloadThread
线程中去更新UI线程中创建的statusTextView
时就会报错。Android为了解决这种问题引入了Handler机制。
handler可以用来在多线程之间进行通信,在另一个线程红去更新UI控件只是Handler使用中的一种典型案例,除此之外,Handler可以做很多其他的事情。每个Handler都绑定了一个线程,假设存在两个线程ThreadA和ThreadB,并且HandlerA绑定了ThreadA,在ThreadB中的代码执行到某处时犹豫某些原因,我们需要让ThreadA执行某些代码,此时我们就可以使用Handler,我们可以在ThreadB中想HandlerA中加入某些信息来告知ThreadA中发作某些处理。Handler是多线程之间通信的桥梁,通过Handler,我们可以在一个线程中控制另一个线程去做某事。
使用post方法
public class MainActivity extends Activity implements Button.OnClickListener {
private TextView statusTextView = null;
//uiHandler在主线程中创建,所以自动绑定主线程
private Handler uiHandler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
statusTextView = (TextView)findViewById(R.id.statusTextView);
Button btnDownload = (Button)findViewById(R.id.btnDownload);
btnDownload.setOnClickListener(this);
System.out.println("Main thread id " + Thread.currentThread().getId());
}
@Override
public void onClick(View v) {
DownloadThread downloadThread = new DownloadThread();
downloadThread.start();
}
class DownloadThread extends Thread{
@Override
public void run() {
try{
System.out.println("DownloadThread id " + Thread.currentThread().getId());
System.out.println("开始下载文件");
//此处让线程DownloadThread休眠5秒中,模拟文件的耗时过程
Thread.sleep(5000);
System.out.println("文件下载完成");
//文件下载完成后更新UI
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Runnable thread id " + Thread.currentThread().getId());
MainActivity.this.statusTextView.setText("文件下载完成");
}
};
uiHandler.post(runnable);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
我们在Activity中创建了一个Handler成员变量uiHandler,Handler有个特点,在执行new Handler()的时候,默认情况下Handler会绑定当前代码执行的线程,我们在主线程中实例化了uiHandler,所以uiHandler就自动绑定了主线程,即UI线程。当我们在DownloadThread中执行完耗时代码后,我们将一个Runnable对象通过post方法传入到了Handler中,Handler会在合适的时候让主线程执行Runnable中的代码,这样Runnable就在主线程中执行了,从而正确更新了主线程中的UI。
使用sendMessage方法
public class MainActivity extends Activity implements Button.OnClickListener {
private TextView statusTextView = null;
//uiHandler在主线程中创建,所以自动绑定主线程
private Handler uiHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 1:
System.out.println("handleMessage thread id " + Thread.currentThread().getId());
System.out.println("msg.arg1:" + msg.arg1);
System.out.println("msg.arg2:" + msg.arg2);
MainActivity.this.statusTextView.setText("文件下载完成");
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
statusTextView = (TextView)findViewById(R.id.statusTextView);
Button btnDownload = (Button)findViewById(R.id.btnDownload);
btnDownload.setOnClickListener(this);
System.out.println("Main thread id " + Thread.currentThread().getId());
}
@Override
public void onClick(View v) {
DownloadThread downloadThread = new DownloadThread();
downloadThread.start();
}
class DownloadThread extends Thread{
@Override
public void run() {
try{
System.out.println("DownloadThread id " + Thread.currentThread().getId());
System.out.println("开始下载文件");
//此处让线程DownloadThread休眠5秒中,模拟文件的耗时过程
Thread.sleep(5000);
System.out.println("文件下载完成");
//文件下载完成后更新UI
Message msg = new Message();
//虽然Message的构造函数式public的,我们也可以通过以下两种方式通过循环对象获取Message
//msg = Message.obtain(uiHandler);
//msg = uiHandler.obtainMessage();
//what是我们自定义的一个Message的识别码,以便于在Handler的handleMessage方法中根据what识别
//出不同的Message,以便我们做出不同的处理操作
msg.what = 1;
//我们可以通过arg1和arg2给Message传入简单的数据
msg.arg1 = 123;
msg.arg2 = 321;
//我们也可以通过给obj赋值Object类型传递向Message传入任意数据
//msg.obj = null;
//我们还可以通过setData方法和getData方法向Message中写入和读取Bundle类型的数据
//msg.setData(null);
//Bundle data = msg.getData();
//将该Message发送给对应的Handler
uiHandler.sendMessage(msg);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
需要说明的是,如果在handleMessage中 不需要判断Message类型,那么就无须设置Message的what值;而且让Message携带数据也不是必须的,只有在需要的时候才需要让其携带数据;如果确实需要让Message携带数据,应该尽量使用arg1或arg2或两者,能用arg1和arg2解决的话就不要用obj,因为用arg1和arg2更高效。