Android的消息机制--Handler

一、四大组件概述

Android的消息机制是由Handler、Message、MessageQueue,Looper四个类支撑,撑起了Android的消息通讯机制,Android是一个消息驱动系统,由这几个类来驱动消息与事件的执行

Handler:

  • 用来发送消息和处理消息
  • 无论使用的post ,还是send,都会执行enqueueMessage 方法,将消息加到队列中
  • 发送的消息并不是立刻得到执行的,所以必须有个地方把它存起来,也就是MessageQueue

MessageQueue

  • 基于单向链表,以触发时间的顺序排放在队列中,链表头部信息被触发的时间是最接近的
  • 队列中的消息怎样让它在必要的时间得到执行,就需要依靠Lopper 

有三种消息类型:

  • BarrierMessage:屏障消息

  • AsyncMessage:异步消息

  • Message:同步消息

Lopper

  • 无限循环驱动器
  • 在它内部有个loop 方法,开启无限循环,从头遍历这个队列,检查满足条件的消息,有就把它取出来进行分发执行,没有或者消息为空时,当前线程就会进入阻塞状态,从而释放掉CPU 的资源占用,当有新消息进来的时候就会唤醒当前线程,从而继续遍历队列中是否有满足条件的消息,所以Looper并不会真的一直无限循环下去

Message

消息

  • long when:该消息被执行的时间戳,这个时间戳是它在队列中排队的唯一依据
  • Message next:消息队列是一条单向链表,每一条消息都会包含下一条消息的引用关系,从而形成单向链表
  • Handler target:代表着该message是由哪一个handler发送的,在消费这条消息时也由这个target来消费
  • 一个线程最多存在一个Lopper
  • 一个Looper对应一个MessageQueue
  • 一个MessageQueue中可以存在多个Message对象
  • 一个MessageQueue 或者一个Looper 对应多个handler

二、消息分发的优先级

  1. Message的回调方法:message.callback.run()  优先级最高
  2. Handler的回调方法:Handler.mCallback.handleMessage(msg)
  3. Handler的默认方法:handler.handleMessage(msg)
       //1.直接在Runnable中处理任务handler.post(runable = Runnable {//这条消息在消费时,首先回调给Message中的callback,也就是runable对象})//2.使用Handler.callback来接收处理消息val handler = object :Handler(callback{//在创建handler的时候是可以传递一个callback 的,在消息分发的时候首先把消息分发给callback 来处理return@callback true})//3.最常用的handler的handlerMessage方法

三、疑问点

1.在使用handler的时候并没有指定Looper ,这三个类是怎么关联起来的

在创建实例对象时虽然没有传递Looper对象,但是在构造函数的重载里会调用Looper.myLooper来获取当前线程绑定的Looper对象

2.主线程的Looper是在哪里创建的?

在ActivityThread的main方法中调用Looper.prepareMainLooper()创建了主线程的Looper对象,然后调用loop()开启消息队列循环,所以在主线程中创建Handler不用给他创建Looper

如果一个线程的Looper对象没有调用prepare 方法,它的Looper是为空的

3.Looper.myLooper是如何保证获取到的Looper是当前线程的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));}

        无论是主线程的Looper 还是子线程的Looper都只能调用prepare()方法或者prepareMain()方法,在prepare()方法中首先判断在这个当前线程这个Looper对象是否已经被创建过了,如果是,再次调用该方法就会报错。

        这就是一个线程最多只能存在一个Looper 对象的保证

        接下来把Looper 对象保存到了ThreadLocal中,在获取Looper 时也是从ThreadLocal 这个对象中得到的

ThreadLocal:

        是用来存储数据的,使其成为线程的私有局部变量,通过它提供的get、set来访问

        好处:可以在线程的任意地方去访问这个局部变量,不用传来传去

