一、前期基础知识储备
当一个应用程序启动之后,android系统会为这个应用程序创建一个主线程(Main Thread),它负责渲染视图,分发事件到响应监听器并执行,对界面进行轮询的监听。因此,一般也叫做“UI线程”(UI Thread)。
android系统不会给应用程序的多个元素组件建立多个线程来执行。一个视图(Activity)中的多个view组件运行在同一个UI线程当中。因此,多个view组件的监听器的执行可能会相互影响。
如果在UI线程中做一些比较耗时的操作,比如访问网络或者数据库,都可能阻塞UI线程,导致时间停止分发(包括绘制事件)。对于用户来说,应用看起来像是卡住了,更坏的情况是,如果UI线程阻塞时间太长(超过5秒),android系统会弹出ANR(application not responding)错误提示框。
代码如下:
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//设置其耗时操作
try {
Thread.sleep(5000); //5秒- 为Activity不响应报ANR错误的时间
} catch (InterruptedException e) {
Log.i("线程沉睡",e.getMessage());
}
int sum=10;
btn.setText("计数:"+sum);
}
});
点击按钮之后,会发现界面卡死,这时就出现了UI阻塞。
二、Android中如何处理耗时操作
上官方文档:
There are basically two main ways of having a Thread execute application code. One is providing a new class that extends Thread and overriding its run() method. The other is providing a new Thread instance with a Runnable object during its creation. In both cases, the start() method must be called to actually execute the new Thread.
官方文档中对于耗时操作提出的两点必须遵守的开发规则:
①不要阻塞UI线程—即不要再UI线程中执行耗时操作;
② 不要在UI线程之外的其他线程中,对视图中的组件进行设置。
上述代码正确的写法:
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
//设置其耗时操作
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Log.i("耗时操作",e.getMessage());
}
int sum=10;
btn.setText("计数:"+sum);
}
}).start();
运行结果,依然报错,我们已经开启支线程处理耗时操作,符合了第一条原则,但是,第二条“任何和UI界面相关的操作都应该放在主线程中完成”没有遵守。
Android对于这种情况,提供了两种方法,从支线程中回到主线程:
1)通过View.post(Runnable)方法;
view.post(new Runnable() { //使用View.post(Runnable)进行组件设置
@Override
public void run() {
//在这里进行UI操作,将结果显示在界面上
btn.setText("计数:"+sum)
)};
2)通过runOnUIThread(Runnable)方法。
runOnUIThread(new Runnable(){
@Override
public void run() {
//在这里进行UI操作,将结果显示在界面上
btn.setText("计数:"+sum)
)};
三种切回主线程的实例:
final Handler handler = new Handler();
new Thread(new Runnable() {
@Override
public void run() {
// 素描算法处理 耗时操作
final Bitmap bitmap1 = SketchUtil.testGaussBlur(finalBitmap,1,1);
final Bitmap bitmap2 = SketchUtil.testGaussBlur(finalBitmap,10,10);
final Bitmap bitmap3 = SketchUtil.testGaussBlur(finalBitmap,20,20);
// 三种切回主线程更新UI的方法
imageView.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap1); // 素描图
}
});
runOnUiThread(new Runnable() {
@Override
public void run() {
orignView.setImageBitmap(bitmap2); // 素描图
}
});
handler.post(new Runnable() {
@Override
public void run() {
threeView.setImageBitmap(bitmap3); // 素描图
}
});
}
}).start();
这种解决方法中,UI线程与我们新建的线程之间的关系类似于生产者与消费者之间的关系,新线程通过View.post(Runnable)和runOnUIThread(Runnable)方法在任务队列中加入任务,而UI线程对任务队列进行轮询,有任务的话就拿出来执行,修改界面。
但这种解决方法可读性和维护性较差,适用于切换线程较少的场景。
注意:使用handler方法切回主线程时,注意handler的实例化要放在主线程中,而不能在新开的子线程中,否则报错:
RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
这是因为,Handler在哪里创建,就获得哪里的Looper。主线程创建的Handler,即默认使用主线程的Looper。
即为Android中实现异步任务的方式:
(1)Handler类,在android中负责发送和处理消息,通过它可以实现其他支线线程与主线程之间的消息通讯。
(2)AsyncTask类,Android从1.5版本之后引入,使用它就可以非常灵活方便地从子线程切换到UI线程。
上代码,具体实现:
—————————————————---我是Handler类分隔线---————————————————
①不同的平台提供了不同的解决方案以实现跨线程跟新UI控件,Android为了解决这种问题引入了Handler机制。Handler类提供了两种方式解决我们在本文一开始遇到的问题(在一个新线程中更新主线程中的UI控件),一种是通过post方法,一种是调用sendMessage方法。
Post()方法:
class DownloadThread extends Thread{
@Override
public void run() {
try{
//此处让线程DownloadThread休眠5秒中,模拟文件的耗时过程
Thread.sleep(5000);
System.out.println("文件下载完成");
//文件下载完成后更新UI
Runnable runnable = new Runnable() {
@Override
public void run() {
MainActivity.this.statusTextView.setText("文件下载完成");
}
};
uiHandler.post(runnable);//post()在支线程中完成UI操作
}catch (InterruptedException e){
e.printStackTrace();
}
};
sendMessage()方法:
class DownloadThread extends Thread {
@Override
public void run() {
try {
//此处让线程DownloadThread休眠5秒中,模拟文件的耗时过程
Thread.sleep(5000);
//文件下载完成后更新UI
Message msg = new Message();
msg.what = 1;
msg.arg1 = 123;
msg.arg2 = 321;//将该Message发送给对应的Handler
uiHandler.sendMessage(msg);//注意此时还是在支线程中
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//uiHandler在主线程中创建,所以自动绑定主线程
private Handler uiHandler = new Handler() {
Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
System.out.println("msg.arg1:" + msg.arg1);
System.out.println("msg.arg2:" + msg.arg2);
MainActivity.this.statusTextView.setText("文件下载完成");
break;
}
}
};
}
注意:sendMessage()方法中,设置Message的what值
Message.what是我们自定义的一个Message的识别码,以便于在Handler的handleMessage方法中根据what识别出不同的Message,以便我们做出不同的处理操作。 设置Message的所携带的数据,简单数据可以通过两个int类型的field arg1和arg2来赋值,并可以在handleMessage中读取。 如果Message需要携带复杂的数据,那么可以设置Message的obj字段,obj是Object类型,可以赋予任意类型的数据。
—————————————————---我是AsyncTask类分隔线---————————————————
②AsyncTask类,异步任务,从字面上说,就是我们的UI主线程运行的时候,异步的完成一些操作。AsyncTask允许我们执行一个异步任务在后台。我们可以将耗时操作放在异步任务当中执行,并随时将任务执行的结果返回给我们的UI主线程来更新我们的UI控件。通过ASyncTask我们就可以轻松的解决多线程之间的通信问题。
private class task extends AsyncTask{
@Override
protected String doInBackground(String... params) {
try {
Thread.sleep(5000);// 执行耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
return "通过AsyncTask设置";
}
@Override
protected void onPostExecute(String result) {
textView.setText(result);
}
};
在activity中建一个内部类,继承AsyncTask,重写doInBackground()和onPostExecute()方法,doInBackground的返回值会做为onPostExecute的参数执行。然后我们只需要在需要调用的时候new task().execute()就可以执行了。
四、两种异步更新UI方法的对比:
①Handler类:
优点:结构清晰,功能定义明确,对于后台多个任务时,简单清晰;
缺点:在单个后台异步处理时,显得代码过多,结构过于复杂(相对性);
②AsyncTask类:
优点:简单快捷过程可控的轻量级异步类;
缺点:在使用多个异步操作和并需要UI变更时,就变得复杂起来;
总结:二者各有优劣,有各自对应的开发场景,开发者都需要掌握。
1)执行延时操作 Handler + Runnable;
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
topBar.setBackgroundColor(getResources().getColor(R.color.translucent_bar_background));
indicatorBar.setBackgroundColor(0x2059524D);
findViewById(R.id.mask_view).setVisibility(View.GONE);
}
}, 1000);
也可拆开实现
// 定义好Handler 和 Runnable接口
Handler handler = new Handler();
Runnable broad_thread = new Runnable() {
public void run() {
mOnScrollerViewUIChange.OnScrollerViewUIChange();
}
};
// 调用 实现延时操作
//handler.removeCallbacks(broad_thread); 移除之前的Runnable
handler.postDelayed(broad_thread, 1000);
2)开启子线程 Thread + Runnable;
// Runnable 接口中只有run()方法,没有start()方法,所以记得加上start()
new Thread(new Runnable() {
@Override
public void run() {
blur_bitmap = FastBlur.doBlur(scale_bitmap, 20, false); // 高斯模糊 耗时操作
}
}).start();
或者不选择使用内部类,而是直接在外部定义Runnable接口
// 定义外部类 实现Runnable接口
public class RestartCameraRunnable implements Runnable {
@Override
public void run() {
glRender.addPreDrawTask(new Runnable() {
@Override
public void run() {
cameraView.restartCamera(); // 重启相机 耗时操作
}
});
}
}
// 使用 开启子线程
new Thread(new RestartCameraRunnable()).start();
注:尽量少在正式项目中使用类似于 new Thread 这种方式去开启子线程 (每次new Thread新建对象性能差,线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom),建议根据实际情况决定:
①如果开启子线程次数有限,不频繁,就使用轻量级的 HandlerThread 实现消息处理操作和与主线程通信;
②如果开启子线程比较频繁,此时建议使用 Java线程池(Android线程概念源至Java)去处理;
3)使用HandlerThread实现消息处理操作和与主线程通信;
public class MainActivity extends AppCompatActivity {
Handler mainHandler,workHandler;
HandlerThread mHandlerThread;
TextView text;
Button button1,button2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text = (TextView) findViewById(R.id.text1);
// 创建与主线程关联的Handler
mainHandler = new Handler();
/**
* 步骤1:创建HandlerThread实例对象
* 传入参数 = 线程名字,作用 = 标记该线程
*/
mHandlerThread = new HandlerThread("handlerThread");
//步骤2:启动线程
mHandlerThread.start();
/**
* 步骤3:创建工作线程Handler & 复写handleMessage()
* 作用:关联HandlerThread的Looper对象、实现消息处理操作 & 与其他线程进行通信
* 注:消息处理操作(HandlerMessage())的执行线程 = mHandlerThread所创建的工作线程中执行
*/
workHandler = new Handler(mHandlerThread.getLooper()){
@Override
public void handleMessage(Message msg)
{
//通过msg来进行识别不同的操作 类似广播的过滤器action 可扩展性非常强大
switch(msg.what){
case 1:
try {
//延时操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 通过主线程Handler.post方法进行在主线程的UI更新操作
mainHandler.post(new Runnable() {
@Override
public void run () {
text.setText("第一次执行");
}
});
break;
case 2:
try {
// 直接在handleMessage内部处理耗时操作
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 调用handler的post()方法处理UI操作
mainHandler.post(new Runnable() {
@Override
public void run () {
text.setText("第二次执行");
}
});
break;
default:
break;
}
}
};
/**
* 步骤4:使用工作线程Handler向工作线程的消息队列发送消息
* 在工作线程中,当消息循环时取出对应消息 & 在工作线程执行相关操作
*/
button1 = (Button) findViewById(R.id.button1);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Message msg = Message.obtain();
msg.what = 1; //消息的标识
msg.obj = "A"; // 消息的存放
// 通过Handler发送消息到其绑定的消息队列
workHandler.sendMessage(msg);
}
});
button2 = (Button) findViewById(R.id.button2);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Message msg = Message.obtain();
msg.what = 2;
msg.obj = "B";
workHandler.sendMessage(msg);
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandlerThread.quit(); // 退出消息循环
workHandler.removeCallbacks(null); // 防止Handler内存泄露 清空消息队列
}
}
HandlerThread在处理信息时,会根据what的值去做不同的操作,类似于动态注册的广播,所以在使用时十分方便。
提供一些其他Android消息机制分析,帮助理解读者理解:
①Handler是Android消息机制的上层接口,通过它可以轻松地将一个任务切换到Handler所在的线程中去执行,该线程既可以是主线程,也可以是子线程,要看构造Handler时使用的构造方法中传入的Looper位于哪里;
②Handler的运行需要底层的MessageQueue和Looper的支撑,Handler创建的时候会采用当前线程的Looper来构造消息循环系统,而线程默认是没有Looper的,如果需要使用Handler就必须为线程创建Looper;
③上述代码中的第一个Handler-mainHandler,实例化的时候,直接在onCreate()方法中new出了实例,其实是其已经在主线程中了,主线程-ActivityThread,ActivityThread被创建时就会初始化Looper,这就是主线程中默认可以直接使用Handler的原因;
④上述代码中的第二个Handler-workHandler,它在实例化的时候,参数传入了 mHandlerThread.getLooper() ,注意,这个Handler使用的就不是主线程的Looper了,而是子线程的Looper,HandlerThread在调用start()方法之后,就可以获取到子线程的Looper,然后将其传入workHandler的构造方法中,那么此时的workHandler就会运行在子线程中,用于处理耗时操作。
⑤Handler的工作原理:Handler创建时会采用当前线程的Looper来构建内部消息循环系统,如果当前线程没有Looper,那么就会报错“Can`t create handler inside thread that has not called Looper.prepare()”解决方法有两个:为当前线程创建Looper即可,像上述代码中workHandler,或者在一个有Looper的线程中创建Handler也行,就像上述代码中的mainHandler一样;
⑥调用Handler的post方法会将一个Runnable投递到Handler内部的Looper中去处理,也可以通过Handler的send方法来发送一个消息,这个消息同样会在Looper中去处理。其实post方法最终也是通过send方法来完成的。每当Looper发现有新消息到来时,就会处理这个消息,最终消息中的Runnable的run方法或者Handler的handleMessage方法就会被调用。注意Looper是运行在创建Handler所在的线程中的,这样一来Handler中的业务逻辑就被切换到创建Handler所在的线程中去执行了;
⑦Looper的工作原理:Looper在Android的消息机制中扮演着消息循环的角色,具体来说就是它会不停地从MessageQueue中查看是否有新消息,如果有新消息就会立刻处理,否则就一直阻塞在那里。注意关注一些重要的Looper的方法:
⑧ActivityThread主线程通过ApplicationThread和AMS进行进程间通信
请使用手机"扫一扫"x