Android FrameWork 层 Handler源码解析

Handler生产者-消费者模型

在android开发中,经常会在子线程中进行一些耗时操作,当操作完毕后会通过handler发送一些数据给主线程,通知主线程做相应的操作。 其中:子线程、handler、主线程,其实构成了线程模型中经典的生产者-消费者模型。

生产者-消费者模型:生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加数据,消费者从存储空间中取走数据。 

这么设计有什么好处呢?

保证数据生产消费的顺序(通过MessageQueue,先进先出) - 不管是生产者(子线程)还是消费者(主线程)都只依赖缓冲区(handler),生产者消费者之间不会相互持有,使他们之间没有任何耦合 。

Handler是Android消息管理机制,屏幕触摸事件、生命周期事件等都是封装成message,发送到handler进行处理。通过源码,我们进一步分析Handler的机制。

Handler机制

Hanlder:发送和接收消息;

Looper:用于轮询消息队列,一个线程只能有一个Looper;

Message: 消息实体;

MessageQueue: 消息队列用于存储消息和管理消息 。

创建Looper

创建 Looper 的方法是调用 Looper.prepare() 方法,在 ActivityThread 中的 main 方法中为我们创建了 prepare。
public static void main(String[] args) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");//其他代码省略...Looper.prepareMainLooper(); //初始化Looper以及MessageQueueActivityThread thread = new ActivityThread();thread.attach(false);if (sMainThreadHandler == null) {sMainThreadHandler = thread.getHandler();}if (false) {Looper.myLooper().setMessageLogging(newLogPrinter(Log.DEBUG, "ActivityThread"));}// End of event ActivityThreadMain.Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);Looper.loop(); //开始轮循操作throw new RuntimeException("Main thread loop unexpectedly exited");
}

Looper.prepareMainLooper(); 

public static void prepareMainLooper() {prepare(false);//消息队列不可以quitsynchronized (Looper.class) {if (sMainLooper != null) {throw new IllegalStateException("The main Looper has already beenprepared.");}sMainLooper = myLooper();}
}

prepare有两个重载的方法,主要看 prepare(boolean quitAllowed) quitAllowed的作用是在创建MessageQueue时标识消息队列是否可以销毁, 主线程不可被销毁 下面有介绍。

public static void prepare() {prepare(true);//消息队列可以quit
}
//quitAllowed 主要
private static void prepare(boolean quitAllowed) {if (sThreadLocal.get() != null) {//不为空表示当前线程已经创建了Looperthrow new RuntimeException("Only one Looper may be created per thread");//每个线程只能创建一个Looper}sThreadLocal.set(new Looper(quitAllowed));//创建Looper并设置给sThreadLocal,这样get的时候就不会为null了
}

创建MessageQueue以及Looper与当前线程的绑定

private Looper(boolean quitAllowed) {mQueue = new MessageQueue(quitAllowed);//创建了MessageQueuemThread = Thread.currentThread(); //当前线程的绑定
}

MessageQueue的构造方法

MessageQueue(boolean quitAllowed) {//mQuitAllowed决定队列是否可以销毁 主线程的队列不可以被销毁需要传入false, 在MessageQueue的quit()方法就不贴源码了mQuitAllowed = quitAllowed;mPtr = nativeInit();
}

Looper.loop()

同时是在main方法中 Looper.prepareMainLooper() 后Looper.loop(); 开始轮询

public static void loop() {final Looper me = myLooper();//里面调用了sThreadLocal.get()获得刚才创建的Looper对象if (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on thisthread.");}//如果Looper为空则会抛出异常final MessageQueue queue = me.mQueue;// Make sure the identity of this thread is that of the local process,// and keep track of what that identity token actually is.Binder.clearCallingIdentity();final long ident = Binder.clearCallingIdentity();for (;;) {//这是一个死循环,从消息队列不断的取消息Message msg = queue.next(); // might blockif (msg == null) {//由于刚创建MessageQueue就开始轮询,队列里是没有消息的,等到Handler sendMessageenqueueMessage后//队列里才有消息// No message indicates that the message queue is quitting.return;}// This must be in a local variable, in case a UI event sets the loggerPrinter logging = me.mLogging;if (logging != null) {logging.println(">>>>> Dispatching to " + msg.target + " " +msg.callback + ": " + msg.what);}msg.target.dispatchMessage(msg);//msg.target就是绑定的Handler,详见后面Message的部分,Handler开始//后面代码省略.....msg.recycleUnchecked();}
}Handler handler=new Handler(){@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);}
};public Handler(Callback callback, boolean async) {//前面省略mLooper = Looper.myLooper();//获取Looper,**注意不是创建Looper**!if (mLooper == null) {throw new RuntimeException("Can't create handler inside thread that has not called Looper.prepare()");}mQueue = mLooper.mQueue;//创建消息队列MessageQueuemCallback = callback; //初始化了回调接口mAsynchronous = async;
}

