Android14 Handle机制

news2024/9/30 13:23:41

Handle是进程内部, 线程之间的通信机制.

handle主要接受子线程发送的数据, 并用此数据配合主线程更新UI

handle可以分发Message对象和Runnable对象到主线程中, 每个handle实例, 都会绑定到创建他的线程中, 它有两个作用,:

(1) 安排消息在某个主线程中某个地方执行

(2) 安排一个动作在不同的线程中执行

Handle, Lopper, MessegeQueue, Message的关系

handle机制中, 主要牵涉的类有如下四个, 它们分工明确, 但又互相作用

1. Message:消息

2. hanlder:消息的发起者

3. Lopper: 消息的遍历者

4.MessageQueue 消息队列

1.Loop类

  public final class Looper {
    
    @UnsupportedAppUsage
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    @UnsupportedAppUsage
    private static Looper sMainLooper;  // guarded by Looper.class
    private static Observer sObserver;
 
    @UnsupportedAppUsage
    final MessageQueue mQueue;
    final Thread mThread;
    private boolean mInLoop;

266      public static void loop() {
267          final Looper me = myLooper();
268          if (me == null) {
269              throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
270          }
271          if (me.mInLoop) {
272              Slog.w(TAG, "Loop again would have the queued messages be executed"
273                      + " before this one completed.");
274          }
275  
276          me.mInLoop = true;
277  
278          // Make sure the identity of this thread is that of the local process,
279          // and keep track of what that identity token actually is.
280          Binder.clearCallingIdentity();
281          final long ident = Binder.clearCallingIdentity();
282  
283          // Allow overriding a threshold with a system prop. e.g.
284          // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
285          final int thresholdOverride =
286                  SystemProperties.getInt("log.looper."
287                          + Process.myUid() + "."
288                          + Thread.currentThread().getName()
289                          + ".slow", -1);
290  
291          me.mSlowDeliveryDetected = false;
292  
293          for (;;) {
294              if (!loopOnce(me, ident, thresholdOverride)) {
295                  return;
296              }
297          }
298      }


330      private Looper(boolean quitAllowed) {
331          mQueue = new MessageQueue(quitAllowed);
332          mThread = Thread.currentThread();
333      }

 创建Looper对象的时候,同时创建了MessageQueue,并让Looper绑定当前线程。但我们从来不直接调用构造方法获取Looper对象,而是使用Looper的prepare()方法

108      public static void prepare() {
109          prepare(true);
110      }
111  
112      private static void prepare(boolean quitAllowed) {
113          if (sThreadLocal.get() != null) {
114              throw new RuntimeException("Only one Looper may be created per thread");
115          }
116          sThreadLocal.set(new Looper(quitAllowed));
117      }
118  


126      @Deprecated
127      public static void prepareMainLooper() {
128          prepare(false);
129          synchronized (Looper.class) {
130              if (sMainLooper != null) {
131                  throw new IllegalStateException("The main Looper has already been prepared.");
132              }
133              sMainLooper = myLooper();
134          }
135      }

prepare()使用ThreadLocal 保存当前Looper对象,ThreadLocal 类可以对数据进行线程隔离,保证了在当前线程只能获取当前线程的Looper对象,同时prepare()保证当前线程有且只有一个Looper对象,间接保证了一个线程只有一个MessageQueue对象

prepareMainLooper统在启动App时,已经帮我们调用了

266      public static void loop() {
267          final Looper me = myLooper();
268          if (me == null) {
269              throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
270          }
271          if (me.mInLoop) {
272              Slog.w(TAG, "Loop again would have the queued messages be executed"
273                      + " before this one completed.");
274          }
275  
276          me.mInLoop = true;
277  
278          // Make sure the identity of this thread is that of the local process,
279          // and keep track of what that identity token actually is.
280          Binder.clearCallingIdentity();
281          final long ident = Binder.clearCallingIdentity();
282  
283          // Allow overriding a threshold with a system prop. e.g.
284          // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
285          final int thresholdOverride =
286                  SystemProperties.getInt("log.looper."
287                          + Process.myUid() + "."
288                          + Thread.currentThread().getName()
289                          + ".slow", -1);
290  
291          me.mSlowDeliveryDetected = false;
292  
293          for (;;) {
294              if (!loopOnce(me, ident, thresholdOverride)) {
295                  return;
296              }
297          }
298      }

Lopper通过loop()开启无限循环,通过MessageQueue的next()获取message对象。一旦获取就调用msg.target.dispatchMEssage(msg)将msg交给handler对象处理(msg.target是handler对象)

所以Looper.loop的作用就是:从当前线程的MessageQueue从不断取出Message,并调用其相关的回调方法。

2. handler类

  1. 创建Handler对象

  2. 得到当前线程的Looper对象,并判断是否为空

  3. 让创建的Handler对象持有Looper、MessageQueu、Callback的引用

    214      public Handler(@Nullable Callback callback, boolean async) {
    215          if (FIND_POTENTIAL_LEAKS) {
    216              final Class<? extends Handler> klass = getClass();
    217              if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
    218                      (klass.getModifiers() & Modifier.STATIC) == 0) {
    219                  Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
    220                      klass.getCanonicalName());
    221              }
    222          }
    223  
    224          mLooper = Looper.myLooper();
    225          if (mLooper == null) {
    226              throw new RuntimeException(
    227                  "Can't create handler inside thread " + Thread.currentThread()
    228                          + " that has not called Looper.prepare()");
    229          }
    230          mQueue = mLooper.mQueue;
    231          mCallback = callback;
    232          mAsynchronous = async;
    233          mIsShared = false;
    234      }
    235  

    使用Handler发送消息主要有两种,一种是sendXXXMessage方式,还有一个postXXX方式,不过两种方式最后都会调用到sendMessageDelayed方法

    下面为sendMessage方式

    发送消息,将消息插入队列:sendMessage->sendMessageDelayed->sendMessageAtTime->enqueueMessage,这里会决定message是否为同步消息或者异步消息

  4. 727      public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
    728          MessageQueue queue = mQueue;
    729          if (queue == null) {
    730              RuntimeException e = new RuntimeException(
    731                      this + " sendMessageAtTime() called with no mQueue");
    732              Log.w("Looper", e.getMessage(), e);
    733              return false;
    734          }
    735          return enqueueMessage(queue, msg, uptimeMillis);
    736      }
    778      private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
    779              long uptimeMillis) {
    780          msg.target = this;
    781          msg.workSourceUid = ThreadLocalWorkSource.getUid();
    782  
    783          if (mAsynchronous) {
    784              msg.setAsynchronous(true);
    785          }
    786          return queue.enqueueMessage(msg, uptimeMillis);
    787      }
    549      boolean enqueueMessage(Message msg, long when) {
    550          if (msg.target == null) {
    551              throw new IllegalArgumentException("Message must have a target.");
    552          }
    553  
    554          synchronized (this) {
    // 一个message只能呢个发送一次
    555              if (msg.isInUse()) {
    556                  throw new IllegalStateException(msg + " This message is already in use.");
    557              }
    558  
    559              if (mQuitting) {
    560                  IllegalStateException e = new IllegalStateException(
    561                          msg.target + " sending message to a Handler on a dead thread");
    562                  Log.w(TAG, e.getMessage(), e);
    563                  msg.recycle();
    564                  return false;
    565              }
    566  // 标记message已经使用
    567              msg.markInUse();
    568              msg.when = when;
    // 拿到头部
    569              Message p = mMessages;
    570              boolean needWake;
    571              if (p == null || when == 0 || when < p.when) {
    572                  // New head, wake up the event queue if blocked.
    // 将消息插入到头部, 指向刚才拿到的消息
    573                  msg.next = p;
    574                  mMessages = msg;
    575                  needWake = mBlocked;
    576              } else {
    // 根据需要把消息插入到消息队列的合适位置,通常是调用xxxDelay方法,延时发送消息
    577                  // Inserted within the middle of the queue.  Usually we don't have to wake
    578                  // up the event queue unless there is a barrier at the head of the queue
    579                  // and the message is the earliest asynchronous message in the queue.
    580                  needWake = mBlocked && p.target == null && msg.isAsynchronous();
    581                  Message prev;
    582                  for (;;) {
    583                      prev = p;
    584                      p = p.next;
    585                      if (p == null || when < p.when) {
    586                          break;
    587                      }
    588                      if (needWake && p.isAsynchronous()) {
    589                          needWake = false;
    590                      }
    591                  }
    592                  msg.next = p; // invariant: p == prev.next
    593                  prev.next = msg;
    594              }
    595  
    596              // We can assume mPtr != 0 because mQuitting is false.
    597              if (needWake) {
    598                  nativeWake(mPtr);
    599              }
    600          }
    601          return true;
    602      }

    //getPostMessage方法是两种发送消息的不同之处

    Post这个方法我们发现也是将我们传入的参数封装成了一个消息,只是这次是m.callback = r,刚才是msg.what=what,至于Message的这些属性就不看了

    434      public final boolean post(@NonNull Runnable r) {
    435         return  sendMessageDelayed(getPostMessage(r), 0);
    436      }
    
    943      private static Message getPostMessage(Runnable r) {
    944          Message m = Message.obtain();
    945          m.callback = r;
    946          return m;
    947      }

    post方法只是先调用了getPostMessage方法,用Runnable去封装一个Message,然后就调用了sendMessageDelayed,把封装的Message加入到MessageQueue中。

    所以使用handler发送消息的本质都是:把Message加入到Handler中的MessageQueue中去

    97      public void dispatchMessage(@NonNull Message msg) {
    98          if (msg.callback != null) {
    99              handleCallback(msg);
    100          } else {
    101              if (mCallback != null) {
    102                  if (mCallback.handleMessage(msg)) {
    103                      return;
    104                  }
    105              }
    106              handleMessage(msg);
    107          }
    108      }
    109  

    可以看出,这个方法就是从MessageQueue中取出Message以后,进行分发处理。

    首先,判断msg.callback是不是空,其实msg.callback是一个Runnable对象,是Handler.post方式传递进来的参数。而hanldeCallback就是调用的Runnable的run方法。

    然后,判断mCallback是否为空,这是一个Handler.Callback的接口类型,之前说了Handler有多个构造方法,可以提供设置Callback,如果这里不为空,则调用它的hanldeMessage方法,注意,这个方法有返回值,如果返回了true,表示已经处理 ,不再调用Handler的handleMessage方法;如果mCallback为空,或者不为空但是它的handleMessage返回了false,则会继续调用Handler的handleMessage方法,该方法就是我们经常重写的那个方法。

    3. MessageQueue

  5. 318      @UnsupportedAppUsage
    319      Message next() {
    320          // Return here if the message loop has already quit and been disposed.
    321          // This can happen if the application tries to restart a looper after quit
    322          // which is not supported.
    323          final long ptr = mPtr;
    324          if (ptr == 0) {
    325              return null;
    326          }
    327  
    328          int pendingIdleHandlerCount = -1; // -1 only during first iteration
    329          int nextPollTimeoutMillis = 0;
    330          for (;;) {
    331              if (nextPollTimeoutMillis != 0) {
    332                  Binder.flushPendingCommands();
    333              }
    334  
    335              nativePollOnce(ptr, nextPollTimeoutMillis);
    336  
    337              synchronized (this) {
    338                  // Try to retrieve the next message.  Return if found.
    339                  final long now = SystemClock.uptimeMillis();
    340                  Message prevMsg = null;
    341                  Message msg = mMessages;
    342                  if (msg != null && msg.target == null) {
    343                      // Stalled by a barrier.  Find the next asynchronous message in the queue.
    344                      do {
    345                          prevMsg = msg;
    346                          msg = msg.next;
    347                      } while (msg != null && !msg.isAsynchronous());
    348                  }
    349                  if (msg != null) {
    350                      if (now < msg.when) {
    351                          // Next message is not ready.  Set a timeout to wake up when it is ready.
    352                          nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
    353                      } else {
    354                          // Got a message.
    355                          mBlocked = false;
    356                          if (prevMsg != null) {
    357                              prevMsg.next = msg.next;
    358                          } else {
    359                              mMessages = msg.next;
    360                          }
    361                          msg.next = null;
    362                          if (DEBUG) Log.v(TAG, "Returning message: " + msg);
    363                          msg.markInUse();
    364                          return msg;
    365                      }
    366                  } else {
    367                      // No more messages.
    368                      nextPollTimeoutMillis = -1;
    369                  }
    370  
    371                  // Process the quit message now that all pending messages have been handled.
    372                  if (mQuitting) {
    373                      dispose();
    374                      return null;
    375                  }
    376  
    377                  // If first time idle, then get the number of idlers to run.
    378                  // Idle handles only run if the queue is empty or if the first message
    379                  // in the queue (possibly a barrier) is due to be handled in the future.
    380                  if (pendingIdleHandlerCount < 0
    381                          && (mMessages == null || now < mMessages.when)) {
    382                      pendingIdleHandlerCount = mIdleHandlers.size();
    383                  }
    384                  if (pendingIdleHandlerCount <= 0) {
    385                      // No idle handlers to run.  Loop and wait some more.
    386                      mBlocked = true;
    387                      continue;
    388                  }
    389  
    390                  if (mPendingIdleHandlers == null) {
    391                      mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
    392                  }
    393                  mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
    394              }
    395  
    396              // Run the idle handlers.
    397              // We only ever reach this code block during the first iteration.
    398              for (int i = 0; i < pendingIdleHandlerCount; i++) {
    399                  final IdleHandler idler = mPendingIdleHandlers[i];
    400                  mPendingIdleHandlers[i] = null; // release the reference to the handler
    401  
    402                  boolean keep = false;
    403                  try {
    404                      keep = idler.queueIdle();
    405                  } catch (Throwable t) {
    406                      Log.wtf(TAG, "IdleHandler threw exception", t);
    407                  }
    408  
    409                  if (!keep) {
    410                      synchronized (this) {
    411                          mIdleHandlers.remove(idler);
    412                      }
    413                  }
    414              }
    415  
    416              // Reset the idle handler count to 0 so we do not run them again.
    417              pendingIdleHandlerCount = 0;
    418  
    419              // While calling an idle handler, a new message could have been delivered
    420              // so go back and look again for a pending message without waiting.
    421              nextPollTimeoutMillis = 0;
    422          }
    423      }

    next方法中,首先设置一个死循环,然后调用nativePollOnce(ptr, nextPollTimeoutMillis)方法,它是一个native方法,用于阻塞MessageQueue,主要是关注它的第二个参数nextPollTimeoutMillis,有如下三种可能:

如果nextPollTimeoutMillis=-1,一直阻塞不会超时。

如果nextPollTimeoutMillis=0,不会阻塞,立即返回。

如果nextPollTimeoutMillis>0,最长阻塞nextPollTimeoutMillis毫秒(超时),如果其间有程序唤醒会立即返回。

我们先继续往下看,开始nextPollTimeoutMillis为0,也就是不会阻塞,则继续往下,这时候有三种情况

1.延时消息,则直接算出目前需要延时的时间nextPollTimeoutMillis,注意,这时候并没有把消息返回,而是继续往下,设置mBlocked为true,表示消息队列已阻塞,并continue执行for循环体,再次执行nativePollOnce方法,这时候nextPollTimeoutMillis>0,则会导致MessageQueue休眠nextPollTimeoutMillis毫秒,接着应该会走到情况2.

2.不是延时消息,则设置mBlocked为false,表示消息队列没有阻塞,直接把消息返回,且把消息出队。

3. 如果消息为空,则调用位置nextPollTimeoutMillis为-1,继续往下,设置mBlocked为true,表示消息队列已阻塞,并continue继续for循环,这时候调用nativePollOnce会一直阻塞,且不会超时。

所以,当消息队列为空时,其实是调用本地方法nativePollOnce,且第二个参数为-1,它会导致当前线程阻塞,且不会超时

