Android中的线程使用

在Android开发中,通常会将耗时的任务放到子线程中执行,以保证UI线程的流畅性。不过,系统要开启一个线程会消耗一定的资源,并且,对于多线程还有额外的工作要处理,比如:线程安全、死锁、内存消耗、对象生命周期管理等。因此,在开发过程中,如果随意开启线程,不但会加重开发任务,还会过多消耗系统资源,达不到令UI线程流畅的效果。

进程优先级

线程是进程的一部分,因此,线程的生命周期直接被进程所影响,而进程的的存活又与其优先级直接相关,所以,在使用线程的时候,通常需要关注进程的优先级。

通常来说,Android系统会系统资源不足的情况下,根据进程优先级杀死进程,优先级越低的进程越容易被杀死。Android系统进程优先级由高到底排列如下:

  • 前台进程(Foreground Process):表明用户正在与该进程进行交互操作。

1.进程持有一个正在与用户交互的Activity(Activity处于Resumed状态)。
2.进程持有一个Service,并且这个Service与一个用户正在交互的Activity进行绑定。
3.进程持有一个前台运行模式的Service(Service调用了startForegroud()方法)。
4.进程持有一个正在执行生命周期方法的Service(Service正在执行onCreate()、onDestroy()等方法)。
5.进程持有一个正在执行onReceive()方法的BroadcastReceiver。

  • 可见进程(Visible Process):表明虽然该进程没有持有任何前台组件,但是它能够影响用户可见的界面。

1.进程持有一个非前台Activity,但这个Activity对用户可见(Activity处于Paused状态)。
2.进程持有一个与可见Activity绑定的Service。

  • 服务进程(Service Process):除了符合前台进程和可见进程条件的Service,其它的Service都会被归类为服务进程。

  • 后台进程(Background Process):持有不可见Activity(Activity处于Stopped状态)的进程。

通常情况下会有很多后台进程,当内存不足的时候,在所有的后台进程里面,会按照LRU(最近使用)规则,优先回收最长时间没有使用过的进程。

  • 空进程(Empty Process):不持有任何活动组件的进程(Activity处于Destroyed状态)。

保持这种进程只有一个目的,就是为了缓存,以便下一次启动该进程中的组件时能够更快响应。当资源紧张的时候,系统会平衡进程缓存和底层的内核缓存情况进行回收。

线程调度

线程优先级

进程的优先级与系统回收资源有关,而线程的优先级与线程调度有关,一般来说,优先级越高的线程能获取到更多的CPU时间片去执行。

但是,在Linux系统中,调度器在分配时间片时,采用的CFS(completely fair scheduler)策略,这种策略不但会参考单个线程的优先级,还会追踪每个线程已经获取到的时间片数量,如果高优先级的线程已经执行了很长时间,但低优先级的线程一直在等待,后续系统会保证低优先级的线程也能获取更多的CPU时间。显然使用这种调度策略的话,优先级高的线程并不一定能在争取时间片上有绝对的优势,所以,Android系统在线程调度上使用了cgroups的概念,cgroups能更好的凸显某些线程的重要性,使得优先级更高的线程明确的获取到更多的时间片。

Android中的线程调度

Android系统将线程分为多个group,其中两类group尤其重要。一类是default group,UI线程属于这一类。另一类是background group,工作线程应该归属到这一类。background group当中所有的线程加起来总共也只能分配到5~10%的时间片,剩下的全部分配给default group,这样设计能保证UI线程的流畅性。

在开启线程时,可以手动将线程归于background group,这样可以保证UI线程的流程性:

1
2
3
4
5
6
new Thread(new Runnable() {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
}
}).start();

因此,在开启新线程时,需要考虑这个线程是否需要和UI线程争夺CPU资源:如果不是,降低线程优先级将其归于background group;如果是,则需要进一步的profile看这个线程是否造成UI线程的卡顿。

虽然,Android系统在任务调度上是以线程为基础单位,设置单个Thread的优先级可以改变其所属的control groups,从而影响CPU时间片的分配,但是,进程的状态的变化也会影响到线程的调度。当一个App进入后台的时候,该App所属的整个进程都将归于background group,以确保处于前台的可见进程能获取到尽可能多的CPU资源。用adb可以查看不同进程的当前调度策略。

