前言

Handler可以说小伙伴们用的非常多了,可以说Handler是支撑整个Android系统运行的基础,本质上Android系统都是由事件驱动的。而处理事件的核心就在于Handler。接下来我们就从简单的使用,到源码分析让你彻彻底底明白Handler的本质。不会再让你发出为什么Looper.loop不会堵塞主线程,Handler是如何切换线程等这类疑惑。

作者:Mlx
链接:https://juejin.im/post/6866015512192876557

简单使用

一般是在主线程中实现一个Handler,然后在子线程中使用它。

class HandlerActivity: AppCompatActivity() {private val mHandler = MyHandler()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 在子线程中通过自定义的 Handler 发消息thread {mHandler.sendEmptyMessageDelayed(1, 1000)}}// 自定义一个 Handlerclass MyHandler: Handler() {override fun handleMessage(msg: Message) {Log.i("HandlerActivity", "主线程:handleMessage: ${msg.what}")}}
}

或者有时候需要在子线程中创建运行在主线程中的Handler

class HandlerActivity: AppCompatActivity() {private var mHandler: Handler? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)thread {//获得main looper 运行在主线程mHandler = MyHandler(Looper.getMainLooper())mHandler!!.sendEmptyMessageDelayed(1, 1000)}}// 自定义一个 Handlerclass MyHandler(): Handler() {override fun handleMessage(msg: Message) {Log.i("HandlerActivity", "子线程:handleMessage: ${msg.what}")}}
}

这就是小伙伴们一般常用的两个用法。大家注意到了在第二个用法中出现了一个Looper.getMainLooper(),使用它作为参数,即使MyHandler是在子线程中定义的,但是它的handleMessage方法依然运行在主线程。我们看一下这个参数究竟是什么东东~

public Handler(@NonNull Looper looper) {this(looper, null, false);
}

可以看到这个Looper就是我们上面传入的参数Looper.getMainLooper(),也就说明了handleMessage方法具体运行在哪个线程是和这个Looper息息相关的。那么这个Looper究竟是何方神圣,它是怎么做到线程切换的呢?

概述

我们先来看一张图

这就是整个Handler在Java层的流程示意图。可以看到,在Handler调用sendMessage方法以后,Message对象会被添加到MessageQueue中去。而这个MessageQueue就是被包裹在了Looper中。那么Looper对象是干什么的呢?它和Handler是什么关系呢?我们来看一下他们具体的职责把~

  • Handle 消息机制中作为一个对外暴露的工具,其内部包含了一个 Looper 。负责Message的发送及处理

    • Handler.sendMessage() :向消息队列发送各种消息事件
    • Handler.handleMessage()处理相应的消息事件
  • Looper 作为消息循环的核心,其内部包含了一个消息队列 MessageQueue ,用于记录所有待处理的消息;通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者,可以看成是消息泵。注意,线程切换就是在这一步完成的。

  • MessageQueue 则作为一个消息队列,则包含了一系列链接在一起的 Message ;不要被这个Queue的名字给迷惑了,就以为它是一个队列,但其实内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。

  • Message 则是消息体,内部又包含了一个目标处理器 target ,这个 target 正是最终处理它的 Handler

哦?原来他们的职责是这样啊,可我还是不懂他们到底是怎么运行起来的。就像你告诉我医生是负责治病,警察是抓坏人的,他们具体是如何去做的呢?

Handler

从我们大家最熟悉的sendMessage方法说起。sendMessage方法见名思意,就是发送一个信息,可是要发送到哪里去呢,这是代码:

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

调用了sendMessageDelayed方法:

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

继而调用sendMessagAtTime方法:

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

眼尖的小伙伴就会发现,等等不对,这代码中出了一个叛徒,啊不对,出了一个奇怪的东西。没错,就是刚才流程图中出现的这个MessageQueue。你看,我没有胡说吧,这个MessageQueue是实打实存在的,并且被作为参数一起传给了enqueueMessage方法。其实无论你是如何使用Handler发送消息,结果都会走到enqueueMessage方法中。

这是方法的调用链:

可以看到无论如何,最后都会走到enqueueMessage方法中。这个enqueueMessage方法具体做了什么事呢:

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

enqueueMessage一共做了两件事情,一个是给Message赋值,一个是调用传进来的这个MessageQueueenqueueMessage方法。注意啊,最后这个enqueueMessage方法是在MessageQueue中的,已经不再是Handler的方法了,也就是说,调用走到了这里。事件的流向已经不归Handler管了。

Handler::enqueueMessage方法中第一行msg.target = this;,这个this是什么呢?这个thishandler方法中自然是handler本身了,也就是说这一行代码将handler自身赋值给了Message对象的target字段。我们可以看以下这个target字段的定义:

//简化后的代码
public final class Message implements Parcelable{@UnsupportedAppUsage/*package*/ Handler target;
}

啊,这样明白了,也就是说每个发出去的Message都持有把它发出去的Handler的引用,对不对?

没错事实就是这样,每个发出去的Message对象内部都会有个把它发出去的Handler对象的引用,也可以理解Message这么做的目的,毕竟Handler把它发射出去了,它不得知道是谁干的,好随后找它报仇么。那么我们继续下一步,msg.setAsynchronous(true)这一行代码是设置异步消息的,这里暂时先不管它。我们先看queue.enqueueMessage(msg, uptimeMillis)这行代码。也就是从这行代码,Message就可以和Handler说拜拜了您讷。

MessageQueue

Handler这个mQueue就是上文我们提到过的MessageQueue对象,在上面的介绍说也说了,这货就是个骗子,明明起名是Queue,却是单链表。你可能误会Google工程师了,名字也确实没什么错了,从机制上看确实很像队列。队列是什么特性啊,先进先出对吧。这个先后就是按时间来划分的,时间靠前的就在前面时间靠后的就在后面。而在这个单链表中也确实是这样实现的,按照时间的先后排序。这个就先不多讲了,一会讲如何实现的消息延时发送的时候会讲到这个。

到这里你可能有疑惑了,这个MessageQueue是什么鬼,从哪里冒出来的。你可能还记得,在上面的sendMessageAtTime方法中有这么一行:

MessageQueue queue = mQueue;

那么这个mQueue是在哪里被赋值的呢?当然是在构造方法中啦~

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

不对啊, 你TM骗我,在最开始你继承的Handler可没有这几个参数。哎呀,小伙子别心急,你看这个无参构造方法不也调用的这个方法么。

public Handler() {this(null, false);
}

在这个有参数的构造方法中呢,可以看到有这么两行:

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;

我们在Handler中使用的mQueue就是在这里赋值的。这里的赋值可不简单,它拿的是人家LooperMessageQueue作为自己的MessageQueue,**而且在上面的代码中有一个很关键的点,就是调用Looper.myLooper()方法中获取这个Looper对象,如果是空的话就要抛出异常。**这一点非常关键,我们先做个记号,一会回过头来会看这一行代码。你就会明白它的作用了。

