The speed and efficiency of a long-running, data-intensive operation often improves when you split it into smaller operations running on multiple threads.
把一個相對耗時且數據操作復雜的任務分割成多個小的操作,然后分別運行在多個線程上,這能夠提高完成任務的速度和效率。在多核CPU的設備上,系統可以並行運行多個線程,而不需要讓每個子操作等待CPU的時間片切換。例如,如果要解碼大量的圖片文件並以縮略圖的形式把圖片顯示在屏幕上,當你把每個解碼操作單獨用一個線程去執行時,會發現速度快了很多。
那問題來了:如何在一個Android應用中創建和使用多線程,以及如何使用線程池對象(thread pool object);如何使得代碼運行在指定的線程中,以及如何讓線程和UI線程通信。
主題一:如何在一個線程中執行特定代碼
如何通過實現 Runnable接口得到一個能在重寫的Runnable.run()方法中執行一段代碼的單獨的線程?
是否可以傳遞一個Runnable對象到另一個對象,然后這個對象可以把它附加到一個線程,並執行它?
一個或多個執行特定操作的Runnable對象有時也被稱為一個任務Task。
Thread和Runnable只是兩個基本的線程類,通過他們能發揮的作用有限,但是他們是強大的Android線程類的基礎類。例如Android中的HandlerThread, AsyncTask和IntentService都是以它們為基礎。Thread和Runnable同時也是ThreadPoolExecutor類的基礎。
ThreadPoolExecutor類能自動管理線程和任務隊列,甚至可以並行執行多個線程。
創建一個實現Runnable的類
public class PhotoDecodeRunnable implements Runnable { ... @Override public void run() { /* * Code you want to run on the thread goes here */ ... } ... }
覆寫其中的run():
class PhotoDecodeRunnable implements Runnable { ... /* * Defines the code to run for this task. */ @Override public void run() { // Moves the current Thread into the background android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); ... /* * Stores the current Thread in the PhotoTask instance, * so that the instance * can interrupt the Thread. */ mPhotoTask.setImageDecodeThread(Thread.currentThread()); ... } ... }
需要注意的是:Remember, though, that the Runnable won't be running on the UI thread, so it can't directly modify UI objects such as View objects.
At the beginning of the run() method, set the thread to use background priority by calling Process.setThreadPriority() with THREAD_PRIORITY_BACKGROUND. This approach reduces resource competition between the Runnable object's thread and the UI thread.
你還應該通過在Runnable 自身中調用Thread.currentThread()來存儲一個引用到Runnable對象的線程。
主題二:如何為多線程創建線程池
什么是線程池?線程池的含義是什么?ThreadPoolExecutor:an object that manages a pool of Thread objects and a queue of Runnable objects.
如果你想在一個數據集中重復執行一個任務,而且你只需要一個執行運行一次。這時,使用一個IntentService將能滿足你的需求。為了在資源可用的的時候自動執行任務,或者允許不同的任務同時執行(或前后兩者),你需要提供一個管理線程的集合。為了做這個管理線程的集合,使用一個ThreadPoolExecutor實例,當一個線程在它的線程池中變得不受約束時,它會運行隊列中的一個任務。為了能執行這個任務,你所需要做的就是把它加入到這個隊列。
一個線程池能運行多個並行的任務實例,因此你要能保證你的代碼是線程安全的,從而你需要給會被多個線程訪問的變量附上同步代碼塊(synchronized block)。 當一個線程在對一個變量進行寫操作時,通過這個方法將能阻止另一個線程對該變量進行讀取操作。典型的,這種情況會發生在靜態變量上,但同樣它也能突然發生在任意一個實例化操作中。
如何定義線程池?
在自己的類中實例化ThreadPoolExecutor類,必須讓自定義的類做以下幾件事情:
Use static variables for thread pools 為線程池使用靜態變量
為了有一個單一控制點用來限制CPU或涉及網絡資源的Runnable類型,你可能需要有一個能管理所有線程的線程池,且每個線程都會是單個實例。比如,你可以把這個作為一部分添加到你的全局變量的聲明中去:
public class PhotoManager { ... static { ... // Creates a single static instance of PhotoManager sInstance = new PhotoManager(); } ...
Use a private constructor 使用私有的構造方法
讓構造方法私有從而保證這是一個單例,這意味着你不需要在同步代碼塊(synchronized block)中額外訪問這個類:
public class PhotoManager { ... /** * Constructs the work queues and thread pools used to download * and decode images. Because the constructor is marked private, * it's unavailable to other classes, even in the same package. */ private PhotoManager() { ... }
Start your tasks by calling methods in the thread pool class. 通過調用線程池類中的方法,開啟任務
該方法能夠實例化一個Task,並添加到線程池的運行隊列中。
public class PhotoManager { ... // Called by the PhotoView to get a photo static public PhotoTask startDownload( PhotoView imageView, boolean cacheFlag) { ... // Adds a download task to the thread pool for execution sInstance. mDownloadThreadPool. execute(downloadTask.getHTTPDownloadRunnable()); ... }
Instantiate a Handler in the constructor and attach it to your app's UI thread. 在構造方法中初始化Handler實例,並將其附加到UI線程中
一個Handler允許你的APP安全地調用UI對象(例如 View對象)的方法。大多數UI對象只能從UI線程安全的代碼中被修改。這個方法將會在與UI線程進行通信(Communicate with the UI Thread)這一課中進行詳細的描述。
private PhotoManager() { ... // Defines a Handler object that's attached to the UI thread mHandler = new Handler(Looper.getMainLooper()) { /* * handleMessage() defines the operations to perform when * the Handler receives a new Message to process. */ @Override public void handleMessage(Message inputMessage) { ... } ... } }
設置線程池的具體屬性值
一旦有了整體的類結構,你可以開始定義線程池了。為了初始化一個ThreadPoolExecutor對象,你需要提供以下數值:
Initial pool size and maximum pool size 線程池的初始化大小和最大的大小
這個是指最初分配給線程池的線程數量,以及線程池中允許的最大線程數量。在線程池中擁有的線程數量主要取決於你的設備的CPU內核數。這個數值可以從系統服務中獲取。
public class PhotoManager { ... /* * Gets the number of available cores * (not always the same as the maximum number of cores) */ private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors(); }
這個數字可能並不反映設備的物理核心數量,因為一些設備根據系統負載關閉了一個或多個CPU內核,對於這樣的設備,availableProcessors()方法返回的是處於活動狀態的內核數量,可能少於設備的實際內核總數。
Keep alive time and time unit 線程保活時間和單位
這個是指線程被關閉前保持空閑狀態的持續時間。這個持續時間通過時間單位值進行解譯,是TimeUnit()中定義的常量之一。
A queue of tasks 任務隊列
這個傳入的隊列由ThreadPoolExecutor獲取的Runnable對象組成。為了執行一個線程中的代碼,一個線程池管理者從先進先出的隊列中取出一個Runnable對象且把它附加到一個線程。當你創建線程池時需要提供一個隊列對象,這個隊列對象類必須實現BlockingQueue接口。為了滿足你的APP的需求,你可以選擇一個Android SDK中已經存在的隊列實現類。為了學習更多相關的知識,可以參考ThreadPoolExecutor類的概述。
public class PhotoManager { ... private PhotoManager() { ... // A queue of Runnables private final BlockingQueue<Runnable> mDecodeWorkQueue; ... // Instantiates the queue of Runnables as a LinkedBlockingQueue mDecodeWorkQueue = new LinkedBlockingQueue<Runnable>(); ... } ... }
所有准備工作就緒,創建一個線程池實例
為了創建一個線程池,可以通過調用ThreadPoolExecutor()構造方法初始化一個線程池管理者對象,這樣就能創建和管理一組可約束的線程了。如果線程池的初始化大小和最大大小相同,ThreadPoolExecutor在實例化的時候就會創建所有的線程對象。
private PhotoManager() { ... // Sets the amount of time an idle thread waits before terminating private static final int KEEP_ALIVE_TIME = 1; // Sets the Time Unit to seconds private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; // Creates a thread pool manager mDecodeThreadPool = new ThreadPoolExecutor( NUMBER_OF_CORES, // Initial pool size NUMBER_OF_CORES, // Max pool size KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, mDecodeWorkQueue); }
主題三:如何在線程池中的一個線程中執行特定代碼
為了達到這個目的,你需要把任務添加到線程池的工作隊列中去,當一個線程變成可運行狀態時,ThreadPoolExecutor從工作隊列中取出一個任務,然后在該線程中執行。
此外,我們還能夠去停止一個正在執行的任務,這個任務可能在剛開始執行時是你想要的,但后來發現它所做的工作並不是你所需要的;你可以取消線程正在執行的任務,而不是浪費處理器的運行時間。例如你正在從網絡上下載圖片且對下載的圖片進行了緩存,當檢測到正在下載的圖片在緩存中已經存在時,你可能希望停止這個下載任務。當然,這取決於你編寫APP的方式,因為可能壓在你啟動下載任務之前無法獲知是否需要啟動這個任務。
如何啟動線程池中的線程以便執行任務?
為了在一個特定的線程池的線程里開啟一個任務,可以通過調用ThreadPoolExecutor.execute(),它需要提供一個Runnable類型的參數,這個調用會把該任務添加到這個線程池中的工作隊列。當一個空閑的線程進入可執行狀態時,線程管理者從工作隊列中取出等待時間最長的那個任務,並且在線程中執行它。
public class PhotoManager { public void handleState(PhotoTask photoTask, int state) { switch (state) { // The task finished downloading the image case DOWNLOAD_COMPLETE: // Decodes the image mDecodeThreadPool.execute( photoTask.getPhotoDecodeRunnable()); ... } ... } ... }
當ThreadPoolExecutor在一個線程中開啟一個Runnable后,它會自動調用Runnable的run()。
如何中斷正在被執行的代碼?
為了停止執行一個任務,你必須中斷執行這個任務的線程。在准備做這件事之前,當你創建一個任務時,你需要存儲處理該任務的線程。
class PhotoDecodeRunnable implements Runnable { // Defines the code to run for this task public void run() { /* * Stores the current Thread in the * object that contains PhotoDecodeRunnable */ mPhotoTask.setImageDecodeThread(Thread.currentThread()); ... } ... }
Thread.currentThread()獲取到當前執行該Runnable的線程。
想要中斷一個線程,可以調用Thread.interrupt()。需要注意的是這些線程對象都被系統控制,系統可以在App進程之外修改該對象。因此,在中斷線程之前,需要把這段代碼放在一個同步代碼塊中對這個線程的訪問加鎖來解決。
public class PhotoManager { public static void cancelAll() { /* * Creates an array of Runnables that's the same size as the * thread pool work queue */ Runnable[] runnableArray = new Runnable[mDecodeWorkQueue.size()]; // Populates the array with the Runnables in the queue mDecodeWorkQueue.toArray(runnableArray); // Stores the array length in order to iterate over the array int len = runnableArray.length; /* * Iterates over the array of Runnables and interrupts each one's Thread. */ synchronized (sInstance) { // Iterates over the array of tasks for (int runnableIndex = 0; runnableIndex < len; runnableIndex++) { // Gets the current thread Thread thread = runnableArray[taskArrayIndex].mThread; // if the Thread exists, post an interrupt to it if (null != thread) { thread.interrupt(); } } } } ... }
在大多數情況下,通過調用Thread.interrupt()能立即中斷這個線程,然而他只能停止那些處於等待狀態的線程,卻不能中斷那些占據CPU或者耗時的連接網絡的任務。為了避免拖慢系統速度或造成系統死鎖,在嘗試執行耗時操作之前,你應該測試當前是否存在處於掛起狀態的中斷請求:
/* * Before continuing, checks to see that the Thread hasn't * been interrupted */ if (Thread.interrupted()) { return; } ... // Decodes a byte array into a Bitmap (CPU-intensive) BitmapFactory.decodeByteArray( imageBuffer, 0, imageBuffer.length, bitmapOptions); ...
主題四:如何與UI線程通信
從執行的任務中發送數據給運行在UI線程中的對象,或者顯示后台運行進度...這個功能允許你的任務可以做后台工作,然后把得到的結果數據轉移給UI元素使用,例如位圖數據。
Every app has its own special thread that runs UI objects such as View objects; this thread is called the UI thread. Only objects running on the UI thread have access to other objects on that thread. 任何一個App都有自己特定的一個線程用來運行UI對象,這個線程被稱為是:“UI線程”。只有運行在UI線程中的對象才能夠訪問在該線程中的其他對象。
因為你的任務執行的線程來自一個線程池而不是執行在UI線程,所以他們不能訪問UI對象。為了把數據從一個后台線程轉移到UI線程,需要使用一個運行在UI線程里的Handler。
在UI線程中定義一個Handler
Handler屬於Android系統的線程管理框架的一部分。一個Handler對象用於接收消息和執行處理消息的代碼。一般情況下,如果你為一個新線程創建了一個Handler,你還需要創建一個Handler,讓它與一個已經存在的線程關聯,用於這兩個線程之間的通信。如果你把一個Handler關聯到UI線程,處理消息的代碼就會在UI線程中執行。
你可以在一個用於創建你的線程池的類的構造方法中實例化一個Handler對象,並把它定義為全局變量,然后通過使用Handler (Looper) 這一構造方法實例化它,用於關聯到UI線程。Handler(Looper)這一構造方法需要傳入了一個Looper對象,它是Android系統的線程管理框架中的另一部分。當你在一個特定的Looper實例的基礎上去實例化一個Handler時,這個Handler與Looper運行在同一個線程里。
private PhotoManager() { ... // Defines a Handler object that's attached to the UI thread mHandler = new Handler(Looper.getMainLooper()) { ...
如上所述,Looper.getMainLooper()獲取到的是主線程的Looper實例,因此該Hanlder則是UI線程的Handler實例。
在這個Handler里需要重寫handleMessage()方法;當這個Handler接收到由該Handler管理的線程發送過來的新消息時,Android系統會自動調用這個方法,而所有線程對應的Handler都會收到相同信息。
/* * handleMessage() defines the operations to perform when * the Handler receives a new Message to process. */ @Override public void handleMessage(Message inputMessage) { // Gets the image task from the incoming Message object. PhotoTask photoTask = (PhotoTask) inputMessage.obj; ... } ... } }
如何把數據從任務中轉移到UI線程
為了從一個運行在后台線程的任務對象中轉移數據到UI線程中的一個對象,首先需要存儲任務對象中的數據和UI對象的引用;接下來傳遞任務對象和狀態碼給實例化Handler的那個對象。在這個對象里,發送一個包含任務對象和狀態的Message給Handler也運行在UI線程中,所以它可以把數據轉移到UI線程。
比如這里有一個Runnable,它運行在一個編碼了一個Bitmap且存儲這個Bitmap到父類PhotoTask對象里的后台線程。這個Runnable同樣也存儲了狀態碼DECODE_STATE_COMPLETED。
// A class that decodes photo files into Bitmaps class PhotoDecodeRunnable implements Runnable { ... PhotoDecodeRunnable(PhotoTask downloadTask) { mPhotoTask = downloadTask; } ... // Gets the downloaded byte array byte[] imageBuffer = mPhotoTask.getByteBuffer(); ... // Runs the code for this task public void run() { ... // Tries to decode the image buffer returnBitmap = BitmapFactory.decodeByteArray( imageBuffer, 0, imageBuffer.length, bitmapOptions ); ... // Sets the ImageView Bitmap mPhotoTask.setImage(returnBitmap); // Reports a status of "completed" mPhotoTask.handleDecodeState(DECODE_STATE_COMPLETED); ... } ... } ...
PhotoTask類還包含一個用於給ImageView顯示Bitmap的handler。雖然Bitmap和ImageViewImageView的引用在同一個對象中,但你不能把這個Bitmap分配給ImageView去顯示,因為它們並沒有運行在UI線程中。
發送狀態到更高層次的對象
PhotoTask是下一個層次更高的對象,它包含將要展示數據的編碼數據和View對象的引用。它會收到一個來自PhotoDecodeRunnable的狀態碼,並把這個狀態碼單獨傳遞到一個包含線程池和Handler實例的對象:
public class PhotoTask { ... // Gets a handle to the object that creates the thread pools sPhotoManager = PhotoManager.getInstance(); ... public void handleDecodeState(int state) { int outState; // Converts the decode state to the overall state. switch(state) { case PhotoDecodeRunnable.DECODE_STATE_COMPLETED: outState = PhotoManager.TASK_COMPLETE; break; ... } ... // Calls the generalized state method handleState(outState); } ... // Passes the state to PhotoManager void handleState(int state) { /* * Passes a handle to this task and the * current state to the class that created * the thread pools */ sPhotoManager.handleState(this, state); } ... }
轉移數據到UI線程
從PhotoTask對象那里,PhotoManager對象收到了一個狀態碼和一個PhotoTask對象的handler。因為狀態碼是TASK_COMPLETE,所以創建一個Message應該包含狀態和任務對象,然后把它發送給Handler。
public class PhotoManager { ... // Handle status messages from tasks public void handleState(PhotoTask photoTask, int state) { switch (state) { ... // The task finished downloading and decoding the image case TASK_COMPLETE: /* * Creates a message for the Handler * with the state and the task object */ Message completeMessage = mHandler.obtainMessage(state, photoTask); completeMessage.sendToTarget(); break; ... } ... }
最終,Handler.handleMessage()會檢查每個傳入進來的Message,如果狀態碼是TASK_COMPLETE,這時任務就完成了,而傳入的Message里的PhotoTask對象里同時包含一個Bitmap和一個ImageView。因為Handler.handleMessage()運行在UI線程里,所以它能安全地轉移Bitmap數據給ImageView。
public class PhotoManager { ... // Handle status messages from tasks public void handleState(PhotoTask photoTask, int state) { switch (state) { ... // The task finished downloading and decoding the image case TASK_COMPLETE: /* * Creates a message for the Handler * with the state and the task object */ Message completeMessage = mHandler.obtainMessage(state, photoTask); completeMessage.sendToTarget(); break; ... } ... }