创建Handler

Handler handler=new Handler(){@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);}
};
在内部调用 this(null, false);
public Handler(Callback callback, boolean async) {//前面省略mLooper = Looper.myLooper();//获取Looper,**注意不是创建Looper**!if (mLooper == null) {throw new RuntimeException("Can't create handler inside thread that has not called Looper.prepare()");}mQueue = mLooper.mQueue;//创建消息队列MessageQueuemCallback = callback; //初始化了回调接口mAsynchronous = async;
}
Looper.myLooper();
//这是Handler中定义的ThreadLocal ThreadLocal主要解多线程并发的问题
// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();public static @Nullable Looper myLooper() {return sThreadLocal.get();
}
sThreadLocal.get() will return null unless you’ve called prepare(). 这句话告诉我们get可能返回null 除非先调用 prepare()方法创建Looper。

创建Message

可以直接new Message 但是有更好的方式 Message.obtain。因为可以检查是否有可以复用的Message,用过复用避免过多的创建、销毁Message对象达到优化内存和性能的目地。
public static Message obtain(Handler h) {Message m = obtain();//调用重载的obtain方法m.target = h;//并绑定的创建Message对象的handlerreturn m;
}public static Message obtain() {synchronized (sPoolSync) {//sPoolSync是一个Object对象,用来同步保证线程安全if (sPool != null) {//sPool是就是handler dispatchMessage 后 通过recycleUnchecked回收用以复用的MessageMessage m = sPool;sPool = m.next;m.next = null;m.flags = 0; // clear in-use flagsPoolSize--;return m;}}return new Message();
}

Message和Handler的绑定

创建Message的时候可以通过 Message.obtain(Handler h) 这个构造方法绑定。当然可以在 在Handler 中的enqueueMessage()也绑定了,所有发送Message的方法都会调用此方法入队,所以在创建Message的时候是可以不绑定的。

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {msg.target = this; //绑定if (mAsynchronous) {msg.setAsynchronous(true);}return queue.enqueueMessage(msg, uptimeMillis);
}

Handler发送消息

Handler发送消息的重载方法很多,但是主要只有2种。 sendMessage(Message) sendMessage方法通过一系列重载方法的调用,sendMessage调用sendMessageDelayed,继续调用sendMessageAtTime,继续调用enqueueMessage,继续调用messageQueue的enqueueMessage方法,将消息保存在了消息队列中,而最终由Looper取出,交给Handler的dispatchMessage进行处理;

我们可以看到在dispatchMessage方法中,message中callback是一个Runnable对象,如果callback不为空,则直接调用callback的run方法,否则判断mCallback是否为空,mCallback在Handler构造方法中初始化,在主线程通直接通过无参的构造方法new出来的为null,所以会直接执行后面的handleMessage()方法。

public void dispatchMessage(Message msg) {if (msg.callback != null) {//callback在message的构造方法中初始化或者使用handler.post(Runnable)时候才不为空handleCallback(msg);} else {if (mCallback != null) {//mCallback是一个Callback对象,通过无参的构造方法创建出来的handler,该属性为null,此段不执行if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);//最终执行handleMessage方法}
}private static void handleCallback(Message message) {message.callback.run();
}

Handler处理消息

在handleMessage(Message)方法中,我们可以拿到message对象,根据不同的需求进行处理,整个Handler机制的流程就结束了。 

总结:

handler.sendMessage 发送消息到消息队列MessageQueue,然后looper调用自己的loop()函数带动MessageQueue从而轮询messageQueue里面的每个Message,当Message达到了可以执行的时间的时候开始执行,执行后就会调用message绑定的Handler来处理消息。

大致的过程如下图所示:

handler机制就是一个传送带的运转机制。

1)MessageQueue就像履带。

2)Thread就像背后的动力,就是我们通信都是基于线程而来的。

