Android Handler机制(四) Message源码分析

news2024/11/15 11:20:41

一. 简介

        接上一篇文章:Android Handler机制(三) Looper源码分析 ,我们来继续分析一下Message源码

这一系列文章都是为了深入理解Handler机制. Message 作为消息传递的载体,源码主要分为以下

几个部分:

1. 操作数据相关,类似 getter()和 setter()这种方法还有之前提到过的 what 和 obj 这类属性。

2. 创建与回收对象实例相关,除了用关键字 new 外,其他得到对象实例的方法。

3. 其他工具类性质的扩展方法。

整个类代码大概600多行, 接下来就开始一起分析下

二. Message数据属性与方法

我们先看看 Message 源码有哪些可供我们使用的属性

public int what :开发者可自定义的消息标识代码,用于区分不同的消息。

public int arg1: 如果要传递的消息只有少量的 integer 型数据,可以使用 这个属性。

public int arg2 :同上面 arg1。

public Object obj :开发者可自定义类型的传输数据。

上面四个属性作为常用的消息传递的数据载体可直接赋值,例如 msg.arg1 = 100;。基本可以满足我们日常开发中简单消息传递。


如果上面几个数据属性不能满足我们的需求,可以使用扩展数据:Bundle 来传递

源码中见Bundle的方法:

  public Bundle getData() {
        if (data == null) {
            data = new Bundle();
        }
        return data;
    }


public void setData(Bundle data) {
        this.data = data;
    }

这段代码也没什么逻辑好分析的,值得一提就是 Bundle data 不是 public 的,
所以我们不能直接操作这个属性,需要通过上面二个方法操作数据, 举个例子:

Bundle bundle = new Bundle(); 
bundle.putString("String","value"); 
bundle.putFloat("float",0.1f); 
Message msg = Message.obtain(); 
msg.setData(bundle);

关于Bundle的API使用

请查看参考此链接:Bundle API 

三. 创建Message 对象的基本方法

首先来看下Message类的构造方法

/** Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}).
    */
    public Message() {
    }

从注释来看, 官方推荐我们使用Message.obtain()来获取Message对象

那这个 obtain()方法干了什么呢?其实就是内部维持了一个链表形式的 Meesage 对象 缓存池,这样会节省重复实例化对象产生的开销成本。

数据结构中的链表一个单元有两个值,当前单元的值 (head)和下一个单元的地址指针(next),如果下一个单元不存在那么 next 就是 null 

 所以,想要实现 Message 对象链表式缓存池就需要额外的两个 Message 类型的 引用 head 和 next,都说了叫缓存池,所以把 head 叫 pool 更合适一点。

有了链表的基础结构我们再想实例化对象的时候就可以先去链表缓存池中看看 有没有,有的话直接从缓存池中拿出来用,没有再 new 一个.

我们主要来分析一下 Message.obtain()方法

// 用于标识当前对象是否存在于缓存池,0 代表不在缓存池中 
int flags;

/*
这个常量是供上面的 flags 使用的,它表示 in use(正在使用)状态
如果 Message 对象被存入了 MessageQueue 消息队列排队等待 Looper 
处理或者被回收到缓存池中等待重复利用时,那么它就是 in use(正在使 用)状态
只有在 new Message()和 Message.obtain()时候才可以清除掉 flags 上 的 in use 状态
*/
static final int FLAG_IN_USE = 1 << 0;


/*
静态常量对象,通过 synchronized (sPoolSync)让它作为线程并发操作时的锁确保同一时刻只有一个线程可以访问当前对象的引用
*/
private static final Object sPoolSync = new Object();

//当前链表缓存池的入口,装载着缓存池中第一个可用的对象 
private static Message sPool;

// 链表缓存池中指向下一个对象引用的 next 指针 
Message next;

// 当前链表缓存池中对象的数量 
private static int sPoolSize = 0;



/**
     * Return a new Message instance from the global pool. Allows us to
     * avoid allocating new objects in many cases.
从缓存池中拿出来一个 Message 对象给你, 可以让我们在许多情况下避免分配新对象。
     */
    public static Message obtain() {
// 上锁,这期间只有一个线程可以执行这段代码
        synchronized (sPoolSync) {
// pool 不等于空就说明缓存池中还有可用的对象,直接取出来
            if (sPool != null) {
// 声明一个 Message 引用指向缓存池中的 pool 对象
                Message m = sPool;
// 让缓存池中 pool 引用指向它的 next 引用的对象
                sPool = m.next;
// 因为该对象已经从缓存池中被取出,所以将 next 指针置空
                m.next = null;
// 将从缓存池中取出的对象的 flags 的 in use 标识清除掉
                m.flags = 0; // clear in-use flag
// 缓存池中 Message 对象数量减去一个
                sPoolSize--;
                return m;
            }
        }
// 如果缓存池中没有可用的对象就 new一个
        return new Message();
    }

