Android性能优化系列---Sending Operations to Multiple Threads
本文源自:http://developer.android.com/training/multiple-threads/index.html
当你将一个需要长时间运行的,数据量大的操作,分割成一些小的操作,并且在多线程中运行的话,那么这个长时间运行的操作的速度和效率将会提升不少。对于有一个有多个处理器(多核)的CPU的设备,系统可以并发的运行多个线程,而不是让每个子操作等到被执行。比如,当你要解码多张图片,以在屏幕上显示的话,你如果将每一个图片的解码操作放在每一个独立的线程的话,那么速度将会很快。
本课程将会向你展示,在Android里如何通过一个线程池对象,来创立和使用多线程。你同样也会学到如何使得代码在线程中运行,以及这些线程如何与主线程通信。
Specifying the Code to Run on a Thread
学习怎么写代码让其运行在一个单一的线程(通过定义一个实现Runnable接口的类)。
Creating a Manager for Multiple Threads
学习如何产生一个管理线程池的类和Runnable队列。该管理线程池的对象叫ThreadPoolExecutor。
Running Code on a Thread Pool Thread
怎么在线程池里在Thread上运行Runnable任务
Communicating with the UI Thread
如何让线程池里的线程和UI线程通信
Specifying the Code to Run on a Thread
Thread和Runnable是java里的基本类,但在Android里仅仅使用它们处理和解决问题有限。但它们是Android里例如HandlerThread、AsyncTask和IntentService等这些功能强大的类的基础。Thread和Runnable也是ThreadPoolExecutor类的基础。这个类自动的管理线程和任务队列,并且能并发的运行多线程。
Define a Class that Implements Runnable
定义一个类实现Runnable接口是直接明了的方式,例如:
public class PhotoDecodeRunnable implements Runnable {
...
@Override
public void run() {
/*
* Code you want to run on the thread goes here
*/
...
}
...
}
Implement the run() Method
在这个类里,Runnable.run()方法包含被执行的代码。通常,任何执行代码都被运行在Runnable里。但请记住,因为Runnable不被运行在UI thread,因此,不要直接的更新UI对象(例如像View对象)。为了通信和UI线程,你必须使用在Communicate with the UI Thread章节介绍的技术。
在run()方法里,通过调用Process.setThreadPriority()设置Thread的级别为background priority( THREAD_PRIORITY_BACKGROUND)。这种方式能降低运行Runnable的线程和UI线程之间的资源竞争。你也应该在Runnble对象里持有运行该Runnable对象的线程的引用。通过调用Thread.currentThread()。
下面的代码片段告诉你怎么写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());
...
}
...
}
Creating a Manager for Multiple Threads
前面告诉你如何定义一个运行在单一线程上的任务。如果你仅仅想运行该任务一次,上面的方式就能满足你的要求。如果你想在不同的数据集上多次运行该任务。但是一次又仅仅运行一次,IntentService能满足你的需求。为了资源可用时自动运行任务或者同时运行多个任务,你需要你个管理线程线程的集合。为了做这,可用使用ThreadPoolExecutor,ThreadPoolExecutor依次的从任务队列中取一个任务运行。为了运行一个任务,所有你必须做的就是将该任务加入到任务队列里。
线程池能并发运行多个任务,因此你必须保证你的代码是线程安全的。确保把能被多个线程访问的变量放在同步代码块里。这能避免一个线程对某个变量读的同时另一个线程对该变量写。特别地,当该变量是静态变量时这种情况更容易出现,当然,这种情况也发生在仅仅实例一次的对象上。如果想了解更多,请阅读Processes and Threads
Define the Thread Pool Class
在某个类里(管理线程池的类)实例化ThreadPoolExecutor。在这个类,做如下事情:
为线程池使用静态变量
在你的应用里,为了对于有限的CPU和网络资源有单一的控制点,你可能需要该线程池是单例的。如果你有许多不同类型的任务,你就不得不对于每一个类型的Runnable实例不同的线程池,但每一种类型的Runnable只对应有一个ThreadPoolExecutor实例。例如,你可以定义该类作为你全局属性声明的一部分:
public class PhotoManager {
...
static {
...
// Creates a single static instance of PhotoManager
sInstance = new PhotoManager();
}
...
用一个私有构造器确保该类单例,那意思是你不必在同步代码块里封装对该类的访问。
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() {
...
}
通过调用线程池里的方法开始你的任务
在线程里定义一个方法,该方法加一个任务到线程池队列里。例如:
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());
...
}
在Manager类的构造器里实例化一个Handler,让该Handler附加到UI线程上。Handler运行你放心的调用UI对象例如View里的方法。大多数的UI对象仅仅在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) {
...
}
...
}
}
Determine the Thread Pool Parameters
一旦你有了总体的类架构,你能开始定义线程池。为了实例一个线程池对象,你需要如下的值:
初始化池大小和最大池大小
分配给线程池的初始化线程个数和最大能运行的线程个数。在线程池里能有的线程的数量主要取决于你的设备的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的物理核数。有些设备的CPU一些处理器可能并不是活跃的。那即是有些核可能并没有使用。对应这些设备, availableProcessors() 返回活动的核数,这可能比总核数少。
保持活动时间和时间单位
线程关闭之前可以处于idle状态持续的时间。时间单位取值于TimeUnit类的常量之一。定义了Keep alive time的单位。
任务队列:
任务队列将传入ThreadPoolExecutor里,ThreadPoolExecutor从该任务队列里去取Runnable对象运行。为了在一个线程上运行代码,线程池管理器从一个先进先出队列取Runnable对象。然后分配其到一个线程运行。当你产生线程池的时候,你能传入该队列对象。任何实现了BlockingQueue接口的对象都可以作为任务队列对象。
为了了解更多,可以参考类ThreadPoolExecutor。使用LinkedBlockingQueue类的示例如下:
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>();
...
}
...
}
Create a Pool of Threads
为了产生一个线程池,通过调用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);
}
Running Code on a Thread Pool Thread
前面论述了怎么定义一个线程池管理类及如何定义运行在里的任务。现在我们将讨论如何在一个线程池里运行一个任务。为了实现这,你需要将你的任务加到一个线程池工作队列。当线程变得有效时ThreadPool -Executor从该队列取一个任务然后运行它。
这课也告诉你如何停止一个正在运行的任务。你可能在一个任务开始的时候你想要停止它。为了不浪费CPU时间,你能取消正在运行的任务。例如,你正从网络下载图片,并使用了缓存。你可能当发现该图片已缓存时需要停止该线程任务。由于你的应用代码实现的原因。你可能在发生了网络请求时才知道该图片是否已缓存。
Run a Task on a Thread in the Thread Pool
为了在线程池里运行一个任务,传递一个Runnable对象到ThreadPoolExecutor.execute()即可。执行execute()会将该任务加到线程池的任务队列。当一个处于Idle的线程可以运行时,线程池管理器取等待时间最长的任务运行:
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在一个Thread运行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.interrupt().注意,线程是由系统控制的。系统能在你的应用进程之外修改该线程。因此,在你中断该线程之前,你需要在该线程上对你的取消操作加锁:将代码放入同步代码块。例如:
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.interrupte()立即的停止线程。然而,该方法仅仅停止处于waiting状态的线程,不中断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);
...
Communicating with the UI Thread
上一节你知道了怎么在一个线程上开始一个任务。该线程通过ThreadPoolExecutor管理。最后,你需要在UI线程上更新在子线程里运行的结果。每一个app都有一个专门运行UI对象例如View的线程。只有在UI线程里才能进行UI操作。因为运行在线程里的任务并不运行在UI线程,不能在该线程里访问UI对象。为了让后台运行的任务的数据结果更新到UI上,可以使用一个运行在UI线程上的Handler。
Define a Handler on the UI Thread
Handler是Android系统框架提供的管理Thread的一个类。Handler收到消息并且运行相关代码处理该消息。正常地,你可以为一个新线程产生一个Handler。你也可以将该handler连接到一个已存在的线程。当你连接一个Handler到UI线程时,该Handler将处理消息到UI线程。
在产生线程池类(将该类定义为全局变量,例如通过单例)的构造方法里实例化Handler。通过Handler (Looper)构造方法连接该Handler到UI线程。该构造器使用Looper对象。当你基于特定的Looper对象实例化你的Handler时,该Handler运行于和Looper一样的线程,例如:
private PhotoManager() {
...
// Defines a Handler object that's attached to the UI thread
mHandler = new Handler(Looper.getMainLooper()) {
...
在Handler内部,你要重写handleMessage()方法。当Handler接收到来自它附属的线程发来的消息时,系统会自动调用handleMessage方法。在Handler内部,你要重写handleMessage()方法。当Handler接收到来自它附属的线程发来的消息时,系统会自动调用handleMessage方法。一个线程里所有的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;
...
}
...
}
}
Move Data from a Task to the UI Thread
如果想将工作线程运行的数据传递给主线程中的某个对象,可以在任务中存储这个数据和UI对象的引用。接下来,将这个任务还有执行这个任务后的状态码传递给创建handler的对象。在这个对象中,将包含这个状态码以及任务对象的消息发送给handler。由于handler运行在主线程中,因此它可以将任务产生的结果传递给主线程。
Store data in the task object
例如,下面是一个运行在后台非UI线程,用于解码Bitmap和在它的父类PhotoTask里存储Bitmap的Runnable。该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的引用。该ImageView用于展示Bitmap。虽然Bitmap和ImageView在相同的对象里都有引用。但你不能在该对象里直接将Bitmap的值赋给ImageView。因为该类并不运行在UI线程。接下来,发送状态码给PhoteTask对象。
Send status up the object hierarchy
PhotoTask维护着图片的引用,也持有显示这个图片的imageview的引用。他会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);
}
...
}
Move data to the UI
PhotoManager从PhotoTask接受状态码和PhotoTask对象的引用。因为该状态码TASK_COMPLETE,产生一个包含该状态码和任务对象的消息,然后发送该消息给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()运行在主线程。你能放心的将Bitmap展示在ImageView上。
private PhotoManager() {
...
mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message inputMessage) {
// Gets the task from the incoming Message object.
PhotoTask photoTask = (PhotoTask) inputMessage.obj;
// Gets the ImageView for this task
PhotoView localView = photoTask.getPhotoView();
...
switch (inputMessage.what) {
...
// The decoding is done
case TASK_COMPLETE:
/*
* Moves the Bitmap from the task
* to the View
*/
localView.setImageBitmap(photoTask.getImage());
break;
...
default:
/*
* Pass along other messages from the UI
*/
super.handleMessage(inputMessage);
}
...
}
...
}
...
}
...
}