4. Message

Message分为3种:普通消息(同步消息)、屏障消息(同步屏障)和异步消息。我们通常使用的都是普通消息,而屏障消息就是在消息队列中插入一个屏障,在屏障之后的所有普通消息都会被挡着,不能被处理。不过异步消息却例外,屏障不会挡住异步消息,

因此可以这样认为:屏障消息就是为了确保异步消息的优先级,设置了屏障后,只能处理其后的异步消息,同步消息会被挡住,除非撤销屏障。

同步屏障是通过MessageQueue的postSyncBarrier方法插入到消息队列的。

屏障消息和普通消息的区别是屏障消息没有tartget。

1.屏障的插队方法

屏障是通过MessageQueue的postSyncBarrier方法插入到消息队列的。

public int postSyncBarrier() {
473          return postSyncBarrier(SystemClock.uptimeMillis());
474      }
475  
476      private int postSyncBarrier(long when) {
477          // Enqueue a new sync barrier token.
478          // We don't need to wake the queue because the purpose of a barrier is to stall it.
479          synchronized (this) {
480              final int token = mNextBarrierToken++;
481              final Message msg = Message.obtain();
482              msg.markInUse();
483              msg.when = when;
484              msg.arg1 = token;
485  
486              Message prev = null;
487              Message p = mMessages;
488              if (when != 0) {
489                  while (p != null && p.when <= when) {
490                      prev = p;
491                      p = p.next;
492                  }
493              }
494              if (prev != null) { // invariant: p == prev.next
495                  msg.next = p;
496                  prev.next = msg;
497              } else {
498                  msg.next = p;
499                  mMessages = msg;
500              }
501              return token;
502          }
503      }
504  
  • 屏障消息和普通消息的区别在于屏障没有tartget,普通消息有target是因为它需要将消息分发给对应的target,而屏障不需要被分发,它就是用来挡住普通消息来保证异步消息优先处理的。
  • 屏障和普通消息一样可以根据时间来插入到消息队列中的适当位置,并且只会挡住它后面的同步消息的分发。
  • postSyncBarrier返回一个int类型的数值,通过这个数值可以撤销屏障。
  • postSyncBarrier方法是私有的,如果我们想调用它就得使用反射。
  • 插入普通消息会唤醒消息队列,但是插入屏障不会。

