您的位置:首页 > 移动开发 > Android开发

Android 的性能 IV-多线程的性能

2016-02-14 18:50 405 查看

概述:

通常, 如果将长时间运行的, 以及数据密集型的操作切割成更小的操作并放在多线程中运行, 都会提高速度和效率. 如果在一个设备上存在多核心的CPU, 那么系统可以并行的运行线程, 而不是让每个子操作一直等待机会才能执行. 栗如, 如果需要解码多个图片文件以便在缩略图中显示, 那么如果把它们放在单独的线程中执行, 将会使得运行速度大幅加快.

指定代码运行在线程中:

本节将介绍如何实现Runnable类, 它将会在Runnable.run()方法中执行代码, 这些代码会运行在一个独立的线程里. 你还可以传递一个Runnable到另一个可以连接到一个线程的对象, 并运行它. 一个或更多的Runnable执行某个特殊的操作, 有些时候被称为任务(Task).

Thread和Runnable都是基础类, 它们单独使用的时候, 只有有限的能力. 但是它们是强力Android类的基础, 比如HandlerThread,AsyncTask和IntentService. Thread和Runnable还是ThreadPoolExecutor类的基础类. 该类会自动管理线程和任务队列, 还可以并行运行多个线程.

定义一个类来实现Runnable:

Runnable的实现是很直截了当的, 比如:

public
class PhotoDecodeRunnable
implements Runnable
{

    ...

    @Override

    public void run()
{

        /*

         * Code you want to run on the thread goeshere

         */

        ...

    }

    ...
}

实现Run方法:

Runnable.run()包含了要执行的代码. 通常, 任何事情都可以在Runnable中运行. 但是还是要记住, Runnable不会运行在UI线程中, 所以它不能直接修改UI对象, 比如View对象.

在run()方法的开头, 通过Process.setThreadPriority()方法设置线程的后台优先级, 参数是THREAD_PRIORITY_BACKGROUND.这种方法可以解决Runnable对象线程和UI线程之间的资源竞争. 你还应该通过Thread.currentThread()方法在Runnable中保存一个Runnable对象的线程引用. 栗如:

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


创建一个多线程管理器:

前面是如何在一个独立的线程中执行代码. 如果只是想要一次性的运行任务, 这可能已经够了. 但是如果你想要重复的运行某个任务, 并指定不同的数据, 但是一次只执行一个任务, 那么IntentService符合你的要求. 要作为资源自动运行任务变得可行, 或者要允许多个任务同时运行, 你需要提供一个线程的集合. 想实现这一点, 就得使用一个ThreadPoolExecutor实例, 当它的(线程)池变得空闲时, 它就会从队列中运行一个任务. 要执行一个任务,
所有你要做的就是将其添加到队列中.

一个线程池可以运行多个并行的任务实例, 所以你应该确保你的代码是线程安全的. 可以被多个线程访问的变量应该加synchronized锁. 这种方法将会组织线程访问一个正在被访问的变量, 但是它也发生在仅一次实例化的任何对象中.

定义线程池类:

在自己的类中实例化ThreadPoolExecutor, 在这个类内部, 做这些事情:

l  为线程池使用静态变量: 为了拥有一个单一的控制点(由于CPU和网络资源的限制), 你可能只是想要一个线程池的单一实例. 如果你拥有了不同的Runnable类型, 你可能会想要为每种类型指定一个线程池, 但是每个都可以是单一的实例. 栗如, 你可以添加这些代码作为你的全局域声明的一部分:

