Android Hanlder 揭密之路- 深入理解异步消息传递机制Looper、Handler、Message三者关系

news2024/9/21 5:48:32

在Android开发中,Handler作为实现线程间通信的桥梁,扮演着至关重要的角色。无论是在主线程执行UI操作,还是在子线程进行耗时任务,Handler都可以高效地将异步消息分派到对应的线程中执行。


本文将全方位解析Handler的工作原理及实现细节,从源码角度介绍Looper , Handler , Message的关系,让你记忆深刻。


一、Handler原理浅析

Handler实际是Android低层面向线程的消息循环机制MessageQueue的一层包装。


1、两个关键组成部分:

  • MessageQueue消息队列 - 用于存放所有通过Handler发送的消息
  • Looper消息循环器 - 负责不断从MessageQueue中取出消息,并按序执行

每个线程都可以通过Looper.prepare()方法创建自己的消息循环,并在循环体内通过Looper.loop()不断获取并执行消息。


Android 中的 Handler 机制是用于在不同线程之间进行通信和消息传递的重要机制。


2、工作流程如下

(1)、创建 Handler 实例,并重写 handleMessage() 方法,用于处理接收到的消息。

(2)、发送消息: 通过 HandlersendMessage() 等方法发送消息到消息队列。

(3)、消息循环: 消息队列会不断地从队列中取出消息,并分发给对应的 Handler 处理。


我们可以通过一个简单的示例来理解它的基本工作流程:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    private Handler handler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 创建 Handler 对象
        handler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                // 处理收到的消息
                switch (msg.what) {
                    case 0:
                        Log.d(TAG"Received message: " + msg.obj);
                        break;
                    default:
                        super.handleMessage(msg);
                }
            }
        };

        // 在子线程中发送消息
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 创建消息对象
                Message message = Message.obtain();
                message.what = 0;
                message.obj = "Hello from worker thread!";

                // 发送消息
                handler.sendMessage(message);
            }
        }).start();
    }
}

在这个示例中,我们做了以下操作:

  • onCreate 方法中,我们创建了一个 Handler 对象,并重写了 handleMessage 方法。这个方法会在收到消息时被调用,我们在这里处理收到的消息。
  • 我们创建了一个子线程,在这个线程中创建了一个 Message 对象,并通过 handler.sendMessage() 方法将其发送给 Handler。
  • 当 Handler 收到消息时,它会将消息添加到消息队列中,然后等待 Looper 取出并处理这个消息。Looper 会调用我们重写的 handleMessage 方法来处理这个消息。

实际上,Handler 的实现机制要复杂得多,涉及到 MessageQueue、Looper 等多个组件的协作。但是理解了这个基本示例,就可以对 Handler 的工作原理有一个基本的认知了。


二、Handler 源码解析


下面我们来深入解析 Handler 的源码实现。

1、 Handler 使用回顾

我们先来回顾下Handler 使用流程

// 在主线程中创建 Handler 来处理子线程发送的消息
private Handler handler = new Handler() {
  @Override
  public void handleMessage(Message msg) {
    super.handleMessage(msg);
    switch (msg.what) {
      case 0:
        //TODO: 处理消息
        break;
    }
  }
};

// 使用方式一:在子线程中发送消息
new Thread(new Runnable() {
  @Override
  public void run() {
    Message message = new Message();
    message.what = 0;
    message.obj = "测试消息";
    // 子线程中发送消息
    handler.sendMessage(message);
  }
}).start();

// 使用方式二:handler.post()
handler.post(new Runnable() {
  @Override
  public void run() {
    // 运行在子线程中...
  }
});

通过上面代码可以看到,在使用 Handler 时首先需要创建 Handler 对象。

接下来,我们看下 Handler 的构造方法。


2、Handler构造方法源码分析

//frameworks/base/core/java/android/os/Handler.java

