从源码分析Handler面试问题

news2024/11/18 3:48:20

Handler 老生常谈的问题了,非常建议看一下Handler 的源码。刚入行的时候,大佬们就说 阅读源码 是进步很快的方式。

Handler的基本原理

Handler 的 重要组成部分

  • Message 消息
  • MessageQueue 消息队列
  • Lopper 负责处理MessageQueue中的消息

消息是如何添加到队列的

对照着上面的大的逻辑图,我们深入一下,看一下,一个消息 是如何被发送到 MessageQueue 又是如何被 Lopper 处理的

handler 发送一个message 的方法如下图所示

而这些方法最终都会执行 Handler 中的 enqueueMessage 方法,我们看一下 enqueueMessage 方法做了什么

//Handler
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    //...
    //这里执行MessageQueue的 enqueueMessage
    return queue.enqueueMessage(msg, uptimeMillis);
}

消息队列如何将消息排序

MessageQueue 收到 消息以后,会根据时间进行排列

//MessageQueue
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;
        //step1 获取头部的message
        Message p = mMessages;
        boolean needWake;
        //step2 头部的message 和 当前的message 对比,如果头部的message 执行时间要 小于 当前message 的时候
        //那么就先执行当前的message
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            //头部的message 就变成了 当前的message
            mMessages = msg;
            needWake = mBlocked;
        } else {
            //step3 将当前消息 插入到 中间排队
            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;
        }

        // We can assume mPtr != 0 because mQuitting is false.
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

Handler的消息队列在哪创建的

回到创建Handler的地方,他的构造方法

//Handler
public Handler() {
    this(null, false);
}
//Handler
public Handler(Callback callback, boolean async) {
    //...
    //获取当前的looper
    mLooper = Looper.myLooper();
    //...
    //获取looper 的 MessageQueue
    mQueue = mLooper.mQueue;
    //...
}
//Looper
final MessageQueue mQueue;

private Looper(boolean quitAllowed) {
    //在这里创建了一个 MessageQueue
    mQueue = new MessageQueue(quitAllowed);
    //...
}

可以看到 Handler其实是拿着Looper 的MessageQueue当做自己的MessageQueue

Loope有什么作用

消息被有序的添加到了消息队列中,而Looper就是负责将消息从消息队列中取出。当执行Looper的loop()方法,Looper会从消息队列中取出消息,然后交给handler的dispatchMessage去处理消息

//Looper
public static void loop() {
    //...
    for (;;) {
        //从消息队列中获取消息
        Message msg = queue.next(); // might block
        //...
            try {
                //msg.traget 就是Handler 
                //使用 Handler 的  dispatchMessage() 处理消息
                msg.target.dispatchMessage(msg);
                //...
        } catch (Exception exception) {
            //...
        }
        //...
    }
}

一个线程有几个Looper

要想知道有几个Lopper,肯定要先知道Looper在哪里创建。Looper有一个prepare方法

//Looper
public static void prepare() {
    prepare(true);
}

在这里会创建一个新的Looper 并且设置到了ThreadLocal

//Looper
private static void prepare(boolean quitAllowed) {
    //通过 sThreadLocal get 检查是否已经有 looper  
    if (sThreadLocal.get() != null) {
        //如果已经有了 就抛出异常
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    //没有的话 就设置一个新的Looper
    sThreadLocal.set(new Looper(quitAllowed));
}

在ThreadLocal可以看到是以map的形式去保存,保证了一个线程只有一个map,又将looper和ThreadLocal进行绑定

//ThreadLocal
public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取 ThreadLocalMap 
    ThreadLocalMap map = getMap(t);
    //有的话 就将当前的 ThreadLocal 和 Looper 绑定在一起,
    if (map != null)
        //set 以后 在上面  sThreadLocal.get() 就不会在为null了
        map.set(this, value);
    else
        //没有的话 创建一个 ThreadLocalMap 在绑定在一起
        createMap(t, value);
}

看到Looper中的 sThreadLocal

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

