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。

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

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

相关文章

MySQL为什么用b+树

索引是一种数据结构&#xff0c;用于帮助我们在大量数据中快速定位到我们想要查找的数据。 索引最形象的比喻就是图书的目录了。注意这里的大量&#xff0c;数据量大了索引才显得有意义&#xff0c;如果我想要在[1,2,3,4]中找到4这个数据&#xff0c;直接对全数据检索也很快&am…

Spring framework Day15:@lmport注解使用

前言 在编程中&#xff0c;import注解通常用于导入外部的类、接口或其他资源&#xff0c;以便在当前代码文件中使用。它可以提供一种简洁、方便的方式来引入外部依赖&#xff0c;并且有以下几个主要的应用场景和好处&#xff1a; 引入外部类/接口&#xff1a;使用import注解可…

1.14.C++项目:仿muduo库实现并发服务器之Util模块的设计

一、Util模块 二、实现思想 &#xff08;一&#xff09;管理 实现一些工具接口读取文件内容向文件写入内容URL编码URL解码通过HTTP状态码获取描述信息通过文件后缀名获取mime判断一个文件是不是目录判断一个文件是否是一个普通文件HTTP资源路径有效性判断 &#xff08;二&am…

#力扣:LCR 182. 动态口令@FDDLC

LCR 182. 动态口令 一、Java class Solution {public String dynamicPassword(String password, int target) {return password.substring(target) password.substring(0, target);} } 二、C #include <string> using namespace std; class Solution { public:strin…

性能测试需求分析

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

PCL 生成球形点云

目录 一、算法原理二、代码实现三、结果展示四、参考链接一、算法原理 生成球体点云的方法有很多种,Marsaglia于1972年提出了一个简单易行的实现方法,它从(-1,1)上的独立均匀分布中选取 x 1 x_1 x

多线程怎么共用一个事务

文章目录 场景分析测试对应的其他类我并没有贴出来,因为大家可以自己找个项目走一波测试testSession测试testTransaction 注意使用同一个sqlsession会导致线程安全问题,testSession方法就是在另外线程里面能读取到数据库里面没有的数据.但是有时候业务就是这么奇怪.扩展总结 场…

DetailView/货币详情页 的实现

1. 创建货币详情数据模型类 CoinDetailModel.swift import Foundation// JSON Data /*URL:https://api.coingecko.com/api/v3/coins/bitcoin?localizationfalse&tickersfalse&market_datafalse&community_datafalse&developer_datafalse&sparklinefalseR…

linux放开8080端口

linux放开8080端口 输入命令&#xff1a; 查看8080端口是否开放 firewall-cmd --query-port8080/tcpno显示端口未打开&#xff0c;yes表示开启&#xff0c;linux开启防火墙默认关闭8080端口 这里是引用&#xff1a;https://blog.csdn.net/weixin_54067866/article/details/1…

Excel 从网站获取表格

文章目录 导入网站数据导入股票实时行情 用 Excel 获取网站数据的缺点&#xff1a;只能获取表格类的数据&#xff0c;不能获取非结构化的数据。 导入网站数据 转到地址之后&#xff1a; 实测该功能经常导致 Excel 卡死。 导入股票实时行情

机器学习的原理是什么?

训过小狗没? 没训过的话总见过吧? 你要能理解怎么训狗&#xff0c;就能非常轻易的理解机器学习的原理. 比如你想教小狗学习动作“坐下”一开始小狗根本不知道你在说什么。但是如果你每次都说坐下”然后帮助它坐下&#xff0c;并给它一块小零食作为奖励&#xff0c;经过多次…

Docker在边缘计算中的崭露头角:探索容器技术如何驱动边缘计算的新浪潮

文章目录 第一部分&#xff1a;边缘计算和Docker容器边缘计算的定义Docker容器的崭露头角1. 可移植性2. 资源隔离3. 自动化部署和伸缩 第二部分&#xff1a;应用案例1. 边缘分析2. 工业自动化3. 远程办公 第三部分&#xff1a;挑战和解决方案1. 网络延迟2. 安全性3. 管理和部署…

边端小场景音视频流分发架构

备注&#xff1a;绿色线条&#xff0c;红色线条&#xff0c;蓝色线条&#xff0c;均是表示同一路流的不同的协议而已 1&#xff09;IPC本身的流媒体的能力有限&#xff0c;一般IPC支持的客户端数10~50个&#xff0c;媒体分发能力&#xff1a;10~20路&#xff0c;看设备品牌能力…

Android---DVM以及ART对JVM进行优化

Dalvik Dalvik 是 Google 公司自己设计用于 Android 平台的 Java 虚拟机&#xff0c;Android 工程师编写的 Java 或者 Kotlin 代码最终都是在这台虚拟机中被执行的。在 Android 5.0 之前叫作 DVM&#xff0c;5.0 之后改为 ART&#xff08;Android Runtime&#xff09;。在整个…

UGUI交互组件ScrollBar

一.ScrollBar的结构 对象说明Scrollbar挂有Image和Scrollbar组件的主体对象Sliding Area表示滑动范围Handle滑块 二.Scrollbar的属性 属性说明Handle Rect控制柄对象的引用Direction拖动控制柄时滚动条值增加的方向Value滚动条的当前值&#xff0c;范围为 0.0 到 1.0Suze控制柄…

Hadoop3教程(四):HDFS的读写流程及节点距离计算

文章目录 &#xff08;55&#xff09;HDFS 写数据流程&#xff08;56&#xff09; 节点距离计算&#xff08;57&#xff09;机架感知&#xff08;副本存储节点选择&#xff09;&#xff08;58&#xff09;HDFS 读数据流程参考文献 &#xff08;55&#xff09;HDFS 写数据流程 …

软件测试学习(四)自动测试和测试工具、缺陷轰炸、外包测试、计划测试工作、编写和跟踪测试用例

目录 自动测试和测试工具 工具和自动化的好处 测试工具 查看器和监视器 驱动程序 桩 压力和负载工具 干扰注入器和噪声发生器 分析工具 软件测试自动化 宏录制和回放 可编程的宏 完全可编程的自动测试工具 随机测试&#xff1a;猴子和大猩猩 使用测试工具和自动…

Shell命令笔记2

大家好&#xff0c;分享下最近工作中用得比较多的shell命令&#xff0c;希望对大家有帮助。 获取数组长度&#xff1a; ${#array_name[*]}获取脚本相对路径 script_path$(dirname "$0")获取脚本的名字 script_name$(basename "$0")获取脚本的绝对路径 …

如何从 Pod 内访问 Kubernetes 集群的 API

Kubernetes API 是您检查和管理集群操作的途径。您可以使用Kubectl CLI、工具(例如curl)或流行编程语言的官方集成库来使用 API 。 该 API 也可供集群内的应用程序使用。Kubernetes Pod 会自动获得对 API 的访问权限,并且可以使用提供的服务帐户进行身份验证。您可以通过使…

论文阅读:Image-to-Lidar Self-Supervised Distillation for Autonomous Driving Data

目录 摘要 Motivation 整体架构流程 技术细节 雷达和图像数据的同步 小结 论文地址: [2203.16258] Image-to-Lidar Self-Supervised Distillation for Autonomous Driving Data (arxiv.org) 论文代码&#xff1a;GitHub - valeoai/SLidR: Official PyTorch implementati…