譯文 | 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)