理论上我们希望 sPool 引用指向了链表缓存池中的第一个对象,让它作为整个缓存池的出入口。所以我们把它设置成 static 的,这样它就与实例化出来的对象 无关,也就是说无论我们在哪个 Message 对象中进行操作,sPool 还是 sPool。

再来梳理一下:

静态方法 obtain()的代码逻辑流程:

先判断缓存池是不是空的:if(sPool != null),如果是空的就直接:return new Message();,

不是空的就声明一个引用让它指向缓存池第一个对象:Message m = sPool;,

而缓存池的链表头部 sPool 引用就指向了链表中下一个对象:sPool = m.next;,因为这个时候缓存池中第一个对象已经取出交给了引用 Message m, 所以需要清除掉这个对象身上的特殊标识,

包括缓存池中的 next 引用和用来标 记对象状态的 flags值:m.next = null;  m.flags = 0;,

最后将缓存池中的对象 数量减一:sPoolSize--;

用图表示更简洁一点:

阶段一:   初始状态, sPool 指向链表头部


阶段二: 声明一个Message 引用 m 也指向链表头部

 

阶段三:  让sPool 指向链表下一个对象

 阶段四: 让m引用指向对象的next=null,  flag 清零.

 到这里,就把消息缓存池中的头部消息取出来了

分析到这里我们知道了为什么官方推荐我们使用 Message.obtain()得到对象 了,因为它是在缓存池中取出来重复利用的,但是通过上面代码也看可以看到,只有缓存池里有东西时也就是 sPool != null 的时候才可以取,Message 是怎么 把对象回收到缓存池中的呢?

四. 回收 Message 对象到缓存池的方法

阅读源码后发现有一个 public void recycle()方法用于回收 Message 对象,但 是它也牵扯出了一堆其他方法与属性:

// 缓存池最大存储值 
private static final int MAX_POOL_SIZE = 50;


// 区分当前 Android 版本是否大于或者等于 LOLLIPOP 版本的全局静态变量,默认初始值为 true 
private static boolean gCheckRecycle = true;


/*** 用于区分当前 Android 版本是否大于或者等于 LOLLIPOP 版本 * 内部隐藏方法,在 APP启动时就会执行该方法,开发者是不可见的 * @hide */ 
public static void updateCheckRecycle(int targetSdkVersion) {
        if (targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) {
            gCheckRecycle = false;
        }
    }

//* 判断当前对象的 flags 是否为 in-use 状态
    /*package*/ boolean isInUse() {
        return ((flags & FLAG_IN_USE) == FLAG_IN_USE);
    }




/*
调用这个方法后,当前对象就会被回收入缓存池中。 
你不能回收一个在 MessageQueue 排队等待处理或者正在交付给 Handler 处理的 Message 对象 
说白了就是 in-use 状态的不可回收
*/
public void recycle() {
// 判断当前对象是否为 in-use 状态
        if (isInUse()) {
// 如果当前版本大于或者等于 LOLLIPOP 则抛出异常
            if (gCheckRecycle) {
                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }
// 如果当前版本小于 LOLLIPOP 什么也不干直接结束方法
            return;
        }

// 回收 Message 对象
        recycleUnchecked();
    }



/**
     * Recycles a Message that may be in-use.
     * Used internally by the MessageQueue and Looper when disposing of queued Messages.
     */
    @UnsupportedAppUsage
    void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        //回收缓存池中消息,标记flag为FLAG_IN_USE 
        // Clear out all other details.    
        //其他属性全部清除
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = UID_NONE;
        workSourceUid = UID_NONE;
        when = 0;
        target = null;
        callback = null;
        data = null;

        // 上锁
        synchronized (sPoolSync) {
            // 如果当前缓存池对象中的数量小于缓存池最大存储值(50)就存入缓存池中
            if (sPoolSize < MAX_POOL_SIZE) {
                // 存入缓存池
                next = sPool;
                sPool = this;
                // 缓存池数量加 1
                sPoolSize++;
            }
        }
    }

