译文 | Android 开发中利用异步来优化运行速度和性能

我们知道,在Android框架中提供了很多异步处理的工具类。然而,他们中大部分实现是通过提供单一的后台线程来处理任务队列的。如果我们需要更多的后台线程的时候该怎么办呢?

大家都知道Android的UI更新是在UI线程中进行的(也称之为主线程)。所以如果我们在UI线程中编写耗时任务都可能会阻塞UI线程更新UI。为了避免这种情况我们可以使用 AsyncTask, IntentService和Threads。在之前我写的一篇文章介绍了Android 中异步处理的8种方法(https://medium.com/android-news/8-ways-to-do-asynchronous-processing-in-android-and-counting-f634dc6fae4e#.bkk6mudb4)。但是,Android提供的AsyncTasks(http://developer.android.com/reference/android/os/AsyncTask.html)和IntentService(http://developer.android.com/reference/android/os/AsyncTask.html)都是利用单一的后台线程来处理异步任务的。那么,开发人员如何创建多个后台线程呢?

更新: Marco Kotz (https://medium.com/u/b49242be2be7)指出结合使用ThreadPool Executor和AsyncTask,后台可以有多个线程(默认为5个)同时处理AsyncTask。

创建多线程常用的方法

在大多数使用场景下,我们没有必要产生多个后台线程,简单的创建AsyncTasks或者使用基于任务队列的IntentService就可以很好的满足我们对异步处理的需求。然而当我们真的需要多个后台线程的时候,我们常常会使用下面的代码简单的创建多个线程。

String[] urls = …    
for (final String url : urls) {        
new Thread(new Runnable() {            
public void run() {                
// 调用API、下载数据或图片             
}         
}).start();

   }

该方法有几个问题。一方面,操作系统限制了同一域下连接数(限制为4)。这意味着,你的代码并没有真的按照你的意愿执行。新建的线程如果超过数量限制则需要等待旧线程执行完毕。 另外,每一个线程都被创建来执行一个任务,然后销毁。这些线程也没有被重用。

常用方法存在的问题

举个例子,如果你想开发一个连拍应用能在1秒钟连拍10张图片(或者更多)。应用该具备如下的子任务:

  • 在一秒的时间内扑捉10张以byte[]形式储存的照片,并且不能够阻塞UI线程。
  • 将byte[]储存的数据格式从YUV转换成RGB。
  • 使用转换后的数据创建Bitmap。
  • 变换Bitmap的方向。
  • 生成缩略图大小的Bitmap。
  • 将全尺寸的Bitmap以Jpeg压缩文件的格式写入磁盘中。
  • 使用上传队列将图片保存到服务器中。

很明显,如果你将太多的子任务放在UI线程中,你的应用在性能上的表现将不会太好。在这种情况下,唯一的解决方案就是先将相机预览的数据缓存起来,当UI线程闲置的时候再来利用缓存的数据执行剩下的任务。

另外一个可选的解决方案是创建一个长时间在后台运行的HandlerThread,它能够接受相机预览的数据,并处理完剩下的全部任务。当然这种做法的性能会好些,但是如果用户想再连拍的话,将会面临较大的延迟,因为他需要等待HandlerThread处理完前一次连拍。

  public class CameraHandlerThread extends HandlerThread            
 implements Camera.PictureCallback, Camera.PreviewCallback {        
private static String TAG = "CameraHandlerThread";       
private static final int WHAT_PROCESS_IMAGE = 0;         
Handler mHandler = null;         
WeakReference<camerapreviewfragment> ref = null;       
 private PictureUploadHandlerThread mPictureUploadThread;        
private boolean mBurst = false;        
private int mCounter = 1;          
CameraHandlerThread(CameraPreviewFragment cameraPreview) {            
super(TAG);             
start();             
mHandler = new Handler(getLooper(), new Handler.Callback() {               
 @Override                 
public boolean handleMessage(Message msg) {                    
if (msg.what == WHAT_PROCESS_IMAGE) {                        
// 业务逻辑                     
}                   
 return true;                
 }            
 });            
 ref = new WeakReference<>(cameraPreview);         
}       
 ...        
@Override         
public void onPreviewFrame(byte[] data, Camera camera) {            
if (mBurst) {                 
CameraPreviewFragment f = ref.get();                
if (f != null) {                     
mHandler.obtainMessage(WHAT_PROCESS_IMAGE, data)                    
.sendToTarget();                    
try {                         
sleep(100);                     
} catch (InterruptedException e) {                         
e.printStackTrace();                     
}                    
if (f.isAdded()) {                         
f.readyForPicture();                    
 }                 
}                
if (mCounter++ == 10) {                     
mBurst = false;                     
mCounter = 1;     
   }             
}        
 }     
}

提醒: 如果你需要学习更多有关于HandlerThreads内容以及如何使用它,请阅读我发表的关于HandlerThreads的文章(https://medium.com/@ali.muzaffar/handlerthreads-and-why-you-should-be-using-them-in-your-android-apps-dc8bf1540341#.co4ilm67m)。

看起来所有的任务都被后台的单一线程处理完毕了,我们性能提升主要得益于后台线程长期运行并不会被销毁和重建。然而,我们后台的单一线程却要和其他优先等级更高的任务共享,而且这些任务只能够顺序执行。

我们也可以创建第二个HandlerThread来处理我们的图像,然后创建第三个HandlerThread来将照片写入磁盘,最后再创建第四个HandlerThread来将照片上传到服务器中。我们能够加快拍照的速度,但是,这些线程相互之间还是遵循顺序执行的规则,并不是真的并发。因为每张照片是顺序处理的,而且处理每一张照片需要一定的时间,导致用户在点击拍照按钮到显示全部缩略图的时候仍然能够明显的感觉到延迟。

使用ThreadPool并发处理任务

我们可以根据需求创建多个线程,但是创建过多的线程会消耗CPU周期影响性能,并且线程的创建和销毁也需要时间成本。所以我们不想创建多余的线程,但是又想能够充分的利用设备的硬件资源。这个时候我们可以使用ThreadPool。

通过创建ThreadPool对象的单例来在你的应用中使用ThreadPool。

 public class BitmapThreadPool {        
private static BitmapThreadPool mInstance;       
private ThreadPoolExecutor mThreadPoolExec;        
private static int MAX_POOL_SIZE;        
private static final int KEEP_ALIVE = 10;         
BlockingQueue<runnable> workQueue = new LinkedBlockingQueue<>();        
public static synchronized void post(Runnable runnable) {            
if (mInstance == null) {                 
mInstance = new BitmapThreadPool();            
 }            
 mInstance.mThreadPoolExec.execute(runnable);        
 }       
 private BitmapThreadPool() {           
 int coreNum = Runtime.getRuntime().availableProcessors();             
MAX_POOL_SIZE = coreNum * 2;             
mThreadPoolExec = new ThreadPoolExecutor(                     
coreNum,                     
MAX_POOL_SIZE,                    
 KEEP_ALIVE,                     
TimeUnit.SECONDS,                     
workQueue);        
 }        
public static void finish() {             
mInstance.mThreadPoolExec.shutdown()
;         }    
 }

然后,在上面的代码中,简单的修改Handler的回调函数为:

mHandler = new Handler(getLooper(), new Handler.Callback() {        
@Override         
public boolean handleMessage(Message msg) {           
 if (msg.what == WHAT_PROCESS_IMAGE) {                 
BitmapThreadPool.post(new Runnable() {                    
@Override                    
 public void run() {                       
 // 做你想做的任何事情                     
}                
 });            
 }            
return true; 
}     
});

优化已经完成!通过下面的视频,我们观察到加载缩略图的速度提升是非常明显的。

这种做法的优点是我们可以定义线程池的大小并且指定空余线程保持活动的时间。我们也可以创建多个ThreadPools来处理多个任务或者使用单个ThreadPool来处理多个任务。但是在使用完后记得清理资源。

我们甚至可以为每一个功能创建一个独立的ThreadPool。譬如说在这个例子中我们可以创建三个ThreadPool,第一个ThreadPool负责数据转换成Bitmap,第二个ThreadPool负责写数据到磁盘中去,第三个ThreadPool上传Bitmap到服务器中去。这样做的话,如果我们的ThreadPool最大拥有4条线程,那么我们就能够同时的转换,写入,上传四张相片。用户将看到4张缩略图是同时显示而不是一个个的显示出来的。

上面这个简单例子代码可以在我的GitHub(https://github.com/alphamu/ThreadPoolWithCameraPreview)上得到,欢迎看完代码后给我反馈

另外,你也可以在Google Play(https://play.google.com/store/apps/details?id=au.com.alphamu.camerapreviewcaptureimage)上面下载演示应用。

使用ThreadPool前: 如果可以,从顶部观察计数器的变化来得知当底部缩略图从开始显示到全部显示完成所耗费的时间。在程序中除了adapter中的notifyDataSetChanged()方法外,我已经将大部分的操作从主线程中剥离,所以计数器的运行是很流畅的。(视频链接(要翻墙))(https://www.youtube.com/embed/YmU8ogom_5g?

wmode=opaque&widget_referrer=https://medium.com/media/6a9266d6d49e3e234f9d60f5763602df?maxWidth=640&enablejsapi=1&origin=https://cdn.embedly.com)

使用ThreadPool后: 通过顶部的计数器,我们发现使用了ThreadPool后,照片的缩略图加载速度明显变快。(视频链接(要翻墙))(https://www.youtube.com/embed/77Lh9XpXArw?wmode=opaque&widget_referrer=https://medium.com/media/53c35a233037c20ad1c4f2cba7528580?maxWidth=640&enablejsapi=1&origin=https://cdn.embedly.com)

原文发布于微信公众号 - 人工智能LeadAI(atleadai)

原文发表时间:2017-10-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Timhbw博客

Hexo-完全免费全平台搭建个人博客(2)-域名主题设置

2017-03-1011:01:58 发表评论 913℃热度 Hexo-完全免费全平台搭建个人博客(1)-整体搭建 上一篇文章把 Hexo 博客整体搭建一遍了...

455120
来自专栏SDNLAB

Netvirt之流表分析(一):Netvirt介绍

1. 架构 最近在看ODL的netvirt项目,netvirt是一个完整的网络虚拟机化解决方案,几乎可以实现neutron的所有功能,包括FWaaS,VPNaa...

38370
来自专栏极客猴

一道关于 TCP 连接的题目

小陈点了点头表示很熟悉,然后一口气将 TCP 连接中三次握手和四次分手详细地说了一遍。心想暗笑,这问题难不倒我的,哈哈。

9310
来自专栏小麦苗的DB宝专栏

在Oracle中,如何定时删除归档日志文件?

1、在Oracle用户下,创建归档日志删除文件del_OCPLHR1_arch.sh

31110
来自专栏landv

烽火2640路由器命令行手册-11-IP语音配置命令

(1)       用户输入的ID若在dialpeer表中已存在,且模式匹配,则进入dialpeer配置模式对相应dialpeer进行配置修改;

17230
来自专栏后端技术探索

nginx防止DDOS攻击配置(二)

我们用的高防服务器只防流量攻击不防CC,现在的攻击多数都是混合型的,而且CC攻击很多,防CC只能自己搞了,按照第一篇的配置,在实际的使用中效果并不理想。限制每秒...

67920
来自专栏SDNLAB

OpenFlow网络中的路由服务

这里,所谓OpenFlow网络指的是相互连接的一组OpenFlow交换机的集合,并且这些交换机全部置于一个OpenFlow Controller或一个OpenF...

46780
来自专栏非典型技术宅

Swift实践:使用CoreData完成上班签到小工具1. CoreData Stack的作用2.创建 CoreData Stack3. 一对多的关系4. 完成Demo,了解使用CoreData St

13230
来自专栏服务器安全

DDOS攻击攻击种类和原理

不过这3种攻击方法最厉害的还是DDoS,那个DRDoS攻击虽然是新近出的一种攻击方法,但它只是DDoS攻击的变形,它的唯一不同就是不用占领大量的“肉鸡”。这三种...

55700
来自专栏猿湿Xoong

咦,Oreo怎么收不到广播了?

32940

扫码关注云+社区

领取腾讯云代金券