標籤:

譯文 | Android 開發中利用非同步來優化運行速度和性能

關注我們的微信公眾號「人工智慧LeadAI」(ID:atleadai),瀏覽更多技術文章。

原文鏈接 : Using concurrency to improve speed and performance in Android()

原文作者 : Ali Muzaffar

譯者 : edvardHua

校對者: JOJO、Jing KE

我們知道,在Android框架中提供了很多非同步處理的工具類。然而,他們中大部分實現是通過提供單一的後台線程來處理任務隊列的。如果我們需要更多的後台線程的時候該怎麼辦呢?

大家都知道Android的UI更新是在UI線程中進行的(也稱之為主線程)。所以如果我們在UI線程中編寫耗時任務都可能會阻塞UI線程更新UI。為了避免這種情況我們可以使用 AsyncTask, IntentService和Threads。在之前我寫的一篇文章介紹了Android 中非同步處理的8種方法。但是,Android提供的AsyncTasks和IntentService都是利用單一的後台線程來處理非同步任務的。那麼,開發人員如何創建多個後台線程呢?

更新: Marco Kotz指出結合使用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的文章。

看起來所有的任務都被後台的單一線程處理完畢了,我們性能提升主要得益於後台線程長期運行並不會被銷毀和重建。然而,我們後台的單一線程卻要和其他優先等級更高的任務共享,而且這些任務只能夠順序執行。

我們也可以創建第二個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上得到,歡迎看完代碼後給我反饋

另外,你也可以在Google Play上面下載演示應用。

使用ThreadPool前: 如果可以,從頂部觀察計數器的變化來得知當底部縮略圖從開始顯示到全部顯示完成所耗費的時間。在程序中除了adapter中的notifyDataSetChanged()方法外,我已經將大部分的操作從主線程中剝離,所以計數器的運行是很流暢的。(視頻鏈接(要翻牆))()

使用ThreadPool後: 通過頂部的計數器,我們發現使用了ThreadPool後,照片的縮略圖載入速度明顯變快。(視頻鏈接(要翻牆))


推薦閱讀:

索尼移動是如何掉隊的?
智能手機操作系統的簡短現狀總結與未來趨勢的個人觀點
Windows平板電腦能否作為大屏手機使用?
完全急救指南- 應急 App 專題(二)
手機跑分的實踐性是否有如一些廠商所吹的那麼高?

TAG:Android |