为什么要学习多线程
在安卓开发中,与用户交互的UI线程又称为主线程。所有跟用户交互的逻辑以及UI效果都是在主线程中执行的,四大组件也运行中主线程中,因此如果在主线程中进行耗时操作,会导致app有一段时间没有响应,产生ANR(Application Not Responding,应用程序无响应)。ANR是为了在app卡死的情况下,用户可以选择强制退出app,从而避免卡机无响应的问题,是Android的一种自我保护机制。
ANR的三种类型包括:
- 按键响应分发超时(5s)
- 广播超时(10s)
- 服务超时(20s)
为了避免ANR,需要将耗时操作移到子线程中操作;当子线程完成了耗时任务之后再由子线程通知主线程完成UI更新。
Android中实现多线程的方式
Java多线程
继承Thread类,重写该类的run()方法
class MyThread extends Thread {
private int i = 0;
@Override
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Thread myThread1 = new MyThread(); // 创建一个新的线程 myThread1 此线程进入新建状态
Thread myThread2 = new MyThread(); // 创建一个新的线程 myThread2 此线程进入新建状态
myThread1.start(); // 调用start()方法使得线程进入就绪状态
myThread2.start(); // 调用start()方法使得线程进入就绪状态
}
}
}
}
实现Runnable接口,并重写run()方法
class MyRunnable implements Runnable {
private int i = 0;
@Override
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Runnable myRunnable = new MyRunnable(); // 创建一个Runnable实现类的对象
Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
Thread thread2 = new Thread(myRunnable);
thread1.start(); // 调用start()方法使得线程进入就绪状态
thread2.start();
}
}
}
}
如果有复杂的线程操作需求,自定义Thread,如果只是简单的在子线程run一下任务,就实现Runnable。如果自己实现runnable的话可以多一个继承。
使用Callable和Future接口创建线程。
具体是创建Callable接口的实现类,并实现call()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。
public class ThridThread {
public static void main(String[] args) {
// lambda 表达式 + functionInterface 类型转换
// Callbable: 有返回值
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
int i =0;
for(;i<100;i++) {
System.out.println(Thread.currentThread().getName() + " "+ i);
}
return i;
});
new Thread(task,"有返回值的线程").start();
try {
System.out.println("子线程的返回值"+task.get());
}catch(Exception e) {
e.printStackTrace();
}
}
}
相比较Runnable接口,Callable接口的功能更加强大:
- 相比较Runnable接口中需要重写的run()方法,Callable接口需要重写的call()方法有返回值
- 该方法可以抛出异常,外边的程序可以利用这个异常获取异常信息
- 支持泛型的返回值
- 需要借助FutureTask类,获取该线程的返回值
Future接口:
- 可以对具体的Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等操作
- FutureTask是Future接口的唯一实现类
- FutureTask同时实现了Runnable、Callable接口,它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
Android多线程
Android多线程本质上也是Java多线程,同时添加了一些不同的特性和使用场景。其中最重要的区别是主线程和子线程的区分,Android中的主线程是UI线程,负责四大组件的运行和与用户交互,不允许耗时操作,否则容易出现ANR现象;子线程负责一些耗时操作,而如果子线程想实现对UI的操作,需要通过Android的handler消息机制。
为什么子线程中不允许对UI进行操作呢?
- 因为Android的UI控件并不是线程安全,多线程的并发访问会带来UI控件的不可预期的状态,且考虑到加锁机制会带来性能上的问题,因此Android在设计初期就禁止子线程处理UI。UI操作时ViewRootImpl会对操作者所在的线程进行checkThread,如果非主线程,会抛出CalledFromWrongThreadException。
Android多线程的几种使用方式
Android的UI主线程主要负责处理用户的按键事件、用户的触屏事件以及屏幕绘图事件等,在子线程中处理耗时的任务,任务完成后通过Handler通知UI主线程更新UI界面
Handler+thread
Android主线程中包含一个消息队列MessageQueue,在消息队列里面可以存入一系列的Message或Runnable对象。通过一个Handler你可以往这个消息队列发送Message或者Runnable对象,并且处理这些对象。每次你新创建一个Handle对象,它会绑定于创建它的线程(也就是UI线程)以及该线程的消息队列,从这时起,这个handler就会开始把Message或Runnable对象传递到消息队列中,并在它们出队列的时候执行它们。
handler使用
Handler可以把一个Message对象或者Runnable对象压入到消息队列中,进而在UI线程中获取Message或者执行Runnable对象,Handler把压入消息队列有两类方式,Post和sendMessage:
- Post方式,把一个ruannable对象入队到消息队列中,方法包括:
- post(Runnable)
- postAtTime(Runnable,long)
- postDelayed(Runnable,long)
- sendMessage方式:允许把一个包含消息数据的Message对象压入到消息队列中。它的方法有:
- sendEmptyMessage(int)
- sendMessage(Message)
- sendMessageAtTime(Message,long)
- sendMessageDelayed(Message,long)
Handler如果使用sendMessage的方式把消息入队到消息队列中,需要传递一个Message对象,而在Handler中,需要重写handleMessage()方法,用于获取工作线程传递过来的消息,此方法运行在UI线程上。
Handler使用的注意事项
- 在子线程使用Handler之前一定要先为子线程创建Looper。创建Looper的方式是直接调用Looper.prepare()方法。前面讲过创建Handler对象时如果没有给它指定Looper,那么它默认会使用当前线程的Looper,而线程默认是没有Looper的。
- 在同一个线程里,Looper.prepare()方法不能被调用两次
- 只有调用了Looper.loop()方法,Handler机制才能正常工作
- Looper.loop()方法一定在调用了Looper.prepare()方法之后调用
- 不要在主线程调用Looper.prepare()方法,Android系统创建主线程的时候就已经调用了Looper.prepare()方法和Looper.loop()方法。
- 当在子线程使用Handler时,如果Handler不在需要发送和处理消息,那么一定要退出子线程的消息轮询。Looper.loop()方法是死循环,如果不主动结束,那么其会一直运行,导致子线程一直运行而不结束。
- 消息值避免定义为0:当调用removeMessages删除消息ID值为0的消息时,会删除runnable,引发postDelayed失效。
- 消息回收:调用removeCallbacksAndMessages(null)将会移除所有消息和回调。
其他注意事项
- 为Handler指定Looper
- Handler除了post()和sendMessage()方法外提供了一系列发送消息的方法
- 使用Message.Obtain()获取Message,在使用中,创建一个Message对象,并不推荐通过硬初始化(new)的方式,而是使用Message.obtain()方法。Android会为Message提供一个缓存池,把使用过的消息缓存起来,方便下次使用。使用Message.obtain()方法获取一个消息时,会先从缓存池获取,如果缓存池没有消息,才会去创建消息,从而优化内存。
- 同一个Message不要发送两次。由于Message都是存放到MessageQuue中存放的,如果一个Message对象已经在MessageQueue中,再次把它存放到MessageQueue时会报错。
HandlerThread
Android中没有对Java中的Thread进行任何封装,而是提供了一个继承自Thread的类HandlerThread类,这个类对Java的Thread做了很多便利的封装。线程开启时也就是run方法运行起来后,线程同时创建一个含有消息队列的looper,并对外提供自己这个对象的get方法,这就是和普通的Thread不一样的地方。可用于多个耗时任务要串行执行。
HandlerThread对象start后可以获得其Looper对象,并且使用这个Looper对象实例Handler,之后Handler就可以运行在其他线程中了。
使用流程:
- 实例对象,参数为线程的名字
HandlerThread handlerThread = new HandlerThread("[handlerName]");
- 启动线程
handlerThread.start();
- 实例主线程的Handler,参数为HandlerThread内部的一个looper。
Handler handler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
注意事项:
要先让子线程start起来,否则创建主线程的handler的参数getLooper()获取不到
AsyncTask
AsyncTask是android提供的轻量级的异步类,可以直接继承AsyncTask,在类中实现异步操作,并提供接口反馈当前异步执行的程度(可以通过接口实现UI进度更新),最后反馈执行的结果给UI主线程。
相对来说AsyncTask更轻量级一些,适用于简单的异步处理,不需要借助线程和Handler即可实现。 但AsyncTask不能完全取代线程,在一些逻辑较为复杂或者需要在后台反复执行的逻辑就可能需要线程来实现了。
AsyncTask是抽象类,定义了三种泛型类型 Params,Progress和Result:
- Params 启动任务执行的输入参数,比如HTTP请求的URL。
- Progress 后台任务执行的百分比。
- Result 后台执行任务最终返回的结果,比如String。
AsyncTask的执行步骤:
- 自定义AsyncTask
- 实现AsyncTask中的一个或几个方法:
- onPreExecute(), 该方法将在执行实际的后台操作前被UI thread调用。可以在该方法中做一些准备工作,如在界面上显示一个进度条。
- doInBackground(Params…), 将在onPreExecute 方法执行后马上执行,该方法运行在后台线程中。这里将主要负责执行那些很耗时的后台计算工作。可以调用 publishProgress方法来更新实时的任务进度。该方法是抽象方法,子类必须实现。
- onProgressUpdate(Progress…),在publishProgress方法被调用后,UI thread将调用这个方法从而在界面上展示任务的进展情况,例如通过一个进度条进行展示。
- onPostExecute(Result), 在doInBackground 执行完成后,onPostExecute 方法将被UI thread调用,后台的计算结果将通过该方法传递到UI thread.
关于AsyncTask的几个准则:
- Task的实例必须在UI thread中创建
- execute方法必须在UI thread中调用
- 不要手动的调用onPreExecute(), onPostExecute(Result),doInBackground(Params…), onProgressUpdate(Progress…)这几个方法
- 该task只能被执行一次,否则多次调用时将会出现异常
- doInBackground方法和onPostExecute的参数必须对应,这两个参数在AsyncTask声明的泛型参数列表中指定,第一个为doInBackground接受的参数,第二个为显示进度的参数,第第三个为doInBackground返回和onPostExecute传入的参数。
- 研究AsyncTask原理
ThreadPoolExecutoer
使用线程池的好处:
- 重用线程池中的线程,避免因为线程的创建和销毁带来的开销
- 有效控制线程池的最大并发数
- 对线程进行简单的管理
构造方法:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory)
FiexedThreadPool
- 线程数量固定的线程池
- 只有核心线程且核心线程不会被回收
- 没有超时机制
- 任务队列大小也没有限制
CachedThreadPool
- 线程数量不固定,只有非核心线程。
- 最大线程数为Integer.MAX_VALUE,即任意大。
- 空闲线程都有超时机制,60s。
- 适合执行大量且耗时较少的任务。
- 整个线程池都处于空闲时,几乎不占用系统资源。
ScheduledThreadPool
- 核心线程数固定,非核心线程数无限制
- 非核心线程闲置时立即回收
- 适用于执行定时任务和具有固定周期的重复任务
SingleThreadPool
- 只有一个核心线程,确保所有任务在同一个线程中按顺序执行
- 统一外界任务到一个线程中,不需要处理线程同步问题
IntentService
- 特殊的Service,继承了Service,是一个抽象类,可用于执行后台的耗时任务,任务执行后会自动停止。
- IntentService用来接收并处理通过Intent传递的异步请求。客户端通过调用startService(Intent)启动一个IntentService,利用一个work线程依次处理顺序过来的请求,处理完成后自动结束Service。
Activity.runOnUiThread(Runnable)
采用runOnUiThread(new Runnable()),这要实现Runnable借口,我们可以直接在这个线程中进行UI的更新。是api提供的方法,较为便捷。
new Thread(){
@Override
public void run() {
final String result = LoginServices.loginByGet(username, password);
if(result != null){
//成功
runOnUiThread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Toast.makeText(MainActivity.this, result, 0).show();
}
});
}else{
//请求失败
runOnUiThread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Toast.makeText(MainActivity.this, "请求失败", 0).show();
}
});
}
};
}.start();
View.post(Runnable)
handler.post®其实这样并不会新起线程,只是执行的runnable里的run()方法,却没有执行start()方法,所以runnable走的还是UI线程。如果像这样,是可以操作ui,但是run还是走在主线程。这就是为什么可以直接在run方法里操作ui,因为它本质还是ui线程。
handler.post(new Runnable(){
public void run(){
Log.e("当前线程:",Thread.currrentThread.getName());//这里打印de结果会是main
setTitle("哈哈");
}
});