3)传送带的滚动需要一个开关给电机通电,那么就相当于我们的loop函数,而这个loop里面的for循环就会带着不断的滚动,去轮询messageQueue。

4)Message就是我们的货物了。 

Handler线程同步问题

Handler是用于线程间通信的,但是它产生的根本并不只是用于UI处理,而更多的是handler是整个app通信的框架,大家可以在ActivityThread里面感受到,整个App都是用它来进行线程间的协调。Handler既然这么重要,那么它的线程安全就至关重要了,那么它是如何保证自己的线程安全呢?

Handler机制里面最主要的类MessageQueue,这个类就是所有消息的存储仓库,在这个仓库中,我们如何的管理好消息,这个就是一个关键点了。消息管理就2点:1)消息入库(enqueueMessage),2)消息出库(next),所以这两个接口是确保线程安全的主要档口。

enqueueMessage 源码:

boolean enqueueMessage(Message msg, long when) {if (msg.target == null) {throw new IllegalArgumentException("Message must have a target.");}if (msg.isInUse()) {throw new IllegalStateException(msg + " This message is already in use.");}// 锁开始的地方synchronized (this) {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;Message p = mMessages;boolean needWake;if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.msg.next = p;mMessages = msg;needWake = 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();Message prev;for (;;) {prev = p;p = p.next;if (p == null || when < p.when) {break;}if (needWake && p.isAsynchronous()) {needWake = false;}}msg.next = p; // invariant: p == prev.nextprev.next = msg;}// We can assume mPtr != 0 because mQuitting is false.if (needWake) {nativeWake(mPtr);}}
//锁结束的地方

synchronized锁是一个内置锁,也就是由系统控制锁的lock unlock时机的。这个锁,说明的是对所有调用同一个MessageQueue对象的线程来说,他们都是互斥的,然而,在我们的Handler里面,一个线程是对应着一个唯一的Looper对象,而Looper中又只有一个唯一的MessageQueue(这个在上文中也有介绍)。所以,我们主线程就只有一个MessageQueue对象,也就是说,所有的子线程向主线程发送消息的时候,主线程一次都只会处理一个消息,其他的都需要等待,那么这个时候消息队列就不会出现混乱。

next函数 源码:

Message next() {....for (;;) {....nativePollOnce(ptr, nextPollTimeoutMillis);synchronized (this) {// Try to retrieve the next message. Return if found....return msg;}} else {// No more messages.nextPollTimeoutMillis = -1;}...}//synchronized 结束之处// Run the idle handlers.// We only ever reach this code block during the first iteration.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);}}}// Reset the idle handler count to 0 so we do not run them again.pendingIdleHandlerCount = 0;// While calling an idle handler, a new message could have been delivered// so go back and look again for a pending message without waiting.nextPollTimeoutMillis = 0;}
}

next函数很多同学会有疑问:我从线程里面取消息,而且每次都是队列的头部取,那么它加锁是不是没有意义呢?

答案是否定的,我们必须要在next里面加锁,因为,这样由于synchronized(this)作用范围是所有 this正在访问的代码块都会有保护作用,也就是它可以保证 next函数和 enqueueMessage函数能够实现互斥。这样才能真正的保证多线程访问的时候messagequeue的有序进行。

Handler消息机制--同步屏障

同步屏障,view绘制中使用,同步屏障的概念,在Android开发中非常容易被人忽略,因为这个概念在我们普通的开发中太少见了,很容易被忽略。

大家经过上面的学习应该知道,线程的消息都是放到同一个MessageQueue里面,取消息的时候是互斥取消息,而且只能从头部取消息,而添加消息是按照消息的执行的先后顺序进行的排序,那么问题来了,同一个时间范围内的消息,如果它是需要立刻执行的,那我们怎么办,按照常规的办法,我们需要等到队列轮询到我自己的时候才能执行哦,那岂不是黄花菜都凉了。所以,我们需要给紧急需要执行的消息一个绿色通道,这个绿色通道就是同步屏障的概念。

同步屏障是什么?

屏障的意思即为阻碍,顾名思义,同步屏障就是阻碍同步消息,只让异步消息通过。如何开启同步屏障呢?使用:MessageQueue#postSyncBarrier()

public int postSyncBarrier() {return postSyncBarrier(SystemClock.uptimeMillis());
}private int postSyncBarrier(long when) {// Enqueue a new sync barrier tokensynchronized (this) {final int token = mNextBarrierToken++;//从消息池中获取Messagefinal Message msg = Message.obtain();msg.markInUse();//就是这里!!!初始化Message对象的时候,并没有给target赋值,因此 target==nullmsg.when = when;msg.arg1 = token;Message prev = null;Message p = mMessages;if (when != 0) {while (p != null && p.when <= when) {//如果开启同步屏障的时间(假设记为T)T不为0,且当前的同步消息里有时间小于T,则prev也不为nullprev = p;p = p.next;}}//根据prev是不是为null,将 msg 按照时间顺序插入到 消息队列(链表)的合适位置if (prev != null) { // invariant: p == prev.nextmsg.next = p;prev.next = msg;} else {msg.next = p;mMessages = msg;}return token;}
}

可以看到,Message 对象初始化的时候并没有给 target 赋值,因此, target == null 的 来源就找到了。上面消息的插入也做了相应的注释。这样,一条 target == null 的消息就进入了消息队列。

同步屏障总结:

1)messageQueue.postSyncBarrier(),发送一个message.target = null消息,开启同步屏障;
2)随后发送一个异步消息(massage.setAsynchronous(true))到messageQueue;
3)messageQueue会优先处理异步消息;
4)异步消息处理完,调用MessageQueue.removeSyncBarrier移除屏障消息。