1
$ adb shell ps -P

当App重新被用户切换到前台的时候,进程当中所属的线程又会回归到原来的group。在用户频繁切换的过程当中,Thread的优先级并不会发生变化,但系统在时间片的分配上却在不停的调整。

Android中的线程形态

在Android中,不同应用场景需要使用不同形态的线程,接下来介绍各种形态的线程的使用方式。

Thread

  • 优点

使用最简单,适用于一些单一的只需要执行耗时任务的场景。

  • 缺点

1.仅仅启动了一个新线程,没有任务的概念,不能做状态管理,任何开始后,会一直执行完毕后才结束,无法中途取消。
2.如果Runnable匿名内部类持有了外部类的引用,在线程结束前,该引用会一直存在,这样可能会阻碍外部类对象被GC回收,造成内存泄露。
3.没有通信接口,如果要与UI进行交互,需要手动实现消息机制。

  • 使用方式

直接创建线程对象开启线程,代码如下所示:

1
2
3
4
5
6
new Thread(new Runnable() {
@Override
public void run() {

}
}).start();

如果从UI线程启动,则该线程优先级默认为Default,归于default group,会平等的和UI线程争夺CPU资源。因此,在对UI性能要求高的场景下需要手动将其优先级设为Background:

1
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

AsyncTask

  • 优点

1.将线程以任务的方式执行,可以管理任务的状态,比如,可以获取任务执行进度,以及终止任务执行。
2.内部封装了Handler,可以与UI线程直接交互。
3.开启的进程默认被设置为Background优先级,对UI线程的执行影响极小。
4.默认会在一个串行的线程池中执行任务,可以重复使用线程资源,避免了频繁开启和关闭线程带来的资源消耗。

  • 缺点

1.只能对任务做粗略地控制。
2.只能与UI线程进行交互。
3.类似于直接开启Thread,也有隐式的持有外部类对象引用的问题,需要特别注意防止出现意外的内存泄漏。

  • 使用方式

通过继承AsyncTask重写相应的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected void onPreExecute() {
super.onPreExecute();
}

@Override
protected Void doInBackground(Void... voids) {
return null;
}

@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
}

@Override
protected void onProgressUpdate(Void... values) {
super.onProgressUpdate(values);
}
}

上面重写了使用AsyncTask常用的4个方法:

  1. onPreExecute():在主线程中执行,异步任务执行之前调用,可以做一些准备工作;
  2. doInBackground():在线程池中执行,并且线程的优先级会被设置设为Background;
  3. onPostExecute():在主线程中执行,异步任务执行完毕后调用,可以获取任务执行完毕的返回值;
  4. onProgressUpdate():在主线程中执行,用于获取任务执行的进度;

因为AsyncTask在内部使用了Handler(消息机制)与主进程交互, 所以异步任务定义完毕后,必须要在主线程中开启任务。开启任务的方式如下所示:

1
new MyAsyncTask().execute();
  • 注意事项

1.可以调用cancel()终止任务,不过,调用方法后,任务并不一定会被终止,要看方法的返回值来确定任务是否被终止。
2.Android3.0以后,为了避免并发错误,如果有多个AsyncTask任务,默认会在一个串行的线程池中执行这些任务,只有当一个任务执行完毕后才会去执行下一个任务;不过,也可以调用其executeOnExecutor()方法在并行的线程池中执行任务,但是,并行执行任务时需要处理线程安全的问题。
3.AsyncTask默认串行执行所有任务,最好不要创建了大量的AsyncTask任务,或者在AsyncTask任务中执行特别耗时的任务,这样会影响其它任务的执行。
4.不要直接调用onPreExecute()、doInBackground()、onPostExecute()和onProgressUpdate()方法。
5.一个MyAsyncTask对象只能执行一次,即只能调用一次execute()方法,否则会运行时异常。

HandlerThread

  • 优点

1.可以对任务做比较精细的控制,适用于线程切换比较频繁的场景。
2.在Thread中封装了消息机制,其它线程都可以通过Handler与其进行交互。
3.类似于AsyncTask,串行执行多个任务,不存在线程安全的问题。
4.每个HandlerThread都有自己的消息队列,在一个HandlerThread中执行多个任务或比较耗时的任务,不会影响其它HandlerThread,这点与AsyncTask不同。

  • 缺点