他是一个静态的 final 保证了 一个Looper只有一个 sThreadLocal

最终保证了,一个线程只有一个Looper

主线程什么时候执行preapre

想要使用Looper,肯定需要先prepare 去创建一个Looper,那么主线程如何创建Looper的呢?我们知道 java 程序的入口是 main 方法, 对于Android来说,其实也有一个main 方法,他的位置在 ActivityThread

//ActivityThread
public static void main(String[] args) {
    //...
    //可以看到在这里 程序启动以后,Android 系统帮我们将主线程的Looper prepare
    Looper.prepareMainLooper();

    //...
    //然后帮助我们启动了 loop
    Looper.loop();
    //...
}

Handler内存泄露

Handler为什么会有可能导致内存泄露? 我们知道 内部类会持有外部类的引用,当我们做一个延时任务,延时10S,然后在10S内退出Activity,在我们sendMessage的时候,handler对象被传递给msg 如👇所示,然后被存放在MessageQueue中。在这10S内,即使Activity销毁了,但是引用关系依然被保存在MessageQueue中,那么即使Activity销毁了,他的对象依然不会被GC销毁,因为他依然被引用。就导致内存未被回收。

//Handler
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    //这里 将 handler 本身的对象 传给 msg 的target
    msg.target = this;
   //...
}

那么如何处理Handler内存泄露呢

1.将Handler改成静态类。原因是因为静态类不会持有外部类的引用 2.继承Handler,将Activity作为弱引用使用 3.在界面退出的时候,调用Handler的removeMessages方法

消息队列没有消息时Handler如何挂起

Looper从MessageQueue中获取message,当获取不到message的时候,会将 nextPollTimeoutMillis置成-1,然后进入下次循环,当执行nativePollOnce方法时候,如果nextPollTimeoutMillis==-1那么就会执行Linux的epoll机制,让线程处于挂起状态,阻塞线程。

//MessageQueue
Message next() {
    for (;;) {
        //step3: nextPollTimeoutMillis == -1 执行native 函数,
        //执行 linux epoll 机制,线程处于等待状态,线程挂起
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            //...
            if (msg != null) {

            } else {
                // step1:如果没有消息  nextPollTimeoutMillis 变成-1
                nextPollTimeoutMillis = -1;
            }

            if (pendingIdleHandlerCount <= 0) {
                // step2:跳出循环 进入下一次循环
                mBlocked = true;
                continue;
            }
        }
    }
}
//Looper
public static void loop() {
    for (;;) {
        //step4:这里也就挂起了
        Message msg = queue.next(); // might block
    }
}

Handler如何退出

使用looper去执行quit方法退出

handler.looper.quit()
//Looper
public void quit() {
    mQueue.quit(false);
}
public void quitSafely() {
    mQueue.quit(true);
}
//MessageQueue
void quit(boolean safe) {
    if (!mQuitAllowed) {
        throw new IllegalStateException("Main thread not allowed to quit.");
    }

    synchronized (this) {
        if (mQuitting) {
            return;
        }
        //step1:将mQuitting 变量变成true
        mQuitting = true;
        //step2:删除所有的消息
        if (safe) {
            removeAllFutureMessagesLocked();
        } else {
            removeAllMessagesLocked();
        }
        //step3:唤醒线程
        nativeWake(mPtr);
    }
}
//MessageQueue
Message next() {
    for (;;) {
        //step4:线程被唤醒。继续执行
        nativePollOnce(ptr, nextPollTimeoutMillis);

        //step5:检查到状态是 true 返回null 出去
        if (mQuitting) {
            dispose();
            return null;
        }
    }
}
//Looper
public static void loop() {
    for (;;) {
        //step6:这里也被唤醒获取到message == null
        Message msg = queue.next(); // might block
        //step7:最终在这里🔚循环
        if (msg == null) {
            return;
        }
    }
}

总结