那么,开启同步屏障后,所谓的异步消息又是如何被处理的呢?

如果对消息机制有所了解的话,应该知道消息的最终处理是在消息轮询器 Looper#loop() 中,而 loop() 循环中会调用 MessageQueue#next() 从消息队列中进行取消息。

    .....//省略一些代码int pendingIdleHandlerCount = -1; // -1 only during first iteration// 1.如果nextPollTimeoutMillis=-1,一直阻塞不会超时。// 2.如果nextPollTimeoutMillis=0,不会阻塞,立即返回。// 3.如果nextPollTimeoutMillis>0,最长阻塞nextPollTimeoutMillis毫秒(超时)// 如果期间有程序唤醒会立即返回。int nextPollTimeoutMillis = 0;//next()也是一个无限循环for (;;) {if (nextPollTimeoutMillis != 0) {Binder.flushPendingCommands();}nativePollOnce(ptr, nextPollTimeoutMillis);synchronized (this) {//获取系统开机到现在的时间final long now = SystemClock.uptimeMillis();Message prevMsg = null;Message msg = mMessages; //当前链表的头结点//关键!!!//如果target==null,那么它就是屏障,需要循环遍历,一直往后找到第一个异步的消息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) {//如果有消息需要处理,先判断时间有没有到,如果没到的话设置一下阻塞时间,//场景如常用的postDelayif (now < msg.when) {//计算出离执行时间还有多久赋值给nextPollTimeoutMillis,//表示nativePollOnce方法要等待nextPollTimeoutMillis时长后返回nextPollTimeoutMillis = (int) Math.min(msg.when - now,Integer.MAX_VALUE);} else {// 获取到消息mBlocked = false;//链表操作,获取msg并且删除该节点if (prevMsg != null)prevMsg.next = msg.next;} else {mMessages = msg.next;}msg.next = null;msg.markInUse();//返回拿到的消息return msg;}} else {//没有消息,nextPollTimeoutMillis复位nextPollTimeoutMillis = -1;}.....//省略
}

