【1】关于Android中工作者线程的思考

本文系技术小黑屋 2015 北京 GDG Devfest分享内容整理。

一、Why

在Android中,我们或多或少使用了工作者线程,比如Thread,AsyncTask,HandlerThread,甚至是自己创建的线程池,使用工作者线程我们可以将耗时的操作从主线程中移走。首先思考以下几个问题?

  1. 在Android系统中为什么存在工作者线程呢?
  2. 常用的工作者线程有哪些不易察觉的问题呢?
  3. 关于工作者线程有哪些优化的方面呢?

本文将一一解答这些问题。

二、Android 的 UI 单线程模型

工作者线程的存在原因:

  1. 因为Android的UI单线程模型,所有的UI相关的操作都需要在主线程(UI线程)执行
  2. Android中各大组件的生命周期回调都是位于主线程中,使得主线程的职责更重
  3. 如果不使用工作者线程为主线程分担耗时的任务,会造成应用卡顿,严重时可能出现ANR(Application Not Responding), 即程序未响应。

关于 ANR 默认情况下,在android 中 Activity 的最长执行时间是5秒,BroadcastReceiver的最长执行时间则是10秒。

在开发Android 应用时必须遵守单线程模型的原则:

  1. 不要阻塞UI线程
  2. 确保只在UI线程中访问Android UI工具包

因而,在Android中使用工作者线程显得势在必行,如一开始提到那样,在Android中工作者线程有很多,接下来我们将围绕 AsyncTask,HandlerThread 等深入研究。

三、AsyncTask异步任务

AsyncTask 是 Android 框架提供给开发者的一个辅助抽象类,使用该类我们可以轻松的处理异步线程与主线程的交互,由于其便捷性,在Android工程中,AsyncTask 被广泛使用。

定义如下:

public abstract class AsyncTask<Params, Progress, Result> {

}

然而 AsyncTask 并非一个完美的方案,使用它往往会存在一些问题。接下来将逐一列举 AsyncTask 不容易被开发者察觉的问题。

3.1、AsyncTask 与 内存泄露

内存泄露是Android开发中常见的问题,只要开发者稍有不慎就有可能导致程序产生内存泄露,严重时甚至可能导致OOM(OutOfMemory,即内存溢出错误)。AsyncTask 也不例外,也有可能造成内存泄露。

以一个简单的场景为例:

在Activity中,通常我们这样使用AsyncTask:

//In Activity
new AsyncTask<String, Void, Void>() {

    @Override
    protected Void doInBackground(String... params) {
        //some code
        return null;
    }
}.execute("hello world");

上述代码使用的匿名内存类创建AsyncTask实例,然而在Java中,非静态内存类会隐式持有外部类的实例引用,上面例子AsyncTask创建于Activity中,因而会隐式持有Activity的实例引用。

而在AsyncTask内部实现中, mFuture 同样使用匿名内部类创建对象,而mFuture 会作为执行任务加入到任务执行器中。

private final WorkerRunnable<Params, Result> mWorker;
public AsyncTask() {
    mFuture = new FutureTask<Result>(mWorker) {
        @Override
        protected void done() {
            //some code
        }
    };
}

而mFuture加入任务执行器,实际上是放入了一个静态成员变量SERIAL_EXECUTOR指向的对象SerialExecutor的一个ArrayDeque类型的集合中。

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();

    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                //fake code
                r.run();
            }
        });
    }
}

当任务处于排队状态,则Activity实例引用被静态常量SERIAL_EXECUTOR 间接持有。

在通常情况下,当设备发生屏幕旋转事件,当前的Activity被销毁,新的Activity被创建,以此完成对布局的重新加载。

而本例中,当屏幕旋转时,处于排队的AsyncTask由于其对Activity实例的引用关系,导致这个Activity不能被销毁,其对应的内存不能被GC回收,因而就出现了内存泄露问题。

关于如何避免内存泄露,我们可以使用静态内部类 + 弱引用的形式解决。

3.2、AsyncTask.canncel() 的问题

AsyncTask作为任务,是支持调用者取消任务的,即允许我们使用AsyncTask.canncel()方法取消提交的任务。然而其实cancel并非真正的起作用。

首先,我们看一下cancel方法:

// mayInterruptIfRunning <tt>true</tt> if the thread executing this, task should be interrupted; otherwise, in-progress tasks are allowed to complete.

public final boolean cancel(boolean mayInterruptIfRunning) {
    mCancelled.set(true);
    return mFuture.cancel(mayInterruptIfRunning);
}