/* 构造方法一 */
public Handler() {
  this(null, false);
}
/* 构造方法二 */
public Handler(Callback callback) {
  this(callback, false);
}
/* 构造方法三 */
public Handler(Looper looper) {
  this(looper, null, false);
}
/* 构造方法四 */
public Handler(Looper looper, Callback callback) {
  this(looper, callback, false);
}
/* 构造方法五 */
public Handler(boolean async) {
  this(null, async);
}
/* 构造方法六 */
public Handler(Callback callback, boolean async) {
  // ...
  mLooper = Looper.myLooper();
  if (mLooper == null) {
    throw new RuntimeException(
      "Can't create handler inside thread that has not called Looper.prepare()");
  }
  mQueue = mLooper.mQueue;
  mCallback = callback;
  mAsynchronous = async;
}
/* 构造方法七 */
public Handler(Looper looper, Callback callback, boolean async) {
  mLooper = looper;
  mQueue = looper.mQueue;
  mCallback = callback;
  mAsynchronous = async;
}

可以看到 Handler 有很多构造方法,常用的是构造方法一(实际也是调用的 :构造方法六)。

我们再来分析下构造方法六的源码:

//frameworks/base/core/java/android/os/Handler.java

/* 构造方法六 */
public Handler(Callback callback, boolean async) {
  // ...
  mLooper = Looper.myLooper();
  if (mLooper == null) {
    throw new RuntimeException(
      "Can't create handler inside thread that has not called Looper.prepare()");
  }
  mQueue = mLooper.mQueue;
  mCallback = callback;
  mAsynchronous = async;
}

可以看到,这里调用了 Looper.myLooper() 方法,当 mLooper 为空时会抛出异常,提示我们需要先调用 Looper.prepare() 方法,我接下来看下 Looper 中的这两个方法。


3、Looper源码分析

  • Looper 是 Handler 的核心组成部分之一。它是一个消息循环器,负责管理消息队列,并按照先进先出的顺序处理消息。
  • 每个线程都可以创建自己的 Looper,并且主线程(UI 线程)默认就会创建一个 Looper。
  • Looper 通过 Looper.prepare() 方法初始化,然后调用 Looper.loop() 方法开启消息循环。

(1)、Looper.java

//frameworks/base/core/java/android/os/Looper.java

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static Looper sMainLooper;

final MessageQueue mQueue;
final Thread mThread;

从上面源码中可以看到 Looper 有 4 个成员变量:

  • sThreadLocal:保存的是当前线程的 Looper。
  • sMainLooper:Application 中主线程中的 Looper。
  • mQueue:当前线程中的 MessageQueue。
  • mThread:创建 Looper 的线程。

(2)、myLooper

//frameworks/base/core/java/android/os/Looper.java

/* Handler 构造方法六中调用的方法 */
public static Looper myLooper() {
  // 返回当前线程中的 looper
  return sThreadLocal.get();
}

从上面的源码可见 myLooper() 逻辑很简单,调用了 ThreadLocal 的 get() 方法。ThreadLocal 我们稍后再分析。


(3)、prepare

在 Handler 构造方法六中可以看到,如果 myLoop() 的结果为空会直接抛出异常,提示需要先调用 prepare() 方法,接下来分析下 prepare() 方法。

/* Handler 构造方法六中调用的方法 */
public static void prepare() {
  prepare(true);
}
/* 带参数的 prepare 方法 */
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 构造方法 */
private Looper(boolean quitAllowed) {
  mQueue = new MessageQueue(quitAllowed);
  mThread = Thread.currentThread();
}


prepare() 方法中调用了 prepare(quitAllowed) 方法,这里判断了 Looper 是否为空。

如果当前线程已经创建了 Looper 直接抛出异常,也就是说一个线程中只能创建一个 Looper,经常使用 Handler 的小伙伴应该对这个异常很熟悉。

如果当前线程没有创建 Looper 会直接调用 Looper(quitAllowed) 的构造方法,创建一个 Looper 并创建一个 MessageQueue,然后保存一下当前线程的信息。