现在先不研究Looper,我们继续看我们的MessageQueue。上面说到,最后发送消息都调用的是MessageQueuequeue.enqueueMessage(msg, uptimeMillis)方法。现在我们已经拿到了queue,进去看看这个方法它做了什么。

// MessageQueue.java
//省略部分代码
boolean enqueueMessage(Message msg, long when) {synchronized (this) {if (mQuitting) {IllegalStateException e = new IllegalStateException(msg.target + " sending message to a Handler on a dead thread");msg.recycle();return false;}msg.markInUse();msg.when = when;//【1】拿到队列头部Message p = mMessages;boolean needWake;//【2】如果消息不需要延时,或者消息的执行时间比头部消息早,插到队列头部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 {//【3】消息插到队列中间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;}if (needWake) {nativeWake(mPtr);}}return true;
}

主要分为3个步骤(见以上代码标注)。

  1. mMessages 是队列的第一消息,获取到它
  2. 判断消息队列是不是空的,是则将当前的消息放到队列头部;如果当前消息不需要延时,或当前消息的执行时间比头部消息早,也是放到队列头部。
  3. 如果不是以上情况,说明当前队列不为空,并且队列的头部消息执行时间比当前消息早,需要将它插入到队列的中间位置。

如何判断这个位置呢?依然是通过消息被执行的时间。

通过遍历整个队列,当队列中的某个消息的执行时间比当前消息晚时,将消息插到这个消息的前面。

可以看到,消息队列是一个根据消息【执行时间先后】连接起来的单向链表。

想要获取可执行的消息,只需要遍历这个列表,对比当前时间与消息的执行时间,就知道消息是否需要执行了。

好了,MessageQueueJava层的分析到这里就结束了。

等等,这就结束了?

没错,到这一步,消息已经添加到了这个名为队列实为单链表的队列中。

不对啊,我handleMessage方法如何被调用呢?消息添加进去就完了?说好的线程切换呢?

其实到这一步真的就结束了,最起码在Java层是结束了。消息到这一步被添加到了队列中,HandlerMessageQueue在发送的过程中做的工作已经做完了。但是既然有队列,那么不可能说光添加不读取把。不然我添加了有什么用?

是的,接下来就是Looper大展神威的时候到了。

Looper

在上面提到了,Handler中的MessageQueue对象其实就是Handler中的Looper它的MessageQueueHandlerMessageQueue中添加消息,其实就是往HandlerLooper所持有的MessageQueue中添加对象。可能有点绕,但是需要明白的是这个MessageQueueLooper的,不是Handler的。明白了这一点,你就能很好的理解后面发生的事情。

可能有的小伙伴会说了,这个Looper哪来的,我创建Handler的时候从没看见过它出现啊。没错,在使用Handler的时候它确实没出现过,但是大家还记得Handler中两个参数的那个构造方法嘛?就是下面这个:

//Handler.java
//省略部分代码
public Handler(@Nullable Callback callback, boolean async) {//敲黑板,划重点就是这一句!!!!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.myLooper方法获取到了Looper对象,当然,也有可能没获取到。不过,你如果没获取到就惨了,就要抛异常了。

在职责分析中我们提到了, 这个Looper对象作为消息循环的核心,不断从它的MessageQueue中取出消息然后进行分发。

说人话可以不?

刚才说到MessageQueue那个队列中那么多的消息没人拿,MessageQueue的老板Looper看不下去了,说你这也太浪费了,来我拿吧,然后它专门负责一个个拿,然后看这是谁发的,然后让谁去处理。

那我们看看这个Looper.myLooper()方法做了什么事情呢。它是如何返回一个Looper对象的呢?

public static @Nullable Looper myLooper() {return sThreadLocal.get();
}

sThreadLocal又是什么鬼?咱们看一下它的定义

//sThreadLocal.get() will return null unless you've called prepare().
@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

可以看到这个sThreadLocal是一个ThreadLocal类,并且它的泛型是Looper对象。ThreadLocal提供了线程的局部变量,每个线程都可以通过set()get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。简要言之:往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

呀呵,源代码中还有行注释,这行注释的意思是除非您已调用prepare(),否则sThreadLocal.get()将返回null。这行注释就有趣了,刚才我还寻思这个ThreadLocal的get方法得有数据才能返回,可这个数据是啥时候塞进去的呢?你这注释就告诉我了,只有我调用了prepare()方法,才有值啊。那我们就去看看这个方法做了些什么。

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));
}
复制代码

可以看得出,最后调用了是prepare(boolean quitAllowed)方法,而这个方法首先判断,如果sThreadLocal有值,就抛异常,没有值才会塞进去一个值。其实很好理解,就是说prepare方法必须调用但也只能调用一次,不调用没有值,抛异常,调用多次也还抛异常。我好难哦~不过大家还记得上面我们重点关注的一个内容吗,在Handler中的有参构造函数中有这么一行代码会报异常:

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;

如果Looper为空就抛异常,现在我们知道了,什么时候Looper为空呢?没有调用prepare方法的时候会为null,也就是说在构造Handler之前,必须得有Looper对象,换言之,**在构造Handler之前,必须调用Looperprepare方法创建Looper。**这句话非常重要,所以我又是下划线又是加粗的,一定要记住这句话。在后面自定义一个Looper的时候会用到。

接下来再看看这行sThreadLocal.set(new Looper(quitAllowed));做了什么吧,它是如何塞进去的呢

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}

set方法首先获取到了当前的线程,然后获取一个map。这个map是以键值对形式存储内容的。如果获取的map为空,就创建一个map。如果不为空就塞进去值。要注意的是,这里面的key是当前的线程,这里面的value就是Looper。也就是说,线程和Looper是一一对应的。也就是很多人说的Looper线程绑定了,其实就是以键值对形式存进了一个map中。没什么高大上的。你来你也行。

而这个Looper的构造方法我们也得去看一下:

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

Looper的构造方法中,可以看到它创建了一个MessageQueue,没错就是那个被Handler无耻使用的MessageQueue。需要注意的一点,上面的分析中提到了prepare方法必须调用但也只能调用一次,调用以后就会创建Looper对象,也就是说一个线程中只会创建一个Looper对象,而一个Looper对象也只会创建一个MessageQueue对象。

现在我们来梳理一下这个流程哈~

首先创建一个无参数的Handler,在这个Handler的构造方法中又去获取Looper对象,当然获取Looper对象其实是为了它的MessageQueueHandler巴结上了人家Looper对象的MessageQueue以后,发送消息的时候,把要发送的消息给了MessageQueue,添加到了队列中。是不是感觉缺少了什么?没错,好像在这个里面Looper的作用没体现出来,说好的分发消息呢?而且你刚刚说了得调用prepare()方法才会创建Looper,可我没调用过这个方法啊。那这个Looper谁创建的?