4.如何让子线程拥有消息分发的能力?如何在子线程中弹出Toast?

        默认子线程是没有Looper 对象的,要主动调用Looper.prepare 方法以及 Looper.loop方法,在这两个方法中间就可以弹出Toast 了,在接着给它创建一个handler,在主线程中拿到这个handler对象,就可以处理在子线程中处理主线程发送过来的消息了

        Toast在没有显示出来之前,它还没有被添加到窗口上,对它的操作就不会触发线程的检查
        实现方式跟ActivityThread方式一样
        需要注意的是一旦给子线程执行了这两个方法,要在必要的时候调用Looper.quit 方法,让Looper 退出循环,否则会一直循环下去,这个线程就不会被销毁,不会被回收

四、消息入队

一条消息被插入到MessageQueue时做了哪些事情?

发送一条消息时主要有两种方法,一种是post开头的,一种是send开头的,无论使用的哪种方式发送,都会以Message的形式插入到队列中

对于post,发送一条消息时,都会通过getPostMessage,把runnable对象包装成Message对象,再次调用sendMessageDelayd方法把它加入到队列中

享元设计模式,共用已创建的对象   避免重复创建对象

        getPostMessage方法中在获取Message对象时,使用的是Message.obtain()方法

Message提供了消息复用池的能力,最多会缓存50条Message对象,通过.obtain()方法来获取 Message对象是可以复用的,不需要每次都去创建一个新的 

        采用了链表的形式来管理消息池,链表的插入和删除比ArrayList快

        还提供了消息回收能力,当消息被分发完了之后就会调用recycleUnchecked,将Message的对象进行重置,情况数据,并将其插入到链表的头节点中,它是一个队头复用机制

通过new Message出来的对象会出现大量的临时的Message对象,会导致内存占用率过高

消息入队,按照消息被执行的时间戳when插入队列

  • 无论是post、还是send,最终都会执行enqueueMessage,把消息插入到队列中
  • postSyncBarrier:发送一条屏障消息

新消息插入时按照消息插入的时间插入到队列中

enqueueMessage

Handler.enqueueMessage

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) {msg.target = this;……return queue.enqueueMessage(msg, uptimeMillis);}

将Message的target赋值成当前的Handler,在下面会对这个target进行判空,空的话直接抛出异常

消息被消费的时候会通过handlerDispatchMessahe方法来完成

