1-6.1 Handler 工作原理简析及手写自定义Handler

[TOC]

1. Handler概述

Handler结构.png

Handler在安卓使用当中主要用于异步消息的处理,主要完成的功能:

  • 处理延时任务
  • 线程间通信

2. 工作原理简析(一起送快递)

为帮助理解记忆,这里将handler的工作过程与快递的运输做上对比。handler在开发使用当中扮演的角色就好比作快递小哥,使用handler发送及处理消息就类似与小哥接收我们的快递或是送达我们的快递,在完成“收发快递”的工作当中,“小哥”背后还有着很多重要的角色来帮助我们实现该功能。

2.1 工作原理简述(如何送快递?)

  • Handler ≈ 快递小哥
  • MessageQueue ≈ 快递仓库
  • Looper ≈ 快递分发流水线
  • Thread ≈ 快递公司
  • Message ≈ 快递

使用handler发送消息,handler会将该消息放入消息队列MessageQueue(小哥取件完成),Looper启动并根据消息队列中消息的时间来决定分发的时间(快递分拣),取出消息并分发至handler进行消息处理(这里有点不同的是,发送及处理消息的handler是同一个,但同一个快递的取件小哥和送件小哥一般不是同一个,handler在将消息放入队列当中时将自身的引用也放至消息中,在分发处理时保证了同一个handler来处理该消息)

2.2 主要相关类(谁在帮你干活?)

1. Handler(快递小哥做什么?)

  • sendMessage ≈ 收快递
    handler拿来用就是为了发消息,小哥上门自然就是取快递嘛,当然发消息也有各种姿势

    Handler发送消息.png
  • enqueueMessage ≈ 放到仓库
    handler将消息放到消息队列,小哥自然是将快递放进仓库等待处理了

  • dispatchMessage ≈ 小哥被派去送快递

  • handleMessage ≈ 小哥送件上门
    消息已送到,接下来处理就看我们的了;小哥送完收工

2. MessageQueue(快递仓库做什么?)

  • enqueueMessage ≈ 收件存储
  • next ≈ 取出第一个需要出库处理的快递

3. Looper(流水线做什么?)

  • prepare ≈ 准备一条能工作的流水线
  • loop ≈ 启动流水线
    启动之后,队列中的消息就会进行取出分发(由消息绑定的handler分发处理);快递分拣完成,小哥开始送件

4. Thread (快递公司做什么?)

当然是提供环境给实现消息功能啦;没有快递公司,小哥也没有了。

5. Message(快递做什么?)

消息就是记录信息等待被发送处理;作为一个快递,当然只能被,不能做啦!

3. 仿照源码通过自定义Handler来熟悉各功能重点

3.1 功能正常工作的基础 ——CustomLooper、CustomMessageQueue

