当前位置: 首页 > news >正文

Android Framework通信:Handler

文章目录

  • 前言
  • 一、Handler源码分析
    • 1、创建Handler
    • 2、发送消息
    • 3、取消息
    • 4、消息处理
    • 5、线程切换的方法(Handler异步消息处理机制流程)
      • handler.sendMessage()
      • handler.post()
      • View.post()
      • Activity中的runOnUiThread()
  • 二、Handler高频面试题
    • 1、为什么要有Handler?
    • 2、为什么要有MessageQueue?
    • 3、为什么要有Looper?
    • 4、主线程的Looper和子线程Looper有什么不同?
    • 5、一个线程可以有几个Handler,几个looper?
    • 6、主线程会为什么会一直阻塞?
    • 7、ANR是什么,发生条件
    • 8、Looper的死循环为什么不会让主线程卡死(或ANR)?:
    • 9、为什么Handler会造成内存泄露?
    • 10、内存抖动如何解决?
    • 11、安卓中的Looper.loop()阻塞为什么不会有问题?

前言

线程间的通信,两个线程使用公共的变量或者公共的其他东西都可以进行通信,但是这种方式不是自主的,不能够自主切换线程执行,所以Handler的最终目的是为了线程间的切换,线程异步消息处理

Android UI操作并不是线程安全的,并且这些操作必须在UI线程执行。
如果非要在子线程中更新UI,那会出现什么情况呢?

android.view.ViewRoot$CalledFromWrongThreadException: 
Only the original thread that created a view hierarchy can touch its views.

很容易抛一个CalledFromWrongThreadException异常。
如果在子线程访问UI线程,Android提供了以下的方式:

Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
Handler

一、Handler源码分析

1、创建Handler

创建两个Handler对象,一个在主线程中创建,一个在子线程中创建,代码如下所示:

public class MainActivity extends AppCompatActivity {private Handler handler1;private Handler handler2;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);handler1 = new Handler();new Thread(new Runnable() {@Overridepublic void run() {handler2 = new Handler();}}).start();}
}

运行程序,你会发现,在子线程中创建的Handler(Handler2)是会导致程序崩溃的:
在这里插入图片描述
我们尝试在子线程中先调用一下Looper.prepare(),运行程序成功,不再报运行时异常

那这加上Looper.prepare()运行成功是为什么呢?此时我们分析一下Handler(基于android13 API 33)源码:
在这里插入图片描述
在第224行调用了Looper.myLooper()方法获取了一个Looper对象,如果Looper对象为空,则会抛出一个运行时异常。也就是我们上述出现的异常。

什么时候Looper对象会为空呢?接着看Looper.myLooper()中的代码:

在这里插入图片描述

sThreadLocal是一个关于Looper的ThreadLocal类

在这里插入图片描述

接着查找sThreadLocal查看是在哪里给sThreadLocal设置Looper,发现是Looper.prepare()方法

在这里插入图片描述

可以看到,首先判断sThreadLocal中是否存在Looper,如果没有则创建一个新的Looper设置进去。这样也就完全解释了为什么我们要先调用Looper.prepare()方法,才能创建Handler对象。同时也可以看出每个线程中最多只会有一个Looper对象。创建一个Looper对象会创建相应的MessageQueue,并且获取当前线程,故:Thread——>Looper——>MessageQueue是唯一对应的

在这里插入图片描述

所以Looper.prepare()的作用是创建一个新的Looper对象并设置到sThreadLocal中

Q:主线程中的Handler也没有调用Looper.prepare()方法,为什么就没有崩溃呢?

这是由于在程序启动的时候,系统已经帮我们自动调用了Looper.prepare()方法。查看ActivityThread中的main()方法,代码如下所示:

public static void main(String[] args) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");// Install selective syscall interceptionAndroidOs.install();// CloseGuard defaults to true and can be quite spammy.  We// disable it here, but selectively enable it later (via// StrictMode) on debug builds, but using DropBox, not logs.CloseGuard.setEnabled(false);Environment.initForCurrentUser();// Make sure TrustedCertificateStore looks in the right place for CA certificatesfinal File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());TrustedCertificateStore.setDefaultUserDirectory(configDir);// Call per-process mainline module initialization.initializeMainlineModules();Process.setArgV0("<pre-initialized>");Looper.prepareMainLooper();// Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.// It will be in the format "seq=114"long startSeq = 0;if (args != null) {for (int i = args.length - 1; i >= 0; --i) {if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {startSeq = Long.parseLong(args[i].substring(PROC_START_SEQ_IDENT.length()));}}}ActivityThread thread = new ActivityThread();thread.attach(false, startSeq);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");
}