MessageQueue.enqueueMessage

  boolean enqueueMessage(Message msg, long when) {if (msg.target == null) {throw new IllegalArgumentException("Message must have a target.");}synchronized (this) {if (msg.isInUse()) {throw new IllegalStateException(msg + " This message is already in use.");}if (mQuitting) {IllegalStateException e = new IllegalStateException(msg.target + " sending message to a Handler on a dead thread");Log.w(TAG, e.getMessage(), e);msg.recycle();return false;}msg.markInUse();msg.when = when;//mMessage始终是队头消息,拿到队头,这个队列才能执行增删改查的操作Message p = mMessages;boolean needWake;//p == null 说明队头为null,则队列为空//when == 0 或者新消息触发的时间戳为0// when < p.when 或者消息被触发的时间小于队头消息被触发的时间if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.//满足条件就把新消息插入到队列的头部//把新消息的next节点指向原来的头结点,然后将新信息赋值给头消息msg.next = p;mMessages = msg;//如果当前Looper处于休眠状态,则本次插入消息之后需要唤醒//mBlocked:是否处于阻塞状态,只有对列为空,队列当中没有可处理消息的时候,线程才会进入阻塞状态,mBlocked才会为trueneedWake = mBlocked;} else {// Inserted within the middle of the queue.  Usually we don't have to wake// up the event queue unless there is a barrier at the head of the queue// and the message is the earliest asynchronous message in the queue.//needWake:要不要唤醒线程,目的是让异步消息尽早的执行//mBlocked:当前线程是否处于休眠状态//p.target == null:队头消息是否为空,队头消息是异步同步屏障消息//msg.isAsynchronous:新消息是异步消息needWake = mBlocked && p.target == null && msg.isAsynchronous();Message prev;//for循环:找到一个合适的位置来插入这条新消息for (;;) {prev = p;p = p.next;//找到合适的位置退出for循环if (p == null || when < p.when) {break;}if (needWake && p.isAsynchronous()) {needWake = false;}}//调整链表中节点的指向位置,实现消息插入队列的目的//msg:新消息  p:下一条消息  prev:上一条消息msg.next = p; // invariant: p == prev.nextprev.next = msg;}// We can assume mPtr != 0 because mQuitting is false.//Looper被唤醒if (needWake) {//线程被唤醒,使用nativeWake native方法nativeWake(mPtr);}}return true;}

enqueueMessage在插入一条新消息时,主要是检查消息是否都具备target对象,否则消息是无法被处理的,还会选择性的决定唤醒当前的线程,继续轮询队列是否有符合条件的消息拿出来处理

postSyncBarrier 同步屏障消息

message.target == null ,这类消息不会真的执行,起到标记作用

MessageQueqe在遍历消息队列时,如果队头是同步屏障消息,那么会忽略同步消息,优先让异步消息得到执行

一般异步消息和同步屏障消息会一同使用

异步消息 & 同步屏障

使用场景:

  • ViewRootImpl接收屏幕垂直同步信息事件用于驱动UI测绘
  • ActivityThread接收AMS的事件驱动生命周期
  • InputMethodMessage分发软键盘输入事件
  • PhoneWindowManager分发电话页面各种事件

目的:

        让重要的消息尽可能早的得到执行

注意:

  • 开发过程中无法使用,只能系统源码使用
  • 必须使用MessageQueue来发生,Handler是无法发送同步屏障消息的,只能用来发送异步消息和同步消息

MessageQueue#postSyncBarrier

    public int postSyncBarrier() {//使用uptimeMillis,意思是:发送了这条消息,在这期间如果设备进入休眠状态,那么消息是不会执行的,设备被唤醒之后才会执行return postSyncBarrier(SystemClock.uptimeMillis());}
  • currentTimeMillis() 系统当前时间,即日期时间,可以被系统设置修改,如果设置系统时间,时间值也会发生改变
  • uptimeMillis() 自开机后,经过的时间,不包括深度休眠的时间

sendMessageDelay.postDelay 也都使用了这个时间戳

问题:

        如果使用handler发送一条消息,然后让设备进入休眠,也就是先熄屏,然后长时间不操作手机,这条消息会不会得到执行,为什么?

        进入休眠之后,消息是不会被触发的,因为设备休眠之后uptimeMillis是不会被累加的

 private int postSyncBarrier(long when) {// Enqueue a new sync barrier token.// We don't need to wake the queue because the purpose of a barrier is to stall it.synchronized (this) {final int token = mNextBarrierToken++;//从消息池复用,构建新消息体final Message msg = Message.obtain();msg.markInUse();//并没有给target赋值//区分是不是同步屏障,就看target是否等于null,等于null,就是同步屏障消息msg.when = when;msg.arg1 = token;Message prev = null;Message p = mMessages;if (when != 0) {//遍历队列所有消息,直到找到一个message.when > msg.when 的消息,决定新消息插入的位置while (p != null && p.when <= when) {prev = p;p = p.next;}}//如果找到了合适的位置则插入if (prev != null) { // invariant: p == prev.nextmsg.next = p;prev.next = msg;} else {//如果没有找到直接放队头msg.next = p;mMessages = msg;}return token;}}

  屏障消息在插入队列时是没有主动唤醒线程的,因为屏障消息并不需要得到执行,也不需要唤醒这个线程去轮询它

屏障消息的移除,谁添加的就由谁来移除

比如ViewRootImpl,在接收到垂直同步信号的到达,发送一条异步消息,并发送了一条屏障消息,当接收到异步消息时,ViewRootImpl就会把同步屏障消息从队列中移除

问题:

        ViewRootImpl是如何在UI测绘的工作优先得到执行的?

        发送了同步屏障和异步消息

五、消息分发

Looper#loop()

队列中的消息之所以能得到分发,是由于Looper中的loop方法,会开启一个无限for循环消息的驱动器,在这个无限for循环中会调用MessageQueue的next方法,去获取一个可执行的msg对象

这个next方法可能使得当前线程进入一个阻塞的状态,此时这个方法不会有返回值,下面的分发代码就不会得到执行,所以这个无限for循环并不会一直空轮询下去,

目的:只是不让这个线程退出,因为一个线程任务执行完成,就会自动退出,如果想让它不退出,开启一个while循环等待一段时间,这里也是一样的道理

直到队列中有可处理的消息才会返回

for(;;){//取出队列中的消息Message msg = me.mQueue.next(); // might blockif (msg == null) {// No message indicates that the message queue is quitting.return false;}……//分发消息msg.target.dispatchMessage(msg);……//回收Messagemsg.recycleUnchecked();
}   

拿到消息之后调用msg.target.dispatchMessage(msg) 去分发消息

当消息处理完成之后调用msg.recycleUnchecked()方法回收Message,留着复用

总结:

        loop方法的作用是从队列中去取消息,然后分发,然后回收

MessageQueue#next()

首先也是开启一个for循环,在消息分发时可能存在消息的插入、删除,而且队列是一个单向链表无法确切知道消息插入时是有多少的,当这个next方法找到一个合适的消息时就会退出for循环

    int nextPollTimeoutMillis = 0;for (;;) {nativePollOnce(ptr, nextPollTimeoutMillis);synchronized (this) {// Try to retrieve the next message.  Return if found.final long now = SystemClock.uptimeMillis();Message prevMsg = null;Message msg = mMessages;if (msg != null && msg.target == null) {// Stalled by a barrier.  Find the next asynchronous message in the queue.do {prevMsg = msg;msg = msg.next;} while (msg != null && !msg.isAsynchronous());}if (msg != null) {//找到了一条消息,但是它的时间,还没到需要执行的时机if (now < msg.when) {// Next message is not ready.  Set a timeout to wake up when it is ready.nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);} else {// Got a message.mBlocked = false;//prevMsg不为空说明需要删除的是中间的消息,只需要上一条消息的next指向msg的next,为空说明要删除的这条消息是队头消息if (prevMsg != null) {prevMsg.next = msg.next;} else {mMessages = msg.next;}msg.next = null;if (DEBUG) Log.v(TAG, "Returning message: " + msg);msg.markInUse();return msg;}} else {// No more messages.//msg对象为空,说明队头对象为空,也就是当前队列为空,此时把nextPollTimeoutMillis置为-1,looper将进入永久休眠,直到有新消息到达nextPollTimeoutMillis = -1;}}

通过nativePollOnce这个native方法进行阻塞,传递的nextPollTimeoutMillis,如果这个值是大于0的,就会使得当前线程进入阻塞,并且释放掉对CPU资源的使用权,尽管Looper中有个无限for循环,但是不会造成CPU资源的过多占用

由于nextPollTimeoutMillis第一次for循环时是等于0的,所以第一次不会使得这个线程进入阻塞的,但是如果在接下来的循环中没有找到一个合适的、需要处理的消息,这个nextPollTimeoutMillis会被更新,在第二次循环的时候如果这个值不等于0,这个线程就会进入阻塞状态,如果这个值等于-1,当前线程就会进入无限阻塞状态,直到有新消息插入时,才会被唤醒

nextPollTimeoutMillis等于多少,这个线程就会被阻塞多少ms,超时之后就会继续执行以下代码

延迟消息是怎么得到保证的?

首先是通过uptimeMillis去计算出应该被执行的时间戳,然后借助nativePoll阻塞一段时间,超时之后自动恢复 ,然后继续往下检查是否有满足条件的消息,如果有就拿出来去执行

如何去取消息?

把队头消息赋值给一个临时变量msg,防止插入消息,队头可能被改变,然后判断msg是否是同步屏障消息(也就是判断msg.target == null),如果是通过do-while循环,在while中判断这个msg 不是一个异步消息,也就是说如果这个消息是异步消息,就会退出do-while循环

这个屏障消息唯一的作用就是:当它处于队头时,next方法在检索消息时会跳过同步消息,会优先检索出所有的异步消息,让它们优先执行

msg.target对象如果不为空,不会执行do-while循环,就会按照时间顺序来检索分发消息

判断消息msg是否为空,说明队头对象为空,也就是当前队列为空,此时把nextPollTimeoutMillis置为-1,looper将进入永久休眠,线程进入无限阻塞状态,直到有新消息到达

不为空时,也就是找到了一条消息,检查是否到达需要执行的时机

如果没有,去更新nextPollTimeoutMillis值,等于应该执行的时间 - 当前时间 ,计算出还应该延迟多久

否则说明找到了这条需要处理的消息,首先需要从队列中移除掉

preMsg不等于null,说明被删除的这条消息是队列中间的这条消息,preMsg代表被删除消息的前一条消息,删除这条消息,只需要将preMsg的next节点指向这个消息的next节点

preMsg等于null,说明被删除的这条消息是队头消息,需要将mMessage指向要删除消息的下一个节点

最后把msg对象返回回去,让它去分发,去执行

idler.queueIdle()

                if (pendingIdleHandlerCount < 0&& (mMessages == null || now < mMessages.when)) {pendingIdleHandlerCount = mIdleHandlers.size();}if (pendingIdleHandlerCount <= 0) {// No idle handlers to run.  Loop and wait some more.mBlocked = true;continue;}

判断队头Message是否为空,也就是队列中没有任务了,或者队头消息时间还没有到达可执行的时机,在第二次for循环中,线程即将进入阻塞状态,在进入阻塞状态之前,称之为空闲状态,判断有没有向MessageQueue中注册IdleHandler,用于监听这个状态

IdleHandler:

        可以监听当前线程是否即将进入空闲状态,也就是说通过事件的监听来做一些延迟的初始化,以及数据加载、日志上报等工作,而不是有任务就提交,从而避免抢占重要的资源

如果pendingIdleHandlerCount大于0,就会去调用idler.queueIdle()方法

            for (int i = 0; i < pendingIdleHandlerCount; i++) {final IdleHandler idler = mPendingIdleHandlers[i];mPendingIdleHandlers[i] = null; // release the reference to the handlerboolean keep = false;try {keep = idler.queueIdle();} catch (Throwable t) {Log.wtf(TAG, "IdleHandler threw exception", t);}if (!keep) {synchronized (this) {mIdleHandlers.remove(idler);}}}

最后会将nextPollTimeoutMillis延迟时间置为0,既然业务层监听了线程的空闲状态,在queueIdle这个方法里,就有可能再次产生新的消息,为了让新消息尽可能早的得到执行,此时不需要让线程进入休眠了

nextPollTimeoutMillis = 0;

在Android中存在两套消息机制,一套是Java的,一套是C++的,本质上是独立的,在Java中MessageQueue调用nativePollOnce的主要原因是借助native消息机制所实现的线程阻塞能力

六、问题解惑

http://t.csdnimg.cn/ZBfg7

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

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

相关文章

纯css实现文字左右循环滚动播放效果

思路&#xff1a;由两个span模块组成&#xff0c;第一个为空的span内容&#xff0c;为的是实现第二个span内容缓慢出现的效果。 代码如下&#xff1a; <div class"scrollingStyle"><span class"first-marquee"></span><span class&q…

Samba是什么?有什么作用?工作流程以及如何搭建

前言&#xff1a; 寒假在家学习Linux近一个月了&#xff0c;最近参加了嘉立创组织的泰山派训练营&#xff0c;从中了解到了现在Linux开发中很方便的一些开发方式和工具。例如&#xff1a;使用MobaXterm终端通过SSH登陆ubuntu服务器进行开发&#xff1b;安装一个FileZilla工具通…

定制线缆厂家推荐:赋能科技互联,精工电联的集成线缆定制服务(小批量、多品类产品高效解决方案)

定制线缆 精工电联&#xff1a;赋能科技互联&#xff0c;精工电联的集成线缆定制服务&#xff08;小批量、多品类产品高效解决方案&#xff09; 随着科技的飞速发展&#xff0c;人们对数据传输速度和稳定性的需求日益增强。精工电联作为高科技智能化产品及自动化设备专用连接线…

第二证券:汽车产业链股爆发,中通客车涨停,星源卓镁等大涨

轿车产业链股21日盘中强势拉升&#xff0c;截至发稿&#xff0c;华阳变速涨超29%&#xff0c;星源卓镁涨超15%&#xff0c;德迈仕涨逾10%&#xff0c;中捷精工、中通客车、合力科技、圣龙股份等涨停。 行业方面&#xff0c;中国轿车工业协会近来公布数据显示&#xff0c;1月&a…

【快速搞定Webpack5】介绍及基本使用(一)

webpack 是一个静态资源打包工具。 他会以一个或多个文件作为打包的入口&#xff0c;将我们整个项目所有文件编译组合成一个或多个文件输出出去。 输出的文件就是编译好的文件&#xff0c;就可以在浏览器端运行了。 我们将 webpack 输出的文件叫做 bundle 。 (将浏览器不识别的…

wo-gradient-card是一款采用uniapp实现的透明辉光动画卡片

采用uniapp-vue3实现&#xff0c;透明辉光动画卡片&#xff0c;卡片内容包含标签、标题、副标题、图片 支持H5、微信小程序&#xff08;其他小程序未测试过&#xff0c;可自行尝试&#xff09; 可用于参考学习 可到插件市场下载尝试&#xff1a; https://ext.dcloud.net.cn/plu…

【Swift】NSSearchField用法和示例

1.简介 NSSearchField 是 macOS 中用于搜索的文本输入框控件&#xff0c;通常用于实现搜索功能。用户可以在 NSSearchField 中输入搜索关键字&#xff0c;然后触发搜索操作。NSSearchField 可以显示搜索图标和清除按钮&#xff0c;还可以设置占位符文本等属性。 2.常用方法 …

学习鸿蒙基础(5)

一、honmonyos的page路由界面的路径 新建了一个page,然后删除了。运行模拟器的时候报错了。提示找不到这个界面。原来是在路由界面没有删除这个page。新手刚接触找了半天才找到这个路由。在resources/base/profile/main_pages.json 这个和微信小程序好类似呀。 吐槽&#xf…

(3)(3.5) 遥测无线电区域条例

文章目录 前言 1 支持不同国家/地区 前言 本专题列出了每个国家/地区可用的主要频率&#xff0c;并列出了无线电设备。 &#xff01;Note 合规是你的责任。检查无线电是否符合所在地区/国家的频率、跳频信道和功率级别规定。 &#xff01;Tip 我们很乐意在此表中添加更多…

初探富文本之文档diff算法

初探富文本之文档diff算法 当我们实现在线文档的系统时&#xff0c;通常需要考虑到文档的版本控制与审核能力&#xff0c;并且这是这是整个文档管理流程中的重要环节&#xff0c;那么在这个环节中通常就需要文档的diff能力&#xff0c;这样我们就可以知道文档的变更情况&#…

力扣精选算法100道——提莫攻击(模拟专题)

目录 &#x1f6a9;题目解析 &#x1f6a9;算法原理 &#x1f6a9;实现代码 &#x1f6a9;题目解析 输入&#xff1a;timeSeries [1,4], duration 2 输出&#xff1a;4 解释&#xff1a;提莫攻击对艾希的影响如下&#xff1a; - 第 1 秒&#xff0c;提莫攻击艾希并使其立即…

图片怎么转换格式jpg?轻松转换图片格式

图片怎么转换格式jpg&#xff1f;在数字化时代&#xff0c;图片作为信息传递的重要载体&#xff0c;其格式转换显得尤为重要。JPG作为一种广泛使用的图片格式&#xff0c;具有压缩比高、兼容性好等特点&#xff0c;深受用户喜爱。那么&#xff0c;如何将其他格式的图片轻松转换…

阿里云服务器安装MySQL、Apache、PHP

节日期间突然想要自己搭建一个个人网站&#xff0c;于是在阿里云申请了一个可以免费使用3个月的服务器&#xff0c;申请的云市场产品Wordpress平台&#xff08; ALinux3 LNMP PHP7.4&#xff09;。官方教程使用的CentOs系统&#xff0c;和我申请的ALinux3操作有一些差异&#x…

Oracle使用exp和imp命令实现数据库导出导入

Oracle和MySQL在SQL语法和一些数据库特性上有一些差异,以下是一些常见的差异: 数据类型: Oracle和MySQL支持的数据类型有所不同。例如,Oracle支持NUMBER、DATE、VARCHAR2等类型,而MySQL支持INT、DATE、VARCHAR等类型。字符串比较: 在 Oracle 中,字符串比较默认是区分大小…

【41 Pandas+Pyecharts | 全国星巴克门店数据分析可视化】

文章目录 &#x1f3f3;️‍&#x1f308; 1. 导入模块&#x1f3f3;️‍&#x1f308; 2. Pandas数据处理2.1 读取数据2.2 查看数据信息2.3 计算营业时长2.4 营业时长区间 &#x1f3f3;️‍&#x1f308; 3. Pyecharts数据可视化3.1 各省星巴克门店数量柱状图3.2 各省星巴克门…

【力扣hot100】刷题笔记Day7

前言 身边同学已经陆陆续续回来啦&#xff0c;舍友都开始投简历了&#xff0c;我也要加油啦&#xff01;刷完hot100就投&#xff01; 73. 矩阵置零 - 力扣&#xff08;LeetCode&#xff09; 标记数组&#xff1a;空间复杂度O(mn) class Solution:def setZeroes(self, matrix:…

【日常聊聊】计算机专业必看的电影

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;日常聊聊 ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 方向一&#xff1a;电影推荐 方向二&#xff1a;技术与主题 方向三&#xff1a;职业与人生 结语 我的其他博客 前言 计算机…

flink operator 1.7 更换日志框架log4j 到logback

更换日志框架 flink 1.18 1 消除基础flink框架log4j 添加logback jar 1-1 log4j log4j-1.2-api-2.17.1.jar log4j-api-2.17.1.jar log4j-core-2.17.1.jar log4j-slf4j-impl-2.17.1.jar 1-2 logback logback-core-1.2.3.jar logback-classic-1.2.3.jar slf4j-api-1.7.25.jar2 …

Linux环境安装Git(详细图文)

说明 此文档Linux环境为&#xff1a;Ubuntu 22.04&#xff0c;本文档介绍两种安装方式&#xff0c;一种是服务器能联网的情况通过yum或apt命令下载&#xff0c;第二种采用源码方式安装。 一、yum/apt方式安装 1.yum方式安装Git 如果你的服务器环境是centos/redhot&#xff…

最新Unity游戏主程进阶学习大纲(2个月)

过完年了&#xff0c;很多同学开始重新规划自己的职业方向,找更好的机会,准备升职或加薪。今天给那些工作了1~5年的开发者梳理”游戏开发客户端主程”的学习大纲&#xff0c;帮助大家做好面试准备。适合Unity客户端开发者。进阶主程其实就是从固定的几个方面搭建好完整的知识体…