4、MessageQueue源码分析

  • MessageQueue 是 Looper 的另一个核心组成部分,它是一个消息队列,负责存储和管理 Message 对象。
  • MessageQueue 提供了 enqueueMessage()next() 等方法来实现消息的入队和出队操作。

我们先看下 MessageQueue 的具体实现:

//frameworks/base/core/java/android/os/Looper.java

final MessageQueue mQueue;

/* Looper 构造方法 */
private Looper(boolean quitAllowed) {
  mQueue = new MessageQueue(quitAllowed);
  mThread = Thread.currentThread();
}

再看下MessageQueue 的构造方法:

//frameworks/base/core/java/android/os/MessageQueue.java

private native static long nativeInit();

MessageQueue(boolean quitAllowed) {
  mQuitAllowed = quitAllowed;
  mPtr = nativeInit();
}

MessageQueue 的构造方法逻辑比较简单。

这里调用了一个 native 方法 nativeInit() 在 native 层进行了初始化。

感兴趣的朋友可以去查看 native 源码,文件如下:

//frameworks/base/core/jni/android_os_MessageQueue.cpp

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
  NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
  if (!nativeMessageQueue) {
    jniThrowRuntimeException(env, "Unable to allocate native queue");
    return 0;
  }

  nativeMessageQueue->incStrong(env);
  return reinterpret_cast<jlong>(nativeMessageQueue);
}

至此, Handler 的创建流程已经分析完了,可以看到 Handler 创建流程如下图所示:

在这里插入图片描述


在创建 Handler 时:

第一步需要先调用 Looper.prepare(),该方法会初始化 Looper,创建 MessageQueue 和 ThreadLocal。

第二步会调用 Looper 中的 myLoop() 方法获取到 Looper 和 MessageQueue 保存到 Handler 中。


5、ThreadLocal 源码分析


如上,我们看见 第一步时,创建了 ThreadLocal 和 MessageQueue 。

(1)、ThreadLocal 到底是做什么的呢?

我们来分析下 ThreadLocal 的作用,源码如下:

ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
sThreadLocal.set(new Looper(quitAllowed)); // 设置变量信息
sThreadLocal.get(); // 读取变量信息

我们可以看到ThreadLocal在Android的消息循环机制中扮演了重要角色。

在Android中,每个线程都有自己的Looper对象,用于从MessageQueue中获取消息并执行。由于Looper对象是线程私有的,因此Android通过ThreadLocal来为每个线程维护自己的Looper实例。

具体来说:

  • ThreadLocal<Looper> sThreadLocal定义了一个线程本地存储对象,用于存放当前线程的Looper实例。

  • sThreadLocal.set(new Looper(quitAllowed))在当前线程中创建一个Looper对象,并通过set方法将其关联到当前线程。

  • sThreadLocal.get()则可以在当前线程中获取之前设置的Looper对象。

通过这种方式,Android就实现了每个线程拥有自己的Looper实例,可以独立地从MessageQueue获取和处理消息,避免了线程间数据混乱和竞争的问题。

ThreadLocal的这种线程隔离机制,使得Android的消息循环模型可以在多线程环境下高效、安全地运行,同时也体现了ThreadLocal在实现线程数据隔离方面的优秀作用。


(2)、ThreadLocal 的具体实现

接下来分析下 ThreadLocal 的具体实现。

//java/lang/ThreadLocal.java

private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode =
    new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

ThreadLocal 通过 threadLocalHashCode 来标识每一个 ThreadLocal 的唯一性。

threadLocalHashCode 通过 CAS 操作进行更新,每次 hash 操作的增量为 0x61c88647。

我们来看看 ThreadLocal 的 set() 方法。

//java/lang/ThreadLocal.java
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

可以看到通过 Thread.currentThread() 方法获取了当前的线程引用,并传给了 getMap(Thread) 方法获取一个 ThreadLocalMap 的实例。

getMap(Thread) 方法中直接返回 Thread 实例的成员变量 threadLocals。它的定义在 Thread 内部,访问级别为 package 级别:

//java/lang/Thread.java

ThreadLocal.ThreadLocalMap threadLocals = null;

到了这里,可以看出,每个 Thread 里面都有一个 ThreadLocal.ThreadLocalMap 成员变量,也就是说每个线程通过 ThreadLocal.ThreadLocalMapThreadLocal 相绑定,这样可以确保每个线程访问到的 ThreadLocal 变量都是本线程的。

获取了 ThreadLocalMap 实例以后,如果它不为空则调用 ThreadLocalMap.ThreadLocalMap.set() 方法设值;

若为空则调用 ThreadLocal.createMap() 方法 new 一个 ThreadLocalMap 实例并赋给 Thread.threadLocals。


(3)、ThreadLocalMap的源码分析

下面我们分析一下 ThreadLocalMap 的实现,可以看到 ThreadLocalMap 有一个常量和三个成员变量:

//java/lang/ThreadLocal.ThreadLocalMap

private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

private int size = 0;

private int threshold; // Default to 0

其中 INITIAL_CAPACITY 代表这个 Map 的初始容量;table 是一个 Entry 类型的数组,用于存储数据;size 代表表中的存储数目; threshold 代表需要扩容时对应 size 的阈值。

Entry 类是 ThreadLocalMap 的静态内部类,用于存储数据。它的源码如下:

//java/lang/ThreadLocal.ThreadLocalMap

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Entry 类继承了 WeakReference<ThreadLocal<?>>,即每个 Entry 对象都有一个 ThreadLocal 的弱引用(作为 key),这是为了防止内存泄露。一旦线程结束,key 变为一个不可达的对象,这个 Entry 就可以被 GC 回收了。

ThreadLocalMap 类有两个构造函数,其中常用的是 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue):

//java/lang/ThreadLocal.ThreadLocalMap

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

构造函数的第一个参数就是本 ThreadLocal 实例(this),第二个参数就是要保存的线程本地变量。构造函数首先创建一个长度为 16 的 Entry 数组,然后计算出 firstKey 对应的哈希值,然后存储到 table 中,并设置 size 和 threshold。


通过上面分析可以看到 ThreadLocal 的工作原理如下:

在这里插入图片描述

如图所示,ThreadLocal 中有一个 ThreadLocalMap 其中以 ThreadLocal 作为 Key,以需要保存的值作为 Value。这样不同的线程访问同一个 ThreadLocal 时,获取到的值也就是各个线程存储时对应的值了。


分析了 ThreadLocal ,接下来,我们再来看看MessageQueue 。

6、MessageQueue 源码分析

(1)、Handler.sendMessage()

我们常用的发消息的方法如下:

//frameworks/base/core/java/android/os/Handler.java

public final boolean sendMessage(Message msg) {
  return sendMessageDelayed(msg, 0);
}

public final boolean sendEmptyMessage(int what) {
  return sendEmptyMessageDelayed(what, 0);
}

public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
  Message msg = Message.obtain();
  msg.what = what;
  return sendMessageDelayed(msg, delayMillis);
}

上面不管哪种发消息的方式,最后都调用了 sendMessageDelayed() 方法。

//frameworks/base/core/java/android/os/Handler.java

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
  if (delayMillis < 0) {
    delayMillis = 0;
  }
  return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(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(MessageQueue queue, Message msg, long uptimeMillis) {
  msg.target = this;
  if (mAsynchronous) {
    msg.setAsynchronous(true);
  }
  return queue.enqueueMessage(msg, uptimeMillis);
}

sendMessageDelayed() 方法最后调用了 MessageQueue.enqueueMessage()


(2)、MessageQueue.enqueueMessage()

我们接着来看 enqueueMessage() 方法的实现:

//frameworks/base/core/java/android/os/MessageQueue.java

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) {
      // 如果消息队列里面没有消息,或者消息的执行时间比里面的消息早,
      // 就把这条消息设置成第一条消息;
			// 一般不会出现这种情况,因为系统一定会有很多消息。
      msg.next = p;
      mMessages = msg;
      needWake = mBlocked;
    } else {
      // 如果消息队列里面有消息
      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.next
      prev.next = msg; // 把消息添加到最后
    }

    if (needWake) {
      nativeWake(mPtr);
    }
  }
  return true;
}

