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

分享者:技术小黑屋

自我介绍

  • 技术小黑屋(droidyue.com)博主
  • 开发者头条的第二大订阅主题创建者
  • InfoQ 译者
  • 傲游浏览器 Android 端开发人员
  • 为什么叫技术小黑屋

然而这并没有什么卵用

干货才是最重要的

分享概括

  • 什么是工作者线程及存在原因
  • Android中的工作者线程(AsyncTask,HandlerThread)介绍
  • 一点工作者线程优先级优化建议

工作者线程

定义

用来处理后台耗时的任务的线程,对应英文词组 Worker Thread

存在即合理的工作者线程

  • UI单线程模式(其他线程无法更新UI)
  • UI线程任务繁重(组件回调,onCreate等)
  • 若不用工作者线程,耗时任务阻塞主线程,App变卡,ANR等问题出现

拿什么开撕

  • 手撕包菜
  • 手撕鬼子
  • AsyncTask

AsyncTask

Question 1 会引发内存泄露问题么

AsyncTask

Answer 有可能

默认情况下,当屏幕旋转...

  • 当前的Activity被销毁
  • 一个新的Activity被创建

一个常用的AsyncTask使用场景

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

    @Override
    protected Void doInBackground(String... params) {
        //some code
        return null;
    }
}.execute("GDG,不是郭德纲");

然而你懂的

在Java中非静态内部类会隐式持有外部类的引用。

mFuture隐式持有AsyncTask实例引用

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

mFuture提交执行后,被静态变量间接持有

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

        public synchronized void execute(final Runnable r) {
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null) {
                scheduleNext();
            }
        }

        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }

理论分析

  • 静态变量属于类,其存活时间与进程几乎一致。
  • GC基于引用遍历,如果对象被强引用持有,则不会回收,即便稍后可能引起OOM。

所以

当任务处于等待或者正在执行时,加以合适的条件会导致内存泄露。

AsyncTask

Question 2 cancel后的效果

cancel方法

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

cancel(false)

  • 不去打断正在执行的线程
  • doInBackground 依旧会执行
  • 只是不去执行onPostExecute

任务完成后的处理

private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        onPostExecute(result);
    }
    mStatus = Status.FINISHED;
}

cancel(true)

  • 仍然无法保证中断正在执行的线程 因为其使用的是Thread.interrupt 并不会停止线程
  • 对于Thread.sleep,wait等方法会中断,抛出InterruptedException
  • 对于其他情况则可能无法中断

interrupt不能停止线程示例

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

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

        return null;
    }
}


task.execute("hello world");
try {
    Thread.sleep(2000);
    task.cancel(true);
} catch (InterruptedException e) {
    e.printStackTrace();
}

AsyncTask

Question 3 并行还是串行,这是个问题

这得看情况

  • Donut(1.6)之前,串行
  • 从Donut到GINGERBREAD_MR1(2.3.4),并行
  • 从HONEYCOMB(3.0)至今,恢复至串行,但可以设置并行执行

如何控制串行

  • 由串行执行器控制任务的初始分发
  • 另一个并行执行器一次执行单个任务,并启动下一个

串行实现代码

public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;

    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

然而问题来了

  • 挖掘机技术哪家强?
  • 你和AsyncTask有什么仇什么怨?
  • 线程的浪费

线程的浪费

ThreadPoolExecutor线程数量

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;


举个栗子

以一个四核的手机(HTC m8t)为例,CORE_POOL_SIZE值为5,MAXIMUM_POOL_SIZE为9

持续调用AsyncTask

  • 在AsyncTask线程数量小于CORE_POOL_SIZE会启动新的线程,不重用之前空闲的线程
  • 当数量超过CORE_POOL_SIZE,才开始重用之前的线程
  • 然而,由于分发串行,执行器并不能并发执行任务
  • 最多会有CORE_POOL_SIZE数量的线程闲置
  • 而AsyncTask中并不存在allowCoreThreadTimeOut(boolean)的调用
  • Core Thread不会销毁,则一直存在

一个类似的问题 in Executors

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

问题

  • corePoolSize 与 maximumPoolSize 相同 无法做到核心线程数与并发之间的最优平衡

建议

  • 自行构造ThreadPoolExecutor,根据业务和设备信息选取合理的corePoolSize和maximumPoolSize

HandlerThread

HandlerThread是一个自带Looper的Thread
利用Handler API,我们可以在一个特定线程中实现
以post相关方法为例:



  • post方法提交优先级一般的任务
  • postAtFrontOfQueue将优先级较高的任务加入到队列前端
  • postAtTime 指定时间提交任务
  • postDelayed 延后提交优先级较低的任务。

HandlerThread封装示例源码

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);
}

HandlerThread应用场景

利用单一线程 + 任务队列这一特点,处理轻量的耗时(单位毫秒级)操作,如本地IO,即文件读写和数据库操作。

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

一点优化建议

  • 控制工作线程的优先级,确保主线程分配更多的时间片

优先级控制

  • 优先级越高,获得的CPU资源越多
  • Android的线程优先级类似于Linux下进程的nice值
  • 优先级的值从-20到19,-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。

如何具体应用

  • 一般的工作者线程,设置成THREAD_PRIORITY_BACKGROUND
  • 对于优先级很低的线程,可以设置THREAD_PRIORITY_LOWEST
  • 其他特殊需求,可以应用具体的优先级
  • 在run方法中加入android.os.Process.setThreadPriority(priority);即可

尾声

由于时间仓促和个人水平有限,如有问题,请指出

未完。。。

致谢

  • 朱凯
  • 周朝
  • GDG工作人员,志愿者
  • 现场各位

提问,回答

一休

Powered By nodePPT v1.3.2