Android跨线程通讯

作者: joeey_y | 来源:发表于2018-04-18 22:52 被阅读0次

    太长了,不想看?

    在Android开发中,我们需要正确的在不同的线程中开展工作:

    1. 总是在UI线程中响应用户操作以及更新UI
    2. 不要阻塞UI线程,在工作线程中执行耗时操作

    因此,我们需要使用UI线程、工作线程相互配合完成业务。而Android提供的Handler机制能让我们能在不同的线程间传递消息、同步工作。

    在此机制中,Handler负责发送以及执行消息,Looper让一个线程有了循环的读取并执行消息的能力,Message封装了一个消息。默认的Thread类在装配Looper后才能配合Handler工作。

    Android官方也封装了一些类,帮助我们在工作中更轻松的使用UI线程与工作线程相配合:

    • HandlerThread: 已经配置了Looper的Thread类,让我们无需进行重复的Looper装配工作。可以直接作为工作线程使用
    • AsyncTask:大家熟悉的类,让我们专注于业务逻辑本身,无需维护线程。缺点是使用上还是有些不方便。他的内部使用线程池来分配工作线程
    • IntentService:一个能异步处理请求的Service基类,在工作线程中处理完所有请求后会通过stopSelf结束掉自己。阿里的Android开发手册中建议使用IntentService来处理后台耗时操作
    • View.post(Runnable):通过Handler将工作交给UI线程去处理
    • Activity.runOnUiThread(Runnable):该方法会判断当前线程是否是UI线程,如果不是就将Runnable交给UI线程的Handler去执行

    这几个类的内部都通过Handler都将耗时操作交给工作线程、将UI操作交给UI线程,参考他们的内部结构能帮助理解与使用Handler机制。

    使用Handler时一个需要谨慎思考的问题是是否会因为耗时操作的执行或等待执行而导致Activity无法被垃圾回收引发内存泄露。


    我们常常说Handler是Android基础而核心的知识点,用来实现跨线程的消息传递。为了更好的理解Handler机制的设计思想以及作用,我们首先需要简单的了解Android的线程机制。

    Android线程机制

    每个Android应用都有一个被称为主线程的线程,我们也称其为UI线程。应用所有的组件都在UI线程中实例化,系统也通过UI线程去调用控件并且分发事件,因此,各种响应系统事件的回调方法也是在主线程中发生的。举例来说,用户界面的渲染事件、各种手势的回调都在UI线程中触发。

    如果我们在UI线程做一些耗时的操作,那么整个UI都将被阻塞,所有的事件都无法分发,包括绘制事件。在用户的视角看来这时应用卡住了,一旦UI线程阻塞几秒钟,通常是5秒,就会导致系统报出ANR(Application Not Responding)异常。

    此外,Android的UI工具大都不是线程安全的,我们不能在UI线程以外的工作线程去操作UI。

    综上,正确使用Android的线程模型有两条基本原则

    1. 总是在UI线程去操作UI
    2. 不要阻塞UI线程,在工作线程中执行耗时操作

    因此,我们在实际工作中需要规范自己的行为

    1. 在UI线程中响应系统与用户的事件
    2. 将耗时操作交给工作线程去做
    3. 在工作线程中需要更新UI时交给UI线程执行

    为了满足这其中的线程同步与调度的需求,系统为我们设计了Handler机制。

    Handler机制

    一个线程需要支持Message Loop功能才能与Handler配合工作,Message Loop的直译大概就是“消息循环”或者“消息环”,意指线程会循环的执行“读取消息 - 执行消息”的操作。消息可以来自其他线程,也就实现了跨线程通信。

    我们需要使用Looper类使一个Thread类支持Message Loop功能,然后使用Handler类发送消息与执行消息。

    Looper类

    Looper只要两步操作,便能使Thread支持Message Loop功能

    1. prepare:Looper的构造方法是私有的,我们使用静态方法prepare()来为一个线程创建Looper实例,每个线程中只能调用prepare()一次
    2. loop:loop()必须在prepare()后执行,循环的从MessageQueue中取出消息并执行

    我们来看一下源码,首先是Looper.prepare()

        private static void prepare(boolean quitAllowed) {
            if (sThreadLocal.get() != null) {
                throw new RuntimeException("Only one Looper may be created per thread");
            }
            sThreadLocal.set(new Looper(quitAllowed));
        }
        
        // sThreadLocal.get() will return null unless you've called prepare().
        static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    

    prepare()的核心就是设置了一个ThreadLocal对象以保存了一个Looper实例。ThreadLocal对象与普通对象的区别是每个Thread会连接一个ThreadLocal对象,不同Thread间的ThreadLocal对象是相互独立的。

    所以每一个线程都可以有他们自己的Looper对象,通过Looper.prepare()来创建,然而一个线程重复创建会报出异常。

    我们通过Looper.myLooper()获得这个创建出来的Looper对象

        /**
         * Return the Looper object associated with the current thread.  Returns
         * null if the calling thread is not associated with a Looper.
         */
        public static @Nullable Looper myLooper() {
            return sThreadLocal.get();
        }
    

    Looper.myLooper()方法的特点就是让我们获得当前线程对应的Looper对象,由于ThreadLocal变量的特征,在不同线程上调用同样的方法也会返回不同的Looper对象。

    再看下Looper.loop()方法

        public static void loop() {
            // 确保当前线程已经通过prepare()创建过对象
            final Looper me = myLooper();
            if (me == null) {
                throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
            }
            final MessageQueue queue = me.mQueue;
    
            ... 
    
            // 循环取出消息并执行。内部会创建一个Linux管道(Pipe),当消息队列为空时,进入空闲等待状况
            for (;;) {
                Message msg = queue.next(); // might block
                if (msg == null) {
                    // No message indicates that the message queue is quitting.
                    return;
                }
                
                ...
                msg.target.dispatchMessage(msg);
                ...
            }
        }
    

    首先是判断该线程已经创建了Looper对象,Looper对象中保存了一个消息队列(Message Queue)。然后会开启一个永真循环,从队列中读取消息并执行。

    需要注意的是,这里的queue.next并不是说当前没有消息就会返回null,他会阻塞着直到收到消息,只有当消息队列可以退出时才会返回null并结束工作。

    因此loop()方法启动后,线程并进入了循环的等待消息 - 读取消息 - 执行消息的流程。

    作为开发者,我们通常不需要对Looper做额外操作,prepare() + loop()即可

    Handler类

    Handler允许我们发送消息到一个线程的消息队列中,从而在loop过程中被那个线程执行。

    通过Handler,我们可以实现两类需求:

    1. 安排操作在指定的时间执行
    2. 安排操作在指定的线程执行

    我们使用Handler时主要关注三个步骤:

    1. 创建Handler,此时Handler将与一个线程的Looper相关联
    2. 向关联的Looper的消息队列中发送消息,可以使用post方法发送一个Runnable,或者用sendMessage发送一个封装过的消息(Message)
    3. 当消息从消息队列中取出时,需要由Handler去执行他。如果是Runnable对象会直接执行,如果是Message则由Handler.handleMessage(Message)执行

    然后我们看下Handler在源码中的实现。

    构造方法

    构造过程中Handler需要绑定一个Looper,如果不显式设置的话则会绑定到当前线程的Looper上。绑定Looper是为了确认该Handler所对应的消息队列。

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            // 当前线程需要装配了Looper才能使用
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
    

    发送消息

    Handler发送消息有send message与post runnable两类。

    我们首先看下send message,send message类型的方法发送的消息是Message类,这是一个消息的封装类,官方推荐我们使用Message.obtain()来获取Message实例,从而避免重复创建的资源消耗。如果我们只是需要标识消息而无需传输额外的信息则可以发送一个只携带了what的空消息。

        public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
            // 获得一个Message实例
            Message msg = Message.obtain();
            msg.what = what;
            return sendMessageDelayed(msg, delayMillis);
        }
    
        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
            MessageQueue queue = mQueue;
            if (queue == null) {
                RuntimeException e = new RuntimeException(
                        this + " sendMessageAtTime() called with no mQueue");
                Log.w("Looper", e.getMessage(), e);
                return false;
            }
            return enqueueMessage(queue, msg, uptimeMillis);
        }
        
        private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
            // Message的执行目标就是当前Handler
            msg.target = this;
            ...
            // 将Message塞入MessageQueue中
            return queue.enqueueMessage(msg, uptimeMillis);
        }
    

    基本的流程就是准备Message - 检查Looper已准备好 - 确定执行时间 - Message的执行目标是当前Handler - 塞入Looper对应的Message Queue

    而在此前Looper的解析中,我们看到当消息队列中取出一个消息是这样执行的:

        msg.target.dispatchMessage(msg);
    

    我们看到进入消息队列时,Message的target会设置为了当前的Handler实例,所以这个Message的执行方法也被定义在了Handler中,下一部分会进行分析。

    另一种发送方式则是post Runnable,这种方法直接发送了需要完成的任务。

        public final boolean postAtTime(Runnable r, long uptimeMillis)
        {
            // 其实还是send message,只是帮我们做了封装
            return sendMessageAtTime(getPostMessage(r), uptimeMillis);
        }
        
        private static Message getPostMessage(Runnable r) {
            Message m = Message.obtain();
            // 这个是此前send message没有设置的
            m.callback = r;
            return m;
        }
    

    post runnable只是在内部将Runnable对象封装成了一个Message然后再走send message的逻辑,这里需要关注的是将Message的Callback设置为了我们的Runnable对象。

    执行消息

    我们的消息总是会被封装成一个Message类并发送到消息队列中,而从队列中取出后,则会调用发送他的Handler的dispatchMessage(Message)方法,我们看下源码:

        /**
         * Handle system messages here.
         */
        public void dispatchMessage(Message msg) {
            if (msg.callback != null) {
                // post runnable 在此执行
                handleCallback(msg);
            } else {
                // send message 在此执行
                if (mCallback != null) {
                    if (mCallback.handleMessage(msg)) {
                        return;
                    }
                }
                handleMessage(msg);
            }
        }
        
        private static void handleCallback(Message message) {
            message.callback.run();
        }
        
        /**
         * Subclasses must implement this to receive messages.
         */
        public void handleMessage(Message msg) {
        }
    

    源码还是很简单的,没什么需要特别解析的,综合对Handler的代码分析,我们可以看出来Handler是提供给我们在开发过程中直接定制与操作的,我们需要做的工作:

    1. 使用Handler向指定线程发送消息,使得消息在指定线程执行
    2. 确定消息的类型与数据,以及消息的执行时间
    3. 在Handler的子类中声明接收到消息时如何去执行

    防止内存泄露

    由于在消息进入队列时会执行设置消息目标的操作,即

        // Message的执行目标就是当前Handler
        msg.target = this;
    

    因此消息进入消息队列后Loop会持有Handler的引用,而在消息执行中Handler本身就是活跃的。若是Handler持有了Activity的引用,那即使Activity销毁GC也无法将其回收,导致内存泄露。

    解决方案

    1. Activity销毁时也处理掉Handler
    2. 若Handler是内部类,则要设为静态内部类,且使用弱引用来获得Activity,避免GC无法回收Activity

    实际应用

    跨线程消息传递的实际应用在SDK中并不少见,我们简单的了解他们的使用场景以及他们是如何使用Handler机制实现的。

    App UI线程响应操作

    当一个应用启动时,会在ActivityThread类的main方法中启动MainLooper,并启动loop

    public static void main(String[] args) {
        ...
        Looper.prepareMainLooper();
    
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
    
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        
        AsyncTask.init();
        if (false) {
            Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread"));
        }
    
        Looper.loop();
        ...
     }
    

    启动loop以后,主线程就开始响应各种消息。

    Activity的生命周期回调就是在接收到消息后执行的。在ActivityThread的内部类ApplicationThread中,定义了一个Handler的子类,会在接收到消息时在主线程执行相应方法。这是系统内部使用Handler的一个例子。

    而作为开发者,我们可以使用View.post(Runnable)或者Activity.runOnUIThread(Runnable)使操作在UI线程执行,他们的内部逻辑也比较简单。

        public boolean post(Runnable action) {
            final AttachInfo attachInfo = mAttachInfo;
            if (attachInfo != null) {
                return attachInfo.mHandler.post(action);
            }
    
            // Postpone the runnable until we know on which thread it needs to run.
            // Assume that the runnable will be successfully placed after attach.
            getRunQueue().post(action);
            return true;
        }
    

    如果已经确定了执行者就直接post runnable,否则先储存起来。

        public final void runOnUiThread(Runnable action) {
            if (Thread.currentThread() != mUiThread) {
                mHandler.post(action);
            } else {
                action.run();
            }
        }
    

    判断当前线程是否是UI线程,若不是则交给UI线程对应的Handler去post runnable。

    HandlerThread

    Android系统只提供给了我们UI线程,我们需要自己创建工作线程。然而普通的Thread并不具有消息队列,无法配合Handler使用,必须为其配置Looper并启动loop。HandlerThread是系统为我们提供的已经配置了Looper的Thread子类,能直接作为工作线程使用,有利于减少重复工作且避免犯错。

        @Override
        public void run() {
            ...
            Looper.prepare();
            synchronized (this) {
                mLooper = Looper.myLooper();
                ...
            }
            ...
            onLooperPrepared();
            Looper.loop();
            ...
        }
        
        public boolean quit() {
            Looper looper = getLooper();
            if (looper != null) {
                looper.quit();
                return true;
            }
            return false;
        }
    

    这个Thread开始运行后便通过启动Looper的loop来接收消息,在结束时也结束Looper。

    AsyncTask

    AsyncTask大家都非常熟悉了,是Android为我们提供的完成异步工作的类,他的优点:

    1. 让我们将工作分为后台处理、进度同步以及结果展示三个部分,代码清晰分离
    2. 让我们的开发只关注于业务部分,无需负责线程的维护

    我们刨除掉一部分细节,看下AsyncTask中后台、前台工作的切换

        public AsyncTask(@Nullable Looper callbackLooper) {
            ...
            mWorker = new WorkerRunnable<Params, Result>() {
                public Result call() throws Exception {
                    Result result = null;
                    try {
                        result = doInBackground(mParams);
                    } finally {
                        postResult(result);
                    }
                    return result;
                }
            };
            ...
        }
    

    我们通过一个Runnable来封装后台操作,并在完成后调用postResult

        // 1. 我们向一个内部Handler发送消息
        private Result postResult(Result result) {
            @SuppressWarnings("unchecked")
            Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
                    new AsyncTaskResult<Result>(this, result));
            message.sendToTarget();
            return result;
        }
        
        // 2. 默认情况下这个Handler会被我们与UI线程的Looper相绑定
        //    由此显示结果的操作将在UI线程执行
        private static Handler getMainHandler() {
            synchronized (AsyncTask.class) {
                if (sHandler == null) {
                    sHandler = new InternalHandler(Looper.getMainLooper());
                }
                return sHandler;
            }
        }
    

    这里我们能看到结果显示的部分交给了UI线程去完成,我们再看下AsyncTask是如何安排工作线程来启动工作的:

        @MainThread
        public final AsyncTask<Params, Progress, Result> execute(Params... params) {
            return executeOnExecutor(sDefaultExecutor, params);
        }
        
        private static void executeOnExecutor() {
            // 在UI线程中执行预显示
            onPreExecute();
            // 为Worker设置了参数,mFuture就是对Worker的封装,执行mFuture
            mWorker.mParams = params;
            // 默认的执行者是一个SerialExecutor
            exec.execute(mFuture);
        }
        
        // Worker的执行者,顺序执行后台任务
        private static class SerialExecutor implements Executor {
            final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
            Runnable mActive;
    
            public synchronized void execute(final Runnable r) {
                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代表工作线程是从线程池中取出的
                    THREAD_POOL_EXECUTOR.execute(mActive);
                }
            }
        }
    

    主流程还是比较清晰的

    1. 在UI线程做一些预显示的操作
    2. 从线程池中取出工作线程,完成后台任务
    3. 后台任务完成后,通过Handler在UI线程上显示结果

    AsyncTask内部虽然精密,但实际使用过程中比较麻烦,需要创建子类、传入三个泛型,因此工作中可能使用自己封装的更易用的异步任务类来代替,这时AsyncTask也有很大的参考意义,使用线程池等操作都是我们可以参考的

    IntentService

    一个能异步处理请求的Service基类,在工作线程中处理完所有请求后会通过stopSelf结束掉自己。阿里的Android开发手册中建议使用IntentService来处理后台耗时操作。

        @Override
        public void onCreate() {
            // TODO: It would be nice to have an option to hold a partial wakelock
            // during processing, and to have a static startService(Context, Intent)
            // method that would launch the service & hand off a wakelock.
    
            super.onCreate();
            // 使用HandlerThread作为工作线程
            HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
            thread.start();
    
            // 启动一个自定义的Handler
            mServiceLooper = thread.getLooper();
            mServiceHandler = new ServiceHandler(mServiceLooper);
        }
    
        @Override
        public void onStart(@Nullable Intent intent, int startId) {
            // 将Intent作为消息的内容发送出去
            Message msg = mServiceHandler.obtainMessage();
            msg.arg1 = startId;
            msg.obj = intent;
            mServiceHandler.sendMessage(msg);
        }
    

    使用一个HandlerThread作为工作线程,接收到Intent后交给工作线程去执行

        private final class ServiceHandler extends Handler {
            public ServiceHandler(Looper looper) {
                super(looper);
            }
    
            @Override
            public void handleMessage(Message msg) {
                // 空方法,我们可以覆盖实现
                onHandleIntent((Intent)msg.obj);
                // Service结束
                stopSelf(msg.arg1);
            }
        }
    

    作为开发者,我们只需要继承IntentService并实现onHandleIntent(Intent)便可,操作完成后Service会结束掉自己。

    参考资料

    相关文章

      网友评论

        本文标题:Android跨线程通讯

        本文链接:https://www.haomeiwen.com/subject/ryjmkftx.html