上面代码的逻辑很清晰,执行 recycle()方法后先判断当前对象是否为 in-use 状 态:if (isInUse()),如果是 in-use 状态的话当前 Android 版本是 LOLLIPOP(5.0) 版本之前直接结束程序,LOLLIPOP及之后版本抛出异常。

如果当前对象不是in-use 状态,那么就执行 recycleUnchecked()方法先将它切换到 in-use 状态:flags = FLAG_IN_USE;   

再把所有的数据属性全部清除,最后把对象存入缓存池链表中。

回收 Message对象 流程图:

 

五. 我们需要手动回收消息嘛?

        现在我们知道了通过执行 recycle()方法回收 Message 对象,但是如果要为每个 Message 对象都进行手动回收岂不是很麻烦?

        庆幸的是开发人员也想到了这一点,从源码中可以看到其实最终真正执行回收操 作的调用 recycleUnchecked()方法,且注释中告诉我们 MessageQueue 和 Looper 内部也会调用该方法执行回收。

         这里先说一个结论,MessageQueue 和 Looper 内部分发处理消息时,当它们得知 当前这个 Message 对象已经使用完毕后就会直接调用 recycleUnchecked()方法 将它回收掉.

MessageQueue中的相关代码:

//MessageQueue.java中 removeMessagexxx 都会调用recycleUnchecked()方法
private void removeAllMessagesLocked() {
        Message p = mMessages;
        while (p != null) {
            Message n = p.next;
            //消息回收
            p.recycleUnchecked();
            p = n;
        }
        mMessages = null;
    }

Looper 中的相关代码:

public static void loop() {

    .....

    for (;;) {
        //从MessageQueue中取出消息
        Message msg = queue.next(); // might block


        try {
        //分发消息
        msg.target.dispatchMessage(msg);
        if (observer != null) {
                    observer.messageDispatched(token, msg);
        }
               

        .....
        //消息分发完之后,回收消息
        msg.recycleUnchecked();

    }

}

         所以,如果我们用实例化 Message 对象是放入 Handler 中去传消息的,那么我们 就不需要手动回收,他们内部自己就回收了。

六. 包含 Handler 参数的 obtain()方法

给 Message 内部装了一个 Handler 起了什么作用呢?首先,我们可以通过上面讲 解我们可以得出以下已知的结论:

1. 表面上看我们使用 Handler 发送消息后,消息直接传回到了 Handler 内部的 handleMessage(Message msg)方法中。

2. 实际上是先把消息传入了 MessageQueue 中,Looper 再从 MessageQueue 依次取出 消息分发给 Handler。

3. Looper 是线程独立的, Looper 和 MessageQueue 是一对一的。 但是,你有没有想过 Looper 和 Handler 是不是一对一的?答案:不是, MessageQueue 只负责队列消息,Looper 只负责取出消息分发。他们的功能很明 确而且通用。 所以,无论当前线程有多少个 Handler,同样都只有一个 Lopper 和一个 MessageQueue。

解释图:

 提个问题:

Question:  既然每个线程只有一个 Looper 和 MessageQueue 的话那么 Looper 分发消息的时 候要如何判断当前这个 Message 是哪个 Handler 的呢?

Answer:  所以开发人员就给 Message内部配置了一个Handler属性,这样Looper分发消息时直接调用Messgae 内部的 Handler 属性就能找到它对应的 handleMessage(Message msg)接收消息的 方法了.

源码很简单,就是在空参方法 obtain()基础上加了个 Handler 属性,还有它的 getter()和 setter():

//handler对象
Handler target;



/**
     * Same as {@link #obtain()}, but sets the value for the <em>target</em> member on the Message returned.
     * @param h  Handler to assign to the returned Message object's <em>target</em> member.
     * @return A Message object from the global pool.
     */

    public static Message obtain(Handler h) {
        Message m = obtain();
        //把handler与Message关联上
        m.target = h;

        return m;
    }


 public void setTarget(Handler target) {
        this.target = target;
    }


 /**
     * Retrieve the {@link android.os.Handler Handler} implementation that
     * will receive this message. The object must implement
     * {@link android.os.Handler#handleMessage(android.os.Message)
     * Handler.handleMessage()}. Each Handler has its own name-space for
     * message codes, so you do not need to
     * worry about yours conflicting with other handlers.
     */
    public Handler getTarget() {
        return target;
    }

还有一个地方也可以找到他们 Message 与 Handler 关联的代码, 就是Handler发送消息的时候就已经关联上了,在 Handler.java文件中:

 //1.发送消息
 public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0);
    }


 //2. 接着调用
