Android多线程方式

1、前言

在Android开发中经常会使用到多线程,这里主要是总结Android开发中常见的多线程实现方式,以及这些多线程实现方式的一些特点
多线程实现方式主要有:

  • 实现Thread的run()方法或者实现Runable接口
  • HandlerThread
  • AsyncTask
  • LoaderManager

2、Thread方式

一般使用异步操作最常见的一种方式,我们可以继承Thread,并重写run()方法,如下所示:

Thread syncTask = new Thread() {
    @Override
    public void run() {
        // 执行耗时操作
    }
};

syncTask.start();

还有另外一种启动线程的方式,即在创建Thread对象时,传入一个实现了Runable接口的对象,如下所示:

Thread syncTask = new Thread(new Runnable() {
    @Override
    public void run() {
        // 执行耗时操作
    }
});

syncTask.start();

Thread类中有几个方法的作用有些模糊,这里给出说明:

  • interrupt( ):
    我们一般会使用该方法中断线程的执行,但该方法并不会中断线程,它的作用只是设置一个中断标志位,我们还得在run( )方法中判断这个标志位,并决定是否继续执行,通过这样的方式来达到中断的效果。 但该方法根据线程状态的不同,会有不同的结果。结果如下:
    1. 当线程由于调用wait( )、join( )、sleep( )而阻塞时,中断标志位将会被清空,并接收到InterruptedException异常。
    2. 当线程由于正在进行InterruptibleChannel类型的I/O操作而阻塞时,中断标志位将会置位,并接收到ClosedByInterruptException异常(I/O流也会自动关闭)
    3. 当线程由于进行Selector操作而阻塞时,中断标志位将会置位,但不会接收到异常

interrupted( )和isInterrupted( )的区别:
两个方法都是判断当前线程的中断标志位是否被置位,但调用interrupted( )方法后,中断标志位将会重置,而isInterrupted()不会被重置。

  • join( )和sleep( )的区别:两个方法都会让线程暂停执行

join()方法是让出执行资源(如:CPU时间片),使得其它线程可以获得执行的资源。所以调用join()方法会使进入阻塞状态,该线程被唤醒后会进入runable状态,等待下一个时间片的到来才能再次执行。
sleep( )不会让出资源,只是处于睡眠状态(类似只执行空操作)。调用sleep()方法会使进入等待状态,当等待时间到后,如果还在时间片内,则直接进入运行状态,否则进入runable状态,等待下个时间片。

3、HandlerThread

有些需求需要子线程不断的从一个消息队列中取出消息,并进行处理,处理完毕以后继续取出下一个处理。对于这个需求我们可以使用第一种方式,实现一个Thread对象,并创建一个消息队列,在Thread对象的run方法中不断的从消息队列中取出消息进行处理。多以该线程的这些特点有点像一个Looper线程,我们可复用Handler机制提供的消息队列MessageQueue,而无需自己重新创建。
HandlerThread的内部实现机制很简单,在创建新的线程后,使该线程成为一个Looper线程,让该线程不断的从MessageQueue取出消息并处理。我们看一下HandlerThread的实现:

public class HandlerThread extends Thread {
    int mPriority;
    Looper mLooper;

    /**
    * Constructs a HandlerThread.
    */
    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }

    @Override
    public void run() {
        // 要想让某个线程成为Looper线程,先调用Looper.prepare()为该线程创建一个Looper对象,并初始化MessageQueue对象
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        // 调用Looper.loop(),让该线程的Looper实例循环从MessageQueue中取出Message进行处理
        Looper.loop();
    }
}

4、AsyncTask

这是我们最经常使用的一种异步方式,在前面的两种多线程方式中,如果在子线程中进行了耗时的处理操作(如:网络请求、读写数据库等),当操作完毕后,我们需要更新UI上的显示状态,但在Android开发中我们是不能在子线程中更新UI界面的,所以还得在子线程中发送一个通知到主线程,让主线程去更新UI。这样的操作流程有些复杂,且都是重复性的工作。所以Android sdk中为我们抽象出AsyncTask这个类。

public class CustomAsyncTask extends AsyncTask {
    @Override
    protected void onPreExecute() {
        // 在开始执行异步操作前回调,该方法在主线程中执行
    }

    @Override
    protected String doInBackground(String... strings) {
        // 在该方法中进行异步操作,参数strings是在启动异步任务时execute(...)传递进来的
        // 该异步任务放回的结果类型为String
        return null;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        // 该方法用户通知用户doInBackground()方法的处理进度,在主线程中被回调,所以可在该方法中更新UI
        // 参数values用于指示处理进度
    }

    @Override
    protected void onPostExecute(String result) {
        // 该方法是在异步操作doInBackground()处理完毕后回调,参数result是doInBackground()的处理结果
        // 该方法在主线程中被回调,可直接更新UI
    }

    @Override
    protected void onCancelled(String result) {
        super.onCancelled(result);

        // 当调用cancel(boolean), 则在doInBackground()完成后回调该方法
        // 注意: 参数result可能为null,
    }
}

