译文 | 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 条评论
登录 后参与评论

相关文章

来自专栏『不羁阁』行走的少年专栏

iOS多线程:『GCD』详尽总结

1176
来自专栏freesan44

主线程中也不绝对安全的 UI 操作

从最初开始学习 iOS 的时候,我们就被告知 UI 操作一定要放在主线程进行。这是因为 UIKit 的方法不是线程安全的,保证线程安全需要极大的开销。那么问题来...

611
来自专栏腾讯Bugly的专栏

《ios爆内存问题解决方案-OOMDetector组件》

组件介绍 在iOS App中,有两种闪退是让人深恶痛绝的,一种是异常退出,另外一种是爆内存杀进程。前者已经有完备的工具协助定位分析,而后者却一直是业界的难以治愈...

4854
来自专栏木宛城主

Unity应用架构设计(10)——绕不开的协程和多线程(Part 2)

在上一回合谈到,客户端应用程序的所有操作都在主线程上进行,所以一些比较耗时的操作可以在异步线程上去进行,充分利用CPU的性能来达到程序的最佳性能。对于Unit...

33711
来自专栏wOw的Android小站

[Objective-C]多线程和GCD

是指在系统中正在运行的一个应用程序。 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。 比如同时打开QQ、Xcode,系统就会分别启动两个进程...

801
来自专栏架构师之路

一分钟掌握数据库垂直拆分

一、缘起 当数据库的数据量非常大时,水平切分和垂直拆分是两种常见的降低数据库大小,提升性能的方法。假设有用户表: user( uid bigint, name ...

3615
来自专栏Java工程师日常干货

透彻理解MyBatis设计思想之手写实现

MyBatis,曾经给我的感觉是一个很神奇的东西,我们只需要按照规范写好XXXMapper.xml以及XXXMapper.java接口。要知道我们并没有提供XX...

511
来自专栏技术墨客

Spring核心——上下文与IoC 原

前面3篇分别介绍了IoC容器与Bean的关系、Bean与Bean之间的关系以及Bean自身的控制和管理。在了解Spinrg核心模式时,一定要谨记他的基本工作元素...

743
来自专栏iOS技术杂谈

iOS多线程——你要知道的NSThread都在这里你要知道的iOS多线程NSThread、GCD、NSOperation、RunLoop都在这里

你要知道的iOS多线程NSThread、GCD、NSOperation、RunLoop都在这里 转载请注明出处 https://cloud.tencent.co...

3159
来自专栏架构专栏

2018最新淘宝面试出炉:分布式锁+集群+一致Hash算法+底层技术原理

1.Java基础还是需要掌握牢固,重点会问HashMap等集合类,以及多线程、线程池等。

111

扫码关注云+社区