分析到这里可以看到,我们通过调用 Handler.sendMessage() 最后将 Message 添加到了 MessageQueue 的消息队列中。

在前面 Looper.loop() 方法中分析过,loop() 方法中有一个死循环一直在读取消息,当读取到刚才添加的消息后会回调到 Handler.dispatchMessage() 方法。


到这里, Handler 的工作流程大家应该已经很清楚了。

如下图所示,假设在 Thread 1 中创建了 Handler,那么 Thread 2 向 Thread 1 发送消息的过程。

在这里插入图片描述


Handler 机制就像是一个传送机器,Looper 就是传送轮一直在不停的旋转,MessageQueue 就是传送带跟着Looper 旋转来运输 Message,Handler 就是机械手在 Thread 2 中将 Message 放到传送带 MessageQueue 上,传送到 Thread 1 后再将 Message 拿下来通知 Thread 1 进行处理。


(3)、Handler.post() 源码

了解了 Handler 工作流程,我们继续来分析下另一种使用方式 Handler.post()

//frameworks/base/core/java/android/os/Handler.java

public final boolean post(Runnable r) {
  return  sendMessageDelayed(getPostMessage(r), 0);
}

可以看到 post() 也是调用了 sendMessageDelayed() 方法。


(4)、getPostMessage 源码

我们再来看下 getPostMessage(r) 方法的实现。

//frameworks/base/core/java/android/os/Handler.java

private static Message getPostMessage(Runnable r) {
  Message m = Message.obtain();
  m.callback = r;
  return m;
}

原来这里创建了一个 Message,将 Runnable 放入了 Message 的 callback 上。


(5)、dispatchMessage 源码

那 Message 最后怎么处理的呢?

Looper.loop() 方法中有这么一句 msg.target.dispatchMessage(msg)

//frameworks/base/core/java/android/os/Handler.java
public void dispatchMessage(Message msg) {
  if (msg.callback != null) {
    handleCallback(msg); // 处理 post 消息,稍后再分析
  } else {
    if (mCallback != null) {
      // 回调到 Handler.handleMessage() 方法
      if (mCallback.handleMessage(msg)) {
        return;
      }
    }
    handleMessage(msg);
  }
}

handleCallback() 就是处理 Handler.post() 发送的消息:

//frameworks/base/core/java/android/os/Handler.java

private static void handleCallback(Message message) {
  message.callback.run();
}

如此简单,就是拿到 Runnable 调用了 run() 方法。

至此, 关于Handler,Looper ,Message 这三者关系上面已经叙述的非常清楚了。


让我们首先总结一下:

  1. Looper.prepare()方法会为当前线程创建一个Looper实例,其内部含有与之关联的MessageQueue对象。同一线程内只能调用一次prepare(),因此MessageQueue在线程内是单例的。

  2. Looper.loop()会让当前线程进入无限循环模式,不断从MessageQueue中读取消息,并通过msg.target.dispatchMessage(msg)将消息分发给相应的Handler进行处理。

  3. 在构造Handler实例时,会获取当前线程的Looper,并将Handler的MessageQueue与Looper内部的MessageQueue建立关联。

  4. 调用Handler的sendMessage()方法时,会给Message设置target为当前Handler实例,并将Message加入关联的MessageQueue中等待分发。

  5. 在Handler的handleMessage()方法中,我们可以重写自定义的消息处理逻辑,这个方法最终会由Looper.loop()中的msg.target.dispatchMessage(msg)回调执行。


我们再来张图一目了然 :

在这里插入图片描述


五、结语

伴随着Android版本不断迭代,Handler机制也在持续优化和完善,Android 12中引入了高效模式MessageQueue和SyncQueueRemover工具等新特性。离开Handler,消息驱动架构也延伸出各种优秀的开源替代品,比如EventBus、RxJava。