从上面可以看出,当消息队列开启同步屏障的时候(即标识为 msg.target == null ),消息机制在处理消息的时候,优先处理异步消息。这样,同步屏障就起到了一种过滤和优先级的作用。

下面用示意图简单说明:

如上图所示,在消息队列中有同步消息和异步消息(黄色部分)以及一道墙----同步屏障(红色部分)。有了同步屏障的存在,msg_2 和 msg_M 这两个异步消息可以被优先处理,而后面的 msg_3 等同步消息则不会被处理。那么这些同步消息什么时候可以被处理呢?那就需要先移除这个同步屏障,即调用 removeSyncBarrier() 。

同步消息的应用场景

似乎在日常的应用开发中,很少会用到同步屏障。那么,同步屏障在系统源码中有哪些使用场景呢?Android 系统中的 UI 更新相关的消息即为异步消息,需要优先处理。

比如,在 View 更新时,draw、requestLayout、invalidate 等很多地方都调用了ViewRootImpl#scheduleTraversals() ,如下:

void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;//开启同步屏障mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//发送异步消息mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);if (!mUnbufferedInputDispatch) {scheduleConsumeBatchedInput();}notifyRendererOfFramePending();pokeDrawLockIfNeeded();}
}

postCallback() 最终走到了 ChoreographerpostCallbackDelayedInternal() :