想要发送消息,势必要有储存消息的消息队列及循环处理分发消息的Looper;handler在使用过程中是存在多线程交互的,这意味着,handler在其他任意子线程中发送的消息都会被存储到当前线程操作的消息队列中,这里要求第一个重点:MessageQueue与Looper的唯一性;这个唯一性由ThreadLocal来确定looper的唯一,由looper的唯一来确定MessageQueue的唯一这就保证了它的正常工作基础。第二个重点:Looper中有循环对消息队列取消息的操作,这是个死循环;这个死循环的阻塞需要由MessageQueue完成;

  • CustomLooper

    1. 确保操作的Looper、MessageQueue唯一
    2. 完成准备、启动循环操作功能
    /**
     * Created by SJ on 2020/2/10.
     */
    public class CustomLooper {
        public CustomMessageQueue mQueue;
        public static ThreadLocal<CustomLooper> sThreadLocal = new ThreadLocal<>();
        private CustomLooper(){
            //只会初始化一次,保证当前线程Looper操作的MessageQueue唯一
            mQueue = new CustomMessageQueue();
        }
        /**
         * 准备工作
         * 初始化当前线程Looper
         */
        public static void prepare(){
            //保证当前线程的Looper唯一
            if (sThreadLocal.get()!=null) {
                throw new RuntimeException("线程只能有一个CustomLooper");
            }
            sThreadLocal.set(new CustomLooper());
        }
        /**
         * 获取当前线程Looper
         */
        public static CustomLooper myLooper() {
            return sThreadLocal.get();
        }
        /**
         * 启动looper,开始操作MessageQueue
         */
        public static void loop(){
            final CustomLooper myLooper = myLooper();
            final CustomMessageQueue mQueue = myLooper.mQueue;
            for (;;){
                CustomMessage message = mQueue.next();
                if (message != null) {
                    message.target.dispatchMessage(message);
                }
            }
        }
    }
    
  • CustomMessageQueue

    1. 实现消息的插入、取出
    2. 能够在无消息要被处理实现阻塞
    3. 消息的插入、取出是一个生产者-消费者模型,子线程操作插入、当前操作线程取出

    在这里是通过BlockingQueue(Java 提供的阻塞队列)实现生-消模型及阻塞功能,源码中不是,源码中还有更多细致的操作,例如根据消息需要处理时间的插入,取消息时的对比时间,存取时的互相唤醒等这些功能,这些之后在叙述

    /**
     * Created by SJ on 2020/2/10.
     */
    public class CustomMessageQueue {
    
        BlockingQueue<CustomMessage> queue;//实现仓库的阻塞功能
        public static final int MAX_COUNT = 10;
        public CustomMessageQueue() {
            queue = new ArrayBlockingQueue<>(MAX_COUNT);
        }
        /**
         * 往消息队列添加消息
         * @param message
         */
        public void enqueueMessage(CustomMessage message) {
            try {
                queue.put(message);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        /**
         * 从消息队列取消息
         * @return
         */
        public CustomMessage next() {
            CustomMessage message = null;
            try {
                message = queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return message;
        }
    }
    

3.2 开始工作——CustomHandler、CustomMessage

基础工作环境都搭好了,现在便是进行工作了。这里一个重点:发送及处理消息的handler必须为同一个

  • CustomHandler

    1. 发送、处理消息
    2. 插入,派发消息
    /**
     * Created by SJ on 2020/2/10.
     */
    public class CustomHandler {
        final CustomLooper looper;
        final CustomMessageQueue messageQueue;
        public CustomHandler() {
            //获取当前线程唯一的Looper
            this.looper = CustomLooper.myLooper();
            //获取当前线程唯一的MessageQueue
            this.messageQueue = looper.mQueue;
        }
        /**
         * 发送消息 
         */
        public void sendMessage(CustomMessage customMessage) {
            enqueueMessage(customMessage);
        }
        /**
         * 插入消息至消息队列
         * @param message
         */
        public void enqueueMessage(CustomMessage message) {
            //将handler与消息做绑定
            message.target = this;
            messageQueue.enqueueMessage(message);
        }
        /**
         * 分发消息
         * @param message
         */
        public void dispatchMessage(CustomMessage message) {
            handleMessage(message);
        }
        /**
         * 处理消息
         * @param message
         */
        public void handleMessage(CustomMessage message) {
    
        }
    }
    
  • CustomMessage

    1. 记录信息
    2. 绑定当前的操作Handler,保证操作Handler唯一
    /**
     * Created by SJ on 2020/2/10.
     */
    public class CustomMessage {
        public CustomHandler target;
        Object object;
        public CustomMessage() {
        }
        public void setObject(Object object) {
            this.object = object;
        }
        @NonNull
        @Override
        public String toString() {
            return object.toString();
        }
    }
    

3.2 自定义Handler工作流程

  1. Looper先准备
  2. 发送消息之后不要忘记启动
CustomLooper.prepare();
final CustomHandler customHandler = new CustomHandler() {
    @Override
    public void handleMessage(CustomMessage message) {
        Log.e(TAG, "receive:" + message.toString());
    }
};
for (int i = 0; i < 10; i++) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                CustomMessage message = new CustomMessage();
                String msg = Thread.currentThread().getName()+":第" + i + "次交互:send " + UUID.randomUUID().toString();
                Log.e(TAG, msg);
                message.setObject(msg);
                customHandler.sendMessage(message);
            }
        }
    }).start();
}
CustomLooper.loop();

