Android Handler 机制解析
1、前言
在 Android 开发中,Handler 的机制和运行原理这方面的知识可以说是每个人都需要熟悉的。这不仅是因为 Handler 是 Android 应用的基石之一,也因为 Handler 整体设计上也是十分优秀的。接下来我就梳理总结一下常见的 Handler 相关知识点。
2、基本使用(GPT)
-
创建Handler对象:要使用Handler,首先需要创建一个Handler对象。Handler可以在UI线程或其他线程中创建,但通常在UI线程中创建,以便将消息发送到UI线程。
Handler handler = new Handler();
-
发送消息:要将消息发送到Handler,可以使用Handler的
post
方法或sendMessage
方法。通常,您将使用post
方法执行一个Runnable任务。handler.post(new Runnable() {@Overridepublic void run() {// 在UI线程执行的任务// 可以更新UI元素} });
或者使用
sendMessage
方法:Message message = handler.obtainMessage(); message.what = MY_MESSAGE_CODE; handler.sendMessage(message);
-
处理消息:在Handler所在的线程中,可以覆盖
handleMessage
方法来处理消息。通常,您需要继承Handler
类并重写handleMessage
方法。class MyHandler extends Handler {@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case MY_MESSAGE_CODE:// 处理消息break;// 可以处理更多不同消息类型}} }
-
关联Handler与Looper:Handler需要与Looper(消息循环)关联,以便能够在消息队列中接收和处理消息。通常,UI线程已经有一个与之关联的Looper,所以在UI线程中创建Handler不需要额外配置。但如果您在其他线程中创建Handler,需要先创建一个Looper。
Looper.prepare(); // 创建一个新的Looper Handler handler = new Handler(); // 关联Handler与新的Looper Looper.loop(); // 开始消息循环,必须调用以使Looper活动
-
从后台线程向UI线程发送消息:通常情况下,Handler最常用于在后台线程执行任务后更新UI线程。例如,如果您在后台线程中进行网络请求,请求完成后,可以使用Handler将结果传递给UI线程以更新UI元素。
new Thread(new Runnable() {@Overridepublic void run() {// 后台线程执行任务// ...// 任务完成后,使用Handler将结果传递给UI线程handler.post(new Runnable() {@Overridepublic void run() {// 更新UI}});} }).start();
这些是Android Handler的基本用法。Handler是Android中处理异步任务和多线程通信的重要工具,可以确保UI更新等操作在UI线程中执行,从而避免应用程序崩溃或出现不稳定行为。
3、流程梳理
从 2 中可以看出 Handler 有两种发送信息的方式。第一种是发送 Message;第二种是直接 post runnable。我们分别看下两种方法的源码处理。
3.1 获取 Message 的方式
发送 Message 首先需要获取一个 Message,当然可以直接 new 一个对象出来,但是也可以通过 Message.obtain() 方法来获取一个消息池里面的消息对象。
public static Message obtain() {synchronized (sPoolSync) {if (sPool != null) {Message m = sPool;sPool = m.next;m.next = null;m.flags = 0; // clear in-use flagsPoolSize--;return m;}}return new Message();}
可见这种方式可以减少内存分配和垃圾回收的开销,因为它避免了频繁创建和销毁 Message 对象,而是重复使用已有对象。这在Android中的消息处理机制中非常有用,因为通常会有大量的消息对象需要创建和处理,如Handler中的消息队列。因此,Message.obtain 方法的使用方式类似于享元模式,通过共享可复用的对象来减少系统资源的消耗,提高性能。这有助于更有效地管理Android应用程序中的消息处理。
3.2 压入消息队列
这里面接着往下看,会通过 Message.enqueueMessage 将这个消息压入消息队列中。这里就要说下这个 Looper 的获取方式了。查看代码可以看到 Looper 是从 ThreadLocal 里面获取到的。ThreadLocal 保证了在每个线程内只有一个 Looper 对象。到这里消息已经进入消息队列中了。
final MessageQueue mQueue;public Handler(@Nullable Callback callback, boolean async) {mLooper = Looper.myLooper();if (mLooper == null) {throw new RuntimeException("Can't create handler inside thread " + Thread.currentThread()+ " that has not called Looper.prepare()");}mQueue = mLooper.mQueue;
}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);
}// Looper.java
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();public static @Nullable Looper myLooper() {return sThreadLocal.get();
}
接下来就是取消息的过程,取消息的方法是在 Looper.java 的 loop 方法中,如果是在子线程使用的情况下需要自己手动启动 Looper.loop 方法开启轮询。主线程中则是由系统在 ActivityThread.java 的 main 方法里面为我们开启了轮询。
public static void main(String[] args) {Looper.prepareMainLooper();ActivityThread thread = new ActivityThread();thread.attach(false, startSeq);if (sMainThreadHandler == null) {sMainThreadHandler = thread.getHandler();}Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");
}
接着看 loop 方法,这里面是一个死循环,会一直从消息队列中获取消息。获取到了后会执行 msg.target.dispatchMessage(msg); 方法。可以看到在 android30 里面已经系统已经集成了检测耗时消息的机制(logSlowDelivery 相关代码)。
public static void loop() {final Looper me = myLooper();if (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");}me.mInLoop = true;final MessageQueue queue = me.mQueue;for (;;) {Message msg = queue.next(); // might blockif (msg == null) {// No message indicates that the message queue is quitting.return;}// Make sure the observer won't change while processing a transaction.final Observer observer = sObserver;final long traceTag = me.mTraceTag;long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;if (thresholdOverride > 0) {slowDispatchThresholdMs = thresholdOverride;slowDeliveryThresholdMs = thresholdOverride;}final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);final boolean needStartTime = logSlowDelivery || logSlowDispatch;final boolean needEndTime = logSlowDispatch;if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {Trace.traceBegin(traceTag, msg.target.getTraceName(msg));}final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;final long dispatchEnd;Object token = null;if (observer != null) {token = observer.messageDispatchStarting();}long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);try {msg.target.dispatchMessage(msg);if (observer != null) {observer.messageDispatched(token, msg);}dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} catch (Exception exception) {if (observer != null) {observer.dispatchingThrewException(token, msg, exception);}throw exception;} finally {ThreadLocalWorkSource.restore(origWorkSource);if (traceTag != 0) {Trace.traceEnd(traceTag);}}if (logSlowDelivery) {if (slowDeliveryDetected) {if ((dispatchStart - msg.when) <= 10) {Slog.w(TAG, "Drained");slowDeliveryDetected = false;}} else {if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",msg)) {// Once we write a slow delivery log, suppress until the queue drains.slowDeliveryDetected = true;}}}if (logSlowDispatch) {showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);}if (logging != null) {logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);}// Make sure that during the course of dispatching the// identity of the thread wasn't corrupted.final long newIdent = Binder.clearCallingIdentity();if (ident != newIdent) {Log.wtf(TAG, "Thread identity changed from 0x"+ Long.toHexString(ident) + " to 0x"+ Long.toHexString(newIdent) + " while dispatching to "+ msg.target.getClass().getName() + " "+ msg.callback + " what=" + msg.what);}msg.recycleUnchecked();}
}
这里面 target 在 enqueueMessage 已经设置成了发送 handler。所以执行逻辑会回到 handler 的 dispatchMessage 方法里面。
3.3 消息执行
这里面可以先看一下 post runnable 方法。其实还是发送的 callback 是 runnable 的 Message。所以处理流程都是统一的。
// Handler.java
public void dispatchMessage(@NonNull Message msg) {// 1、如果 msg 存在 callback 则直接执行,post 方式都会走到这里if (msg.callback != null) {handleCallback(msg);} else {// 2、mCallback 不为空则进入这里处理,这个 mCallback 可以通过构造方法传入if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;}}// 3、最后会走到自身 handleMessage 方法,这个方法可以通过继承重写handleMessage(msg);}
}public final boolean post(@NonNull Runnable r) {return sendMessageDelayed(getPostMessage(r), 0);
}private static Message getPostMessage(Runnable r) {Message m = Message.obtain();m.callback = r;return m;
}
走到这里,可以发现我们的 Handler 机制在 Java 层已经完全梳理一遍了。下面继续看下 native 层的部分。这里就引入了一个经典问题:那就是主线程 loop 方法是死循环,系统为什么不会卡死呢?
从源码可以看到 loop 方法里面调用了消息队列的 next 方法,这里面会调用继续调用 native 的 nativePollOnce(ptr, nextPollTimeoutMillis); 方法。这里面会通过 epoll 机制,当等待消息的时候,会释放系统资源,当被唤醒时再继续操作。唤醒操作 nativeWake(mPtr); 。
// 0 立即返回,2000 等待 2s,-1 永久休眠
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,jlong ptr, jint timeoutMillis) {NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}// 底层通过 epoll 的方式监听读端,会进入等待,等待写入端有数据写入
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);// 插入消息的时候,会调用 wake 方法,会写入了 1,唤醒
uint64_t inc = 1;
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
3.4 消息屏障
消息屏障的典型用例是在UI线程中执行UI更新,以确保UI更新的操作按照它们被提交的顺序执行。例如,如果在后台线程中进行了多次UI更新,并将这些更新消息发送到UI线程的消息队列中,可以使用 sendMessageAtFrontOfQueue 方法来确保这些UI更新按照它们被发送的顺序执行,从而避免UI显示的不一致性。通过 sendMessageAtFrontOfQueue 方法会将时间设置为 0,在进入消息队列的过程中,会直接插入到队列头部,所以可以确保执行优先级较高。
3.5 IdleHandler
IdleHandler
是Android中的一个回调接口,它用于在主线程空闲时执行任务。当主线程没有处理消息时(即处于空闲状态),IdleHandler
中的回调方法将被触发,允许您执行一些耗时较长的任务,而不会影响到UI的响应性。这在某些情况下非常有用,例如在后台预加载数据或执行其他非UI相关的工作。
// 创建一个IdleHandler
MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {@Overridepublic boolean queueIdle() {// 在主线程空闲时执行的任务// 可以执行一些耗时操作,不会阻塞UI线程return false; // 返回true表示继续监听,false表示不再监听}
};// 注册IdleHandler
Looper.myQueue().addIdleHandler(idleHandler);
这块处理逻辑是在 MessageQueue 的 next 方法内部。
4、总结
到这里基本梳理了 Handler 的一些使用和原理,虽然各种框架和 Kotlin 都可以很方便的执行切换线程的操作了,但是这些原理性的东西还是值得我们学习并了解的。
相关文章:
Android Handler 机制解析
1、前言 在 Android 开发中,Handler 的机制和运行原理这方面的知识可以说是每个人都需要熟悉的。这不仅是因为 Handler 是 Android 应用的基石之一,也因为 Handler 整体设计上也是十分优秀的。接下来我就梳理总结一下常见的 Handler 相关知识点。 2、基…...
酒店固定资产管理怎么分类
在酒店业中,固定资产的管理是至关重要的一环。它不仅影响到企业的运营效率和盈利能力,而且直接影响到客户体验和品牌形象。因此,对于酒店管理者来说,合理、有效地进行固定资产管理是一项必不可少的任务。本文将探讨酒店固定资产的…...
OpenCV(三十一):形态学操作
1.形态学操作 OpenCV 提供了丰富的函数来进行形态学操作,包括腐蚀、膨胀、开运算、闭运算等。下面介绍一些常用的 OpenCV 形态学操作函数: 腐蚀操作(Erosion): erode(src, dst, kernel, anchor, iteration…...
Python之面向对象(二)
目录 属性和方法静态属性/方法、普通属性/方法、类方法保护和私有属性/方法魔术方法构造方法(\_\_new__/\_\_init\_\_)析构方法(\_\_del__)调用方法(\_\_call__)toString方法\_\_str__、\_\_repr\_\_\_\_getitem__、setitem、delitem\_\_add__、\_\_gt\_…...
ESP32用作经典蓝牙串口透传模块与手机进行串口通信
ESP32用作经典蓝牙串口透传模块与手机进行串口通信 简介ESP32开发板Arduino程序手机与ESP32开发板进行蓝牙串口透传通信总结 简介 ESP32-WROOM-32模组集成了双模蓝牙包括传统蓝牙(BR/EDR)、低功耗蓝牙(BLE)和 Wi-Fi,具…...
Python 操作 CSV
使用过 CSV 文件都知道:如果我们的电脑中装了 WPS 或 Microsoft Office 的话,.csv 文件默认是被 Excel 打开的,那么什么是 CSV 文件?CSV 文件与 Excel 文件有什么区别?如何通过 Python 来操作 CSV 文件呢?带…...
自动化运维工具Ansible教程(二)【进阶篇】
文章目录 前言Ansible 入门到精通自动化运维工具Ansible教程(一)【入门篇】自动化运维工具Ansible教程(二)【进阶篇】精通篇 进阶篇1. Ansible 的高级主题(例如:角色、动态清单、变量管理等)**1. 角色(Roles)**&#x…...
嵌入式Linux基础学习笔记目录
1. 嵌入式Linux应用开发基础知识 1.1 交叉编译 1.2 GCC编译器 1.3 makefire 1.4 文件I/O 1.5 Framebuffer应用编程 1.6 文字显示及图象显示 1.7 输入系统应用编程 1.8 网络编程 1.9 多线程编程 1.10 串口编程 1.11 I2C应用编程 2. 源码分析 2.1 MQTT源码 2.2 蓝牙源码 2.3 MJP…...
JVM | 垃圾回收器(GC)- Java内存管理的守护者
引言 在编程世界中,有效的内存管理是至关重要的。这不仅确保了应用程序的稳定运行,还可以大大提高性能和响应速度。作为世界上最受欢迎的编程语言之一,通过Java虚拟机内部的垃圾回收器组件来自动管理内存,是成为之一的其中一项必…...
yolov5添加ECA注意力机制
ECA注意力机制简介 论文题目:ECA-Net: Efficient Channel Attention for Deep Convolutional Neural Networks 论文地址:here 基本原理 🐸 ECANet的核心思想是提出了一种不降维的局部跨通道交互策略,有效避免了降维对于通道注意…...
华为在高端智能手机市场再次撕开了一道深深的口子
智能手机市场趋于饱和,增长变得越来越难,智能手机厂商从被动卷走向了主动卷。 8月29日,华为宣布推出“HUAWEI Mate 60 Pro先锋计划”,并于当天中午在官方商城、部分线下门店上线销售新机Mate 60 Pro,它是全球首款支持卫…...
前端设计模式和设计原则之设计模式
作为前端开发,在code时,或多或少地都会践行设计模式,但是你清楚自己用到的是何种设计模式吗? 为什么前端开发一定要懂设计模式呢? code时不遵从设计模式,又能怎样呢? 上面的问题可以留作思考…...
提高在速卖通产品上的曝光率——自养号测评优势全面解析!
速卖通是国际贸易的一个平台,是国内外企业之间的一个桥梁。在速卖通中,如果要让产品得到更好地推广,就必须让产品得到更多的消费者认可。而产品的认可度除了品质保障和售后服务之外,评测也是非常重要的环节。 对于速卖通店铺销量…...
指针进阶(二)
指针进阶 5.函数指针6. 函数指针数组7. 指向函数指针数组的指针8. 回调函数案例:使用回调函数,模拟实现qsort(采用冒泡的方式)。案例:测试qsort排序结构体数据 5.函数指针 补: &函数名就是函数的地址 …...
【HCIE】03.BGP高级特性
每一条BGP路由都可以携带多个路径属性,针对其属性也有特有的路由匹配工具,包括:AS Path Filter和Community Filter。 import方向的属性,出现在如策略里面,加入到BGP路由表中,再传给路由表里,出去…...
单个处理数据祖籍列表层级关系
CREATE DEFINERroot% FUNCTION sys_organization_getAncestorsNames(deptId varchar(36)) RETURNS varchar(1000) CHARSET utf8DETERMINISTIC BEGINDECLARE parentDeptId varchar(36) default ; -- 父部门iddeclare parentDeptName varchar(100) default ; -- 父部门名称decla…...
Maven部署打包多环境(开发、测试、生产)配置教程
Maven打包多环境(开发、测试、生产)配置教程 1、多环境配置的必要性1.1 没有进行多环境配置进行的操作复杂性1.2 不影响运行时配置 2、配置方案2.1 添加profile属性2.1 添加两个插件2.3 主配置文件中添加插值变量 3、效果展示3.1 勾选prod环境3.2 控制台…...
【计算思维题】少儿编程 蓝桥杯青少组计算思维 数学逻辑思维真题详细解析第9套
蓝桥杯青少组计算思维 数学逻辑思维真题详细解析第9套 第十四届蓝桥杯省赛真题 1、要把下面4张图片重新排列成蜗牛的画像,该如何排列这些图片 A、 B、 C、 D、 答案:A 考点分析:主要考查小朋友们的观察能力空...
【Hello Algorithm】贪心算法
本篇博客介绍: 简单介绍下贪心算法 贪心算法 介绍贪心算法最小字典序的字符串拼接最多会议数切棍子的最小成本IPO灯塔问题 介绍贪心算法 贪心算法是一种极具有自然智慧的算法 它会使用以一种局部最功利的标准来做出一个当前看来最好的选择 如果说我们根据局部最优…...
TOP-K问题
目录 问题描述 解法及思想 第一种方式 算法思想 具体实现 第二种方式 算法思想 具体实现 问题描述 Top-K问题是一个十分经典的问题,一般有以下两种方式来描述问题: 在10亿的数字里,找出其中最大的100个数;在一个包含n个整…...
【保姆级从0到1】UE5 蓝图入门教程1:关卡、蓝图入门
20230113 1、新建项目 新建选择 UE 5.1 项目 选择蓝图,项目位置 改变编辑器布局,选择经典布局 2、关卡与蓝图 选择 File -> New Level 准备创建关卡 选择 Basic,点击 Create 进行创建 Ctrl S 保存新建的关卡 关卡蓝图的打开 鼠标右键&…...
【码银送书第六期】《ChatGPT原理与实战:大型语言模型的算法、技术和私有化》
写在前面 2022年11月30日,ChatGPT模型问世后,立刻在全球范围内掀起了轩然大波。无论AI从业者还是非从业者,都在热议ChatGPT极具冲击力的交互体验和惊人的生成内容。这使得广大群众重新认识到人工智能的潜力和价值。对于AI从业者来说…...
redis 报错 Redis protected-mode 配置文件没有真正启动
(error) DENIED Redis is running in protected mode because protected mode is enabled Redis protected-mode 是3.2 之后加入的新特性,在Redis.conf的注释中,我们可以了解到,他的具体作用和启用条件 链接redis 时只能通过本地localhost …...
解决a标签内容中img标签和p标签垂直方向间隔太大的问题
现象如下: 对应的html结构: 解决办法:给a标签设置:display: inline-block和line-height属性。 然后问题解决: 具体原理如下(由chatgpt回答): display: inline-block 可以减少垂直方…...
如何选择靠谱的全景平台?VR全景加盟从哪方面对比?
VR全景行业经过近几年的发展,已经逐渐普及开来,线下各个行业都有实体商家开始引入VR全景去做营销宣传推广了。不少老板也意识到线上线下双渠道的重要性,而VR全景的存在就刚好满足各行各业的需求,从这一点不难看出,VR全…...
CentOS系统环境搭建(十八)——CentOS7安装Docker20.10.12和docker compose v2
centos系统环境搭建专栏🔗点击跳转 CentOS7安装Docker20.10.12和docker compose v2 关于Docker旧版本和docker compose1.0版本的安装可以看这一篇CentOS系统环境搭建(三)——Centos7安装Docker&Docker Compose。 1.卸载旧版本 卸载do…...
NebulaGrap入门介绍和集群安装部署
长风破浪八千里,落日晚霞不回头。 ——大宁。 NebulaGrap——分布式图数据库 官方文档: NebulaGraph Database手册 官方文档 介绍 简介: NebulaGraph 一款开源、分布式图数据库,擅长处理超大规模数据集。 Nebula …...
thinkphp5.0 composer 安装oss提示php版本异常
场景复现: 本地 phpstudy 环境,安装的有7.0到7.3三个版本,首先确认composer已经安装 composer安装阿里云oss的命令为:composer require aliyuncs/oss-sdk-php 运行报错: Problem 1- Root composer.json requires php…...
AList dokcer安装及百度网盘挂载
AList是开源的网盘管理工具。本文介绍如何通过docker安装AList并挂载百度网盘 1、AList安装 1.1、系统安装 通过docker命令进行安装,命令如下: docker run -d --restartalways -v /etc/alist:/opt/alist/data -p 5244:5244 --name"alist" xhofe/alist:…...
whereIn 遇到了最大限制,临时表处理方式
当使用whereIn 遇到了限制,比如whereIn target ID 已经超过了10万级别,但是又没办法join其他库表时,可以使用临时表的方式解决,临时表是以一种会话的方式存在,意味着你断开了mysql 这个临时会话会自动销毁。 为了创建…...
韩国什么网站是专做皮草的/宁波seo推广哪家好
先看一下效果 选择分组 选择服务器 开始链接 为什么写 之前写过一个字符界面的链接工具,但是看起来比较简陋,他是这个样子的: 看起来十分不好看。后来在网上看到shell中有一个whiptail工具可以制作各种命令行里的工具,于是就搜索了…...
一个数据库怎么做二个网站/百度收录技术
今天终于考完了70-528,长长舒口气 呵呵,可以安安心心的回国度假了。这次考试真是一波三折啊。下面先说说考试过程 一个月前开始准备考MCTS,但是当时只是似懂非懂这个东西,一直都觉得可有可无,所以当时也没有太放在心上。天天70-536的Training…...
网站建设价钱/贵州二级站seo整站优化排名
嵌入式介绍与应用1 概念桌面对比2 特点3 发展历史3.1 计算机发展3.2 嵌入式发展4 开发能力要求5 应用6 规模参考1 概念 嵌入式系统由硬件和软件组成。是能够独立进行运作的器件。其软件内容只包括软件运行环境及其操作系统。硬件内容包括信号处理器、存储器、通信模块等在内的…...
wordpress ip改域名/微信公众号推广方法有哪些
文章目录类C中可以使用struct、class来定义一个类变量名规范参考类实例化成为了对象,对象的内存空间由成员变量决定对象对象的内存布局代码段(代码区)数据段(全局区)栈空间堆空间thisthis指针必须用->,不能用. &…...
一级门户网站建设费用/网站营销推广有哪些
原文出处:http://blog.csdn.net/lhw1204/article/details/8439229 Window--Preferences--"General"--"Content Types",右边有个树形的text,展开找到"JSP",最底下有个"Default encoding",里面默认的是…...
哪里有可以做空比特币的网站/宁波seo推广如何收费
1、常量用易于理解的清楚的名称替代了含义不明确的数字或字符串,使程序更易于阅读。2、常量使程序更易于修改。例如,在C#程序中有一个SalesTax常量,该常量的值为6%。如果以后销售税率发生变化,把新值赋给这个常量,就可…...