private void postCallbackDelayedInternal(int callbackType,Object action, Object token, long delayMillis) {if (DEBUG_FRAMES) {Log.d(TAG, "PostCallback: type=" + callbackType- ", action=" + action + ",token=" + token =" + delayMillis);}synchronized (mLock) {final long now = SystemClock.uptimeMillis();final long dueTime = now + delayMillis;mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);if (dueTime <= now) {scheduleFrameLocked(now);} else {Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);msg.arg1 = callbackType;msg.setAsynchronous(true); //异步消息mHandler.sendMessageAtTime(msg, dueTime);}}
}

这里就开启了同步屏障,并发送异步消息,由于 UI 更新相关的消息是优先级最高的,这样系统就会优先处理这些异步消息。最后,当要移除同步屏障的时候需要调用 ViewRootImpl#unscheduleTraversals() 。

void unscheduleTraversals() {if (mTraversalScheduled) {mTraversalScheduled = false;//移除同步屏障mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);mChoreographer.removeCallbacks(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);}
}

小结:

同步屏障的设置可以方便地处理那些优先级较高的异步消息。当我们调用
Handler.getLooper().getQueue().postSyncBarrier() 并设置消息的 setAsynchronous(true) 时,target 即为 null ,也就开启了同步屏障。当在消息轮询器 Looper 在 loop() 中循环处理消息时,如若开启了同步屏障,会优先处理其中的异步消息,而阻碍同步消息。

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

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

相关文章

STM32存储左右互搏 I2C总线FATS读写EEPROM ZD24C1MA

STM32存储左右互搏 I2C总线FATS读写EEPROM ZD24C1MA 在较低容量存储领域&#xff0c;EEPROM是常用的存储介质&#xff0c;可以通过直接或者文件操作方式进行读写。不同容量的EEPROM的地址对应位数不同&#xff0c;在发送字节的格式上有所区别。EEPROM是非快速访问存储&#xf…

vue2+Spring Boot2.7 大文件分片上传

之前我们文章 手把手带大家实现 vue2Spring Boot2.7 文件上传功能 将了上传文件 但如果文件很大 就不太好处理了 按正常情况甚至因为超量而报错 这里 我弄了个足够大的文件 我们先搭建 Spring Boot2.7 环境 首先 application.yml 代码编写如下 server:port: 80 upload:path:…

【佳佳怪文献分享】使用点云从半监督到全监督房间布局估计

标题&#xff1a;From Semi-supervised to Omni-supervised Room Layout Estimation Using Point Cloud 作者&#xff1a;Huan-ang Gao, Beiwen Tian, Pengfei Li, Xiaoxue Chen, Hao Zhao, Guyue Zhou , Yurong Chen and Hongbin Zha 来源&#xff1a;2023 IEEE Internation…

根据源码,模拟实现 RabbitMQ - 通过 SQLite + MyBatis 设计数据库(2)

目录 一、数据库设计 1.1、数据库选择 1.2、环境配置 1.3、建库建表接口实现 1.4、封装数据库操作 1.5、针对 DataBaseManager 进行单元测试 一、数据库设计 1.1、数据库选择 MySQL 是我们最熟悉的数据库&#xff0c;但是这里我们选择使用 SQLite&#xff0c;原因如下&am…

手机出现 不读卡 / 无信号时应该怎么办?

当手机屏幕亮起&#xff0c;一般在屏幕最上方都会有代表手机卡状态的显示&#xff0c;其中网络信号和读卡状态的标识&#xff0c;依旧有很多人分不太清&#xff0c;更不清楚改怎么办了。 1、当我们的手机里有两张卡时&#xff0c;则会有两个信号显示 2、信号状态一般是由短到…

CSS自己实现一个步骤条

前言 步骤条是一种用于引导用户按照特定流程完成任务的导航条&#xff0c;在各种分步表单交互场景中广泛应用。例如&#xff1a;在HIS系统-门诊医生站中的接诊场景中&#xff0c;我们就可以使用步骤条来实现。她的执行步骤分别是&#xff1a;门诊病历>遗嘱录入>完成接诊…

ArcGIS Pro基础入门、制图、空间分析、影像分析、三维建模、空间统计分析与建模、python融合、案例全流程科研能力提升

目录 第一章 入门篇 GIS理论及ArcGIS Pro基础 第二章 基础篇 ArcGIS数据管理与转换 第三章 数据编辑与查询、拓扑检查 第四章 制图篇 地图符号与版面设计 第五章 空间分析篇 ArcGIS矢量空间分析及应用 第六章 ArcGIS栅格空间分析及应用 第七章 影像篇 遥感影像处理 第八…

Python random模块用法整理

随机数在计算机科学领域扮演着重要的角色&#xff0c;用于模拟真实世界的随机性、数据生成、密码学等多个领域。Python 中的 random 模块提供了丰富的随机数生成功能&#xff0c;本文整理了 random 模块的使用。 文章目录 Python random 模块注意事项Python random 模块的内置…

30行JS代码带你手写自动回复语音聊天机器人

&#x1f942;(❁◡❁)您的点赞&#x1f44d;➕评论&#x1f4dd;➕收藏⭐是作者创作的最大动力&#x1f91e; 前言 现如今生活中到处都是聊天机器人的身影&#xff0c;聊天机器人不仅仅能减少人工的聊天压力&#xff0c;而且十分的可爱有趣&#xff0c;安卓系统的小AI&#xf…

Springboot整合Mybatis调用Oracle存储过程

1、配置说明 Oracel11g+springboot2.7.14+mybatis3.5.13 目标:springboot整合mybatis访问oracle中的存储过程,存储过程返回游标信息。 mybatis调用oracle中的存储过程方式 2、工程结构 3、具体实现 3.1、在Oracle中创建测试数据库表 具体数据可自行添加 create table s…

Lodash——使用与实例

1. 简介 Lodash是一个一致性、模块化、高性能的JavaScript实用库。Lodash通过降低array、number、objects、string等等的使用难度从而让JavaScript变得简单。Lodash的模块方法&#xff0c;非常适用于&#xff1a; 遍历array、object 和 string对值进行操作和检测创建符合功能的…

字符个数统计(同类型只统计一次)

思路&#xff1a;因为题目圈定出现的字符都是 ascii 值小于等于127的字符&#xff0c;因此只需要定义一个标记数组大小为128 &#xff0c;然后将字符作为数组下标在数组中进行标记&#xff0c;若数组中没有标记过表示第一次出现&#xff0c;进行计数&#xff0c;否则表示重复字…

简单线性回归:预测事物间简单关系的利器

文章目录 &#x1f340;简介&#x1f340;什么是简单线性回归&#xff1f;&#x1f340;简单线性回归的应用场景使用步骤&#xff1a;注意事项&#xff1a; &#x1f340;代码演示&#x1f340;结论 &#x1f340;简介 在数据科学领域&#xff0c;线性回归是一种基本而强大的统…

Kali Linux助您网络安全攻防实战

Kali Linux&#xff1a;黑客与防御者的神器 Kali Linux是一款专为网络安全测试和攻防实践而设计的操作系统。它汇集了大量的安全工具&#xff0c;可以用于渗透测试、漏洞扫描、密码破解等任务&#xff0c;不仅为黑客提供了强大的攻击能力&#xff0c;也为安全防御者提供了测试和…

Kafka 入门到起飞 - 什么是 HW 和 LEO?何时更新HW和LEO呢?

上文我们已经学到&#xff0c; 一个Topic&#xff08;主题&#xff09;会有多个Partition&#xff08;分区&#xff09;为了保证高可用&#xff0c;每个分区有多个Replication&#xff08;副本&#xff09;副本分为Leader 和 Follower 两个角色&#xff0c;Follower 从Leader同…

爬虫逆向实战(十八)--某得科技登录

一、数据接口分析 主页地址&#xff1a;某得科技 1、抓包 通过抓包可以发现数据接口是AjaxLogin 2、判断是否有加密参数 请求参数是否加密&#xff1f; 查看“载荷”模块可以发现有一个password加密参数和一个__RequestVerificationToken 请求头是否加密&#xff1f; 无…

【Linux】Reactor模式

Reactor模式 Reactor模式的定义 Reactor反应器模式&#xff0c;也叫做分发者模式或通知者模式&#xff0c;是一种将就绪事件派发给对应服务处理程序的事件设计模式。 Reactor模式的角色构成 Reactor主要由以下五个角色构成&#xff1a; reactor模式的角色 角色解释Handle(句…

保姆级别讲解Python数据处理,你绝对能会

名字&#xff1a;阿玥的小东东 学习&#xff1a;Python、C/C 主页链接&#xff1a;阿玥的小东东的博客_CSDN博客-python&&c高级知识,过年必备,C/C知识讲解领域博主 目录 1. 文件读取 2. 数据处理 3. 处理结果输出 总的来说 为了咱们让程序跑起来&#xff0c;我们需…

DAY3,ARM(LED点灯实验)

1.汇编实现开发板三盏灯点亮熄灭&#xff1b; .text .global _start _start: /**********LED123点灯**************/RCC_INIT:1使能PE10 PF10 PE8RCC..寄存器,E[4]1 F[5]1 0x50000a28ldr r0,0x50000a28ldr r1,[r0]orr r1,r1,#(0x3 << 4)str r1,[r0]LED1_INET:2初始化LED…

酷开系统 | 酷开科技大数据,更好的与目标消费人群建立联系

众所周知&#xff0c;OTT的一大优势在于强曝光&#xff0c;能够给消费者带来强烈的视觉冲击&#xff0c;强化品牌认知。但是&#xff0c;要想达到提升品牌认知&#xff0c;首先要保证OTT的流量规模&#xff0c;实现对目标人群的有效覆盖。得年轻消费者得“天下”&#xff0c;年…