未来Handler如何发展,我们拭目以待,不过这些基础底层原理的理解对开发者而言永远都是重中之重,期待您在实践中有更多思考和总结。


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

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

相关文章

2024数维杯数学建模C题思路代码

2024年数维杯&电工杯思路代码在线文档​https://www.kdocs.cn/l/cdlol5FlRAdE 这道题想要做出好的结果&#xff0c;必须要结合插值法和分布函数来做&#xff0c;主要还是因为勘探点太少&#xff0c;直接用插值法效果不太好&#xff0c;以下是我做的&#xff0c;函数分布可…

Python的while循环

目录 while循环的结构 示例 关键字 break continue while循环的结构 while condition&#xff08;循环条件&#xff09;: # 循环的内容 循环内容的执行与结束需要通过循环条件控制。 在执行循环之前需要设立一个循环条件的初始值&#xff0c;以便while循环体判断循环条件。…

Loongnix系统替换内核操作

Loongnix系统替换内核操作 一、终端下执行命令 sudo apt search linux-image* 返回结果中格式如: linux-image-4.19.0-19-loongson-3 为最新的内核源码。 二、下载内核源码包 sudo apt source linux-image-4.19.0-19-loongson-3 如提示&#xff1a;E: 您必须在 sources.li…

网络安全等级保护的发展历程

1994年国务院147号令第一次提出&#xff0c;计算机信息系统实行安全等级保护&#xff0c;这也预示着等保的起步。 2007年《信息安全等级保护管理办法》的发布之后。是等保在各行业深耕落地的时代。 2.0是等保版本的俗称&#xff0c;不是等级。等保共分为五级&#xff0c;二级…

C#语音播报(通过CoreAudioAPI完成对扬声器的控制)

1&#xff0c;效果&#xff1a; 作用&#xff1a; 可对当前内容&#xff08;例如此例中的重量信息&#xff09;进行语音合成播报 。可设置系统扬声器音量与状态(是否静音),同时根据扬声器状态同步更新当前控件状态与值&#xff0c;实现强制PC扬声器按照指定的音量进行播报&…

Ansible常用变量【上】

转载说明&#xff1a;如果您喜欢这篇文章并打算转载它&#xff0c;请私信作者取得授权。感谢您喜爱本文&#xff0c;请文明转载&#xff0c;谢谢。 在Ansible中会用到很多的变量&#xff0c;Ansible常用变量包括以下几种&#xff1a; 1. 自定义变量——在playbook中用户自定义…

函数重载和函数模板