cancel 方法接受一个boolean 类型的参数,名称为mayInterruptIfRunning,意思是是否可以打断正在执行的任务。

当我们调用cancel(false),不打断正在执行的任务,对应的结果是:

  • 处于doInBackground中的任务不受影响,继续执行
  • 任务结束时不会去调用onPostExecute方法,而是执行onCancelled方法

当我们调用cancel(true),表示打断正在执行的任务,会出现如下情况:

  • 如果doInBackground方法处于阻塞状态,如调用Thread.sleep,wait等方法,则会抛出InterruptedException。
  • 对于某些情况下,有可能无法打断正在执行的任务。

如下,就是一个cancel方法无法打断正在执行的任务的例子:

AsyncTask<String,Void,Void> task = new AsyncTask<String, Void, Void>() {

    @Override
    protected Void doInBackground(String... params) {
        boolean loop = true;
        while(loop) {
            Log.i(LOGTAG, "doInBackground after interrupting the loop");
        }
        return null;
    }
}


task.execute("hello world");
try {
    Thread.sleep(2000);//确保AsyncTask任务执行
    task.cancel(true);
} catch (InterruptedException e) {
    e.printStackTrace();
}

上面的例子,如果想要使cancel正常工作需要在循环中,需要在循环条件里面同时检测 isCancelled() 才可以。

//  After invoking this method, you should check the value returned by {@link #isCancelled()} periodically from {@link #doInBackground(Object[])} to finish the task as early as possible.

// java.util.concurrent.atomic.AtomicBoolean  保证 mCancelled 只被初始化一次  
private final AtomicBoolean mCancelled = new AtomicBoolean();

public final boolean isCancelled() {
        return mCancelled.get();
    }

3.3、AsyncTask 串行带来的问题

Android团队关于AsyncTask执行策略进行了多次修改,修改大致如下:

  1. 自最初引入到Donut(1.6)之前,任务串行执行。
  2. 从Donut到GINGERBREAD_MR1(2.3.4),任务被修改成了并行执行。
  3. 从HONEYCOMB(3.0)至今,任务恢复至串行,但可以设置executeOnExecutor() 实现并行执行。

然而AsyncTask的串行实际执行起来是这样的逻辑:

  1. 由串行执行器控制任务的初始分发;
  2. 并行执行器一次执行单个任务,并启动下一个;

3.4、AsyncTask 中线程利用率的问题

在AsyncTask中,并发执行器实际为 ThreadPoolExecutor 的实例,其CORE_POOL_SIZE为当前设备CPU数量+1,MAXIMUM_POOL_SIZE值为CPU数量的2倍 + 1。

ThreadPoolExecutor 和 SERIAL_EXECUTOR 初始化如下:

/*** An {@link Executor} that can be used to execute tasks in parallel.  【用于并行执行任务】*/ 
    public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

    /**An {@link Executor} that executes tasks one at a time in serial 【用于默认串行执行任务】
     * order.  This serialization is global to a particular process. */
    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
     private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

//SERIAL_EXECUTOR本质是在THREAD_POOL_EXECUTOR的基础上添加一个mTasks的集合来保证任务的顺序执行。
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();  //cpu数目
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;//核心线程数为 cpu数目 + 1
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; // 最大并发数为  cpu数目 * 2 + 1
    private static final int KEEP_ALIVE = 1;

线程池创建对象时各参数解释:

  1. corePoolSize: 核心线程数,会一直存活,即使没有任务,线程池也会维护线程的最少数量;
  2. maximumPoolSize: 线程池维护线程的最大数量;
  3. keepAliveTime: 线程池维护线程所允许的空闲时间,当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0;
  4. unit: 线程池维护线程所允许的空闲时间的单位、可选参数值为:TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS;
  5. workQueue: 线程池所使用的缓冲队列,常用的是:java.util.concurrent.ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue;

以一个四核手机为例,当我们持续调用AsyncTask任务过程中:

  1. 在AsyncTask线程数量小于 CORE_POOL_SIZE(5个)时,会启动新的线程处理任务,不重用之前空闲的线程
  2. 当数量超过CORE_POOL_SIZE(5个),才开始重用之前的线程处理任务

但是由于AsyncTask 属于默认线性执行任务,导致并发执行器总是处于某一个线程工作的状态,因而造成了 ThreadPool 中其他线程的浪费。同时由于 AsyncTask中 并不存在allowCoreThreadTimeOut(boolean)的调用,所以 ThreadPool 中的核心线程即使处于空闲状态也不会销毁掉。