刚才提到了,Looper在创建的时候会被当成value塞入到一个map中去,这个mapThreadLocal。而key就是创建Looper时所在的线程。也就是所谓的Looper线程绑定。我们一般在用的时候从没创建过Looper,但是我们知道handle中的回调handleMessage方法是运行在主线程中的。Looper的职责不就是分发消息么,也就是说Looper对象在主线程中把消息分发给了Handler。那么这下就明白了,在我们创建Looper的时候,Looper所在的线程是主线程,换言之,与这个Looper绑定的线程就是主线程。

明白了,我这就去和面试官对线。

既然是主线程,那么大家应该知道,主线程是谁创建的?ActivityThread类。ActivityThread类也正是整个app的入口。以前我也很好奇,既然Android是用Java写的,按理说Java不应该是有个什么main方法么?怎么我写Android没用过这个main方法呢?其实呢,在ActivityThread中就有这个main方法,它是程序的入口,也就说当你点开app以后,首先会进入到这个main方法中,然后做了一大堆事情,这里就不分析了。你只需要知道,这个main方法才是真正的入口。

那我们来看看这个main方法到底干了什么事情:

//ActivityThread.java
//省略部分代码
public static void main(String[] args) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");Process.setArgV0("<pre-initialized>");//1 敲黑板,划重点,就是这一句!Looper.prepareMainLooper();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);//2 敲黑板,划重点,这一句!Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");}

这段代码是不是很符合我们平常写的java程序呢?熟悉的main方法又回来了。main方法中可以看到,它调用了Looper的prepareMainLooper方法:

public static void prepareMainLooper() {//设置不允许退出的Looperprepare(false);synchronized (Looper.class) {if (sMainLooper != null) {throw new IllegalStateException("The main Looper has already been prepared.");}sMainLooper = myLooper();}
}

可以看到注释1,这个方法最终还是调用了Looperprepare方法,这个方法干嘛的?创建Looper并且把它和当前线程一起塞进map中的啊。当前线程是哪个线程?主线程啊!

一切到这里就真相大白了,在APP启动的时候,入口方法中已经自动帮我们创建好了Looper,并且也自动的帮我们和主线程绑定了。也就是说我们平常用的Handler中的Looper就是主线程中创建的这个Looper。细心的小伙伴应该会发现,这个prepareMainLooper方法你是不能调用的。为啥?因为这个方法在入口的时候执行了一次,所以里面的sMainLooper不为Null了,如果你在调用一次,不就要抛异常了么~

现在Looper也有了,LooperMessageQueue也有了。接下来该分发消息了吧?我Handler发送消息可是已经很久过去了,你这里分析一大通,我还干不干活了?

好,我们现在先假设一个场景。

你买了一个快递,你知道迟早会给你送到,但是不确定到底什么时候才会送到。你想早点拿到快递应该怎么做?

你会不停的问快递公司,我的快递到哪了,到哪了。当然,现实中一般都是等快递员打电话才去拿快递~问题在于,这是程序。

Looper虽说要分发消息,但是它又不知道你什么时候会发送消息,只能开启一个死循环,不断的尝试从队列中拿数据。这个死循环在哪里开始的?没错就是注释2处,Looper.loop()开启了一个死循环,然后不断的尝试去队列中拿消息。

// Looper.java
public static void loop() {//拿到当前线程的Looperfinal Looper me = myLooper();if (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");}//拿到Looper的消息队列final MessageQueue queue = me.mQueue;// 省略一些代码...//1 这里开启了死循环for (;;) {Message msg = queue.next(); // might blockif (msg == null) {// No message indicates that the message queue is quitting.return;}try {msg.target.dispatchMessage(msg);//省略一些代码...} catch (Exception exception) {//省略一些代码...throw exception;} finally {//省略一些代码...}//省略一些代码...msg.recycleUnchecked();}
}

在循环中Looper不停的取出消息,拿到Message对象以后,会去调用Messagetarget字段的dispatchMessage方法,这个target字段还有印象吗?没错,就是发送它的Handlermessage在被发送出去的时候就已经暗暗记下了是谁发送出去的。现在轮到它报仇了~

我们可以跟进看一下这个dispatchMessage方法:

//Handler.java
public void dispatchMessage(@NonNull Message msg) {if (msg.callback != null) {handleCallback(msg);} else {if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);}
}

可以看到,消息会先分发给Meesgaecallback,我们没有定义这个callback,那我们接下来看,还有一个mCallback。这个mCallback是创建Handler的时候可以选择传一个CallBack回调,里面依然是handleMessage方法。也就是说你可以自定义一个类继承Handler,重写handleMessage方法,也可以直接new一个Handler传一个回调。当然,这个都很简单,我就不再赘述了,大家可以自行尝试体验。

我们关注重点,当Looper拿到Message以后,并且根据Messagetarget字段找到了发送消息的Handler,紧接着调用了HandlerhandleMessage方法。重点来了,这个Looper是在哪个线程运行的?主线程,它调用方法是在哪个线程运行的?依然是主线程!handleMessage方法此时在哪个线程运行的?依然是主线程!不知不觉中,线程已经切换过来了,神奇不?其实并不神奇,其实就是主线程中的Looper不断的尝试调用handleMessage方法,如果有消息就调用成功了,此时handleMessage方法就是在主线程中调用的。而handler在哪个线程,Looper并不关心,我反正只在主线程调用你的handleMessage方法。这就是线程切换的本质。就是没有线程切换,主线程的Looper不断的尝试调用而已。

可能有的小伙伴已经懵逼了,我们再次从头到尾梳理一下哈~

  1. mainThreadActivityThread首先创建了一个运行在主线程的Looper,并且把它和主线程进行了绑定。
  2. Looper又创建了一个MessageQueue,然后调用Looper.loop方法不断地在主线程中尝试取出Message
  3. Looper如果取到了Message,那么就在主线程中调用发送这个MessageHandlerhandleMessage方法。
  4. 我们在主线程或者子线程中通过Looper.getMainLooper为参数创建了一个Handler
  5. 在子线程中发送了Message,主线程中的Looper不断循环,终于收到了Message,在主线程中调用了这个HandlerhandleMessage方法。

这里需要注意的是,Looper.loop方法中取到的Looper对象并不一定就是主线程的,因为它是取出当前线程的Looper对象。只不过在ActivityThread这里是主线程,所以拿到的是主线程的Looper对象。所以如果我们要在子线程中创建一个Looper也是可以的,一会我们就实现一下。

到这里可能有的小伙伴还是懵逼,我还是不太明白怎么切换的线程。我们通过一个比喻很好的解释一下

首先有一个小学生小明,小明的任务是写作业。然后有一个老师,老师的任务是批改作业。这个班里还有一个学习委员,学习委员的任务就是负责收作业然后交给老师去批改。