AsyncTask的内部使用了两个线程池,我们大概看一下AsyncTask的内部实现

// 顺序执行任务的线程池,注意这个线程池是静态的,每个AsyncTask对象共用这个线程池
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

// 我们启动异步任务的三个方法,都是向SerialExecutor.execute(runable)传递一个runable对象
public final AsyncTask execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
}

public final AsyncTask executeOnExecutor(Executor exec,
        Params... params) {
    ...
    exec.execute(mFuture);
    ...
    return this;
}

public static void execute(Runnable runnable) {
    sDefaultExecutor.execute(runnable);
}

看一下SerialExecutor的实现

private static class SerialExecutor implements Executor {
    // 存储待执行的异步任务
    final ArrayDeque mTasks = new ArrayDeque();
    Runnable mActive;

    public synchronized void execute(final Runnable r) {
        // 其实并没有马上执行,而是添加到队列mTasks中, 进行一个排队
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    // 一个任务执行完后,再执行下一个
                    scheduleNext();
                }
            }
        });

        // 当前没有异步任务执行时,启动开始执行
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            // 使用另外一个线程池分配线程,并执行任务
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

所以在使用AsyncTask执行异步操作时,会先在SerialExecutor进行一个顺序排队, 后再用ThreadPoolExcutor线程池为你分配一个线程并执行。而整个应用的AsyncTask任务都在排同一条队,有可能等待排队的任务很多,所以一般不会使用AsyncTask执行一些优先级比较高的异步任务。
当然我们是可以跳过不需要进行排队,直接就通过线程池分配一个线程并执行异步任务,但需要注意同时执行太多的异步任务,会影响用户体验,我想Google就是为了限制同时创建太多的线程才会采用一个排队机制的

/** @hide */
public static void setDefaultExecutor(Executor exec) {
    sDefaultExecutor = exec;
}

该方法是隐藏,但可使用反射,设置一个线程池。

5、Loader&LoaderManager

上面三种异步方式都可以用来加载一些耗时的数据,但有时我们加载数据的过程与Activity、Fragment的生命息息相关的。所以在使用上面说的那几种异步方式进行异步数据加载时,是需要去考虑Activity(Fragment)的生命周期是处于哪个阶段的。于是Android在Android 3.0以后引入了LoaderManager,主要用于执行一些耗时的异步数据加载操作,并根据Activity生命周期对异步处理进行调整,LoaderManager可以解决的问题包括:

  1. 加载的数据有变化时,会自动通知我们,而不自己监控数据的变化情况,如:用CursorLoader来加载数据库数据,当数据库数据有变化时,可是个展示变化的数据
  2. 数据的请求处理时机会结合Activity和Fragment的生命周期进行调整,如:若Acivity销毁了,那就不会再去请求新的数据

使用该方法加载数据涉及到两个类重要的类,Loader和LoaderManager:

Loader:该类用于数据的加载 ,类型参数D用于指定Loader加载的数据类型

public class Loader {
}

一般我们不直接继承Loader,而是继承AsyncTaskLoader,因为Loader的加载工作并不是在异步线程中。而AsyncTaskLoader实现了异步线程,加载流程在子线程中执行。注意:对该类的调用应该在主线程中完成。

LoaderManager:
LoaderManager用于管理与Activity和Fragment关联的Loader实例,LoaderManager负责根据的Activity的生命周期对Loader的数据加载器进行调度,所以这里分工明确,Loader负责数据加载逻辑,LoaderManager
负责Loader的调度,开发者只需要自定义自己的Loader,实现数据的加载逻辑,而不再关注数据加载时由于Activity销毁引发的问题。

注意:其实AsyncTaskLoader内部实现异步的方式是使用AsyncTask完成的,上面我们说过AsyncTask的内部是有一个排队机制,但AsyncTaskLoader内部使用AsyncTask进行数据异步加载时,异步任务并不进行排队。而直接又线程池分配新线程来执行。

6、总结

我们来总结一下异步处理的方式,以及每种处理方式适合什么样的场景

  • 直接使用Thread实现方式,这种方式简单,但不是很优雅。适合数量很少(偶尔一两次)的异步任务,但要处理的异步任务很多的话,使用该方式会导致创建大量的线程,这会影响用户交互。
  • HandlerThread,这种方式适合子线程有序的执行异步操作,异步任务的执行一个接着一个。
  • AsyncTask, 通常用于耗时的异步处理,且时效性要求不是非常高的那种异步操作。如果时效性要求非常高的操作,不建议使用这个方式,因为AsyncTask的默认实现是有内部排队机制,且是整个应用的AsyncTask的任务进行排队,所以不能保证异步任务能很快的被执行。
  • LoaderManager,当请求处理时机需要根据Activity的生命周期进行调整,或需要时刻监测数据的变化,那LoaderManager是很不错的解决方案。

你可能感兴趣的:(Android-多线程)