public boolean sendMessageAtTime(@NonNull 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);
    }

// 3.消息关联上handler

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        //这句话是两者关联
        msg.target = this;

        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

 

这样子就知道 这个消息的源头是与哪个Handler相关联的,  当然处理消息时,自然就回到了对应线程的Hanlder的 handleMessage方法中了. 这样就保证消息发送和处理的准确性了.


 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/383285.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JAVASE】注解

文章目录1.概述2.JDK内置注解2.1override注解2.2 Deprecated注解3.元注解4.注解中定义属性4.1 属性value4.2 属性是一个数组5. 反射注解6.注解在开发中的作用1.概述 注解&#xff0c;也叫注释&#xff0c;是一种引用数据类型。编译后也同样生成class字节码文件。 语法 [修饰…

QT获取dll库文件详细信息

一、需求背景获取软件下依赖的dll库的版本信息&#xff0c;如下图所示版本为1.0.7.1018二、实现方法2.1步骤windows下实现&#xff0c;基于version.lib(version.dll)提供的函数获取这些信息首先使用GetFileVersionInfoSizeA(W)获取VersionInfo的大小&#xff0c;申请缓冲区&…

团队API管理工具-YAPI

团队API管理工具-YAPI 推荐一款接口管理平台&#xff0c;操作简单、界面友好、功能丰富、支持markdown语法、可使用Postman导入、Swagger同步数据展示、LDAP、权限管理等功能。 YApi是高效、易用、功能强大的api管理平台&#xff0c;旨在为开发、产品、测试人员提供更优雅的接…

stm32智能家居+微信小程序接收控制