一般情况下,老师是学校已经聘请好的,我们不需要自己去聘请老师。老师一般也就只在办公室批改作业,办公室我们可以理解为主线程。学校就是我们的app。老师就是Looper对象。而小明同学就是Handler,学习委员就是MessageQueue,作业就是Message。老师(Looper)在办公室(主线程)不断的从学习委员(MessageQueue)那里拿到下一本要批改的作业(Message),老师突然发现作业里有错误,老师很生气,于是就从作业本上的姓名知道了是谁写的这个作业(对应Messagetarget字段),于是老师把小明(Handler)叫到办公室(主线程),让小明在办公室(主线程)把作业改好(handleMessage)。在这个例子中,小明作为Handler,他可以在任何地方写作业(sendMessage),也就是说他可以在家里写作业,可以在教室写作业,也可以在小公园写作业,这里的各个地方就是不同的线程。但是写完作业以后一定要交给学习委员,学习委员手里有一摞作业,这一摞作业就是消息的队列,而学习委员就是MessageQueue,他负责收集作业,也就是收集Message。老师在办公室批改作业,发现出错了,就把小明叫到了办公室,让小明在办公室改错。在这里,办公室就是主线程,老师不会管小明是在哪里写的作业,老师只是关心作业出错了,需要小明在办公室里改错。小明在办公室里改错这就是handleMessage方法运行在了主线程。但是也有个问题,不能说你小明在办公室改错改个没完没了,那岂不是影响了后面同学作业的批改?如果小明真的改错改的没完没了,也就是在主线程上作耗时操作很久,那么老师也无法进行下一个同学的作业批改,时间一长,教学就没法进行了。这就是著名的ANR问题。不知道这样比喻,小伙伴们能不能理解线程切换的意思和ANR的意思。如果还不能理解,那么你来砍我吧~

Looper和ANR

很多面试官喜欢问,Looperloop方法是个死循环,而loop方法又是运行在主线程的,主线程上有死循环为什么不会导致ANR存在呢?

其实这里面很有趣的一个点就是,很多小伙伴把Looperloop方法当做一个普通方法来看待,所以才会有这样的疑问。但是这个loop方法并不是一个普普通通的方法。

我们先思考一点,如果我们写一个app,里面一行代码也不写的话,app会不会崩溃?

答案显而易见,是不会的。

可是在上面提到了,本质上App就是一个Java程序,Java程序就有main方法,在ActivityThread类中也确实有这个main方法。我们一般写java程序的时候,是不是main方法中的代码执行完,程序也就结束了。但是app并没有,只要你不退出,它一直运行。那这是为什么呢?

很多小伙伴应该想到了,没错,让程序不退出的话,写一个死循环,那么main方法中的代码永远不会执行完,这样程序就不会自己退出了。Android当然也是这么干的,而且不止Android,基本上所有的GUI程序都是这么干的。正是因为Looper.loop方法这个死循环,它阻塞了主线程,所以我们的app才不会退出。那你可能有疑问了,那既然这里有死循环了,那我其他的代码怎么运行?界面交互怎么办?你问到点子上了。

本质上Android就是事件驱动的程序,界面刷新也好,交互也好,本质上都是事件,这些事件最后通通被作为了Message发送到了MessageQueue中。由Looper来进行分发,然后在进行处理。用人话来说就是,我们的Android程序就是运行在这个死循环中的。一旦这个死循环结束,app也就结束了。

那么ANR是什么呢?ANRApplication Not Responding也就是Android程序无响应。为什么没响应呢?因为主线程做了耗时操作啊。可我还是不明白,明明Looperloop方法就是阻塞了主线程,为什么不ANR呢。那我们就来说道说道,什么是响应?响应就是界面的刷新,交互的处理等等对吧。那么这个响应是谁来响应的?没错,就是loop方法中进行响应的。没响应什么意思?就是loop方法中被阻塞了,导致无法处理其他的Message了。

所以结论就来了,主线程做耗时操作本质上不是阻塞了主线程,而是阻塞了Looperloop方法。导致loop方法无法处理其他事件,导致出现了ANR事件。对比小明这个比喻的话,就是因为小明在办公室里没完没了的改作业,占用了老师的时间,让老师没法批改下一个同学的作业,才导致了教学活动无法正常进行。而老师不断的批改作业,这本身就是正常的教学活动,也正是因为老师不断批改作业,同学们才有提高,教学才能继续。