Looper会先将消息队列中的消息全部清空,然后使用nativeWake的native方法唤醒线程,在上面我们介绍了,当消息队列中没有消息的时候,线程会挂起,处于等待状态,当我们唤醒以后,Looper的loop方法会继续执行下去,然后从MessageQueue中获取到一个null的Message,最终将Looper的loop()方法退出

主线程能够Quit么?

我们知道了 主线程是在ActivityThread的main方法中执行了Looper.prepareMainLooper()创建的Looper

//Looper
@Deprecated
public static void prepareMainLooper() {
    //step1: 注意看这里是一个false
    prepare(false);
}
//Looper
private static void prepare(boolean quitAllowed) {
    //step2:new的Looper传入的是false
    sThreadLocal.set(new Looper(quitAllowed));
}
//Looper
private Looper(boolean quitAllowed) {
    //step3:创建的MessageQueue 传入的也是false
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
//MessageQueue
MessageQueue(boolean quitAllowed) {
    //step4:将mQuitAllowed 变量变成了false
    mQuitAllowed = quitAllowed;
}
//MessageQueue
void quit(boolean safe) {
    //step5:如果是false 就是主线程 会直接抛出错误
    if (!mQuitAllowed) {
        throw new IllegalStateException("Main thread not allowed to quit.");
    }
}

回头在看一下 Looper的prepare方法,只有主线程可以创建一个不可以quit的MessageQueue,其他线程创建的都是可以quit的

//Looper
//公开方法 prepare 传入的是true
public static void prepare() {
    prepare(true);
}
//私有方法
private static void prepare(boolean quitAllowed) 

//主线程 传入的是false
public static void prepareMainLooper() {
    prepare(false);
}

为什么设计主线程不能被quit

在ActivityThread中,定义了一个H的类,继承了Handler,这个H的handler执行了Android所有的主要事件,比如广播,service,Activity生命周期等都是在这里进行处理,所以不能把主线程quit

//ActivityThread
class H extends Handler {

}

消息如何知道是由哪个Handler发送的?

一个线程可以有多个Handler,想new几个都可以,在我们往MessageQueue中添加消息的时候,会加入一个target标记是哪个handler发送的

//Handler
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    //step1:在这里 就标记了是哪一个handler 发送的 
    msg.target = this;
    //...
}
//Looper
public static void loop() {
    //...
    for (;;) {
        //...
            try {
                //step2:这里就对应起来是哪一个handler 发送的message 
                msg.target.dispatchMessage(msg);
                //...
        } catch (Exception exception) {
            //...
        }
        //...
}

Handler如何确保线程安全的

//MessageQueue
boolean enqueueMessage(Message msg, long when) {
    //step1:通过加锁的方式,保证了添加消息到消息队列的安全
    synchronized (this) {
    }
}
//MessageQueue
Message next() {
    for (;;) {
        //step2:通过枷锁的方式保证了读取消息的安全
        synchronized (this) {
        }
    }
}

Message如何复用的

看一下我们quit的时候,是怎么从消息队列中清空消息的

//MessageQueue
void quit(boolean safe) {
    synchronized (this) {
        //step1: 清除所有的消息
        if (safe) {
            removeAllFutureMessagesLocked();
        } else {
            removeAllMessagesLocked();
        }
    }
}
//MessageQueue
private void removeAllMessagesLocked() {
    Message p = mMessages;
    while (p != null) {
        Message n = p.next;
        //step2:执行message的方法
        p.recycleUnchecked();
        p = n;
    }
    mMessages = null;
}
//Message
void recycleUnchecked() {
    //step3:将所有的变量全部清空
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = UID_NONE;
    workSourceUid = UID_NONE;
    when = 0;
    target = null;
    callback = null;
    data = null;

    synchronized (sPoolSync) {
        //默认50个Message
        if (sPoolSize < MAX_POOL_SIZE) {
            //step4:将已经清空状态的Message 放到一个新的链表中
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

使用obtain方法会从之前清空状态的链表中取出一个Message去使用,减少创建Message带来的内存消耗。

//Message
public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            //step5:从已经清空状态的链表中取出一个Message使用
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

这种设计模式称为享元设计模式

为什么主线程loop不会导致ANR

首先要知道ANR是怎么出现的,ANR出现的条件有两个

  • 5秒内没有响应输入的事件,触摸反馈等
  • 广播10秒内没有执行完毕

在上面我们分析知道,所有的事件都是由Handler进行分发,在主线程上,发送一个事件,这个事件耗时,将主线程的loop()给卡主,让他只能执行当前任务,不能去处理其他事件就出现了ANR

ANR的本质是由于不能及时处理消息导致的,和他的loop是没有任何关系的

Handler同步屏障

同步屏障概念

啥叫同步屏障,字面意思,就是阻挡同步消息,那么Handler同步屏障是干啥的,没错,你没听错,就是阻挡同步消息,让异步消息过去。阻挡同步消息 这就是同步屏障

在发送消息的时候,mAsynchronous 控制着是否发送的消息是否为异步消息

//Handler
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    //如果是true 则将消息标记为异步消息
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

在Handler构造方法中,控制则是否是异步消息。但是这个方法是hide,正常我们是不能调用的

//Handler
@hide
public Handler(@Nullable Callback callback, boolean async) {
    //这里控制着变量
    mAsynchronous = async;
}

开启同步屏障

那么如何开启同步屏障呢,MessageQueue 中提供了一个 postSyncBarrier 方法 开启同步屏障,

//MessageQueue
public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}
//MessageQueue
private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        //👇 注意这里 开启以后没有设置target, 所以Messaged的target 是 null
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        //返回一个 token 用来取消同步屏障时候使用
        return token;
    }
}

同步屏障工作原理

开启以后,同步屏障如何将异步消息传递出去,将同步消息阻挡下来呢

//MessageQueue
Message next() {
    //...
    //step1:👇 看到这里 一旦收到target == null 表示同步屏障打开了
    if (msg != null && msg.target == null) {
        do {
            prevMsg = msg;
            msg = msg.next;
            //step2:👇 这里就做一个循环, 寻找异步消息
        } while (msg != null && !msg.isAsynchronous());
    }
    //step3:当找到异步消息以后
    if (msg != null) {
        //step4:判断是否到了要执行异步消息的时间
        if (now < msg.when) {
            //如果还没到,就等nextPollTimeoutMillis
            nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
        } else {
            //如果到了执行时间 从链表中移除他
            mBlocked = false;
            if (prevMsg != null) {
                prevMsg.next = msg.next;
            } else {
                mMessages = msg.next;
            }
            msg.next = null;
            if (DEBUG) Log.v(TAG, "Returning message: " + msg);
            msg.markInUse();
            return msg;
        }
    }
}

取消同步屏障

取消同步屏障以后,会唤醒线程,去处理之前未被处理的同步消息。

//MessageQueue
public void removeSyncBarrier(int token) {

    synchronized (this) {
        Message prev = null;
        Message p = mMessages;
        //step1:通过token 寻找设置的同步屏障
        while (p != null && (p.target != null || p.arg1 != token)) {
            prev = p;
            p = p.next;
        }
        if (p == null) {
            throw new IllegalStateException("The specified message queue synchronization "
                    + " barrier token has not been posted or has already been removed.");
        }
        final boolean needWake;
        //step2:从链表中移除
        if (prev != null) {
            prev.next = p.next;
            needWake = false;
        } else {
            mMessages = p.next;
            needWake = mMessages == null || mMessages.target != null;
        }
        //step3:将Message清空
        p.recycleUnchecked();

        if (needWake && !mQuitting) {
            //step4:唤醒线程
            nativeWake(mPtr);
        }
    }
}

GIF演示

下面以一个简单的示例更佳直观的表现,示例分成3中情况

  • 没有启动同步屏障,发送同步消息 发送异步消息
  • 开启同步屏障,发送同步消息 发送异步消息
  • 开启同步屏障,发送同步消息 发送异步消息 在取消同步屏障

没有启动同步屏障,发送同步消息 发送异步消息

可以看到,如果不开启同步屏障,对于Handler 来说 消息都是会被发送出去

开启同步屏障,发送同步消息 发送异步消息

通过对比能够发现,当开启同步屏障以后,发送的同步消息并没有打印,只有异步消息打印了,说明同步屏障确实只能够允许异步消息通过

开启同步屏障,发送同步消息 发送异步消息 在取消同步屏障

当我们移除同步屏障以后,之前没有收到的同步消息,会立马同步过来

演示代码


class HandlerAct : AppCompatActivity() {

    companion object {
        const val TAG = "handler-tag"
        const val MESSAGE_TYPE_SYNC = 0x01
        const val MESSAGE_TYPE_ASYN = 0x02
    }

    private var index = 0

    private lateinit var handler :Handler

    private var token: Int? = null

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_handler)
        initHandler()

        linear.addView(MaterialButton(this).apply {
            text = "插入同步屏障"
            setOnClickListener {
                sendSyncBarrier()
            }
        })

        linear.addView(MaterialButton(this).apply {
            text = "移除屏障"
            setOnClickListener {
                removeSyncBarrier()
            }
        })

        linear.addView(MaterialButton(this).apply {
            text = "发送同步消息"
            setOnClickListener {
                sendSyncMessage()
            }
        })

        linear.addView(MaterialButton(this).apply {
            text = "发送异步消息"
            setOnClickListener {
                sendAsynMessage()
            }
        })

    }

    private fun initHandler() {
        Thread {
            Looper.prepare()
            handler = Handler(){
                when(it.what){
                    MESSAGE_TYPE_SYNC -> {
                        Log.i(TAG, "收到同步消息<========== index:${it.arg1}")
                    }
                    MESSAGE_TYPE_ASYN -> {
                        Log.i(TAG, "收到异步消息<========== index:${it.arg1}")
                    }
                }
                true
            }
            Looper.loop()
        }.start()
    }

    private fun sendSyncMessage() {
        index++
        Log.i(TAG, "插入同步消息==========> index:$index")
        val message = Message.obtain()
        message.what = MESSAGE_TYPE_SYNC
        message.arg1 = index
        handler.sendMessageDelayed(message, 1000)
    }

    //往消息队列插入异步消息
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
    private fun sendAsynMessage() {
        index++
        Log.i(TAG, "插入异步消息==========> index:$index")
        val message = Message.obtain()
        message.what = MESSAGE_TYPE_ASYN
        message.arg1 = index
        message.isAsynchronous = true
        handler.sendMessageDelayed(message, 1000)
    }

    @RequiresApi(Build.VERSION_CODES.M)
    @SuppressLint("DiscouragedPrivateApi")
    fun sendSyncBarrier() {
        try {
            Log.d(TAG, "插入同步屏障")
            val queue: MessageQueue = handler.looper.queue
            val method: Method = MessageQueue::class.java.getDeclaredMethod("postSyncBarrier")
            method.isAccessible = true
            token = method.invoke(queue) as Int
            Log.d(TAG, "token:$token")
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    //移除屏障
    @SuppressLint("DiscouragedPrivateApi")
    @RequiresApi(api = Build.VERSION_CODES.M)
    fun removeSyncBarrier() {
        Log.i(TAG, "移除屏障")
        try {
            val queue: MessageQueue = handler.looper.queue
            val method: Method = MessageQueue::class.java.getDeclaredMethod(
                "removeSyncBarrier",
                Int::class.javaPrimitiveType
            )
            method.isAccessible = true
            method.invoke(queue, token)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

}

总结

在面试工作中还要许多的小细节需要我们去注意,上面这些面试题目是我在之前网上收集整理的一小部分,由于文档的篇幅长度限制。就在下面用图片展现给大家看了,如果有需要这些面试题参考(内含参考答案):https://qr18.cn/CgxrRy

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

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

相关文章

githack的安装步骤+一次错误体验

一.githack的安装步骤 1.要在Kali Linux上安装GitHack工具&#xff0c;您可以按照以下步骤操作&#xff1a; 打开终端并使用以下命令克隆GitHack存储库&#xff1a; git clone https://github.com/lijiejie/GitHack.git2.进入GitHack目录&#xff1a; cd GitHack3.安装依赖项…

『HarmonyOS』万物互联,分布式操作系统

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位喜欢写作&#xff0c;计科专业大二菜鸟 &#x1f3e1;个人主页&#xff1a;starry陆离 &#x1f552;首发日期&#xff1a;2022年7月5日星期二 &#x1f30c;上期文章&#xff1a;『首期文章』 &#x1f4da;订阅专栏&…

leetcode刷题(柠檬水找零、接雨水、宝石与石头、将数组和减半的最少操作次数、更新数组后处理求和查询、删除每行中的最大值、并行课程③)

目录 1、柠檬水找零 2、接雨水 3、宝石与石头 4、将数组和减半的最少操作次数 5、更新数组后处理求和查询 6、删除每行中的最大值 7、并行课程③ 1、柠檬水找零 class Solution:def lemonadeChange(self, bills: List[int]) -> bool:dollars [0, 0] # 美元数组&am…

每日一题——有序链表去重

题目 删除给出链表中的重复元素&#xff08;链表中元素从小到大有序&#xff09;&#xff0c;使链表中的所有元素都只出现一次。 例如&#xff1a;给出的链表为1→1→2,返回1→2。 给出的链表为1→1→2→3→3,返回1→2→3。 数据范围&#xff1a;链表长度满足 0≤n≤100&#…

深度学习训练营之DCGAN网络学习

深度学习训练营之DCGAN网络学习 原文链接环境介绍DCGAN简单介绍生成器&#xff08;Generator&#xff09;判别器&#xff08;Discriminator&#xff09;对抗训练 前置工作导入第三方库导入数据数据查看 定义模型初始化权重定义生成器generator定义判别器 模型训练定义参数模型训…

7.28

1.思维导图 2.qt的sever #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTcpServer> //服务器类 #include<QTcpSocket> //客户端类 #include<QMessageBox> //对话框类 #include<QList> …

计组 [指令系统] 预习题目

PPT第5章 第2部分预习题目 预习内容及相关问题 什么是R,I,J型指令&#xff0c;它们的特点&#xff1f; &#xff08;二&#xff09;R型指令的数据通路 &#xff08;指令功能与其对应的逻辑结构&#xff09; 功能&#xff1a;R[rd] ← R[rs] op R[rt]&#xff0c;如&#xff1a…

React的UmiJS搭建的项目集成海康威视h5player播放插件H5视频播放器开发包 V2.1.2

最近前端的一个项目&#xff0c;大屏需要摄像头播放&#xff0c;摄像头厂家是海康威视的&#xff0c;网上找了一圈都没有React集成的&#xff0c;特别是没有使用UmiJS搭脚手架搭建的&#xff0c;所以记录一下。 海康威视的开放平台的API地址&#xff0c;相关插件和文档都可以下…

行列转换.

表abc&#xff1a; &#xff08;建表语句在文章末尾&#xff09; 想要得到&#xff1a; 方法一 with a as(select 年,产 from abc where 季1), b as(select 年,产 from abc where 季2), c as(select 年,产 from abc where 季3), d as(select 年,产 from abc where 季4) selec…

图像识别概述

图像识别的过程 图像识别技术的过程分以下几步&#xff1a; 1. 信息的获取&#xff1a; 是指通过传感器&#xff0c;将光或声音等信息转化为电信息。也就是获取研究对象的基本信息并通过某种方法将其转变为机器能够认识的信息。 2. 预处理&#xff1a; 主要是指图像处理中的…

行业追踪,2023-07-28

自动复盘 2023-07-28 凡所有相&#xff0c;皆是虚妄。若见诸相非相&#xff0c;即见如来。 k 线图是最好的老师&#xff0c;每天持续发布板块的rps排名&#xff0c;追踪板块&#xff0c;板块来开仓&#xff0c;板块去清仓&#xff0c;丢弃自以为是的想法&#xff0c;板块去留让…

Android中绘制的两个天气相关的View

文章目录 一、前言二、降雨的代码三、风向代码 一、前言 开发天气相关软件时候&#xff0c;做了两个自定义View&#xff0c;这里进行记录&#xff0c;由于涉及类较多&#xff0c;这里仅包含核心代码&#xff0c;需要调整后才可以运行&#xff0c;自定义View范围仅包含网格相关…

机器学习伦理:探讨隐私保护、公平性和透明度

文章目录 &#x1f340;引言&#x1f340;隐私保护&#x1f340;公平性&#x1f340;透明度&#x1f340;结论 随着机器学习技术的不断发展和应用&#xff0c;我们必须面对伦理问题&#xff0c;以确保这些智能系统的发展和使用是符合道德和法律规范的。本文将就机器学习伦理的关…

Revit二次开发 插件加密、打包、发布、授权全套教程

目录 代码加密及授权 添加授权工具引用 添加授权验证代码段 使用VMProtect进行代码保护 代码加密标记 代码加密 发布产品 软件打包 软件发布 相关文件的获取地址 本教程基于mxbim.com所提供的服务。 Revit二次开发 插件加密、打包、发布、授权全套教程 本网站(www.…

实锤研究,ChatGPT能力掉线!

早在一个多月前&#xff0c;ChatGPT性能下降的传闻便开始在网上流行&#xff0c;不少订阅了Plus版的用户纷纷表示&#xff0c;感觉ChatGPT在经历了几轮更新后开始降智&#xff0c;甚至有时反应速度也会出现问题。而如今&#xff0c;这一传闻终于得到了证实。 就在本周&#xf…

如何学好Java并调整学习过程中的心态:学习之路的秘诀

文章目录 第一步&#xff1a;建立坚实的基础实例分析&#xff1a;选择合适的学习路径 第二步&#xff1a;选择合适的学习资源实例分析&#xff1a;参与编程社区 第三步&#xff1a;动手实践实例分析&#xff1a;开发个人项目 调整学习过程中的心态1. 不怕失败2. 持续学习3. 寻求…

ORA-38760: This database instance failed to turn on flashback database

早晨接一个任务&#xff0c;使用rman备份在虚拟化单机上恢复实例&#xff0c;恢复参数文件、控制文件和数据文件都正常&#xff0c;recover归档时报错如下&#xff1a; Starting recover at 2023-07-28 10:25:01 using channel ORA_DISK_1 starting media recovery media reco…

实时云渲染技术:VR虚拟现实应用的关键节点

近年来&#xff0c;虚拟现实&#xff08;Virtual Reality, VR&#xff09;技术在市场上的应用越来越广泛&#xff0c;虚拟现实已成为一个热门的科技话题。相关数据显示&#xff0c;2019年至2021年&#xff0c;我国虚拟现实市场规模不断扩大&#xff0c;从2019年的282.8亿元增长…

攻防世界-Reverse-simple-unpack

题目描述&#xff1a;菜鸡拿到了一个被加壳的二进制文件 1. 思路分析 提示很清楚了&#xff0c;加壳的二进制文件&#xff0c;正好对这一块知识点是残缺的&#xff0c;先了解下加壳到底是什么 通过这段描述&#xff0c;其实加壳的目的是使得逆向起来更难了&#xff0c;因此这里…

基于SSM实现个人随笔分享平台:创作心灵,分享自我

项目简介 本文将对项目的功能及部分细节的实现进行介绍。个人随笔分享平台基于 SpringBoot SpringMVC MyBatis 实现。实现了用户的注册与登录、随笔主页、文章查询、个人随笔展示、个人随笔查询、写随笔、草稿箱、随笔修改、随笔删除、访问量及阅读量统计等功能。该项目登录模…