四、Executors

Executors是Java API中一个快速创建线程池的工具类,然而在它里面也是存在问题的。

以Executors中获取一个固定大小的线程池方法为例:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,0L, 
        TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}

在上面代码实现中,CORE_POOL_SIZE 和 MAXIMUM_POOL_SIZE都是同样的值,如果把nThreads当成核心线程数,则无法保证最大并发,而如果当做最大并发线程数,则会造成线程的浪费。因而Executors这样的API 导致了我们无法在最大并发数和线程节省上做到平衡。

为了达到最大并发数和线程节省的平衡,建议自行创建ThreadPoolExecutor,根据业务和设备信息确定 CORE_POOL_SIZE 和MAXIMUM_POOL_SIZE 的合理值。

五、HandlerThread

HandlerThread 是 Android 中提供特殊的线程类,使用这个类我们可以轻松创建一个带有 Looper 的线程,同时利用Looper我们可以结合Handler实现任务的控制与调度。以Handler的post方法为例,我们可以封装一个轻量级的任务处理器

private Handler mHandler;
private LightTaskManager() {
    HandlerThread workerThread = new HandlerThread("LightTaskThread");
    workerThread.start();
    mHandler = new Handler(workerThread.getLooper());
}

public void post(Runnable run) {
    mHandler.post(run);
}

public void postAtFrontOfQueue(Runnable runnable) {
    mHandler.postAtFrontOfQueue(runnable);
}

public void postDelayed(Runnable runnable, long delay) {
    mHandler.postDelayed(runnable, delay);
}

public void postAtTime(Runnable runnable, long time) {
    mHandler.postAtTime(runnable, time);
}

在本例中,我们可以按照如下规则提交任务:

  1. post 提交优先级一般的任务
  2. postAtFrontOfQueue 将优先级较高的任务加入到队列前端
  3. postAtTime 指定时间提交任务
  4. postDelayed 延后提交优先级较低的任务

上面的轻量级任务处理器利用 HandlerThread 的单一线程 + 任务队列的形式,可以处理类似本地IO(文件或数据库读取)的轻量级任务。在具体的处理场景下,可以参考如下做法:

  1. 对于本地IO读取,并显示到界面,建议使用postAtFrontOfQueue
  2. 对于本地IO写入,不需要通知界面,建议使用postDelayed
  3. 一般操作,可以使用post

六、线程优先级调整

在Android应用中,将耗时任务放入异步线程是一个不错的选择,那么为异步线程调整应有的优先级则是一件锦上添花的事情。众所周知,线程的并行通过CPU的时间片切换实现,对线程优先级调整,最主要的策略就是降低异步线程的优先级,从而使得主线程获得更多的CPU资源。

Android中的线程优先级和Linux系统进程优先级有些类似,其值都是从-20至19。其中Android中,开发者可以控制的优先级有:

  • THREAD_PRIORITY_DEFAULT,默认的线程优先级,值为0
  • THREAD_PRIORITY_LOWEST,最低的线程级别,值为19
  • THREAD_PRIORITY_BACKGROUND 后台线程建议设置这个优先级,值为10
  • THREAD_PRIORITY_MORE_FAVORABLE 相对
  • THREAD_PRIORITY_DEFAULT稍微优先,值为-1
  • THREAD_PRIORITY_LESS_FAVORABLE 相对
  • THREAD_PRIORITY_DEFAULT稍微落后一些,值为1

为线程设置优先级也比较简单,通用的做法是在run方法体的开始部分加入下列代码:

android.os.Process.setThreadPriority(priority);

通常设置优先级的规则如下:

一般的工作者线程,设置成THREAD_PRIORITY_BACKGROUND
对于优先级很低的线程,可以设置THREAD_PRIORITY_LOWEST
其他特殊需求,视业务应用具体的优先级。

七、总结

在Android中工作者线程如此普遍,然而潜在的问题也不可避免,建议在开发者使用工作者线程时,从工作者线程的数量和优先级等方面进行审视,做到较为合理的使用。

致谢:
(1)、http://www.infoq.com/cn/articles/android-worker-thread
(2)、进程和线程
(3)、Android 单线程模型
(4)、译文:Android中糟糕的AsyncTask
(5)、Android AsyncTask两种线程池分析和总结
(6)、聊聊并发(三)——JAVA线程池的分析和使用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值