android异步编程

达芬奇密码2018-06-26 11:50

在android开发中,有两条很重要的编程准则

  • 不要堵塞ui线程
  • 不要在非ui线程操作ui控件

开发者必须这两个遵守单线程模型的准则,将耗时的逻辑转移到非ui线程进行,得出计算结果后,通知ui线程进行数据的展现。本文将介绍一下android的异步编程。

android线程模型

同hotspot vm一样,在daivlk vm中,采取的是1:1线程模型,每一个android thread对应一个Native Linux thread;linux内核通过cfs(completely fair scheduler)来进行线程调度,在cfs中着影响一个线程时间分配的因素有两个:

  • thread priority
  • thread group

thread group

线程的thread group是动态改变的,在android framework层面,android的应用有5个等级,分别是

  • foreground process
  • visible process
  • service process
  • backgroud process
  • empty process

它们的thread group如图,


在实际的分配中,系统会将90%的cpu时间分配给foregroud thread group, 如果某个应用处于foreground或visible level,那么它创建的所有thread都属于foreground group。 当应用的可见状态被改变时,例如按home健被切入到后台运用,应用从foregroud process变成了backgroud process,应用对应的的thread group也切换到了backgroud group

thread priority

线程的优先级通过Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)设置;

线程分类

在一个android应用中,存在三种类型的线程:

  • UIThread,即工作主线程
  • Binder Thread:与其他进程进行binder通信的线程,通过一个线程池进行维护; 每个进程维护了一个线程池用于与其他进程的通信,binder thread隐藏在后台,开发者一般不需要关心;

  • backgroud thread,即后台线程;

注意每一个后台线程都是UIThread的子线程,意味着他的线程优先级和ui thread是完全相同的,同时由于他们处在同一个thread group中,那么linux内核对ui线程和后台线程一视同仁;

dalvik有一个最大线程个数的限制,但不意味着应用可以随意生成低于这个限制值的线程个数;由于linux内核平等的对待ui线程和后台线程,一旦滥用后台,当ui线程不能抢占到足够的cpu时间片时,也会抛出anr异常;另一方面,由于thread是gc mark的root,过多的线程也会对gc造成影响。

Thread&&Executor

Thread和Executor都是jdk原生的异步机制,不再赘述。在使用时,原生的Thread的存在两个缺陷:

  • 不可复用,线程的创建、销毁都需要时间开销以及空间开销
  • 无原生的cancel机制,你需要额外的工作去管理的thread生命周期

而Executor线程池,解决Thread不可复用,减少thread的重复创建。

HandlerThread

HandlerThread是Thread的子类,内部封装了Message/Looper,Message/Looper是一个顺序执行的队列,开发者可以利用这一特性进行异步逻辑之间的组合及交互,因此非常适合做一个状态机:最典型的例子即条码处理库zxing,摄像头扫描过程中每一个状态都通过Message同步给了ui线程;同样使用HandlerThread,另一个使用度非常高的android-async-http则是一个典型的反例,async-http是一个异步网络库,它以callback作为数据协同的方式,导致的结果是代码充斥了不可读的callback嵌套callback。

适用场景

  • 需要一个长期运行的thread
  • 顺序执行的消息loop
    • 避免多个按钮同时点击
    • 状态机
    • 细粒度的消息控制

AsyncTask

AsyncTask是android中最常用的并发机制,api非常简洁,开发者只需简单的继承AsyncTask即可完成异步逻辑以及数据协同, 数据的协同,内部通过Handler/Message进行,异步逻辑doInBackground则利用Executor完成。 必须要了解是,AysncTask是一个全局行为,在不同的组件中创建Asynctask,最终的执行都会在同一个Executor中:

作为最常用的异步编程解决方案,AsyncTask也是被批评最多的异步机制,原因是其在不同android版本中的不同表现, 在1.6版本之前,AsyncTask的Executor是一个单线程,所有AsyncTaskd都是顺序执行; 在1.6到3.0版本中,AsyncTask修改成Executor线程池并发执行 android版本发展到3.0后,默认的并发机制重新修改成了顺序执行,同时提供了一个executeOneExecutor api支持多个并行task, 但这不表示的在3.x版本之后,你的task是默认一定是顺序执行的,它还受taregetSdkVersion的影响,AcitivityThread.java中有这样一段代码

// If the app is Honeycomb MR1 or earlier, switch its AsyncTask
// implementation to use the pool executor.  Normally, we use the
// serialized executor as the default. This has to happen in the
// main thread so the main looper is set right.
if (data.appInfo.targetSdkVersion <= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) {
        AsyncTask.setDefaultExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}

如果targetSDKVersion低于3.2版本,AsyncTask是并行执行的,否则将会被顺序执行,考虑不同版本的表现,在使用AsyncTask时需要规避task之间有依赖执行顺序的逻辑;

另一个经常被批评的点是AsyncTask的全局行为,在组件中使用Asynctask时,经常会将他定义在一个匿名的内部类,这时候一个潜在的内存泄漏就产生了,由于Asynctask的生命周期可能比它关联的组件对象长,导致其关联的组件无法被回收,幸运的是,asynctask提供了cancel机制。在组件生命周期结束后及时调用cancel api,在asynctask未被执行时,可以将asynctask从队列去除;cancel api有一个mayInterruptIfRunning参数:cancel(true) == cancel(false)+interrpt

IntentService

IntentService为Service的子类,其内部实现包含了一个HandlerThread,它兼具了service/message looper/hanlder的优势,android官方文档将其列为后台任务的最佳实践

不同与Service,当一个intent被提交,系统会将intent提交到HandlerThread中顺序执行,而非在ui线程执行; IntentService中工作队列不能被打断,必须等待所有的intent被处理完成之后,intentservcie自动关闭;官方建议通过broadcast机制来进行协同,避免产生耦合;

Loader/AsyncQueryHandler

将Loader和AsyncQueryHandler放在一起,主要是因为他们api适合做为数据加载的接口;Loader内部利用AsyncTask,AsyncQueryHandler使用ThreadHandler 。 Loader机制在api 11中引入,通过support包的方式支持已有版本,两者都支持:

  • 后台加载数据
  • 数据改变时候的回调
  • 生命周期自动管理

延伸阅读

一些现有的解决方案

  • rxandroid 函数式响应框架,rxjava的android版本,主要贡献者是JakeWharton大神,提供了异步逻辑的组合、过滤,不过目前暂不适合在生产环境使用,建议关注

  • android anotation 如名,基于java注解,提供了一个简化的线程模型,提供编程效率

  • EventBus:基于生产/消费者模型的优雅实现

本文来自网易实践者社区,经作者郑文授权发布。