Android视图手册之Handler机制

第二篇 Handler机制

如果问我21世纪最大的进步是什么,我觉得是便捷的生活,通过手机我们可以直接下单想要购买的商品,然后静等快递小哥把它们送到家门,我们只要签收即可,电饭煲让我们摆脱旧时代的柴火,无需旁人照看,就可以在煮好饭的时候提醒你,待你忙完手头的工作,就能直接享用香喷喷的米饭了。总而言之,原来一个人需要去做的各类事情,我们把它剥离出去,除了吃饭睡觉这些必须自己亲自操作的事情,其他你都可以让别人或者一些家居家电来实现。
为了能让你知道米饭已经做好了,快递已经送达了,都需要通过某种发生通知你,Handler在Android的线程中正是充当这么一个角色,因为你不可能在送快递的同时又在家烧火做饭,所以你把这些活都派给了其他人,这些人就是多线程,那么如何知道他们的工作进度或工作结果呢?比如还有多久饭才煮好,快递已经送达哪里?Google的解决方案让你可以给这些线程派一个经理人(Handler),通过他可以让你在需要的时候了解他们的情况而不需要时刻关注。

概述

如果说Android整个应用进程是一个公司的话,每个线程都代表其中的部门或者其他岗位,而主线程就是公司总裁(CEO),我们可以叫他U总(主线程 一般也叫UI线程),U总自然是负责处理一些重要的工作,包括一些很重要的业务决策,这些工作都是比较直观且一般需要快速决定的。(主线程 控制UI界面的显示,且不能做耗时操作)。U总事务繁忙,所以不是什么活都做,所以他开始招人(创建线程),他先招了个小秘和自己一起工作(此时handler绑定的looper是主线程所在的looper),然后又招了几个程序员帮自己干活(线程),虽然U总很忙,但也经常让小秘去跟进进度,当程序员门完成到了某个进度时就将进度成果发送到小秘的微信上,小秘刷着微信,一收到程序员发来的消息里面记录的成果(MessageQueue 消息队列),就会依次把收到的的项目名称(msg.what)的项目进度(msg.obj)告诉U总,一旦项目弄完,U总手头又没有其他事就可以拿着项目成果去和客户洽谈(界面展示)。
主线程不去做这些耗时操作,且能将子线程里的最终成果告诉主线程,这就是Handler的主要目的。如果你认为线程间需要通信时,就可以通过handler去实现,无论是主线程和子线程间的,还是子线程和子线程间的,都可以通过handler进行通信。但一个handler只能绑定一个线程,就像上面提到的小秘只为U总服务一样,因为也给别人划分业务区吧,就像不同部门的经理,他们之间可以有业务数据的往来和合作,但实际上还是属于不同的工作区域。

角色表

角色 真实身份 作用 备注
U总 主线程 一般也叫UI线程 主要处理一些Ui事件和负责Broadcast消息的接收,同时也能创建子线程来处理其他工作,如果主线程处理耗时任务,当用户进行ui交互后在超过5秒后未处理,此时会触发ANR 启动应用程序自动开启的主线程
程序员 Thread 子线程 主要负责处理一些耗时的工作 如网络请求,文件读写 手动开启的线程
小秘 Handler 处理者 负责线程间的消息传递 ,能够将Message传到消息队列MessageQueue
处理通过Looper发送来的消息
一个线程可以有多个Handler
小秘的工作汇报 Looper 循环器 将在主线程绑定的消息队列的消息进行循环取出 同时一个线程只有一个looper
小秘的微信 MessageQueue 消息队列 负责存放各个线程推送来的消息Message -
小秘收到微信消息 Message 消息 线程间通讯的数据单元,存储要通信的内容 -

主线程Handler业务逻辑图

结合概述里的描述,我将主线程里的Handler的主要工作进行下方的流程进行描述。
在这里插入图片描述