2.屏障的工作原理

通过postSyncBarrier方法屏障就被插入到消息队列中了,那么屏障是如何挡住普通消息只允许异步消息通过的呢?我们知道MessageQueue是通过next方法来获取消息的。

通过messaQequeue中的next(),如果碰到屏障就遍历整个消息链表找到最近的一条异步消息,在遍历的过程中只有异步消息才会被处理执行到 if (msg != null){}中的代码。可以看到通过这种方式就挡住了所有的普通消息

3,同步消息还是异步消息的决定时间

Handler有几个构造方法,可以传入async标志为true,这样构造的Handler发送的消息就是异步消息。不过可以看到,这些构造函数都是hide的,正常我们是不能调用的,不过利用反射机制可以使用@hide方法。

/**
  * @hide
  */
public Handler(boolean async) {}
 
/**
 * @hide
 */
public Handler(Callback callback, boolean async) { }
 
/**
 * @hide
 */
public Handler(Looper looper, Callback callback, boolean async) {}

也可以直接调用message.setAsynchronous(true);来进行设置为异步消息

509      public void setAsynchronous(boolean async) {
510          if (async) {
511              flags |= FLAG_ASYNCHRONOUS;
512          } else {
513              flags &= ~FLAG_ASYNCHRONOUS;
514          }
515      }