public class PhotoManager {
    ...
    static  {
        ...
        // Creates a single static instance of PhotoManager
        sInstance = new PhotoManager();
    }
    ...


l  使用一个私有的构造方法: 使用私有的构造方法可以保证它是唯一的(singleton), 这意味着你不必使用synchronized锁来封装访问:

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() {
    ...
    }


l  通过线程池类中的方法来启动你的任务: 在线程池类中定义一个方法来用于将任务添加到线程池队列中, 栗如:

public class PhotoManager {
    ...
    // Called by the PhotoView to get a photo
    static public PhotoTask startDownload(
        PhotoView imageView,
        boolean cacheFlag
137c6
) {
        ...
        // Adds a download task to the thread pool for execution
        sInstance.
                mDownloadThreadPool.
                execute(downloadTask.getHTTPDownloadRunnable());
        ...
    }


l  在构造方法中实例化一个Handler并关联它到APP的UI线程: 一个Handler可以让你的APP安全的调用UI对象的方法, 比如View对象. 大多数的UI对象可能只在UI线程中修改才安全. 栗如:

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对象, 你需要这些值:

l  初始化线程池的大小和最大容量: 线程池中可以拥有的线程数主要取决于设备可用的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()会返回可用的核心数, 它可能比总共的核心数要小.

l  保持存活时间和时间单元: 线程在关闭之前会保持空闲一段时间, 这段时间的长度将由时间单元的值决定, 这是一个定义在TimeUnit中的常量.

l  一个任务队列: ThreadPoolExecutor中的输入队列, 需要Runnable对象. 要在一个线程中启动一段代码,线程池管理器会从一个先进先出的队列中取得一个Runnable对象并将其与一个线程关联. 当你创建线程池的时候需要提供这个队列的对象, 使用任何实现了BlockingQueue接口的队列都可以. 要匹配你的APP的需求, 你可以从可用队列实现中选择; 想了解更多关于它们的信息可以参考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>();
        ...
    }
    ...
}


创建一个线程池:

要创建一个线程池, 首先要通过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会从队列中获取一个任务并在线程中运行它.

在线程池中的线程内运行一个任务:

要启动一个线程池中的任务对象, 需要给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的时候, 它会自动调用对象的run()方法.

中断运行的代码:

要停止一个任务, 你需要中断任务线程. 想要实现这个, 你需要在创建任务的时候保存一个handler在任务线程中. 栗如:

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()方法. 注意Thread对象是系统控制的, 可以在APP进程的外部修改. 出于这种原因, 你需要在中断之前对线程的访问上锁, 通过将访问放在synchronized所中. 栗如:

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元素, 比如bitmap. 每个APP都有它自己独立的线程来运行UI对象, 比如View对象. 这个线程被称为UI线程. 只有运行在UI线程的对象可以访问该线程的其它对象. 因为在线程池中运行的任务并不运行在UI线程中, 所以它们不能访问UI对象. 要将后台线程中的数据传给UI线程, 需要用到一个在UI线程中的Handler.

在UI线程中定义一个Handler:

Handler是Android系统框架用来管理线程的一部分. 一个Handler对象接收消息并运行代码来处理消息. 通常, 你需要为一个新的线程创建一个Handler, 但是你还可以创建一个Handler来连接到一个已经存在的线程. 当你连接一个Handler到你的UI线程时, 处理消息的代码将会运行在UI线程中.

为一个创建了线程池的类实例化一个Handler对象, 并将对象保存在全局变量中. 通过Handler(Looper)构造方法将该对象与UI线程连接. 这个构造方法使用了一个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()) {
    ...


在Handler中, 重写handleMessage()方法. 当收到新的消息时, Android系统调用该方法. 所有相同Thread的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. 因为Handler运行在UI线程中, 所以消息就可以发送给UI线程了.

在任务对象中保存数据:

栗如, 下面是一个Runnable, 运行在一个后台进程中, 它解码了一个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.就算是Bitmap和ImageView的引用在同一个对象中, 你还是不能关联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:

PhotoManager对象从PhotoTask对象接收状态码和一个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()检查每个收到的消息的状态码. 如果状态码符合TASK_COMPLETE,那么就表示任务完成, 在消息中的PhotoTask对象包含了Bitmap和ImageView. 因为handleMessage()已经是运行在UI线程中的方法, 所以它可以安全的操作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);
                    }
                    ...
                }
                ...
            }
            ...
    }
...
}


参考: https://developer.android.com/shareables/training/ThreadSample.zip
https://developer.android.com/training/multiple-threads/index.html

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 线程 线程池