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; ... } ... }