使用起来比AsyncTask复杂,需要编写更多代码。

  • 使用方式

以下是使用HandlerThread的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class MyHandlerThread<Token> extends HandlerThread {

private static final String TAG = "MyHandlerThread";
private static final int MESSAGE_DOWNLOAD = 0;

private Handler mHandler;
private Handler mResponseHandler;
private Listener<Token> mListener;

public interface Listener<Token> {
void onRequestHandled(Token token);
}

public void setListener(Listener<Token> listener) {
mListener = listener;
}

public MyHandlerThread(Handler responseHandler) {
super(TAG);
mResponseHandler = responseHandler;
}

/**
* HandlerThread.onLooperPrepared()方法的调用发生在Looper.loop()方法之前,
* 因此,在该方法中创建Handler,并执行相关任务。
*/
@SuppressLint("HandlerLeak")
@Override
protected void onLooperPrepared() {
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == MESSAGE_DOWNLOAD) {
Log.i(TAG, "Got a Message");
@SuppressWarnings("unchecked")
Token token = (Token) msg.obj;
handleRequest(token);
}
}
};
}

/**
* 其它线程给该线程发送消息的接口。
*/
public void sendRequest(Token token) {
Log.i(TAG, "Got a Request");
mHandler.obtainMessage(MESSAGE_DOWNLOAD, token).sendToTarget();
}

/**
* 处理其它线程发送的消息。
*/
private void handleRequest(final Token token) {
Log.i(TAG, "Handle Request");
// 因为mResponseHandler与主线程的Looper关联,所以UI更新代码是在主线程中完成的。
mResponseHandler.post(new Runnable() {
@Override
public void run() {
// 回调更新UI
mListener.onRequestHandled(token);
}
});
}

/**
* 如果需要更新的View已经销毁,清空消息队列。
*/
public void clearQueue() {
mHandler.removeMessages(MESSAGE_DOWNLOAD);
}

}
  • 注意事项

1.HandlerThread默认被设置为Default优先级。
2.HandlerThread使用了消息机制,会一直循环处理消息,不会自动停止,如果不再使用HandlerThread时,可以通过quit()或则quitSafely()方法来终止线程的执行,避免资源消耗。

IntentService

  • 优点

1.在Service组件中封装了HandlerThread,优先级(所属进程的优先级)比一般后台线程要高,不容易被系统杀死,可以用来执行优先级比较高的后台任务。
2.类似于HandlerThread,如果IntentService被开启了多次,会串行执行这些任务,不过,所有任务都执行完毕后,IntentService会自动停止。

  • 使用方式

以下是使用IntentService的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyIntentService extends IntentService {

private static final String TAG = "MyIntentService";

public MyIntentService() {
super(TAG);
}

@Override
protected void onHandleIntent(@Nullable Intent intent) {
// 处理任务
Log.i(TAG, "Handle Task.");
}

@Override
public void onDestroy() {
Log.i(TAG, "Service Destroyed.");
super.onDestroy();
}

}
  • 注意事项

停止IntentService一般使用stopSelfResult(int startId)和stopSelf()方法。其中stopSelfResult(int startId)相对比较安全,如果还有其它任务还未执行,那么会等待其它任务执行完毕后才停止服务;而stopSelf()直接停止当前服务,不管是否还有任务需要执行。

Android中的线程池

前面介绍线程的形态时,如果有多个线程,一般都是串行执行的。串行执行虽然没有线程同步的问题,但是,如果有大量的耗时任务需要执行,串行执行不是一个好的选择,这时,就需要并发的执行多个线程。在Android中,如果要并发执行线程,需要用的线程池,一般来说,线程池有以下优点:

  • 重用线程池中的线程,避免了频繁创建和销毁线程带来的性能开销。
  • 能够控制线程的最大并发数,避免了大量线程因相互抢占系统资源而导致的阻塞现象。
  • 能对线程进行简单的管理,还能提供定制执行、或者间隔循环执行线程等功能。

不过,要特别注意处理线程安全的问题,因为多线程并发运行导致的Bug往往是偶现的,不方便调试。

ThreadPoolExecutor

