Handler深入(分析源码,手写一套Handler)

前言

项目结构

在安卓当中提供了异步消息处理机制的两种方式来解决线程之间的通信,一种是是AsynchTask,另外一种就是现在我们主要分析的Handler。

Handler是Android类库提供的用于接受、传递和处理消息或Runnable对象的处理类,它结合Message、MessageQueue和Looper类以及当前线程实现了一个消息循环机制,用于实现任务的异步加载和处理。

简单使用分析

总所周知,安卓中子线程是不能更新UI的,如果在子线程更新,那么程序就会崩溃,那么这时候我们就使用到了handler,子线程操作完成通知主线程更新UI。我们先看下handler机制的分析图,和架构图:

handler流程图

Handler架构图
  • Looper有一个MessageQueue消息队列;
  • MessageQueue有一组待处理的Message;
  • Message中有一个用于处理消息的Handler;
  • Handler中有Looper和MessageQueue。

一个Handler对应一个Looper对象,一个Looper对应一个MessageQueue对象,使用Handler生成Message,所生成的Message对象的Target属性,就是该Handler对象。而一个Handler可以生成多个Message,所以说,Handler和Message是一对多的关系。

Android中主线程向子线程发送消息

1. 创建Handler

在安卓的ui线程中创建一个Handler

 Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        Log.e("TAG", "当前线程: " + Thread.currentThread().getName() + " 收到消息:" + msg.obj);
        }
    };
2. 开启子线程向主线程的handler发送消息
bt.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Message obtain = Message.obtain();
                obtain.obj = Thread.currentThread().getName() + ": 发送消息";
                handler.sendMessage(obtain);
            }
        }).start();
    }
});
3. 结果分析

结果:

当前线程: main  消息:Thread-4: 发送消息

结果说明我们在主线程中创建handler,然后点击按钮子线程向主线程发送消息成功

==问题==:

在一个activity中如上使用handler,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用,通常handler都伴随着一个耗时线程出现,例如访问网络,访问成功,使用handler刷新界面。但是在访问网络的时候这时候activity关闭了,这时候handler依然持有activity的引用,导致内存泄露了,当过多的内存泄露,导致程序OOM了,是不是感觉handler还是有点坑?

==答案==

  • 方法一:通过程序逻辑来进行保护。

    1. 在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。
    2. 如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。
  • 方法二:将Handler声明为静态类。

      PS:在Java 中,非静态的内部类和匿名内部类都会隐式地持有其外部类的引用,静态的内部类不会持有外部类的引用。
    

静态类不持有外部类的对象,所以你的Activity可以随意被回收。由于Handler不再持有外部类对象的引用,导致程序不允许你在Handler中操作Activity中的对象了。所以你需要在Handler中增加一个对Activity的弱引用(WeakReference)。代码如下:

static class MyHandler extends Handler
    {
        WeakReference<Activity> mWeakReference;
        public MyHandler(Activity activity) 
        {
            mWeakReference=new WeakReference<Activity>(activity);
        }
        @Override
        public void handleMessage(Message msg)
        {
            final Activity activity=mWeakReference.get();
            if(activity!=null)
            {
                if (msg.what == 1)
                {
                     //刷新UI
                }
            }
        }
    }

分析

我们按照刚才的流程图来 分析一遍。

首先我们在主线程创建了一个Handler,那么在主线程中也会对应有一个Looper对象在轮询消息,一个Looper对象有一个MessageQueue,于是主线程中也有一个MessageQueue。我们的流程就是主线程中looper对象在一直轮询消息,如果消息队列中没有任何消息的话,那么当前线程暂时阻塞,直到子线程中获取handler对象发送消息,这时候,handler会对主线程中的MessageQueue中添加消息,当消息添加成功时,将阻塞的线程唤醒。于是looper轮询到有新消息,将新消息返回给handler对象,因为handler对象是在主线程中创建,所以消息将会在主线程中显示。

这个流程和生产者消费者模型有一点相似,一个线程生成消息,一个线程消费消息。所以在MessageQueue中的添加消息,和消费消息都会有一把锁。将这两个方法锁住;首先避免多个线程同时操作消息列队,和避免再写入消息的时候读取消息,导致消息错乱的问题。如下图,MessageQueue源码中锁住当前对象:

源码
源码
  • 也许这里有些难懂,但是没关系,我们继续向下分析

子线程向主线程发送消息

上面我们操作了子线程向主线线程发送消息,接下来我们使用handler主线程向子线程发送消息。

1. 子线程中创建handler对象。
2. 为当前子线程创建一个looper对象。(这里我们使用ThreadLocal来保存Looper副本)
3. 开启子线程looper轮询消息
    new Thread(new Runnable() {
            @Override
            public void run() {
                //对当前线程创建一个looper副本
                Looper.prepare();
                handler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                        Log.e("TAG", "当前线程: " + Thread.currentThread().getName() + " 收到消息:" + msg.obj);
                    }
                };
                //开启轮询消息
                Looper.loop();
            }
        }).start();
4. 主线程向子线程发送消息
 Message obtain = Message.obtain();
 obtain.obj = Thread.currentThread().getName() + "线程发送的消息";
 handler.sendMessage(obtain);
5. 结果分析

结果如下:

当前线程: Thread-4  收到消息:main线程发送的消息

这时候我们就成功重主线程发送了一条消息给子线程

分析:

我们重上面代码注意到相比子线程发送消息给主线程我们主线程发送消息给子线程多了两行代码:

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

这里我们使用了一个ThreadLocal来保存每一个接收线程中的Looper对象副本。由于子线程是我们手动开启的线程,所以我们要初始化一个looper副本。由于安卓主线程中,安卓系统自动维护了一个安卓主线程的looper对象副本并让looper轮询着消息。

  1. Looper.loop();

我们翻阅源码如下:

 public static void loop() {
     //获取looper对象
     final Looper me = myLooper();
     if (me == null) {
         throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
     }
     //获取messageQueue对象
     final MessageQueue queue = me.mQueue;
                   ...............
     //轮询消息
     for (;;) {
         //轮询messageQueue中的消息,没有消息就再这里阻塞。
         Message msg = queue.next();
         if (msg == null) {
             // No message indicates that the message queue is
             return;
         }
                   ...........
         try {
             //发送消息
             msg.target.dispatchMessage(msg);
                    ............
         } finally {
             if (traceTag != 0) {
                 Trace.traceEnd(traceTag);
             }
         }
                    ............
     }
 }

这里安卓系统也是在主线程中轮询着消息。

手写一套handler。

我们经过上面的使用和简单分析了以后,也许还是有一些懵逼。所以下面我们自己通过生产者/消费者模型来模仿安卓Handler手写一套。代码如下:

Handler

public class Handler {
    private Looper mLooper;
    private MessageQueue mQueue;

    public Handler() {
        //获取当前线程的looper
        mLooper = Looper.myLooper();
        //获取当前线程的消息列队
        mQueue = mLooper.messageQuene;
    }

    /**
     * 发送消息
     * @param message
     */
    public void sendMessage(Message message) {
        message.target = this;
        mQueue.enqueueMessage(message);
    }

    /**
     * 处理消息
     * @param message
     */
    public void handleMessage(Message message) {

    }

    /**
     * 分发消息
     * @param message
     */
    public void dispatchMessage(Message message) {
        handleMessage(message);
    }
}

Looper

public class Looper {
    final MessageQueue messageQuene;
    private static ThreadLocal<Looper> threadLocal = new ThreadLocal<>();

    public Looper() {
        messageQuene = new MessageQueue();
    }

    /**
     * 为当前线程初始化一个looper副本对象
     */
    public static void prepare() {
        if (threadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        threadLocal.set(new Looper());
        System.out.println("looper初始化");
    }

    /**
     * 获取当前线程的looper副本对象
     *
     * @return
     */
    public static Looper myLooper() {
        return threadLocal.get();
    }

    /**
     * 轮询消息
     */
    public static void loop() {
        //获取当前线程的looper对象
        Looper me = myLooper();
        Message msg;
        //开始轮询消息
        for (; ; ) {
            //轮询消息,没有消息就阻塞
            msg = me.messageQuene.next();
            if (msg == null || msg.target == null) {
                System.out.println("Looper:" + "空消息");
                continue;
            }
            System.out.println("Looper:" + "looper轮询到了消息,发送消息");
            //轮询到了消息分发消息
            msg.target.dispatchMessage(msg);

        }
    }
}

Message

public class Message {
    
