Android10_原理机制系列_Android消息机制(Handler)详述
概述
在Android中的多进程、多线程中提过,只有主线程(UI线程)可以更新UI,其他线程不可以,所以一般耗时操作放到子线程。子线程可以通过Handler将相关信息通知到主线程。
Android的消息机制主要是Handler机制。Handler的工作过程,还有两个重要部分MessageQueue(消息队列,下面简称MQ)和Looper。
由于下面总结中穿插了不少源码 便于理解,导致篇幅比较长(加代码有600多行)。所以先大致总结并给出大致目录,提前了解是否是需要的内容。
大致总结
消息机制的大致流程
- 线程创建Looper(调用Looper.preper()),然后运行Looper.looper()开启循环。Looper是和线程绑定的,一个线程只能有一个Looper ,一个Looper内部维护一个MQ。
- Handler调用sendMessage()方法发送消息,将消息加入到MQ中(enqueueMessage())。
- Looper死循环,通过MessageQueue.next()取出符合的消息。
- 取出消息后,通过msg.target.dispatchMessage()分发消息。
- Handler对接受到的消息进行具体处理,调用dispatchMessage()。
- 调用Looper的quit()方法终止,即消息队列退出、looper循环退出。
注意点
- 创建Handler的线程中一定先有Looper对象, 才能创建Handler,否则会抛异常。下面详解中通过代码就能看出来。
- 一个线程只能有一个Looper。但可以创建多个Handler
- MQ是Looper内存维护的。
- 主线程在应用启动后默认创建Looper 且不能退出。
跨线程大致理解
Handler能做到跨线程,主要是Looper及内部的消息队列。最常见的:程序启动主线程创建Looper并绑定了消息队列,在主线程创建Handler,这个Handler与Looper绑定的。在其他线程(任何地方)通过这个Handler发送消息,消息都加入到了主线程Looper内部的消息队列(消息发送到的MQ是 创建Handler时绑定的Looper内部MQ),当消息被Looper循环取出,自然就回到了主线程
大致目录
1 Looper、Handler与MQ
1.1 Looper
1.1.1 Looper的创建:prepare()
1.1.2 Looper循环:loop()
1.2 Handler
1.2.1 Handler的创建
1.2.2 Handler发送消息
1.2.3 Handler分派处理:dispatchMessage
1.3 MessageQueue
1.3.1 入队:enqueueMessage()
1.3.2 next()方法
1.3.3 退出:quit()
2 其他注意点
2.1 Handler一般使用
2.2 消息池
2.3 子线程到主线程的方法
2.4 主线程的Looper
2.5 ANR问题
Looper、Handler与MQ
Looper
Looper是循环器,为一个线程运行消息循环,不断检查消息队列中是否有新的消息。
Looper.prepare()为当前线程创建一个looper,并在其内部维护一个MQ。
Looper.loop()即looper开始工作,运行消息循环。
下面是Looper部分的几处源码,有助于理解。
Looper的创建:prepare()
// sThreadLocal.get() will return null unless you've called prepare().
@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
@UnsupportedAppUsage
final MessageQueue mQueue;
final Thread mThread;
public static void prepare() {
prepare(true);
}
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));
}
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
Looper通过prepare()方法创建,主要有以下几点:
- 默认创建Looper时,设置参数的为true,即消息队列是能够退出的。MQ的退出也提供了安全退出和非安全退出两种方法(在下面MQ部分中详述)。
- 一个线程只能创建一个Looper,否则会抛出RuntimeException。
- Looper对象保存在ThreadLocal中的。ThreadLocal是一个线程内部的数据存储类,每个线程都有自己独立访问的变量副本。 libcore/ojluni/src/main/java/java/lang/ThreadLocal.java下有相关源码。
- Looper创建,即创建了一个内部消息队列mQueue,并绑定了当前线程。
- 主线程(UI线程)创建的Looper是不可退出的,应用启动后默认创建好了的。(下面有单独讲解)
Looper循环:loop()
Message.java
@UnsupportedAppUsage
/*package*/ Handler target;
@UnsupportedAppUsage
/*package*/ Runnable callback;
Looper.java
public static void loop() {
......
for (;;) {
//不断取出下一条消息,mgs为null即消息队列退出,若没有消息且没有退出 消息队列一直阻塞的
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
......
try {
//分派消息,通过Handler处理
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
......
msg.recycleUnchecked();
}
}
public static @Nullable
Looper myLooper() {
return sThreadLocal.get();
}
由于代码比较长,截取了关键代码,......表示该处有省略的代码。
loop()是消息循环运行的关键,整体把握这里关注两行代码:Message msg = queue.next(); 和 msg.target.dispatchMessage(msg);
。这两个分别在MQ部分和Handler部分有详述。
- for(;;)死循环,这是关键,真正的消息循环。跳出的唯一条件是queue.next()为null,但实际只有Looper执行quit()才能达成这一条件(next()方法MQ中详述)。
- next()不断取出下一条消息,mgs为null即消息队列退出,循环停止。若没有消息且没有退出 消息队列一直阻塞的
- 当next()返回一条消息,Looper调用msg.target.dispatchMessage(msg)进行分派处理。这里的msg.target就是一个Handler,即调用了Handler的dispatchMessage()方法(Handler中详述)。
Handler
Handler主要包含消息的发送和接收处理。
Handler的创建
@UnsupportedAppUsage
final Looper mLooper;
final MessageQueue mQueue;
@UnsupportedAppUsage
final Callback mCallback;
final boolean mAsynchronous;
/**
* @hide
*/
public Handler(@Nullable Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
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;
mCallback = callback;
mAsynchronous = async;
}
注意几点:
- Handler有好几种构造方法,一种是指定了Looper,另一种没指定。所有构造方法都基本确定4个值:Hanlder关联的Looper;关联的MQ;关联的Callback;是否异步。
- 上述是没指定Looper最终的方法。从中可以看到,没有Looper就无法创建Handler。
Handler发送消息
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;
}
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
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);
}
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通过post()、postDelayed()、sendMessage()、sendMessageDelayed()等方法发送消息,最终都是走到了上面的sendMessageAtTime()中。
- Handler各种发送方式,最终到sendMessageAtTime()。这个过程,参数uptimeMillis 即 消息发处的绝对时间也就是when。
- post()等发送方法传入的参数Runnable即消息的callback(msg.callback),在dispatchMessage中可以看到。
- 发送消息,Handler仅是将消息加入消息队列中(enqueueMessage()在MQ中详述),这个消息队列就是Handler在创建时绑定的Looper的内部消息队列。
这里都是使用SystemClock.uptimeMillis(),简单说明下SystemClock.uptimeMillis()与System.currentTimeMillis()区别:
System.currentTimeMillis()是1970年1月1日(UTC)到现在的毫秒值。
SystemClock.uptimeMillis()是设备启动到现在的时间的毫秒值。(不包括深度睡眠)
SystemClock.elapsedRealtime()是设备启动到现在时间的毫秒值。(包括深度睡眠)
为什么基本都用SystemClock.uptimeMillis()作为时间间隔的获取方法呢?
System.currentTimeMillis()通过设置设备的时间是可以改变的,这样设置后 那些计划的执行明显会发生异常。
Handler分派处理:dispatchMessage
@UnsupportedAppUsage
/*package*/ Handler target;
@UnsupportedAppUsage
/*package*/ Runnable callback;
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
private static void handleCallback(Message message) {
message.callback.run();
}
public interface Callback {
/**
* @param msg A {@link android.os.Message Message} object
* @return True if no further handling is desired
*/
boolean handleMessage(@NonNull Message msg);
}
/**
* Subclasses must implement this to receive messages.
*/
public void handleMessage(@NonNull Message msg) {
}
Looper循环通过queue.next()获取到一条消息,再通过Handler的dispatchMessage()分派处理。
- 若msg.callback不为空,即上述所说(Handler发送消息 部分)的是否传入了Runnable参数,有则执行Runnable。(Handler发送消息时传入)
- 如果msg.callback为空,在Handler创建时指定了Callback参数,即实现了handleMessage()的类作为参数,则直接执行handleMessage()。(Handler创建时传入)
- 若上面两种回调都不存在,可由handleMessage()处理。(创建的Handler重写该方法)
MessageQueue
消息队列MQ。主要列出Looper和Handler中提到的几个关于MQ的重要过程。
消息队列是单链表实现的,这属于数据结构,了解的话可以参考数据结构之队列(Queue)。
入队:enqueueMessage()
boolean enqueueMessage(Message msg, long when) {
//Handler为空
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;
//队列是空或者msg比队列中其他消息要先执行,该msg作为队首入队。
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
//加入队首。p是指向之前队首的,了解队列链表实现很容易理解
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循环,跳出时:p指向null,prev指向队尾最后一个消息,即msg最后执行。
//或者p指向第一个when大于msg的消息,prev则指向前面一个(最后一个when小于msg的消息)
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
//msg插入到对应的位置
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
Handler发送消息,将消息加入了消息队列,即上面的enqueueMessage的方法。
这个方法不难理解,可以看添加的中文注释。
这里主要注意的是消息的处理时间,看入队逻辑 可以看出消息队列是按消息处理时间排队的。
next()方法
@UnsupportedAppUsage
Message next() {
......
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) {
//还没有到消息处理时间,设置阻塞时间nextPollTimeoutMillis,进入下次循环的时候会调用nativePollOnce(ptr, nextPollTimeoutMillis)
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;
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 {
//没有消息要处理,nextPollTimeoutMillis设置为-1。
// No more messages.
nextPollTimeoutMillis = -1;
}
// Process the quit message now that all pending messages have been handled.
//消息队列退出,返回null
if (mQuitting) {
dispose();
return null;
}
......
}
......
}
}
这个方法比较复杂,代码比较长。上面只截取了部分关键代码,可以看下添加的中文注释,能够理解。
注意两个地方:
- 消息屏障(同步屏障)。可以通过MessageQueue.postSyncBarrier()设置,即msg.target为null。这里相当于异步优先。
- 仅当mQuitting为true时,即消息队列退出(quit()),next()才返回null。Looper的looper()循环才停止。
最后来看下消息队列的退出
退出:quit()
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
// We can assume mPtr != 0 because mQuitting was previously false.
nativeWake(mPtr);
}
}
private void removeAllFutureMessagesLocked() {
final long now = SystemClock.uptimeMillis();
Message p = mMessages;
if (p != null) {
if (p.when > now) {
removeAllMessagesLocked();
} else {
Message n;
for (;;) {
n = p.next;
if (n == null) {
return;
}
if (n.when > now) {
break;
}
p = n;
}
p.next = null;
do {
p = n;
n = p.next;
p.recycleUnchecked();
} while (n != null);
}
}
}
private void removeAllMessagesLocked() {
Message p = mMessages;
while (p != null) {
Message n = p.next;
p.recycleUnchecked();
p = n;
}
mMessages = null;
}
这个也不复杂,简单关注两点:
- quit()有安全退出和非安全退出,两者的差别从上面也能看到。非安全退出removeAllMessagesLocked(),直接退出 清空队列消息;安全退出removeAllFutureMessagesLocked(),消息处理时间在现在now之后的消息会被直接清空,而在now之前的会继续保留 由next()继续获取处理。
- quit()调用后才有mQuitting = true,这样next()才会返回null,最终Looper.looper()循环才会停止。
其他注意点
Handler一般使用
Handler使用,一般是子线程进入主线程更新UI。下面是常见的操作。
主要注意Hanler的创建(多种方式的选择)以及回调的处理,发送消息的方式。
private final String TAG = "HandlerActivity";
private final int MAIN_HANDLER_1 = 1;
private Handler mMainHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage( msg );
switch (msg.what) {
case MAIN_HANDLER_1:
//Do something. like Update UI
break;
}
}
};;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate( savedInstanceState );
Log.d( TAG, "onCreate: MainHandler Looper=" + mMainHandler.getLooper() );
SubThread subThread = new SubThread();
subThread.start();
}
private class SubThread extends Thread {
@Override
public void run() {
//Do something
Message message = mMainHandler.obtainMessage();
message.what = MAIN_HANDLER_1;
mMainHandler.sendMessage(message);
}
}
消息池
Message内部保存了一个缓存的消息池,我们可以通过Message.obtain()或者mMainHandler.obtainMessage()从缓存池获得一个消息对象。避免每次创建Message带来的资源占用。
Message.obtain()的多种方法以及mMainHandler.obtainMessage()最终都是调用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 flag
sPoolSize--;
return m;
}
}
return new Message();
}
子线程到主线程的方法
Handler的一个重要作用就是子线程进入主线程更新UI。
Android中的多进程、多线程也提到过2种
Activity.runOnUiThread(Runnable);View.post(Runnable)/View.postDelayed(Runnable, long)。
这2种方法其实就是Handler机制实现的。
Activity.java
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
View.java
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
......
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions( info.mHandler );
mRunQueue = null;
}
......
}
HandlerActionQueue.java
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
主线程的Looper
ActivityThread中的main()是主线程的入口。
从下面代码中可以看出来,应用启动 主线程默认创建了Looper,它是不可退出的。Looper有单独保存并获取主线程Looper的方法。
主线程Looper创建参数为false(prepare(false)),即looper()的循环是不会停止的,当没有消息时,一直是阻塞的。
Run|Debug
public static void main(String[] args) {
......
Looper.prepareMainLooper();
......
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
/// M: ANR Debug Mechanism
mAnrAppManager.setMessageLogger(Looper.myLooper());
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
Looper.java
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
ANR问题
looper()死循环为什么没导致ANR?ANR具体什么造成?ANR和Looper有什么关系?
---这篇已经过长,这些问题在ANR部分总结更好。
Android10_原理机制系列_Android消息机制(Handler)详述的更多相关文章
- android开发系列之消息机制
最近接触到一个比较有挑战性的项目,我发现里面使用大量的消息机制,现在这篇博客我想具体分析一下:android里面的消息到底是什么东西,消息机制到底有什么好处呢? 其实说到android消息机制,我们可 ...
- Win32窗口消息机制 x Android消息机制 x 异步执行
如果你开发过Win32窗口程序,那么当你看到android代码到处都有的mHandler.sendEmptyMessage和 private final Handler mHandler = new ...
- 【原创】源码角度分析Android的消息机制系列(六)——Handler的工作原理
ι 版权声明:本文为博主原创文章,未经博主允许不得转载. 先看Handler的定义: /** * A Handler allows you to send and process {@link Mes ...
- Android10_原理机制系列_事件传递机制
前言和概述 Android的输入设备,最常用的就是 触摸屏和按键 了.当然还有其他方式,比如游戏手柄,比如支持OTG设备,则可以链接鼠标.键盘等. 那么这些设备的操作 是如何传递到系统 并 控制界面的 ...
- iOS开发系列--通知与消息机制
概述 在多数移动应用中任何时候都只能有一个应用程序处于活跃状态,如果其他应用此刻发生了一些用户感兴趣的那么通过通知机制就可以告诉用户此时发生的事情.iOS中通知机制又叫消息机制,其包括两类:一类是本地 ...
- 【原创】源码角度分析Android的消息机制系列(一)——Android消息机制概述
ι 版权声明:本文为博主原创文章,未经博主允许不得转载. 1.为什么需要Android的消息机制 因为Android系统不允许在子线程中去访问UI,即Android系统不允许在子线程中更新UI. 为什 ...
- iOS开发系列--通知与消息机制--转
来自:http://www.cocoachina.com/ios/20150318/11364.html 概述 在多数移动应用中任何时候都只能有一个应用程序处于活跃状态,如果其他应用此刻发生了一些用户 ...
- android 进程间通信 messenger 是什么 binder 跟 aidl 区别 intent 进程间 通讯? android 消息机制 进程间 android 进程间 可以用 handler么 messenger 与 handler 机制 messenger 机制 是不是 就是 handler 机制 或 , 是不是就是 消息机制 android messenge
韩梦飞沙 韩亚飞 313134555@qq.com yue31313 han_meng_fei_sha messenger 是什么 binder 跟 aidl 区别 intent 进程间 通讯 ...
- android 进程/线程管理(四)----消息机制的思考(自定义消息机制)
关于android消息机制 已经写了3篇文章了,想要结束这个系列,总觉得少了点什么? 于是我就在想,android为什么要这个设计消息机制,使用消息机制是现在操作系统基本都会有的特点. 可是andro ...
随机推荐
- MS SQL SERVER执行大脚本文件时,提示“内存不足”的解决办法
问题描述: 当客户服务器不允许直接备份时,往往通过导出数据库脚本的方式来部署-还原数据库, 但是当数据库导出脚本很大,用Microsoft SQL Server Management Studio执行 ...
- mac 搭建 Robot Framework
前提介绍,我的mac上python2和python3是都要有的,然后大家可以看看我其他的文章,这些文章虽然很多都是连接,是别人的博客或者资料,但都是自己试过没有问题的,只是比较懒然后就没有自己写. r ...
- C# Webservice中如何实现方法重载--(方法名同名时出现的问题)
本文摘抄自:http://blog.sina.com.cn/s/blog_53b720bb0100voh3.html 1.Webservice中的方法重载问题(1)在要重载的WebMethod上打个M ...
- Zeal(文档)安装使用
Zeal是一个为软件开发者提供的离线文档浏览器. 一.下载安装 下载地址:https://zealdocs.org/ 二.安装后下载自己需要的文档 1.通过Zeal原生源直接下载文档 Tools -& ...
- Mybatis入门 Mybatis存在的意义 解决的问题 基本操作
Mybatis入门 Mybatis的作用 解决的问题 基本操作 为什么要学MyBatis 我们链接操作数据库需要做的步骤 package Test; import java.sql.*; public ...
- centos8 curl: (35) error:141A318A:SSL routines:tls_process_ske_dhe:dh key too small
centos8操作系统,curl -k https:/www.xxx.com 报错 curl: (35) error:141A318A:SSL routines:tls_process_ske_dh ...
- Java学习的第二十五天
1.字节流输出内容 用字节流读文件内容 字符输出流写入 字符输入流 2.没问题 3.明天学习到缓冲流
- NLP文本多标签分类---HierarchicalAttentionNetwork
最近一直在做多标签分类任务,学习了一种层次注意力模型,基本结构如下: 简单说,就是两层attention机制,一层基于词,一层基于句. 首先是词层面: 输入采用word2vec形成基本语料向量后,采用 ...
- 浅析 AC 自动机
目录 简述 AC 自动机是什么 AC 自动机有什么用 AC 自动机·初探 AC 自动机·原理分析 AC 自动机·代码实现 AC 自动机·更进一步 第一题 第二题 第三题 从 AC 自动机到 fail ...
- VSCode--HTML代码片段(基础版,react、vue、jquery)
起因是最近在学习前端,看的网上的demo也是在react.vue.jquery之间穿插,为了方便一键生成html模板(懒)写demo,有了以下折腾. 本人使用的前端编辑工具是vscode(方便.懒), ...