在Android中使用併發來提高速度和性能
Android框架提供了很實用的異步處理類。然而它們中的大多數在一個單一的後臺線程中排隊。當你需要多個線程時你是怎麼做的?
衆所周知,UI更新發生在UI線程(也稱爲主線程)。在主線程中的任何操作都會阻塞UI更新,因此當需要大量計算時可以使用AsyncTask, IntentService 和 Threads。事實上,在不久前我寫了在android中異步處理的8種方式。然而,Android中的AsyncTasks運行在一個單一後臺線程並且IntentService也同樣如此。因此,開發者應該怎麼做?
更新:Marco Kotz 指出,你可以通過ThreadPool Executor和AsyncTask使用多個後臺線程。
大多數開發者所做的
在大多數情況下,你不需要多個線程,簡單地分離AsyncTasks 或者在IntentService 中排隊操作就足夠用了。然而,當你真正需要多個線程時,通常,我看到的開發者只是簡單地平分舊的線程。
String[] urls = …
for (final String url : urls) {
new Thread(new Runnable() {
public void run() {
//Make API call or, download data or download image
}
}).start();
}
使用這種方法有幾個問題。第一個是操作系統限制了相同的域名的連接數量爲4(我相信)。意思是這段代碼不會按照你想的那樣去執行。它創建的線程在開始執行操作之前不得不等待另一個線程執行完畢。還有就是每個線程被創建,用來執行一個任務,然後被銷燬。這個沒有被重用。
這爲什麼是一個問題?
讓我們說一個例子,你想開發一個急速連拍應用,從Camera 預覽每秒捕獲10張照片或更多。應用的功能如下:
- 用byte[]存儲10張照片,且不能阻塞UI。
- 轉換每個byte[]的格式從YUV 到RGB 。
- 使用轉換後的數組創建一個Bitmap 。
- 修復Bitmap 的方向。
- 生成一個縮略圖大小的Bitmap 。
- 把完整大小的Bitmap壓縮成Jpeg寫到磁盤上。
- 排隊把完整圖片上傳到服務器。
可以理解的是,如果你在主UI線程做所有的操作,你的應用性能將會很低下。唯一的方法是當UI空閒時緩存camera預覽數據並處理它。
另一種可能是創建一個一直運行的HandlerThread,可以用來在後臺線程接收camera預覽並做這些所有的處理。雖然這樣會更好,但在隨後的急速連拍之間將會有太多的延遲,因爲所有的操作都需要處理。
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) {
//Do everything
}
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 處理圖片和第三個將它們寫到磁盤和第四個上傳到服務器。我們可以快速捕獲圖片,然而,這些線程仍然將以線性的方式依賴其它的線程。這不是真正的併發。我們可以快速捕獲圖片,然而,因爲處理每個圖片需要時間,當用戶點擊按鈕和縮略圖被顯示之間用戶仍能感受到很大的滯後。
使用線程池提高性能
雖然我們可以根據需要創建很多線程,但創建線程和銷燬它是一個時間成本。我們也不想創建不需要的線程並且想要充分利用可用的硬件。太多的線程會通過消耗CPU週期影響性能。解決方案是使用一個線程池(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() {
//do everything
}
});
}
return true;
}
});
這樣就OK了。性能明顯提升,可以看看下面的視頻!
這裏的優勢在於我們可以定義池大小,甚至在回收之前指定線程保持多長時間。我們也可以針對不同的操作創建不同的線程池或只使用一個線程池。需要小心的是當你的線程執行完成後做適當的清理。
我們甚至可以針對不同的操作創建不同的線程池,一個線程池轉換數據到Bitmaps,一個線程池寫數據到磁盤,第三個線程池上傳Bitmaps 到服務器。在這一過程中,如果我們的線程池最大可以有4個線程,我們可以在同一時間轉換,寫和上傳4張圖片而不是1張。用戶可以在同一時間看到4張圖片而不是一張。
上面是一個簡化的例子,可以從GitHub上查看完整的代碼然後給我一些反饋。
你也可以從Google Play下載demo應用。
實現線程池前:如果可以,當縮略圖顯示在底部時盯着屏幕頂部的定時器。因爲我已經把除了 adapter的notifyDataSetChanged()之外的所有操作放到了主線程之外,計數器應該會運行得很流暢。
實現線程池後:屏幕頂部的定時器依然運行得很流暢,然而,圖片縮略圖的顯示快了很多。