根据这个流程图我们可以将主要工作分为如下的步骤:
1、异步线程通信前准备
此步骤主要在主线程中对Looper对象(处理器),Handler对象,MessageQueue对象(消息队列)的创建,而这些都是主线程相关对象。
2、消息循环和消息发送
子线程通过Handler将消息Message对象发送到主线程绑定的MessageQueue中,同时Looper会不断循环消息队列,一旦从里面取出消息就会发送给创建该消息的处理者(Handler),Handler在handleMessage方法中进行对应的处理操作
3、消息处理
Handler在handleMessage方法中进行对应的处理操作,如果是在主线程创建的Handler,可以执行UI操作。

子线程Handler逻辑图

Handler处理可以将子线程里的消息传递给主线程,子线程间也可以通过Handler互相通信,如下图所示:
在这里插入图片描述

由上图我们需要注意以下几点关系:

  • 1个线程(Thread)只能绑定 1个循环器(Looper),但可以有多个处理者(Handler)
  • 1个循环器(Looper) 可绑定多个处理者(Handler),这些Handler可以是不同线程的
  • 1个处理者(Handler) 只能绑定1个1个循环器(Looper)
  • Handler发送消息的MessageQueue得看创建的时候实际绑定的Looper

Tips:多个线程Handler可以往一个Looper所持有的MessageQueue中发送消息,但Looper所要分发消息的Handler是原先发送消息的,也就是自己发送的消息到最后实际上也是自己处理,原因可看下方小知识。

Handler使用

下面简单回顾下,一个Handler可以如何使用:
1、创建Handler 和 消息处理
通过继承Handler和复写handleMessage方法可实现消息接收后的处理
其中创建的handler实例的作用域,需要包含需要发送消息的线程,否则无效进行消息发送。

    private  Handler handler=new Handler(){
    
    
        @Override
        public void handleMessage(Message msg) {
    
    
           super.handleMessage(msg);// 需执行的操作

        }
    };

2、从线程中发出消息

      Message message=handler.obtainMessage();
      message.what=100;
      message.arg1=i;
      message.obj="消息内容";
      handler.sendMessage(message);

其中Message对象

属性名 类型 作用
message.what int类型 可用来进行发送消息类型的识别
message.arg int类型 可用来传递int型的消息内容,arg1 arg2都可赋值,占用空间会相较于obj小
message.obj Object类型 可用来传递各种类型的消息内容

3、使用post发送和处理消息
除了sendMessage方法,我们也可以通过post分发进行消息发送,只不过post方法是将runner封装成Messgae进行发送和立即处理,其中的工作原理其实是相似的。

    mHandler.post(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                ... // 需执行的操作 
            }
    });

小知识

1、Looper如何判断Message由哪个Handler处理?
通过下方路径进入mHandler.sendMessage(msg)内部的源码里,我们可以发现在实际发送消息时,Message.target为把自己作为属性进行引用传递,Looper在循环到Message消息时会通过该target向指定Handler进行消息分发。

方法路径:
sendMessage(msg)
– sendMessageDelayed(Message msg, long delayMillis)
 — sendMessageAtTime(Message msg, long uptimeMillis)
   ---- enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    
    
                 // 1. 将msg.target赋值为this
                 // 把当前的Handler实例对象作为msg的target属性
                 msg.target = this;
                 // Looper的loop()在消息循环时,会从消息队列中取出每个消息msg,然后执行msg.target.dispatchMessage(msg)去处理消息
                 // 实际上则是将该消息派发给对应的Handler实例        

                // 2. 调用消息队列的enqueueMessage()
                // 即:Handler发送的消息,最终是保存到消息队列
                return queue.enqueueMessage(msg, uptimeMillis);
 }

2、Handler 的内存泄露

当一个对象已经不再使用了,本该被回收时,而有另一个正在使用的对象持有它的引用从而导致对象不再被回收。这种导致了本该被回收的对象而停留在堆内存中,就产生了内存泄漏。