Handler在Java层几个需要注意的点

  1. 子线程Looper

    如果要创建Handler,必须通过Looper.prepare()方法创建Looper,在主线程中ActivityThread已经帮我们创建好了,我们不需要自己去创建,但如果在子线程中创建Handler,要么使用Looper的mainLooper,要么自己调用Looper.prepare()方法创建属于这个线程的looper对象。如下是创建了一个子线程的Looper对象:

    class LooperThread extends Thread {public Handler mHandler;public void run() {Looper.prepare();  mHandler = new Handler() {  public void handleMessage(Message msg) {//TODO 定义消息处理逻辑. }};Looper.loop(); }
    }
    
  2. 消息池

    在生成消息的时候,最好是用 Message.obtain() 来获取一个消息,这是为什么呢?

    // Message.javapublic static Message obtain() {synchronized (sPoolSync) {if (sPool != null) {Message m = sPool;sPool = m.next;m.next = null;m.flags = 0; // clear in-use flagsPoolSize--;return m;}}return new Message();
    }
    

    可以看到,obtain方法是将一个Message对象的所有数据清空,然后添加到链表头中。sPool就是个消息池,默认的缓存是50个。而且在Looper的loop方法中最后一行是这样的

    msg.recycleUnchecked();
    

    Looper在分发结束以后,会将用完的消息回收掉,并添加到回收池里。

  3. Handler导致的内存泄露问题

    什么是内存泄露?简而言之就是该回收的东西没有回收。在Handler中一般是这样使用:

    class HandlerActivity: AppCompatActivity() {private val mHandler = MyHandler()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 在子线程中通过自定义的 Handler 发消息thread {mHandler.sendEmptyMessageDelayed(1, 1000)}}// 自定义一个 Handlerclass MyHandler: Handler() {override fun handleMessage(msg: Message) {Log.i("HandlerActivity", "主线程:handleMessage: ${msg.what}")}}
    }
    

    乍一看没有问题,但是有没有想过一个问题,就是说再发送延时消息之前,app推出了,那么handleMessage方法还会执行吗?答案是会的。为什么?我明明退出了,为什么还会执行呢?其实这和java有关系

    MyHandlerHandlerActivity 的内部类,会持有 HandlerActivity 的引用。

    在进入页面以后,发送了一个延时 1s 的消息,如果 HandlerActivity 在 1s 内退出了,由于 Handler 会被 Message 持有,保存在其 target 变量中,而 Message 又会被保存在消息队列中,这一系列关联,导致 HandlerActivity 在退出的时候,依然会被持有,因此不能被 GC 回收,这就是内存泄漏!当这个 1s 延时的消息被执行完以后,HandlerActivity 会被回收。

    虽然最终结果还是会被回收,但是内存泄露问题我们也必须去解决,如何解决?

    1.将 MyHandler 改为静态类,这样它将不再持有外部类的引用。可以将 HandlerActivity 作为弱引用放到 MyHandler 中使用,页面退出的时候可以被及时回收。

    2.页面退出的时候,在 onDestroy 中,调用 HandlerremoveMessages 方法,将所有的消息 remove 掉,这样也能消除持有链。

  4. 同步消息屏障

    什么是同步消息屏障?

    在Looper的loop方法中通过Message msg = queue.next();这么一行代码拿到Message进行分发,这个MessageQueue的next方法中有这么一行:

    //MessageQueue.java
    //省略部分代码
    Message next() {for (;;) {if (nextPollTimeoutMillis != 0) {Binder.flushPendingCommands();}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;//1 这一行很关键,同步消息屏障的关键点所在if (msg != null && msg.target == null) {do {prevMsg = msg;msg = msg.next;} while (msg != null && !msg.isAsynchronous());}}}}
    

    注释1下面的这一行代码,首先会判断msg不为null,然后紧接着判断msgtarget为null。我们知道messagetarget就是发送它的handler,所有的message都有一个handler,这里怎么可能没有handller呢?针对同步消息还真的是所有的message都有handler,而这里是异步消息。满足target == null的消息就是异步消息同步屏障是用来阻挡同步消息执行的。说得好,那么同步屏障有什么用呢?

    似乎在日常的应用开发中,很少会用到同步屏障。那么,同步屏障在系统源码中有哪些使用场景呢?Android 系统中的 UI 更新相关的消息即为异步消息,需要优先处理。简而言之,如果在启动绘制之前,用户(开发者)插入了一个非常耗时的消息到队列中,那就会导致 UI 不能按时绘制,导致卡顿掉帧。,同步消息屏障就可以用来保证 UI 绘制的优先性。

Handler在C++层

如果你的目标是理解Handler在Java层是如何实现的,下面就不需要看了。下面主要讲解Handler在C++层是如何工作并实现的。

首先,细心的小伙伴们可能会有疑问。Looper一直处于死循环中,就像老师一直不断的问学习委员要作业批改,老师也是人,不会累么?你问对了,老师当然不会一直不断的问学习委员要作业,正常情况下,是有人交了作业以后,学习委员送过来,老师才会去批改。没有作业的时候,老师可能在休息,可能在玩游戏**。Looper也是一样,在消息队列为空的时候,Looper实际上处于休眠状态,只要当有Handler发送消息的时候,Looper才会被唤醒,去进行分发消息**。那么是怎么实现的呢?

在整个消息机制中,MessageQueue是连接Java层和Native层的纽带,换言之,Java层可以向MessageQueue消息队列中添加消息,Native层也可以向MessageQueue消息队列中添加消息。

这是MessageQueue中的Native方法:

// MessageQueue.javaprivate native static long nativeInit();
private native static void nativeDestroy(long ptr);
private native void nativePollOnce(long ptr, int timeoutMillis); 
private native static void nativeWake(long ptr);
private native static boolean nativeIsPolling(long ptr);
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);

在MessageQueue的构造方法中是这样的:

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

调用了nativeInit方法,在native层创建了native层的MessageQueue,mPtr是保存了NativeMessageQueue的指针,后续的线程挂起和线程的唤醒都要通过这个指针来完成,其实就是通过Native层的MessageQueue来完成。

//android_os_MessageQueue.cpp
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {//初始化native消息队列NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();nativeMessageQueue->incStrong(env); //增加引用计数return reinterpret_cast<jlong>(nativeMessageQueue);
}

这个是NativeMessageQueue的构造方法:

//android_os_MessageQueue.cpp
NativeMessageQueue::NativeMessageQueue(): mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {mLooper = Looper::getForThread(); //功能类比于Java层的Looper.myLooper();if (mLooper == NULL) {mLooper = new Looper(false); //创建native层的LooperLooper::setForThread(mLooper); //保存native层的Looper到TLS,功能类比于Java层的ThreadLocal.set();}
}

Looper的构造方法是这样的:

//Looper.cpp
Looper::Looper(bool allowNonCallbacks) :mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {mWakeEventFd = eventfd(0, EFD_NONBLOCK); //构造唤醒事件的fdAutoMutex _l(mLock);rebuildEpollLocked();  //重建Epoll事件
}void Looper::rebuildEpollLocked() {if (mEpollFd >= 0) {close(mEpollFd); //关闭旧的epoll实例}mEpollFd = epoll_create(EPOLL_SIZE_HINT); //创建新的epoll实例,并注册wake管道struct epoll_event eventItem;//新建唤醒监听事件memset(& eventItem, 0, sizeof(epoll_event)); //把未使用的数据区域进行置0操作eventItem.events = EPOLLIN; // 设置监听内容可读事件eventItem.data.fd = mWakeEventFd;//将唤醒事件(mWakeEventFd)添加到epoll实例(mEpollFd)int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);for (size_t i = 0; i < mRequests.size(); i++) {const Request& request = mRequests.valueAt(i);struct epoll_event eventItem;request.initEventItem(&eventItem);//将request队列的事件,分别添加到epoll实例int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, request.fd, & eventItem);}
}

等等,你上来给我这一大段C++代码,我怎么可能看得懂。还有这个epoll是什么?不是讲如何Looper怎么休眠和唤醒的么?

没错,就是讲的Looper怎么休眠和唤醒的。Looper的休眠和唤醒都是在Native层实现的,实现的原理是Linux上的epoll机制。

什么是epoll机制呢?

epoll你可以简单的理解为一个监听事件,在Linux上通过epoll机制监听一个事件,没什么事的时候我就让出CPU,进行休眠,当这个事件触发的时候我就从沉睡中唤醒开始处理。就像按钮的点击事件一样,点击了,监听到这个点击事件就会触发按钮的onClick方法。不过在LInxu上是通过文件的读写来完成的。类比于

include <sys/epoll.h>// 创建句柄 相当于初始化onClickListener
int epoll_create(int size);
// 添加/删除/修改 监听事件  相当于addOnClicklistener
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 进入等待  这就相当于onCLick方法了
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_wait就类似于Java中的onCLick方法,当监听的文件有变化的时候,它就会收到结果。其实更像是Kotlin协程中的suspend方法,就一直在等着,阻塞着,有结果才会进行下一步。onClick方法是使用接口回调的形式来实现的,是非阻塞的。而epoll_wait方法是阻塞的。

在上面的Looper构造方法中,调用了rebuildEpollLocked方法,这个方法就是设置监听器的,可以理解为setOnClickListener,不过它监听的是文件的可读事件。 即eventItem.events = EPOLLIN;这行代码。什么是可读事件?就是说,文件里面有内容了是不是就可以读了,没错就是这样喵~

好了,事件也已经监听了,那么Looper是在哪沉睡的呢?

是在MessageQueue中的这行代码:

//MessageQueue.java
Message next() {final long ptr = mPtr;if (ptr == 0) {return null;}for (;;) {...nativePollOnce(ptr, nextPollTimeoutMillis); //阻塞操作...
}

就是通过这行代码进行阻塞操作。

调用关系是这样的:

MessageQueue::nativePollOnce ->NativeMessageQueue::pollOnce()->Looper::pollOnce()->Looper::pollInner

int Looper::pollInner(int timeoutMillis) {struct epoll_event eventItems[EPOLL_MAX_EVENTS];//1\. 等待事件发生或者超时,如果nativeWake()方法中向管道写端写入字符,则该方法会返回; int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);mPolling = false;mLock.lock();if (mEpollRebuildRequired) {mEpollRebuildRequired = false;rebuildEpollLocked();goto Done;}if (eventCount < 0) {if (errno == EINTR) {goto Done;}result = POLL_ERROR;goto Done;}if (eventCount == 0) {result = POLL_TIMEOUT;goto Done;}//循环遍历,处理所有的事件for (int i = 0; i < eventCount; i++) {int fd = eventItems[i].data.fd;uint32_t epollEvents = eventItems[i].events;//唤醒事件if (fd == mWakeEventFd.get()) {if (epollEvents & EPOLLIN) {已经唤醒了,则读取并清空管道数据【7】awoken();} else {ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);}} else {// 处理其他事件,Handler没有// 省略一些代码...}}
Done: ;//省略一些代码...// Release lock.mLock.unlock();//省略一些代码...  return result;
}

代码到了注释1处就开始了阻塞,也就是所谓的休眠。那么什么时候才能唤醒它呢?超时了,或者文件发生了变化,可以读了就可以唤醒了。注意,这个超时就是在Java层设置的延时发送,也就是说Java的sendMessageDelayed方法最后是通过epoll设置超时的机制实现延迟发送的。

不知道大家注意到没有,在我们发送Message的时候有这么一行代码:

// MessageQueue.javaboolean 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 {//消息插到队列中间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;}if (needWake) {// 敲黑板划重点:唤醒nativeWake(mPtr);}}return true;
}

在最后nativeWake(mPtr);这行代码进行了唤醒。不过必须neekWaketrue的时候才会唤醒,那么neekWake什么时候才是True呢?

两种情况会唤醒线程:

  1. 队列为空 || 消息无需延时 || 或消息执行时间比队列头部消息早) && (线程处于挂起状态时(mBlocked = true)
  2. 【线程挂起(mBlocked = true)&& 消息循环处于同步屏障状态】,这时如果插入的是一个异步消息,则需要唤醒。

唤醒操作具体是如何去做的?

调用链是这样的:

MessageQueue::nativeWake—>android_os_MessageQueue_nativeWake()—>NativeMessageQueue::wake()—>Looper::wake()

//Looper.cpp
void Looper::wake() {uint64_t inc = 1;// 向管道mWakeEventFd写入字符1ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));if (nWrite != sizeof(uint64_t)) {if (errno != EAGAIN) {ALOGW("Could not write wake signal, errno=%d", errno);}}
}

前面说了,epoll_wait在监听文件的可读事件,那么现在有消息来了,我要触发这个事件只需要往文件里随便写点什么就可以,Looper里面只是写了一个字符1。成功的唤醒了线程。然后开始轮询取出消息分发。

总结

Handler在C++层也有自己的一套消息轮询机制,和Java的基本一样,这里就不做分析了。

Handler是构成整个Android系统的基础,正是Looper的死循环才让Android程序能够不退出。所有的类似于屏幕刷新,UI互动都是一种事件,通过Handler发送给了Looper来进行分发。整个Android程序可以说就是运行在这个死循环中。

Looper就是不断批改作业的老师,MessageQueue就是催你交作业的学习委员,Message就是作业,上面写了写作业人的名字,Handler就是写作业的小明。

在一个线程中只能有一个Looper,也只能有一个MessageQueue,但是可以有多个HandlerMessageQueue也可以处理多个Handler发来的消息。

Looper的唤醒与挂起是靠Linux中的epoll机制来实现的,通过对文件的可读事件的监听来实现唤醒。

整个过程中,MessageQueue是实现Java层与C++层的互动的纽带,Native方法基本都是靠MessageQueue来实现的。

Handler与线程的绑定是依靠ThreadLocal中的map来实现的。另外,消息处理流程是先处理Native Message,再处理Native Request,最后处理Java Message。理解了该流程,也就明白有时上层消息很少,但响应时间却较长的真正原因。

参考并且致谢:

开发者的猫

gityuan

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

相关文章

  1. 《金字塔原理》读书笔记

    ...

    2024/4/27 13:26:52
  2. 7. Vue.js Axios*

    Axios*Axios是一个开源的用在浏览器端和NodeJS的异步通信框架,它的主要作用就是实现AJAX异步通信。<!DOCTYPE html> <html lang="en" xmlns:v-on="http://www.w3.org/1999/xhtml" xmlns:v-bind="http://www.w3.org/1999/xhtml"> &l…...

    2024/4/27 13:54:20
  3. Excel 如何保存文件为 UTF-8 CSV 格式

    如果你的 CSV 在 Excel 在默认打开的时候是简体中文的,但是你在保存的时候没有保存为 UTF-8 的格式,那么你文件中的简体中文可能会显示为乱码。如下图所示。因此在你完成对文件的编辑后,你需要将你的文件保存为 UTF-8 格式。保存格式步骤在文件中,选择另存为(Save As)随后…...

    2024/4/27 15:46:11
  4. mysql 的安装| mysql 8

    进入mysql 官网(https://dev.mysql.com/downloads)按照下图顺序点击下载页面 第一步第二步点击上图的第二步,进入下面界面第三步:安装mysql数据库 1.双击安装软件开始安装2.按下图勾选同意协议,然后下一步3.左边界面是安装到哪一步了,下图是选择安装类型,选择server onl…...

    2024/4/27 16:17:44
  5. 安装配置 Vue CLI

    一、配置Node.js环境:Node.js官网:Download版本解释:12.18.3 LTS 长期支持版 14.9.0 Current 当前版本 推荐下载长期支持版本:Download下载Node.js直接安装,一直Next即可 安装完成进入终端输入node -v检查是否安装成功,若出现对应版本号则安装成功,输入npm -v查看npm版…...

    2024/4/27 15:48:20
  6. 打地鼠小游戏,比较简陋见谅

    在这里插入代码片 <body bgcolor="#000000"><script src="./public.js"></script><script>window.onload=function showImg(){setInterval(createImg,1000);function createImg(){var oImg=document.createElement("img"…...

    2024/4/22 19:56:12
  7. MFC多线程的创建与关闭-基础篇

    多抽出一分钟学习,让你的生命更加精彩 线程和进程的关系:(本人自己理解) 线程是进程的可执行单元,是计算机分配CPU时的基本单元。一个进程可以包含一个或多个线程,进程是通过线程去执行代码的,同一个进程的多个线程共享该进程的资源和操作系统分配给该进程的内存空间。每…...

    2024/4/27 14:25:56
  8. BUUCTF [极客大挑战 2019] Http WriterUp

    BUUCTF地址:https://buuoj.cn/. 打开网页,浏览之后发现没有什么有用的信息打开burp对其进行抓包发现Secret.php打开Secret.php出现如下界面发现这里需要在http报头里加上一个Referer 于是伪造Referer: https://www.Sycsecret.com返回的报文为这里需要使用"Syclover"…...

    2024/4/11 15:32:53
  9. Unity的C#编程教程_39_循环语句挑战:计数程序

    设计一个累计程序每3秒钟计数+1达到一个随机生成的上限时,累计停止方法一:using System.Collections; using System.Collections.Generic; using UnityEngine;public class CountLoop : MonoBehaviour {public int countNum = 0;public int maxNum;// Start is called before…...

    2024/4/27 13:25:42
  10. Spring-Boot 整合Thymeleaf

    SpringBoot并不推荐使用jsp,但是支持一些其他的模板引擎技术,官方推荐使用Thymeleaf Thymeleaf Thymeleaf 是一个跟 Velocity、FreeMarker 类似的模板引擎,它可以完全替代 JSP 。相较于其他的模板引擎,它有如下四个极吸引人的特点:动静结合:Thymeleaf 在有网络和无网络的…...

    2024/4/23 6:37:54
  11. Confluence 7 文件编辑修改你的默认应用

    Confluence 允许你使用操作系统的默认应用来编辑文件。这个默认应用是通过文件类型来识别的(例如,.psd 文件将会使用 Photoshop 来打开)。如果你希望修改 Confluence 针对文件类型的默认打开应用,你需要通过修改你的操作系统的默认文件打开方式来实现。https://www.cwiki.u…...

    2024/4/12 21:12:35
  12. 8. Vue.js计算属性

    计算属性简单点来说,计算出来的结果,保存在属性中,内存中运行,可以想象为缓存。<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>Title</title><!-- 导入Vue.js --><script src…...

    2024/4/22 9:11:12
  13. 手把手教你小程序卡券开发

    手把手教你小程序卡券开发 摘要:实话说,我第一次看这个官方文档是挺懵的!但由于项目需要还是通过各种方式摸到了成功的果实。避免走更多的弯路,让我们一起了解下,希望对您有所帮助。 官方文档:查看微信卡包中的卡券 启发文档:小程序与微信会员卡打通教程 网页H5添加卡券…...

    2024/4/27 13:58:01
  14. js获取本周周一的日期

    function getMonsDay() {var nowTemp = new Date(); //当前时间var oneDayLong = 24 * 60 * 60 * 1000; //一天的毫秒数var c_time = nowTemp.getTime(); //当前时间的毫秒时间var c_day = nowTemp.getDay() || 7; //当前时间的星期几var m_time = c_time - (c_day - 1) * oneD…...

    2024/4/11 18:57:25
  15. PR 2019 快速入门(14)

    5.5剪辑的6种类型——画面位置剪辑 画面位置剪辑:这种剪辑又被称为定向剪辑或者定位剪辑 通常情况下,一个强烈的视觉元素占据画面的一侧。切入新的镜头后,被关注物通常显示在相反的一侧 5.6剪辑的6种类型——连续动作与声音 连续动作:连续动作剪辑属于直切的范畴。一组连续…...

    2024/4/11 18:57:24
  16. 2020-9-4数据结构与算法笔记(1)

    1.什么是数据结构把数据元素按照一定关系组织起来的集合,用来组织和储存数据2.数据结构分类逻辑结构数据与数据之间的关系来分类,是一种抽象意义上的分类第一种,集合结构:数据元素属于同一集合,之间没有其它任何关系第二种,线性结构:一对一第三种,树形结构:一对多第四…...

    2024/4/20 13:41:39
  17. VUE element-ui组件中input和select组件的绑定的值偶尔异常的情况

    element-ui组件中input和select组件是我们比较常用的两个组件 在使用的过程中偶尔会出现一个比较奇怪的现象,显示的值总是慢一拍,不是自己刚刚输入/选中的,上一次输入/选中的值 例如input在输入后,每次输入值后,显示的都是上一次的值: 键盘输入: a 此时input文本框中没有…...

    2024/4/27 14:26:41
  18. Nginx的动静分离

    为什么Nginx要帮我们把动态资源和静态资源进行分离下面的是默认让其默认index.html...

    2024/4/11 18:57:21
  19. 9. Vue.js slot

    slot在Vue中使用<slot>元素作为承载分发内容的出口,称之为插槽,可以应用在组合组件的场景中<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>Title</title><!-- 导入Vue.js -->…...

    2024/4/11 18:57:20
  20. OpenCV:详解阈值分割的处理

    阈值分割是图像处理中非常实用的操作,对我们提取目标区域,使图像信息更加简单(0和1)来加速后续的处理速度。Targets使用固定阈值、自适应阈值”二值化”图像OpenCV函数:cv2.threshold(), cv2.adaptiveThreshold() Simple Thresholding 固定阈值固定阈值分割很直接,一句话说…...

    2024/4/11 18:57:19

最新文章

  1. 更易使用,OceanBase开发者工具 ODC 4.2.4 版本升级

    亲爱的朋友们&#xff0c;大家好&#xff01;我们的ODC&#xff08;OceanBase Developer Center &#xff09;再次迎来了重要的升级V 4.2.4&#xff0c;这次我们诚意满满&#xff0c;从五个方面为大家精心打造了一个更加易用、贴心&#xff0c;且功能更强的新版本&#xff0c;相…...

    2024/4/27 16:31:00
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. 深入浅出 -- 系统架构之微服务中Nacos的部署

    前面我们提到过&#xff0c;在微服务架构中&#xff0c;Nacos注册中心属于核心组件&#xff0c;通常我们会采用高性能独立服务器进行部署&#xff0c;下面我们一起来看看Nacos部署过程&#xff1a; 1、环境准备 因为Nacos是支持windows和Linux系统的&#xff0c;且服务器操作…...

    2024/4/23 6:26:15
  4. Linux常用命令2

    1.shell 输出&#xff1a; echo 输出环境变量$PATH&#xff1a; echo $PATH 2.设置一个新的环境变量HELLO &#xff0c;值为 hello export HELLO"hello" 3.清除环境变量 HELLO unset HELLO 4. sed 命令&#xff08;按行筛选文本&#xff09; 显示web.xml 所…...

    2024/4/26 4:57:35
  5. 416. 分割等和子集问题(动态规划)

    题目 题解 class Solution:def canPartition(self, nums: List[int]) -> bool:# badcaseif not nums:return True# 不能被2整除if sum(nums) % 2 ! 0:return False# 状态定义&#xff1a;dp[i][j]表示当背包容量为j&#xff0c;用前i个物品是否正好可以将背包填满&#xff…...

    2024/4/27 1:53:53
  6. 【Java】ExcelWriter自适应宽度工具类(支持中文)

    工具类 import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet;/*** Excel工具类** author xiaoming* date 2023/11/17 10:40*/ public class ExcelUti…...

    2024/4/27 3:39:11
  7. Spring cloud负载均衡@LoadBalanced LoadBalancerClient

    LoadBalance vs Ribbon 由于Spring cloud2020之后移除了Ribbon&#xff0c;直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件&#xff0c;我们讨论Spring负载均衡以Spring Cloud2020之后版本为主&#xff0c;学习Spring Cloud LoadBalance&#xff0c;暂不讨论Ribbon…...

    2024/4/27 12:24:35
  8. TSINGSEE青犀AI智能分析+视频监控工业园区周界安全防范方案

    一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;…...

    2024/4/27 12:24:46
  9. VB.net WebBrowser网页元素抓取分析方法

    在用WebBrowser编程实现网页操作自动化时&#xff0c;常要分析网页Html&#xff0c;例如网页在加载数据时&#xff0c;常会显示“系统处理中&#xff0c;请稍候..”&#xff0c;我们需要在数据加载完成后才能继续下一步操作&#xff0c;如何抓取这个信息的网页html元素变化&…...

    2024/4/27 3:39:08
  10. 【Objective-C】Objective-C汇总

    方法定义 参考&#xff1a;https://www.yiibai.com/objective_c/objective_c_functions.html Objective-C编程语言中方法定义的一般形式如下 - (return_type) method_name:( argumentType1 )argumentName1 joiningArgument2:( argumentType2 )argumentName2 ... joiningArgu…...

    2024/4/27 3:39:07
  11. 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】

    &#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格…...

    2024/4/27 3:39:07
  12. 【ES6.0】- 扩展运算符(...)

    【ES6.0】- 扩展运算符... 文章目录 【ES6.0】- 扩展运算符...一、概述二、拷贝数组对象三、合并操作四、参数传递五、数组去重六、字符串转字符数组七、NodeList转数组八、解构变量九、打印日志十、总结 一、概述 **扩展运算符(...)**允许一个表达式在期望多个参数&#xff0…...

    2024/4/27 12:44:49
  13. 摩根看好的前智能硬件头部品牌双11交易数据极度异常!——是模式创新还是饮鸩止渴?

    文 | 螳螂观察 作者 | 李燃 双11狂欢已落下帷幕&#xff0c;各大品牌纷纷晒出优异的成绩单&#xff0c;摩根士丹利投资的智能硬件头部品牌凯迪仕也不例外。然而有爆料称&#xff0c;在自媒体平台发布霸榜各大榜单喜讯的凯迪仕智能锁&#xff0c;多个平台数据都表现出极度异常…...

    2024/4/26 17:59:13
  14. Go语言常用命令详解(二)

    文章目录 前言常用命令go bug示例参数说明 go doc示例参数说明 go env示例 go fix示例 go fmt示例 go generate示例 总结写在最后 前言 接着上一篇继续介绍Go语言的常用命令 常用命令 以下是一些常用的Go命令&#xff0c;这些命令可以帮助您在Go开发中进行编译、测试、运行和…...

    2024/4/26 22:35:59
  15. 用欧拉路径判断图同构推出reverse合法性:1116T4

    http://cplusoj.com/d/senior/p/SS231116D 假设我们要把 a a a 变成 b b b&#xff0c;我们在 a i a_i ai​ 和 a i 1 a_{i1} ai1​ 之间连边&#xff0c; b b b 同理&#xff0c;则 a a a 能变成 b b b 的充要条件是两图 A , B A,B A,B 同构。 必要性显然&#xff0…...

    2024/4/26 17:00:23
  16. 【NGINX--1】基础知识

    1、在 Debian/Ubuntu 上安装 NGINX 在 Debian 或 Ubuntu 机器上安装 NGINX 开源版。 更新已配置源的软件包信息&#xff0c;并安装一些有助于配置官方 NGINX 软件包仓库的软件包&#xff1a; apt-get update apt install -y curl gnupg2 ca-certificates lsb-release debian-…...

    2024/4/27 3:39:03
  17. Hive默认分割符、存储格式与数据压缩

    目录 1、Hive默认分割符2、Hive存储格式3、Hive数据压缩 1、Hive默认分割符 Hive创建表时指定的行受限&#xff08;ROW FORMAT&#xff09;配置标准HQL为&#xff1a; ... ROW FORMAT DELIMITED FIELDS TERMINATED BY \u0001 COLLECTION ITEMS TERMINATED BY , MAP KEYS TERMI…...

    2024/4/27 13:52:15
  18. 【论文阅读】MAG:一种用于航天器遥测数据中有效异常检测的新方法

    文章目录 摘要1 引言2 问题描述3 拟议框架4 所提出方法的细节A.数据预处理B.变量相关分析C.MAG模型D.异常分数 5 实验A.数据集和性能指标B.实验设置与平台C.结果和比较 6 结论 摘要 异常检测是保证航天器稳定性的关键。在航天器运行过程中&#xff0c;传感器和控制器产生大量周…...

    2024/4/27 13:38:13
  19. --max-old-space-size=8192报错

    vue项目运行时&#xff0c;如果经常运行慢&#xff0c;崩溃停止服务&#xff0c;报如下错误 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 因为在 Node 中&#xff0c;通过JavaScript使用内存时只能使用部分内存&#xff08;64位系统&…...

    2024/4/27 1:03:20
  20. 基于深度学习的恶意软件检测

    恶意软件是指恶意软件犯罪者用来感染个人计算机或整个组织的网络的软件。 它利用目标系统漏洞&#xff0c;例如可以被劫持的合法软件&#xff08;例如浏览器或 Web 应用程序插件&#xff09;中的错误。 恶意软件渗透可能会造成灾难性的后果&#xff0c;包括数据被盗、勒索或网…...

    2024/4/27 3:22:12
  21. JS原型对象prototype

    让我简单的为大家介绍一下原型对象prototype吧&#xff01; 使用原型实现方法共享 1.构造函数通过原型分配的函数是所有对象所 共享的。 2.JavaScript 规定&#xff0c;每一个构造函数都有一个 prototype 属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象…...

    2024/4/26 21:29:56
  22. C++中只能有一个实例的单例类

    C中只能有一个实例的单例类 前面讨论的 President 类很不错&#xff0c;但存在一个缺陷&#xff1a;无法禁止通过实例化多个对象来创建多名总统&#xff1a; President One, Two, Three; 由于复制构造函数是私有的&#xff0c;其中每个对象都是不可复制的&#xff0c;但您的目…...

    2024/4/27 3:39:00
  23. python django 小程序图书借阅源码

    开发工具&#xff1a; PyCharm&#xff0c;mysql5.7&#xff0c;微信开发者工具 技术说明&#xff1a; python django html 小程序 功能介绍&#xff1a; 用户端&#xff1a; 登录注册&#xff08;含授权登录&#xff09; 首页显示搜索图书&#xff0c;轮播图&#xff0…...

    2024/4/26 23:53:24
  24. 电子学会C/C++编程等级考试2022年03月(一级)真题解析

    C/C++等级考试(1~8级)全部真题・点这里 第1题:双精度浮点数的输入输出 输入一个双精度浮点数,保留8位小数,输出这个浮点数。 时间限制:1000 内存限制:65536输入 只有一行,一个双精度浮点数。输出 一行,保留8位小数的浮点数。样例输入 3.1415926535798932样例输出 3.1…...

    2024/4/26 9:43:45
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57