3.屏障的移除

移除屏障可以通过MessageQueue的removeSyncBarrier方法:

517      public void removeSyncBarrier(int token) {
518          // Remove a sync barrier token from the queue.
519          // If the queue is no longer stalled by a barrier then wake it.
520          synchronized (this) {
521              Message prev = null;
522              Message p = mMessages;
523              while (p != null && (p.target != null || p.arg1 != token)) {
524                  prev = p;
525                  p = p.next;
526              }
527              if (p == null) {
528                  throw new IllegalStateException("The specified message queue synchronization "
529                          + " barrier token has not been posted or has already been removed.");
530              }
531              final boolean needWake;
532              if (prev != null) {
533                  prev.next = p.next;
534                  needWake = false;
535              } else {
536                  mMessages = p.next;
537                  needWake = mMessages == null || mMessages.target != null;
538              }
539              p.recycleUnchecked();
540  
541              // If the loop is quitting then it is already awake.
542              // We can assume mPtr != 0 when mQuitting is false.
543              if (needWake && !mQuitting) {
544                  nativeWake(mPtr);
545              }
546          }
547      }

总结下Handler消息机制主要的四个类的功能

  1. Message:信息的携带者,持有了Handler,存在MessageQueue中,一个线程可以有多个
  2. Hanlder:消息的发起者,发送Message以及消息处理的回调实现,一个线程可以有多个Handler对象
  3. Looper:消息的遍历者,从MessageQueue中循环取出Message进行处理,一个线程最多只有一个
  4. MessageQueue:消息队列,存放了Handler发送的消息,供Looper循环取消息,一个线程最多只有一个

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

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