c语言中函数名字不可重复,但是可以写代码实现 普通的函数重载 这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同和返回值没有关系(因为就像我想调用Add(1,2),Add重载的几个函数仅仅返回值不同,编辑器就不知道去找哪一个,就有歧义了) 情况1-数组 int ave(int*pa,i…

用 Supabase CLI 进行本地开发环境搭建

文章目录 &#xff08;零&#xff09;前言&#xff08;一&#xff09;Supabase CLI&#xff08;1.1&#xff09;安装 Scoop&#xff08;1.2&#xff09;用 Scoop 安装 Supabase CLI &#xff08;二&#xff09;本地项目环境&#xff08;2.1&#xff09;初始化项目&#xff08;2…

【全开源】微凌客洗护小程序FastAdmin+Uniapp(源码搭建/上线/运营/售后/维护更新)

一款基于FastAdminUniapp开发的洗护小程序系统&#xff0c;适用于线上下单到店核销的业务场景&#xff0c;拥有会员卡、优惠券、充值提现、商户管理等功能&#xff0c;提供Uniapp后台无加密源代码。 线上线下融合&#xff1a;微凌客洗护小程序适用于线上下单到店核销的业务场景…

nacos命名空间的配置

给微服务配置namespace 给微服务配置namespace只能通过修改配置来实现。 例如&#xff0c;修改order-service的application.yml文件&#xff1a; spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZnamespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f…

C语言数据结构 - 选择题集合(二叉树)

一生负气成今日 四海无人对夕阳 目录 树的专辑 树的专辑 1.有n个元素的完全二叉树的深度是&#xff08; &#xff09; A.nlogn B.nlogn1 C.logn D.logn1 答案&#xff1a;D 解析&#xff1a; 设完全二叉树的节点数为 N&#xff0c;高度为 h &#xff0c;高度为 h 时空的结点…

python零基础知识 - 定义列表的三种方式,循环列表索引值

这一小节&#xff0c;我们将从零基础的角度看一下&#xff0c;python都有哪些定义列表的方式&#xff0c;并且循环这个列表的时候&#xff0c;怎么循环&#xff0c;怎么循环他的索引值&#xff0c;怎么拿到的就是元素值。 说完循环&#xff0c;我们会说一说关键的break和contin…

分布式存储故障导致数据库无法启动故障处理---惜分飞

国内xx医院使用了国外医疗行业龙头的pacs系统,由于是一个历史库,存放在分布式存储中,由于存储同时多个节点故障,导致数据库多个文件异常,数据库无法启动,三方维护人员尝试通通过rman归档进行应用日志,结果发现日志有损坏报ORA-00354 ORA-00353,无法记录恢复,希望我们给予支持 M…

AI智能分析高精度烟火算法EasyCVR视频方案助力打造森林防火建设

一、背景 随着夏季的来临&#xff0c;高温、干燥的天气条件使得火灾隐患显著增加&#xff0c;特别是对于广袤的森林地区来说&#xff0c;一旦发生火灾&#xff0c;后果将不堪设想。在这样的背景下&#xff0c;视频汇聚系统EasyCVR视频融合云平台AI智能分析在森林防火中发挥着至…

人脸消费给传统食堂带来的变化

消费的技术基础是脸部识别&#xff0c;脸部识别是基于人的容貌特征信息进行认证的生物特征识别技术&#xff0c;其突出的特征是以非接触方式进行识别&#xff0c;避免个人信息的泄露。 面部识别和指纹识别、掌纹识别、视网膜识别、骨骼识别、心率识别等都是人体生物特征识别技术…

自然资源-城镇开发边界内详细规划编制技术指南解读

自然资源-城镇开发边界内详细规划编制技术指南解读

护眼台灯和普通台灯差别很大吗?专业护眼灯品牌有哪些?

随着科技的不断演进&#xff0c;台灯的设计也日益脱胎换骨&#xff0c;从曾经的笨重造型转变为如今轻盈雅致的外观。它们的功能同样经历了多样化的革新&#xff0c;变得更加人性化和便捷。作为学习、阅读和办公环境中不可或缺的照明工具&#xff0c;台灯所提供的光线舒适度至关…

redis抖动问题导致延迟或者断开的处理方案

目录&#xff1a; 1、使用背景2、redis重试机制3、redis重连机制4、其他一些解决redis抖动问题方案 1、使用背景 客户反馈文件偶现打不开&#xff0c;报错现象是session not exist&#xff0c;最终定位是redis抖动导致的延迟/断开的现象&#xff0c;最终研发团方案是加入redis…

电脑nvidia驱动和合适版本的duda--自用 回忆版

参考文献&#xff1a;http://t.csdnimg.cn/ecDuG 内容很多抄的这个&#xff0c;主要害怕链接失效 一、Ubuntu 18.04 安装NVIDIA显卡驱动 1、查看本机显卡能够配置的驱动信息 ubuntu-drivers devices所以可以看出&#xff0c;推荐 nvidia-driver-530 - distro non-free 2、安…

HTML/CSS2

1.前置说明 HTML/CSS1 2.img元素 格式&#xff1a; <img src"图片地址" alt"占位文字" width"图片宽度" height"图片高度">其中alt是当图片加载失败时显示的文字 而且不同内核的浏览器显示出来的占位文字的效果也是不尽相同…