可以看到,在第20行调用了Looper.prepareMainLooper()方法,而这个方法又会再去调用Looper.prepare()方法,代码如下所示:

在这里插入图片描述

我们应用程序的主线程开启的时候就会创建一个Looper对象,从而不需要再手动去调用Looper.prepare()方法了。

这样基本就将Handler的创建过程完全搞明白了,总结一下就是在主线程中可以直接创建Handler对象,而在子线程中需要先调用ooper.prepare()才能创建Handler对象。

2、发送消息

@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);handler1 = new Handler();new Thread(new Runnable() {@Overridepublic void run() {Message message = Message.obtain();message.arg1 = 1;Bundle bundle = new Bundle();bundle.putString("data", "data");message.setData(bundle);handler1.sendMessage(message);}}).start();
}

这里Handler到底是把Message发送到哪里去了呢?为什么之后又可以在Handler的handleMessage()方法中重新得到这条Message呢?看来又需要通过阅读源码才能解除我们心中的疑惑了:

在这里插入图片描述

调用了sendMessageDelayed:

在这里插入图片描述

接着调用了sendMessageAtTime,这个方法的源码如下所示:

在这里插入图片描述

sendMessageAtTime()方法接收两个参数,其中msg参数就是我们发送的Message对象,而uptimeMillis参数则表示发送消息的时间,它的值等于自系统开机到当前时间的毫秒数再加上延迟时间,如果你调用的不是sendMessageDelayed()方法,延迟时间就为0,然后将这两个参数都传递到MessageQueue的enqueueMessage()方法中。

boolean enqueueMessage(Message msg, long when) {···synchronized (this) {···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);}}return true;
}

MessageQueue并没有使用一个集合把所有的消息都保存起来,它只使用了一个mMessages对象表示当前待处理的消息。然后观察上面的代码我们就可以看出,所谓的入队其实就是将所有的消息按时间来进行排序,这个时间就是msg.when。具体的操作方法就根据时间的顺序调用msg.next,从而为每一个消息指定它的下一个消息是什么。当然如果你是通过sendMessageAtFrontOfQueue()方法来发送消息的,它也会调用enqueueMessage()来让消息入队,只不过时间为0,这时会把mMessages赋值为新入队的这条消息,然后将这条消息的next指定为刚才的mMessages,这样也就完成了添加消息到队列头部的操作。

3、取消息

入队操作我们就已经看明白了,那出队操作是在哪里进行的呢?这个就需要看一看Looper.loop()方法的源码了,如下所示:

在这里插入图片描述
此方法最后进入了一个死循环,然后不断地调用loopOnce()方法,这个方法作用为

Poll and deliver single message, return true if the outer loop should continue.

轮询并传递单个消息,如果外部循环应该继续,则返回true。

在这里插入图片描述

它的简单逻辑就是如果当前MessageQueue中存在mMessages(即待处理消息),就将这个消息出队,然后让下一条消息成为mMessages,否则就进入一个阻塞状态,一直等到有新的消息入队。每当有一个消息出队,就将它传递到msg.target的dispatchMessage()方法中,那这里msg.target又是什么呢?其实就是Handler,查看Message类源码:
在这里插入图片描述

PS:这里msg.target通过target将Handler存入Message,是为了解决在多个Hander的情况无法找到处理当前消息的Handler问题。实际上是一种架构设计上的妥协,我们常见的Hander内存泄漏问题也是源于此。最终导致Activity无法及时回收:
Thread–>Looper–>MessageQue–>Message.target–>mHandler–>Activity

4、消息处理

那么发送消息后,最终又是怎么调用到handleMessage的呢?接下来看一下Handler中dispatchMessage()方法的源码,如下所示:

在这里插入图片描述

在第101行进行判断,如果mCallback不为空,则调用mCallback的handleMessage()方法,否则直接调用Handler的handleMessage()方法,并将消息对象作为参数传递过去。这样我相信大家就都明白了为什么handleMessage()方法中可以获取到之前发送的消息了吧!

5、线程切换的方法(Handler异步消息处理机制流程)

handler.sendMessage()

我们接下来继续分析一下,为什么使用异步消息处理的方式就可以对UI进行操作了呢?这是由于Handler总是依附于创建时所在的线程,比如我们的Handler是在主线程中创建的,而在子线程中又无法直接对UI进行操作,于是我们就通过一系列的发送消息、入队、出队等环节,最后调用到了Handler的handleMessage()方法中,这时的handleMessage()方法已经是在主线程中运行的,因而我们当然可以在这里进行UI操作了。整个异步消息处理流程的示意图如下图所示:

在这里插入图片描述

在这里插入图片描述

handler.post()

在这里插入图片描述

还是调用了sendMessageDelayed()方法去发送一条消息啊,并且还使用了getPostMessage()方法将Runnable对象转换成了一条消息,我们来看下这个方法的源码:

在这里插入图片描述

在这个方法中将消息的callback字段的值指定为传入的Runnable对象。咦?这个callback字段看起来有些眼熟啊,在Handler的dispatchMessage()方法中原来有做一个检查,如果Message的callback等于null才会去调用handleMessage()方法,否则就调用handleCallback()方法。那我们快来看下handleCallback()方法中的代码吧:

在这里插入图片描述

竟然就是直接调用了一开始传入的Runnable对象的run()方法。因此在子线程中通过Handler的post()方法进行UI操作就可以这么写:

public class MainActivity extends Activity {private Handler handler;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);handler = new Handler();new Thread(new Runnable() {@Overridepublic void run() {handler.post(new Runnable() {@Overridepublic void run() {// 在这里进行UI操作}});}}).start();}
}

虽然写法上简洁很多,但是原理是完全一样的,我们在Runnable对象的run()方法里更新UI,效果完全等同于在handleMessage()方法中更新UI。

View.post()

在这里插入图片描述

原来就是调用了Handler中的post()方法

Activity中的runOnUiThread()

在这里插入图片描述

如果当前的线程不等于UI线程(主线程),就去调用Handler的post()方法,否则就直接调用Runnable对象的run()方法

二、Handler高频面试题

1、为什么要有Handler?

主要目的是要解决线程切换问题,handler里的Message机制解决了线程间通信

2、为什么要有MessageQueue?

MessageQueue是一个单向链表,next()调用nativePollOnce->lunx的epoll_wait()等待,实现阻塞时队列;

队列的出现解决了"处理消息"阻塞到"发送消息"的问题,由于队列是生产者消费者模式,而要使用队列需要至少两个线程与一个死循环;

  1. 一个线程负责生产消息;
  2. 一个线程消费消息;
  3. 死循环需要取出放入队列里的消息;

3、为什么要有Looper?

为了循环取出队列里的消息

4、主线程的Looper和子线程Looper有什么不同?

子线程Looper是可以退出的,主线程不行

5、一个线程可以有几个Handler,几个looper?

多个handler,每个handler都会配一个MessageQueue

Lopper和MessageQueue绑定,为防止创建多个messageQueue,Looper创建也只能被调用一次;

一个Looper,放在ThreadLocalMap中;
假如Looper对象由Handler创建,每创建一个Handler就有一个Looper,那么调用Looper.loop()时开启死循环;在外边调用Looper的地方就会阻塞

一个线程可以有多个Handler,并且每一个Handler都可以处理消息队列中的消息。每个Handler在创建时会与当前线程的消息队列相关联,因此可以通过Handler向该线程的消息队列发送消息。

需要注意的是,不同的Handler可能会被关联到相同的Looper(消息循环器)上,也可能不同的Handler使用各自独立的Looper来实现消息处理。例如,一个Activity可能会创建多个Handler对象,其中一些Handler会在主线程上执行,而另一些Handler则会在新建的子线程上执行,它们分别使用了不同的Looper来处理消息队列中的消息。

因此,可以说一个线程可以拥有多个Handler,这取决于应用程序设计的具体情况和需要。但是,由于在Android中每个线程都只有一个消息队列,因此多个Handler之间处理消息时可能会存在竞争和同步问题,需要开发者进行合理的规划和处理,以避免出现不必要的问题。

looper的生命周期是当前线程的生命周期长度,如何保证一个线程中只有一个Looper,可以通过线程ThreadLocal,ThreadLocal中会有一个ThreadLocalMap保存一个Looper,通过调用ThreadLocal的get()来判断是否能获取到Looper,如果能得到说明已经有了Looper直接返回一个异常通知已经有了Looper

Looper.prepare():保证只有一个Looper。存入Looper,存Looper时ThreadLocalMap的key为ThreadLocal,value为Looper;

在这里插入图片描述

sThreadLocal为ThreadLocal类
在这里插入图片描述

进入ThreadLocal类:获取当前线程:Thread.currentThread()

在这里插入图片描述
在这里插入图片描述

进入Thread类
ThreadLocalMap:类似于HashMap;每个Thread对象都有一个对应的ThreadLocalMap;

在这里插入图片描述

Looper.loop():循环提取消息并最终调用handlerMessage()去处理;

在这里插入图片描述
在这里插入图片描述

6、主线程会为什么会一直阻塞?

是的,如果主线程不进行looper.loop()阻塞,一下子执行完成,整个程序就直接结束了,不可能有机会去执行其他的任务了。

Android是事件为驱动的操作系统,事件过来就去handler里执行,没有事件就阻塞在那里显示界面;

sendMessage是生产者,handlerMessage是消费者;消息在队列中排队(MessageQueue),这样解决大量的消息过来的问题,不会造成主线程sendMessage阻塞,所有消息都会直接放在队列中排队等候执行;

7、ANR是什么,发生条件

ANR:Application Not Responding指的是应用程序无响应的错误,它表示应用程序在执行某个操作时长时间没有响应。在Android系统中,如果一个应用程序在主线程中执行了耗时的操作而导致主线程被阻塞,那么系统就会弹出一个对话框警告用户当前应用程序出现了ANR错误,并提示用户选择“等待”或“关闭应用程序”。

ANR通常是由于一些长时间的I/O操作、耗时的计算或者其他阻塞主线程的原因引起的。当主线程被阻塞时,应用程序的用户界面就会无响应,用户无法与应用程序进行交互,这就给用户带来了不好的体验。

ANR发生条件是:
Activity:应用在 5 秒内未响应用户的输入事件(如按键或者触摸)
BroadCastReceiver :BroadcastReceiver 未在 10 秒内完成相关的处理
Service:20 秒(均为前台)。Service 在20 秒内无法处理完成如果Handler收到以上三个相应事件在规定时间内完成了,则移除消息,不会ANR;若没完成则会超时处理,弹出ANR对话框;

为了避免ANR错误,开发人员可以采取以下措施:

  • 将耗时的操作放在子线程中执行,避免在主线程中执行。
  • 使用异步任务或线程池等机制来执行耗时的操作,从而避免阻塞主线程。
  • 在主线程中使用Handler或者AsyncTask等机制来更新UI界面。
  • 优化应用程序的代码,减少不必要的计算和I/O操作。

8、Looper的死循环为什么不会让主线程卡死(或ANR)?:

我们的UI线程(主线程)其实是ActivityThread所在的线程,而一个线程只会有一个Looper;

ActivityThread.java的main函数是一个APP进程的入口,如果不一直循环,则在main函数执行完最后一行代码后整个应用进程就会退出;

android是以事件为驱动的操作系统,当有事件来时,就去做对应的处理,没有时就显示静态界面;


App进程的入口为ActivityThread.java的main()函数,注意ActivityThread不是一个线程;

应用的UI主线程实际是调用ActivityThread.java的main()函数执行时所在的线程,而这个线程对我们不可见,但是这就是主线程:

在ActivityThread.java的main()函数中,会调用Looper.prepareMainLooper()Looper.prepareMainLooper()会创建一个Looper并放到主线程的变量threadLocals中进行绑定,threadLocals是一个ThreadLocal.ThreadLocalMap在ActivityThread.java的main()函数结尾,开启Looper.loop()进行死循环,不让main函数结束,从而让App进程不会结束;Android系统是以事件作为驱动的操作系统,当有事件来时,就去做对应处理,没有事件时,就显示当前界面,不做其他多余操作(浪费资源)在Looper.loop()的死循环中,不仅要取用户发的事件,还要取系统内核发的事件(如屏幕亮度改变等等)在调用Looper.loop()时,从MessageQueue.next()中获取事件,若没有则阻塞,有则分发MessageQueue其实不是一个队列,用epoll机制实现了阻塞。

在Looper.prepareMainLooper()时,会调用c++函数:

epoll_create()将App注册进epoll机制的红黑树中得到fd的值,
epoll_ctl()给每个App注册事件类型并监听fd值是否改变Linux中事件都会被写入文件中,如触摸屏幕事件会写入到:dev/input/event0文件中),fd有改变时唤醒epoll_wait,
epoll_wait()有事件时就分发,没事件就阻塞

在这里插入图片描述

9、为什么Handler会造成内存泄露?

内存泄漏:由于疏忽或错误造成程序未能释放已经不再使用的内存的情况

内存泄漏的原因:在Activity中,将Handler声明成非静态内部类或匿名内部类,这样Handle默认持有外部类Activity的引用。如果Activity在销毁时,Handler还有未执行完或者正在执行的Message,而Handler又持有Activity的引用,导致GC无法回收Activity,导致内存泄漏。如以下两种情形可能导致内存泄漏

1、在Activity内将Handler声明成匿名内部类

	//匿名内部类private Handler mHandler = new Handler() {@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);}};
   new Handler().postDelayed(new Runnable() {@Overridepublic void run() {//大量的操作,activity要销毁时还没结束}},1000);

2、在Activity内将Handler声明成非静态内部类:

	//非静态内部类private class MyHandler extends Handler{@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);}}private MyHandler mHandler = new MyHandler();

内存泄露的本质:长生命周期持有短生命周期,造成短生命周期得不到释放就会造成内存泄露;线程的生命周期长,Activity的生命周期短,Activity运行完Handler得不到释放;

1.Activity中实例化Handler导致了,Activity中持有handler对象;2.Message和Handler的持有是由于在Lopper中进行循环遍历的时候,Message需要被执行,所以要使用handler的handleMessage()3.MessageQueue是Message的集合对象,所以造成持有关系;4.Looper又和MessageQueue进行了绑定,造成了LooperMessageQueue的持有;最终:线程----->Looper----->MessageQueue----->Message----->Handler------>Activity,一系列持有造成的内存泄露

内存泄露两大解决方案:
1、静态内部类 + 弱引用

	private static class MyHandler extends Handler {//弱引用,在垃圾回收时,activity可被回收private WeakReference<MainActivity> mWeakReference;public MyHandler(MainActivity activity) {mWeakReference = new WeakReference<>(activity);}@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);}}

2、在Activity销毁时,清空Handler中未执行或正在执行的Callback以及Message

    @Overrideprotected void onDestroy() {super.onDestroy();//清空handler管道和队列mHandler.removeCallbacksAndMessages(null);}

10、内存抖动如何解决?

内存抖动根本的解决方式是复用handler.obtainMessage();
Message的创建方式有两种:

1.new Message()

2.obtainMessage()

内存抖动是为啥?因为短时间创建大量的对象并销毁。

使用obtainMessage创建一个Message,会有复用的作用,涉及到一个回收池,回收池中存放的是Message,会有一定的数量,使用单项链表MessageQueue来存放这些Message。每个message对象指向下一个Message对象

obtainMessage()创建对象是从回收池中获取,没有的才会进行创建,回收池中获取一个Message需要将管理回收池的列表同这个取出来的Message的关联进行切断,所以需要将此获取的Message的next引用置为空。并且sPool变量的引用将会变成下一个Message,同时单向列表的size-1

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();}
}

new Message()产生的对象是不会进回收池的。

从Looper的回收池中取Message;MessageQueue是一个单向链表,MessageQueue不是一个单纯的对象,而是一个链表集合,最大长度固定50个

11、安卓中的Looper.loop()阻塞为什么不会有问题?

Android是事件为驱动的操作系统,事件过来就去handler里执行(handler处理包括了创建服务,创建广播,结束服务,等等事件处理),如果没有事件过来就阻塞在那里,显示静止界面,有事件就去执行事件;

利用epoll机制可以定位到是哪个app接受这个事件,运用到红黑树,事件过来之后查找app来执行这个事件,事件带有一个标记,查找对应的app。

相关文章:

Android Framework通信:Handler

文章目录 前言一、Handler源码分析1、创建Handler2、发送消息3、取消息4、消息处理5、线程切换的方法&#xff08;Handler异步消息处理机制流程&#xff09;handler.sendMessage()handler.post()View.post()Activity中的runOnUiThread() 二、Handler高频面试题1、为什么要有Han…...

Redis的安装和配置

一、Redis的安装 使用命令将redis安装到linux服务器 yum -y install redis配置redis配置文件 redis的配置文件默认路径为/etc/redis.conf&#xff0c;对配置文件进行修改。 &#xff08;1&#xff09;注释掉bind 127.0.0.1&#xff1b; bind配置项设置的是redis允许的ip地址访问…...

Java武侠文字游戏

import java.util.Random;public class Role {//姓名private String name;//血量private int blood;//性别private char gender;//长相(随机)private String face;String[] boyfaces {"风流俊雅", "气宇轩昂", "相貌英俊", "五官端正"…...

数字化时代下,汽车行业如何突破现有营销困境?

之前三年的“口罩”时期&#xff0c;给全球和中国汽车市场带来不小影响&#xff0c;汽车销售市场整体下滑&#xff0c;传统营销模式很难适应现阶段汽车营销需求&#xff0c;那么在当下&#xff0c;汽车行业应该如何突破现有营销困境呢&#xff1f;接下来就由媒介盒子跟大家聊聊…...

19 | 如何搞清楚事务、连接池的关系?正确配置是怎样的

事务的基本原理 在学习 Spring 的事务之前&#xff0c;你首先要了解数据库的事务原理&#xff0c;我们以 MySQL 5.7 为例&#xff0c;讲解一下数据库事务的基础知识。 我们都知道 当 MySQL 使用 InnoDB 数据库引擎的时候&#xff0c;数据库是对事务有支持的。而事务最主要的作…...

备忘录模式-撤销功能的实现

在idea写代码的过程中&#xff0c;会经常用到一个快捷键——“crtl z”,即撤销功能。“备忘录模式”则为撤销功能提供了一个设计方案。 1 备忘录模式 备忘录模式提供一种状态恢复机制。在不破坏封装的前提下&#xff0c;捕获对象内部状态并在该对象之外保存这个状态。可以在…...

C++入门(二)

文章目录 一、缺省参数1、概念2、缺省参数分类1、全缺省参数2、半缺省参数 3、特性总结 二、函数重载1、引入函数重载2、函数重载概念3、函数重载分类4、C支持函数重载的原理--名字修饰(name Mangling) 三、 引用1、引用概念2、引用特性3、 常引用4、 使用场景1、做参数2、做返…...

【软件设计师】面向对象类图的六种关系

面向对象类图的六种关系&#xff08;继承、实现、依赖、关联、聚合、组合&#xff09; 1、泛化&#xff08;继承&#xff09;2、实现3、依赖4、关联5、聚合6、组合 面向对象类图的六种关系&#xff08;继承、实现、依赖、关联、聚合、组合&#xff09; 进行面向对象设计时&…...

二十七、【四种蒙版】

文章目录 图层蒙版剪贴蒙版快速蒙版矢量蒙版 图层蒙版 在当前图层加上蒙版&#xff0c;黑色画笔的可以让当前图层消失&#xff0c;白色的画笔可以让当前图层出现&#xff1a; 无论填充什么样的颜色&#xff0c;蒙板只有黑白灰三种颜色。模板最简单应用就是我们在插入图形的时候…...

卡尔曼家族从零解剖-(00)目录最新无死角讲解

讲解关于slam一系列文章汇总链接:史上最全slam从零开始&#xff0c;针对于本栏目讲解的 卡尔曼家族从零解剖 链接 :卡尔曼家族从零解剖-(00)目录最新无死角讲解&#xff1a;https://blog.csdn.net/weixin_43013761/article/details/133846882 文末正下方中心提供了本人 联系…...

Linux系统之ip命令的基本使用

Linux系统之ip命令的基本使用 一、ip命令介绍1.1 ip命令简介1.2 ip命令的由来1.3 ip命令的安装包 二、ip命令使用帮助2.1 ip命令的help帮助信息2.2 ip命令使用帮助 三、查看网络信息3.1 显示当前网络接口信息3.2 显示网络设备运行状态3.3 显示详细设备信息3.4 查看路由表3.5 查…...

【推荐算法】ctr cvr联合建模问题合集

ctr和cvr分开建模相比ctcvr的优势&#xff1f; 在电商搜索推荐排序中&#xff0c;将ctr和cvr分开建模&#xff0c;相比直接建模ctcvr的优势是什么&#xff1f; - 萧瑟的回答 - 知乎 总结&#xff1a; 1、ctr的数据可以试试获取&#xff0c;能实时训练。但是cvr存在延迟现象&…...

安装njnx --chatGPT

gpt: 要在 Debian 11 上安装 Nginx&#xff08;通常称为 "nginx"&#xff09;&#xff0c;您可以使用 apt 包管理器执行以下步骤&#xff1a; 1. **登录到您的 Debian 11 服务器**。您可以使用 SSH 客户端以 root 或具有管理员权限的用户身份登录。 2. **更新软件…...

性能测试需求分析

1、客户方提出 客户方能提出明确的性能需求&#xff0c;说明对方很重视性能测试&#xff0c;这样的企业一般是金融、电信、银行、医疗器械等&#xff1b;他们一般对系统的性能要求非常高&#xff0c;对性能也非常了解。提出需求也比较明确。 曾经有一个银行项目&#xff0c;已经…...

logback服务器日志删除原理分析

查看以下的logback官方文档 Chapter 4: Appendershttps://logback.qos.ch/manual/appenders.html 按文档说明&#xff0c;maxHistory是设置保存归档日志的最大数量&#xff0c;该数量的单位受到fileNamePattern里的值%d控制&#xff0c;如果有多个%d,只能有一个主%d&#xff0…...

到底什么才是真正的商业智能(BI)

随着人工智能、云计算、大数据、互联网、物联网等新一代信息化、数字化技术在各行各业内开始大规模的应用&#xff0c;社会上的数字化、信息化程度不断加深&#xff0c;而数据价值也在这样的刺激下成为了个人、机构、企业乃至国家的重要战略资源&#xff0c;成为了继土地、劳动…...

Pulsar Manager配置自定义认证插件访问

Pulsar Manager配置自定义认证插件访问 Pulsar Manager和dashboard部署和启用认证 pulsar自定义认证插件开发 前面博客讲了以token方式访问pulsar 这节博客讲如何配置自定义认证插件的方式访问pulsar #启动pulsar-manager docker run --name pulsar-manager -dit \-p 9527:…...

Java SimpleDateFormat linux时间字符串转时间轴的坑

Mon Oct 16 09:51:28 2023 这是linux 的 date命令得到的时间&#xff0c;要转换称时间戳。 EEE MMM dd HH:mm:ss yyyy 这样的格式&#xff0c;看起来就是正确的&#xff0c;可是就是报错 Unparseable date: "Mon Oct 16 09:51:28 2023" 下面是正确的代码 String[…...

202、RabbitMQ 之 使用 fanout 类型的Exchange 实现 Pub-Sub 消息模型---fanout类型就是广播类型

目录 ★ 使用 fanout 类型的Exchange 实现 Pub-Sub 消息模型代码演示&#xff1a;生产者&#xff1a;producer消费者&#xff1a;Consumer01消费者&#xff1a;Consumer02测试结果 完整代码ConnectionUtilPublisherConsumer01Consumer02pom.xml ★ 使用 fanout 类型的Exchange …...

web 性能优化详解(Lighthouse工具、优化方式、强缓存和协商缓存、代码优化、算法优化)

1.性能优化包含的方面 优化性能概念宽泛&#xff0c;可以从信号、系统、计算机原理、操作系统、网络通信、DNS解析、负载均衡、页面渲染。只要结合一个实际例子讲述清楚即可。 2.什么是性能&#xff1f; Web 性能是客观的衡量标准&#xff0c;是用户对加载时间和运行时的直观…...

docker-compose部署elk(8.9.0)并开启ssl认证

docker部署elk并开启ssl认证 docker-compose部署elk部署所需yml文件 —— docker-compose-elk.yml部署配置elasticsearch和kibana并开启ssl配置基础数据认证配置elasticsearch和kibana开启https访问 配置logstash创建springboot项目进行测试kibana创建视图&#xff0c;查询日志…...

解决java.lang.IllegalArgumentException: servlet映射中的<url pattern>[demo1]无效

当我使用tomcat启动使用servlet项目时&#xff0c;出现了报错&#xff1a; java.lang.IllegalArgumentException: servlet映射中的<url pattern>[demo1]无效 显示路径错误&#xff0c;于是去检查Web.xml中的配置&#xff0c;发现是配置文件的路径写错了&#xff0c;少写了…...

软件测试学习(三)易用性测试、测试文档、软件安全性测试、网站测试

目录 易用性测试 用户界面测试 优秀Ul由什么构成 符合标准和规范 直观 一致 灵活 舒适 正确 实用 为有残疾障碍的人员测试&#xff1a;辅助选项测试 测试文档 软件文档的类型 文档测试的重要性 软件安全性测试 了解黑客的动机 威胁模式分析 网站测试 网页基…...

Java中,对象一定在堆中分配吗?

在我们的日常编程实践中&#xff0c;我们经常会遇到各种类型的对象&#xff0c;比如字符串、列表、自定义类等等。这些对象在内存中是如何存储的呢&#xff1f; 你可能会毫不犹豫地回答&#xff1a;“在堆中&#xff01;”如果你这样回答了&#xff0c;那你大部分情况下是正确…...

AI:38-基于深度学习的抽烟行为检测

🚀 本文选自专栏:AI领域专栏 从基础到实践,深入了解算法、案例和最新趋势。无论你是初学者还是经验丰富的数据科学家,通过案例和项目实践,掌握核心概念和实用技能。每篇案例都包含代码实例,详细讲解供大家学习。 📌📌📌本专栏包含以下学习方向: 机器学习、深度学…...

Hadoop 配置 Kerberos 认证

1、安装 Kerberos 服务器和客户端 1.1 规划 服务端&#xff1a; bigdata3 客户端&#xff08;Hadoop集群&#xff09;&#xff1a; bigdata0 bigdata1 bigdata2 192.168.50.7 bigdata0.example.com bigdata0 192.168.50.8 bigdata1.example.com bigdata1 192.168.50.9 b…...

在 Elasticsearch 中实现自动完成功能 2:n-gram

在第一部分中&#xff0c;我们讨论了使用前缀查询&#xff0c;这是一种自动完成的查询时间方法。 在这篇文章中&#xff0c;我们将讨论 n-gram - 一种索引时间方法&#xff0c;它在基本标记化后生成额外的分词&#xff0c;以便我们稍后在查询时能够获得更快的前缀匹配。 但在此…...

美客多、亚马逊卖家如何运用自养账号进行有效测评?

到了10月&#xff0c;卖家朋友们都在忙着准备Q4旺季吧&#xff01; 首先&#xff0c;祝愿所有看到这条推文的卖家朋友&#xff0c;今年旺季都能爆单&#xff0c;赚得盆满钵满&#xff01; 测评是珑哥常谈&#xff0c;一直备受关注&#xff0c;不论是新老卖家都是一个逃不开的…...

MyBatis的缓存,一级缓存,二级缓存

10、MyBatis的缓存 10.1、MyBatis的一级缓存 一级缓存是SqlSession级别的&#xff0c;通过同一个SqlSession对象 查询的结果数据会被缓存&#xff0c;下次执行相同的查询语句&#xff0c;就 会从缓存中&#xff08;缓存在内存里&#xff09;直接获取&#xff0c;不会重新访问…...

GitLab(1)——GitLab安装

目录 一、使用设备 二、使用rpm包安装 Gitlab国内清华源下载地址&#xff1a; ①下载命令如下&#xff1a; ②安装命令如下&#xff1a; ③删除rpm包 ④配置 ⑤重载 ⑥重启 ⑦配置自启动 ⑧打开8989端口并重启防火墙 三、GitLab登录 ①访问GitLab的URL ②输入用户…...

网站生成海报功能怎么做/网站如何快速收录

1。确定c:\windows\system32\下有xcopy.exe文件 2。我的电脑---右键---属性----高级----环境变量----在系统变量中找到path(不分大小写&#xff09;---双击它----在其变量值 (V)中添加“c:\windows\system32”&#xff01;&#xff0c;添加方法为&#xff1a;在原变量值后面加英…...

Dedecms 手机网站示例/百度的广告推广需要多少费用

几种破解mysql root密码的几种方法: 方法一 使用phpmyadmin&#xff0c;这是最简单的了&#xff0c;修改mysql库的user表&#xff0c;不过别忘了使用PASSWORD函数。 方法二 使用mysqladmin&#xff0c;这是前面声明的一个特例。 mysqladmin -u root -p password mypasswd 输入。…...

建什么网站比较好/中国十大电商公司排名

1. 当点击某个按钮&#xff0c;后台其实进行了很多步的操作&#xff0c;但原型上无法体现的时候&#xff0c;我们需要尽可能地在原型上做好这些交互。 比如1&#xff1a; 点击添加按钮&#xff0c;添加一条信息&#xff0c;然后保存成功&#xff0c;显示这条信息。 实则后台的处…...

手机网站的css模板/网站搭建服务

昨天Google I/O开发者大会上宣布&#xff0c;Android Studio 1.0的前瞻版发布了&#xff0c;今早马上下载尝下鲜。 下载地址如下&#xff1a; https://developer.android.com/sdk/installing/studio.html 中文介绍http://www.apkbus.com/android-1844-1.html 很显然的IntelliJ…...

wordpress的数据库主机/佛山做seo推广公司

问题 描述 Description 学校实行学分制。每门的必修课都有固定的学分&#xff0c;同时还必须获得相应的选修课程学分。学校开设了N&#xff08;N<300&#xff09;门的选修课程&#xff0c;每个学生可选课程的数量M是给定的。学生选修了这M门课并考核通过就能获得相应的学分。…...

颍东网站建设/北京seo软件

请用C语言实现 输出和为一个给定整数的所有组合启动2012/*请用C语言实现 输出和为一个给定整数的所有组合 */#include <stdio.h> //包含头文件stdio.h 为程序提供基本输入输出功能 #include <stdlib.h> //包含标准库头文件stdlib.h 以便调用函数system("pa…...