相关文章

解放生产力,AI加持你也能做这些事!

去年网上流行一个说法叫一人企业或超级IP。一个人就是一家公司&#xff0c;可以更加专注于自身核心技能。既能对工作拥有更大的自主性和控制力&#xff0c;又能舍弃了传统公司管理等繁琐的事务工作&#xff0c;可以全面释放自己的兴趣和潜力。 这个概念给笔者留下了比较深的印…

开源的python 游戏开发库介绍

本文将为您详细讲解开源的 Python 游戏开发库&#xff0c;以及它们的特点、区别和应用场景。Python 社区提供了多种游戏开发库&#xff0c;这些库可以帮助您在 Python 应用程序中实现游戏逻辑、图形渲染、声音处理等功能。 1. Pygame 特点 - 基于 Python 的游戏开发库。…

第3章 数据链路层(1)

3.1数据链路层的功能 加强物理层传输原始比特流的功能,将可能出差错的物理连接改成逻辑上无差错的数据链路[节点的逻辑通道] 3.1.1 为网络提供服务 (1).无确认的无连接服务 适合通信质量好的有线传输链路(实时通信或误码率较低的通信信道)【例如以太网】(2).有确认的无连接服务…

WIN32部分知识介绍

&#x1f308;前言&#xff1a;此篇博客是为下一篇的《贪吃蛇》的做的前戏工作&#xff0c;这篇会讲到贪吃蛇所用到的一些工具以及函数。 首先在讲WIN32的内容时我们想了解一下他的基本概念&#xff1a; Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外…

指数移动平均(EMA)

文章目录 前言EMA的定义在深度学习中的应用PyTorch代码实现yolov5中模型的EMA实现 参考 前言 在深度学习中&#xff0c;经常会使用EMA&#xff08;指数移动平均&#xff09;这个方法对模型的参数做平均&#xff0c;以求提高测试指标并增加模型鲁棒。实际上&#xff0c;_EMA可以…

全栈的自我修养 ———— css中常用的布局方法flex和grid

在项目里面有两种常用的主要布局:flex和grid布局&#xff08;b站布局&#xff09;&#xff0c;今天分享给大家这两种的常用的简单方法&#xff01; 一、flex布局1、原图2、中心对齐3、主轴末尾或者开始对其4、互相间隔 二、grid布局1、基本效果2、加间隔3、放大某一个元素 一、…

数据的加密方式及操作方法

目录 一 什么是加密 二 加密方法 对称加密&#xff08;如AES加密&#xff09; 非对称加密&#xff08;如RSA加密&#xff09; 散列&#xff08;如MD5加密&#xff09; 三 加密操作 1 MD5加密&#xff08;散列&#xff09; 2 AES加密&#xff08;对称加密&#xff09; …

HTMK5七天学会基础动画网页10(2)

制作立方体 学完前面的基础内容&#xff0c;制作立方体是个不错的练习方法&#xff0c;先看成品 再分析一下&#xff0c;六个面让每个面旋转平移就可以实现一个立方体&#xff0c;来看代码: <title> 制作立方体</title> <style> *{ margin: 0; padding: 0; …

如何搭建财务数据运营体系:基于财务五力模型的分析

在当今复杂多变的商业环境中,财务数据作为企业决策的重要参考依据,其运营体系的搭建显得尤为关键。一个健全、高效的财务数据运营体系不仅能够为企业提供准确的财务数据支持,还能帮助企业在激烈的市场竞争中保持领先地位。基于财务五力模型的分析,我们可以从收益力、安定力…

基于深度学习YOLOv8+Pyqt5的抽烟吸烟检测识别系统(源码+跑通说明文件)

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;39抽烟 获取完整源码源文件4000张已标注的数据集配置说明文件 可有偿59yuan一对一远程操作跑通 效果展示 基于深度学YOLOv8PyQt5的抽烟吸烟检测识别系统&#xff08;完整源码跑通说明文件&#xff09; 各文件说明 模型评价…