4. 补充

4.1 ThreadLocal

ThreadLocal是为解决多线程程序的并发问题的一种解决方案

它为每个使用该变量的线程提供独立的变量副本,每个线程都可以独立修改自己的副本并不会影响其它线程所对应的副本

可以简单将ThreadLoca当成一种特殊的HashMap,它的key固定为Thread,通过get和set方法来对Value做修改

4.2 主线程使用handler不用Looper准备和启动的原因

在ActivityThread的Main方法中已经准备和启动了

主线程Looper准备与启动.png

4.3 自定义Handler中死循环中阻塞会导致应用无响应,为什么主线程中Looper死循环不会?

主线程loop结束.png

主线程loop在取不到消息时会 退出循环,继而抛出throw new RuntimeException("Main thread loop unexpectedly exited");异常,退出应用,所以说应用在运行时,主线程的loop时会不断的接收消息处理消息的。

Android整体的交互都是有事件驱动的,looper.loop会不断的接收事件处理事件,如果它停止了应用也就停止了,所以一旦出现了ANR,说明是某个消息或者对某个消息的处理超时,阻塞了Looper.loop,从而造成ANR

自定义中死循环阻塞超时导致了主线程的loop阻塞超时引起ANR

而主线程中死循环正常运行是程序正常运行,相反它的死循环被阻塞超时则会引起ANR

4.4 子线程能更新UI的特殊情况

  • onResume执行之前更新UI
    主线程更新UI的检测是通过ViewRootImp的checkThread方法来检查的;
    ViewRootImpl是在onResume之后创建的
  • surfaceView
    SurfaceView在子线程刷新不会阻塞主线程,适用于界面频繁更新、对帧率要求较高的情况:相机预览,游戏

4.5 Handler使用当中的补充

  • Message的发送优化
    享元设计模式,使用obtainMessage 方便回收复用Message,避免新建太多耗费内存

  • 子线程创建Handler时需要准备Looper

  • 注意Handler使用不当造成的OOM
    为什么存在内存泄漏?

    //可能存在泄漏写法
    CustomHandler customHandler = new CustomHandler() {
        @Override
        public void handleMessage(CustomMessage message) {
            Log.e(TAG, "receive:" + message.toString());
        }
    };
    //原因:当使用内部类(包括匿名内部类)来创建Handler的时候,Handler对象会隐式的持有一个外部类对象(一般是Activity)的引用(处理消息时通常会操作Activity的方法或View),如果此时一个很耗时的后台线程在完成任务后通过消息机制通知handler(线程此时持有handler的引用)且Activity此时已被关闭,这就导致应该被销毁回收的Activity无法被GC回收,即内存泄漏;同理,在使用handler进行延时任务时,同样情况下也会造成内存泄漏。最终引起占用内存过高导致OOM
    //解决办法:使用静态内部类和弱引用。静态类不持有外部类的对象,所以Activity可以随意被回收。由于Handler不再持有外部类对象的引用,导致程序不允许在Handler中操作Activity中的对象了。所以你需要在Handler中增加一个对Activity的弱引用(WeakReference)
    private static class MyHandler extends CustomHandler{
        WeakReference<Activity> weakReference;
    
        public MyHandler(Activity activity) {
            this.weakReference = new WeakReference<>(activity);
        }
    
        @Override
        public void handleMessage(CustomMessage message) {
    
        }
    }
    //其他方案:主动对Handler进行管理,在页面销毁时主动对耗时线程关闭,对延时任务取消
    
  • 注意使用Handler容易造成空指针异常
    handler在接收到消息进行处理时可能页面已经被销毁,引用资源找不到

5. 参考代码

SJDemoApp-important-handler

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

推荐阅读更多精彩内容