这里写目录标题项目介绍mqtt服务器相关知识![在这里插入图片描述](https://img-blog.csdnimg.cn/9ad065fb8fac48b1b975fc3a48b99763.png)下位机代码项目需要的一些开发工具项目介绍 本项目芯片使用STM32F103ZET6,微信小程序开发使用微信开发者工具。 stm32作为下位机&#xff…

【实现点击下载按钮功能 Objective-C语言】

一、实现点击下载按钮功能, 1.接下来,我们再实现另外一个功能,是什么,点击下载按钮吧: 点击下载按钮,是不是要有效果啊, 就是给大家实现这个功能, 首先,我们要实现单击这个效果,是不是要给按钮注册单击事件吧, 请问,这个按钮在哪里啊,是在控制器里面吗,不是,…

Spark性能优化一 概念篇

&#xff08;一&#xff09;宽依赖和窄依赖 窄依赖(Narrow Dependency)&#xff1a;指父RDD的每个分区只被子RDD的一个分区所使用&#xff0c;例如map、filter等 这些算子一个RDD&#xff0c;对它的父RDD只有简单的一对一的关系&#xff0c;也就是说&#xff0c;RDD的每个part…

Linux 系统 /var/log/journal/ 垃圾日志清理

CentOS系统中有两个日志服务&#xff0c;分别是传统的 rsyslog 和 systemd-journal systemd-journald是一个改进型日志管理服务&#xff0c;可以收集来自内核、系统早期启动阶段的日志、系统守护进程在启动和运行中的标准输出和错误信息&#xff0c;还有syslog的日志。systemd…

datax导入到hive的数据量翻倍

现象 mysql->hive 或者oracle->hdfs 源表数据100w 结果hive表数据200w。 这个现象很容易发生&#xff0c;只要你同一时间调度这个json两次。 原因 "writeMode" : "append", "nonconflict","truncate" * append&#xff…

无线WiFi安全渗透与攻防(二)之打造专属字典

系列文章 无线WiFi安全渗透与攻防(一)之无线安全环境搭建 打造专属字典 什么在破解之前先准备专用字典&#xff0c;因为对于一般家庭来说&#xff0c;常用 一个是预共享密钥PSK&#xff0c;一个是PIN码。 也不是所有的路由都开起了PIN码&#xff0c;一般都会开启域共享密钥…

【蓝桥杯嵌入式】定时器实现按键单击,双击,消抖以及长按的代码实现

&#x1f38a;【蓝桥杯嵌入式】专题正在持续更新中&#xff0c;原理图解析✨&#xff0c;各模块分析✨以及历年真题讲解✨都在这儿哦&#xff0c;欢迎大家前往订阅本专题&#xff0c;获取更多详细信息哦&#x1f38f;&#x1f38f;&#x1f38f; &#x1fa94;本系列专栏 - 蓝…

盘点一下那些远程办公的神仙公司

其实远程办公已经有50多年的历史了&#xff0c;这几年&#xff0c;这种工作方式越来越受到大家的喜欢&#xff0c;对于员工来说&#xff0c;工作效率可以大幅提高&#xff0c;节省下来的通勤时间和成本&#xff0c;有更多的时间花在工作上。可以更好的平衡工作与生活。对于公司…

最近我的视频播放浅学总结

因为想做一个类似苹果的同播共享功能&#xff0c;这一段时间对音视频做了一些浅浅的学习&#xff0c;现简单总结记录。 我的需求是找到一个尽可能简单的方案来两人播放一段视频&#xff0c;并且能够进度和操作同步&#xff0c;所以基本不能有延迟&#xff0c;同时能够显示WebV…

12.1 基于Django的服务器信息查看应用(系统信息、用户信息)

文章目录新建Django项目创建子应用并设置本地化创建数据库表创建超级用户git管理项目&#xff08;requirements.txt、README.md、.ignore&#xff09;主机信息监控应用的框架搭建具体功能实现系统信息展示前端界面设计视图函数设计用户信息展示视图函数设计自定义过滤器的实现前…

华为OD机试用Python实现 -【广播服务器】

华为OD机试题 最近更新的博客华为 OD 机试 300 题大纲广播服务器题目输入输出示例一输入输出示例二输入输出Python代码代码编写思路最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题

常见的电脑运行卡顿原因及解决方法

大家在日常使用电脑过程中&#xff0c;会发现多开几个文件就卡顿&#xff0c;其实很多时候都跟C盘长期不清理有关&#xff0c;C盘的内存被下载的软件安装包、页面文件、休眠文件、更新文件等一系列的文件占据。大的文件甚至能占到20-30G&#xff0c;驱动人生就为大家带来几种解…

App防抓包的四种绕过方法(详细)

App防抓包的四种绕过方法简介&#xff1a;1、ssl证书校验&#xff08;https证书校验http请求ssl证书校验&#xff09;方法演示&#xff1a;安卓5.0怎么安装证书演示&#xff1a;安卓7及以上怎么把证书安卓到系统目录2、代理屏蔽3、证书绑定 &#xff08;SSL pinning&#xff09…

Easyrecovery数据恢复软件工作原理及使用介绍教程

Easyrecovery是一款强大的数据恢复软件&#xff0c;它专门解决磁盘数据恢复问题。在计算机世界里&#xff0c;数据丢失经常是一件令人头疼的事情&#xff0c;但是有了Easyrecovery&#xff0c;您可以放心大胆地享受数据备份和恢复的乐趣。EasyRecovery使用Ontrack公司复杂的模式…

(JUC)核心线程 和 救急线程的区别;Executors-固定大小线程池单线程线程池

核心线程 和 救急线程的区别 救急线程是有个生存时间的&#xff0c;它执行完任务了&#xff0c;过了一段时间&#xff0c;没有新任务了&#xff0c;救急线程就会销毁掉&#xff0c;变成结束的状态 核心线程没有生存时间&#xff0c;它执行完任务后&#xff0c;它仍然会被保存…

人机交互(软件工程视角)第一、二章部分题目答案

我认为日常生活中&#xff0c;我们学校的选课系统就在选课的时候就很不方便&#xff0c;具体是这样的&#xff0c;因为本来我们学校的选课的时候服务器负载能力就比较差&#xff0c;大家着急忙慌地选课的时候&#xff0c;很容易因为界面选课控件比较小&#xff0c;从而直接点击…

软测入门(四)Appium-APP移动测试基础

Appium 用来测试手机程序。 测试方面&#xff1a; 功能测试安装卸载测试升级测试兼容测试 Android系统版本不同分辨率不同网络 网络切换、中断测试使用中来电话、短信横竖屏切换 环境搭建 Java安装&#xff08;查资料&#xff09;Android SDK安装&#xff0c;配置 HOME和P…