    //发送的消息
    public Object obj;

    //目标Handler
    public Handler target;

    @Override
    public String toString() {
        return obj.toString();
    }
}

MessageQueue

要实现生产者/消费者模型,首先的有锁,这里使用ReentrantLock
主要考虑的重写入,它可以根据设定的变量来唤醒不同类型的锁,也就是说当我们队列有数据时,我们需要唤醒read锁;当队列有空间时,我们需要唤醒写锁。

public class MessageQueue {

    Message[] mItems;
    int mPutIndex;
    //队列中消息数
    private int mCount;
    private int mTakeIndex;
    //锁
    Lock mLock;
    //唤醒,沉睡某个线程操作
    Condition getCondition;//可取
    Condition addCondition;//可添加


    public MessageQueue() {
        mItems = new Message[50];
        mLock = new ReentrantLock();
        getCondition = mLock.newCondition();
        addCondition = mLock.newCondition();
    }

    /**
     * 消息队列取消息 出队
     *
     * @return
     */
    Message next() {
        Message msg = null;
        try {
            mLock.lock();
            //检查队列是否空了
            while (mCount <= 0) {
                //阻塞
                System.out.println("MessageQueue:" + "队列空了,读锁阻塞");
                getCondition.await();
            }
            msg = mItems[mTakeIndex];//可能空
            //消息被处理后,置空数组中该项
            mItems[mTakeIndex] = null;
            //处理越界,index大于数组容量时,取第一个item
            mTakeIndex = (++mTakeIndex >= mItems.length) ? 0 : mTakeIndex;
            mCount--;
            //通知生产者生产
            addCondition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            mLock.unlock();
        }

        return msg;
    }

    /**
     * 添加消息进队列
     *
     * @param message
     */

    public void enqueueMessage(Message message) {

        try {
            mLock.lock();
            //检查队列是否满了
            while (mCount >= mItems.length) {
                //阻塞
                System.out.println("MessageQueue:" + "队列空了,写锁阻塞");
                addCondition.await();
            }

            mItems[mPutIndex] = message;
            //处理越界,index大于数组容量时,替换第一个item
            mPutIndex = (++mPutIndex >= mItems.length) ? 0 : mPutIndex;
            mCount++;
            //通知消费者消费
            getCondition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            mLock.unlock();
        }
    }
}

测试:

public class Test {
    public static Handler handler;

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                handler = new Handler() {
                    @Override
                    public void handleMessage(Message message) {
                        super.handleMessage(message);
                        System.out.println("Test:" + Thread.currentThread().getName() + "线程接收到:" + message.obj);
                    }
                };
                Looper.loop();
            }
        }).start();
        //睡0.5s,保证上面的线程中looper初始化好了
        Thread.sleep(500);
        new Thread(() -> {
            Message message = new Message();
            message.obj = Thread.currentThread().getName() + "发送的消息 ";
            handler.sendMessage(message);
        }).start();
        new Thread(() -> {
            Message message = new Message();
            message.obj = Thread.currentThread().getName() + "发送的消息 ";
            handler.sendMessage(message);
        }).start();
    }
}

结果分析

结果:

looper初始化
MessageQueue:队列空了,读锁阻塞
Looper:looper轮询到了消息,发送消息
Test:Thread-0线程接收到:Thread-1发送的消息 
Looper:looper轮询到了消息,发送消息
Test:Thread-0线程接收到:Thread-2发送的消息 
MessageQueue:队列空了,读锁阻塞

分析:

到这里我们的手写的一套Handler就完成了。自己手写一次handler消息处理机制,再回过头来看看handler是不是很简单了,再也不怕面试中被问到。当然android源码中的handler处理机制移值到C层处理了.

demo地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容