泄露原因
当MessageQueue 存在Message没有被释放时(未处理的消息 / 正在处理消息时),说明有Handler的实例在被Message持有,持有原因由第一个知识点中的Message会把当前的Handler实例对象作为msg的target属性就可以知道。
由于Handler = 非静态内部类 / 匿名内部类(2种使用方式),故又默认持有外部类的引用(即Activity实例),一旦Activity要被回收时,而Handler实例却停留在堆内存中,因此就导致了activity无法回收,进而导致内存泄漏。
在这里插入图片描述

解决方案
1、使用静态内部类
原理:静态内部类不默认持有外部类的引用,从而使得 “未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系 不存在。
具体方案:将Handler的子类设置成静态内部类。此外,还可使用WeakReference弱引用持有外部类,保证外部类能被回收。因为:弱引用的对象拥有短暂的生命周期,在垃圾回收器线程扫描时,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
解决代码:

    // 设置为:静态内部类
    private static class SHandler extends Handler{
    
    

        // 定义 弱引用实例
        private WeakReference<Activity> reference;

        // 在构造方法中传入需持有的Activity实例
        public SHandler(Activity activity) {
    
    
        // 使用WeakReference弱引用持有Activity实例
         reference = new WeakReference<Activity>(activity); }

        @Override
        public void handleMessage(Message msg) {
    
    
        
        }
    }

2、利用外部类的生命周期,清空Handler内消息队列
原理:不仅使得 “未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系 不复存在,同时 使得 Handler的生命周期(即 消息存在的时期) 与 外部类的生命周期 同步
具体方案:当 外部类(此处以Activity为例) 结束生命周期时(此时系统会调用onDestroy()),清除 Handler消息队列里的所有消息(调用removeCallbacksAndMessages(null))
解决代码:

@Override
    protected void onDestroy() {
    
    
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
        // 外部类Activity生命周期结束时,同时清空消息队列 & 结束Handler生命周期
    }

3、快速切换到主线程执行的方案
1、通过创建绑定主线程的Looper的Handler来切换

    new Handler(Looper.getMainLooper()).post(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            //此时已在主线程中
        }
    });

2、 activity.runOnUiThread(Runnable action)方法

public void updateOnUI(Activity activity) {
    
    
        activity.runOnUiThread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                //此时已在主线程中
            }
        });
    }

3、view.post(Runnable action)

public void updateOnUI(View view) {
    
    
        	view.post(new Runnable() {
    
    
        	@Override
      		public void run() {
    
    
      	      //此时已在主线程中
      	    }
    	});
	}

可能遇到的相关问题

1、handler.sendMessage()与handler.post()的区别?
查看Handler的源码可以发现,最终发送至MessageQueue的方法都是Handler中的enqueueMessage(),但是对于Message的处理,在分发时,会将消息的结果返回至其callback 中。
2、一个线程可以有几个Handler?几个Looper?几个MessageQueue?
Handler:多个,由于通过Message的target方法在添加和分发时标注Handler,实现准确的分发,因此同一个线程可以创建多个Handler对象.
Looper:一个,获取Looper需要先调用Looper.prepare()方法,查看下面源码可以发现,如果包含多个Looper对象会抛出异常。

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

MessageQueue:一个,在调用Looper.prepare()时,会判断该线程的Looper是否为空,只有为空的情况才会调用Looper的构造方法,创建MessageQueue,因此只有一个。

private Looper(boolean quitAllowed) {
    
    
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

面试技巧之自我介绍

面试开头让面试者先进行自我介绍已经是固定的流程和步骤了,可在这方面我们可以通过以下几点来提升我们竞争能力:
1、自我介绍时间控制在1-3分钟,注意不要超过3分钟
2、不要只说形容词而没有具体能体现的经历,将自己的经历用数据,成效来表现,比如表达自己的技术能力,可以以时间,技术栈,成效作为关键字来进行描述,如用谷歌CameraX和Detect,在3天左右的时间,封装了一个人脸识别的sdk,包括多人脸和单人脸的展现形式和可以用来作为识别人脸封装的View。
3、把自己的优势说出来,尽量往岗位需求靠拢
4、面试可以适度进行对自己进行包装,但切记不要撒谎

猜你喜欢

转载自blog.csdn.net/number_cmd9/article/details/123977367