Mybatis-Plus——07,性能分析插件

性能分析插件 一、导入插件二、SpringBoot中配置环境为dev或test环境三、运行测试————————创作不易&#xff0c;笔记不易&#xff0c;如觉不错&#xff0c;请三连&#xff0c;谢谢~~ MybatisPlus也提供了性能分析插件&#xff0c;如果超过这个时间就停止运行&#xff0…

常见3大web漏洞

常见3大web漏洞 XSS攻击 描述&#xff1a; 跨站脚本&#xff08;cross site script&#xff09;-简称XSS&#xff0c;常出现在web应用中的计算机安全漏桶、web应用中的主流攻击方式。 攻击原理&#xff1a; 攻击者利用网站未对用户提交数据进行转义处理或者过滤不足的缺点。 …

前端文件上传

文件上传方式 前端文件上传有两种方式&#xff0c;第一种通过二进制blob传输&#xff08;formData传输&#xff09;&#xff0c;第二种是通过base64传输 文件相关的对象 file对象其实是blob的子类 blob对象的第一个参数必须是一个数组&#xff0c;你可以把一个file对象放进去…

Oracle SQL优化(读懂执行计划 一)

目录 SQL执行计划的作用示例演示执行计划概念介绍执行计划实例DISPLAY_CURSOR 类型DISPLAY_AWR 类型 指标详解 SQL执行计划的作用 示例演示 执行计划概念介绍 执行计划实例 DISPLAY_CURSOR 类型 DISPLAY_AWR 类型 指标详解

Vivado原语模板

1.原语的概念 原语是一种元件&#xff01; FPGA原语是芯片制造商已经定义好的基本电路元件&#xff0c;是一系列组成逻辑电路的基本单元&#xff0c;FPGA开发者编写逻辑代码时可以调用原语进行底层构建。 原语可分为预定义原语和用户自定义原语。预定义原语为如and/or等门级原语…

【电路笔记】-PNP晶体管

PNP晶体管 文章目录 PNP晶体管1、概述2、PNP晶体管电路示例3、PNP晶体管识别1、概述 PNP 晶体管与我们在上一篇教程中看到的 NPN 晶体管器件完全相反。 在这种类型的 PNP 晶体管结构中,两个互连的二极管相对于之前的 NPN 晶体管是相反的。 这会产生正-负-正类型的配置,箭头…

vxe-table配合Export2Excel导出object类型数据{type,count}。表格数据呈现是利用插槽,导出只要count该怎么做

先贴一张数据来&#xff1a; 一、然后是vxe-grid的columns配置&#xff1a; 然后就正常用封装好的Export2Excel就行。 碰到一次在控制台报错&#xff1a; 没复现出来&#xff0c;大概就说是count咋样咋样。 以后碰到的话再说&#xff0c;各位要用的话也注意看看 二、或者 用js…

不知道吧,腾讯云轻量应用服务器使用有一些限制!

腾讯云轻量应用服务器相对于云服务器CVM是有一些限制的&#xff0c;比如轻量服务器不支持更换内网IP地址&#xff0c;不支持自定义私有网络VPC&#xff0c;内网连通性方面也有限制&#xff0c;轻量不支持CPU内存、带宽或系统盘单独升级&#xff0c;只能整个套餐整体升级&#x…

AWS 入门实践-远程访问AWS EC2 Linux虚拟机

远程访问AWS EC2 Linux虚拟机是AWS云计算服务中的一个基本且重要的技能。本指南旨在为初学者提供一系列步骤&#xff0c;以便成功地设置并远程访问他们的EC2 Linux实例。包括如何上传下载文件、如何ssh远程登录EC2虚拟机。 一、创建一个AWS EC2 Linux 虚拟机 创建一个Amazon…

MySQL通过SQL语句进行递归查询

这里主要是针对于MySQL8.0以下版本&#xff0c;因为MySQL8.0版本出来了一个WITH RECURSIVE函数专门用来进行递归查询的 先看下表格数据&#xff0c;就是很普通的树结构数据&#xff0c;通过parentId关联上下级关系 下面我们先根据上级节点id递归获取所有的下级节点数据&#x…