Android中的线程池来源于JDK,使用Executor表示一个线程池,它是一个接口,线程池的实现类是ThreadPoolExecutor。这个实现类提供了一系列参数来配置线程池,下面是ThreadPoolExecutor的构造方法:

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
  • corePoolSize
    线程池的核心线程数。默认情况下,核心线程会在线程池中一直存活,即使处于闲置状态。如果将线程池的allowsCoreThreadTimeOut属性设置为true,那么闲置的核心线程在等待新任务到来时会有超时策略,这个时间由keepAliveTime决定,如果超时,核心线程也会被终止。

  • maximumPoolSize
    线程池所能容纳的最大线程数,当活动线程数达到这个数值后,后续的新任务将会被阻塞。

  • keepAliveTime
    非核心线程闲置的超时时间,如果超过这个时间,非核心线程就会被回收。如果将线程池的allowsCoreThreadTimeOut属性设置为true,这个超时也会作用于核心线程。

  • unit
    用于指定keepAliveTime的时间单位,是一个枚举量。

  • workQueue
    线程池中的任务队列,通过线程池的execute()方法提交的Runnable对象会被存储在这个队列中。

  • threadFactory
    线程工厂,为线程池提供创建新线程的能够。它是一个接口,只有Thread newThread(Runnable r)一个方法。

  • RejectedExecutionHandler
    这个参数不常用。当线程池无法执行新任务时(可能是任务队列已满,或者无法成功执行任务),线程池会调用参数的rejectedExecution()方法通知调用者,默认抛出RejectedExecutionException。

ThreadPoolExecutor执行任务一般遵循以下规则:

  1. 如果线程池中的线程数量未达到核心线程的数量,直接启动一个核心线程来执行任务。
  2. 如果线程池中的线程数量已经到达或超过核心线程的数量,那么任务会被插到任务队列中等待执行。
  3. 如果步骤2中的任务队列已满,但是线程数量没有达到线程池能容纳的最大线程数,那么会立刻启动一个非核心线程来执行任务。
  4. 如果步骤3中线程数量已达到线程池能容纳的最大线程数,那么拒绝执行此任务,调用rejectedExecution()方法通知调用者。

常用的线程池

如果直接使用ThreadPoolExecutor创建一个线程池会比较繁琐,可以使用Executors工厂类直接创建要使用的线程池实例,去实现特定功能的线程池。接下来介绍Android中常用的几种线程池。

  • FixedThreadPool

通过Executors创建FixedThreadPool的源代码如下所示:

1
2
3
4
5
6
7
8
9
public class Executors {
...
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
...
}

它是一种线程数量固定的线程池,当其中的线程处于空闲状态时,也不会被回收,除非线程池被关闭。

由于FixedThreadPool只有核心线程并且这些核心线程都不会被回收,所以,它能够快速响应外界的请求。

  • CachedThreadPool

通过Executors创建CachedThreadPool的源代码如下所示:

1
2
3
4
5
6
7
8
9
public class Executors {
...
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
...
}

它是一种线程数量不确定的线程池,并且只有非核心线程。当线程池中所有的线程都处于活动状态时,就立刻创建新线程处理新任务,否则,就会使用闲置的线程来处理新任务。如果某个线程空闲时间超过60s,这个闲置线程会被回收。因此,该线程池的任务队列相当于是一个空集合。

从CachedThreadPool的特性来看,它比较适合执行大量的、耗时较少的任务。

  • ScheduledThreadPool

通过Executors创建ScheduledThreadPool的源代码如下所示:

1
2
3
4
5
6
7
public class Executors {
...
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
...
}
1
2
3
4
5
6
7
8
9
10
11
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
...
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
...
}

它的核心线程数量是固定的,而非核心线程数量没有限制。当非核心线程闲置时,会被回收。

ScheduledThreadPool主要用于执行定时任务,或者具有固定周期的重复任务。

  • SingleThreadExecutor

通过Executors创建SingleThreadExecutor的源代码如下所示:

1
2
3
4
5
6
7
8
9
10
public class Executors {
...
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
...
}

它的线程池内只有一个核心线程,能确保所有的任务都在同一个线程中按顺序执行。

SingleThreadExecutor用于串行执行所有的任务,让这些任务之间不用考虑线程同步的问题。

坚持原创技术分享,您的支持将鼓励我继续创作!