Handler机制(一)

news2024/9/21 16:28:57

Handler基础

Handler机制是什么?

Handler是用来处理线程间通信的一套机制。

初级使用

第一步:在主线程中定义Handler

    private Handler mHandler = new Handler(Looper.myLooper()){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            if(msg.what == 0){
                ((Button) findViewById(R.id.bt_test)).setText((String)msg.obj); //定义的一个button
            }
        }
    };

第二步:在子线程中生成Message并发送

        findViewById(R.id.bt_test).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(2000);//模拟子线程耗时任务
                            Message msg = new Message();
                            msg.what = 0;
                            msg.obj = "end";
                            mHandler.sendMessage(msg);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }).start();
            }
        });

上面做了以下几个步骤:1.在主线程中创建Handler对象并实现handleMessage方法用来处理收到的Message消息。2.在子线程中创建Message消息并调用主线程中的Handler通过sendMessage发送给主线程。

Handler机制中的主要类

Message

Message用来封装线程之间的消息。Message会存储在MessageQuenue中,Message也是链表中的结点。
主要有以下数据

    public int what; //定义任务类型
    public int arg1; //发送Int类型消息
    public int arg2;
    public Object obj;//发送任意类型的消息
    /*package*/ Handler target; //用来处理消息的Handler对象
    /*package*/ Runnable callback;//用来处理消息的回调
     Message next;//指向下一个Message
     public long when;//注意,这是不是发送消息的时间,而是消息应该被处理的时间
     private static Message sPool;//用来存储使用过的Message对象
     private static int sPoolSize = 0;
     private static final int MAX_POOL_SIZE = 50;
Message的消息池设计

官方建议使用消息池来获取Message对象。消息池使用链表实现

private static final Object sPoolSync = new Object();//消息池的锁
private static Message sPool; //消息池
private static int sPoolSize = 0;//消息池中回收的消息数量
private static final int MAX_POOL_SIZE = 50;//消息池最大容量

sPool被static修饰, static 变量有以下特点:

  • 在类装载的时候进行初始化。
  • 多个实例的 static变量会共享同一块内存区域。
    所有Message对象共享这个消息池
    在使用消息池获得消息时,都会调用无参的obtain()方法。具体代码如下:
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next; //将消息池更新到下一个
                m.next = null;
                m.flags = 0; // 重新标识当前Message没有使用过
                sPoolSize--;
                return m;
            }
        }
        return new Message();//消息池为空,就直接创建一个新Message返回
    }

在Message消息回收中,消息的回收实际方法是:

    void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE; //表示当前Message消息已经被使用过了
        //情空 Message中的数据
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = UID_NONE;
        workSourceUid = UID_NONE;
        when = 0;
        target = null;
        callback = null;
        data = null;
        //将当前清空数据的的Message加入到消息池中
        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;//头插法,当前节点指向之前消息池的header
                sPool = this;//将消息池中的header替换为当前Message对象
                sPoolSize++;
            }
        }
    }

Handler核心方法

boolean sendMessage(@NonNull Message msg) :发送一个即时消息到消息队列,内部调用的是sendMessageDelayed(msg,0)
boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) :发送一个延时消息,当前时间+delayMillis时间后,handleMessage接收到消息,内部调用sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) {
        msg.target = this; //给Message设置发送消息的Hanlder
        msg.workSourceUid = ThreadLocalWorkSource.getUid();
        // 如果此Handler是异步的,则发送的所有消息都是异步的。
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        //核心步骤,将Message加入到消息队列
        return queue.enqueueMessage(msg, uptimeMillis);
    }

在这里插入图片描述

boolean post(@NonNull Runnable r) :将Runnable中的任务在Handler线程中执行,内部调用 sendMessageDelayed方法
在这里插入图片描述

MessageQueue

消息队列,用来存储Message消息,内部通过单链表实现。
为什么需要一个MessageQueue?
Handler在SendMessage时,会有延时消息,需要等待到指定时间发送,这个就需要MessageQueue来处理,存入和取出逻辑。Message需要延迟处理,那么MessageQueen应该通过时间戳的大小来顺序存储,时间戳小的Message放到队列的头部。

MessageQueue何时被初始化?

MessageQueue并不需要我们手动初始化,Handler中mQueue是来自于Looper,由Looper在构造函数中初始化。

    /**
     * 插入队列
     * @param msg message
     * @param when Handler handleMessage 时间
     * @return false 插入失败 成功
     */
    boolean enqueueMessage(android.os.Message msg, long when) {

        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }

        //加锁,线程同步,因为Handler可能在不同线程中调用这个方法
        synchronized (this) {
            //message 是 in-use状态时,是不允许入列
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }
            ///如果当前MessageQueen已经退出抛出异常并释放Message
            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(); //回收Message并放入Message回收池
                return false;
            }
            //将Message设置为in-use状态
            msg.markInUse();
            //设置Message应该被处理的时间
            msg.when = when;
            //获取MessageQueen的Header
            Message p = mMessages;
            //是否需要唤醒nativeWake方法
            boolean needWake;
            //p == null  代表链表为空
            // when == 0 表示要立即处理该消息
            //when < p.when 表示Message的时间戳比当前队列的Header的时间戳小,那么应该插入到链表最前面
            //满足以上任意一个条件,应该将该msg插入到队列头部
            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 {
               //需要唤起nativeWake,需要满足一下3个条件:线程已经被阻塞&&Handler为空&&异步消息
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                android.os.Message prev;
                //通过for循环将,当前msg插入到合适的位置中
                for (;;) {
                    prev = p;//从队列的Header开始查找
                    p = p.next;
                    //p == null 表示到队列末尾
                    //when < p.when 时,msg应该插入到p端的前面
                    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;
    }

enqueueMessage方法做了哪些工作
1.判断需要入列的消息是否满足要求:1)msg.target != null 2)msg.isInUse()为false 3)MessageQueen的mQuitting 为 false
2.按照Message中的when加入队列中
3.判断是否需要调用nativeWake

既然有存消息入队列,那么也一定有方法取出消息,取出消息通过next方法

Message next() {
        //如果消息循环已经退出并被处理,请返回此处
        //mPtr是从native方法中得到的NativeMessageQueue地址
        //如果mptr等于0说明队列不存在或者被清除掉了
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
        //待处理的IdleHandler数量,因为代表数量,所以只有第一次初始化时为-1
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        //线程将被阻塞的时间
        //-1 一直被阻塞
        //0 : 不阻塞
        //>:0 :阻塞nextPollTimeoutMillis毫秒
        int nextPollTimeoutMillis = 0;
        //进入死循环
        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;
                //判断队列头是不是同步拦截器
                if (msg != null && msg.target == null) {
                    //如果是拦截器就向后找一个异步消息
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                //判断队列是否有可以取出的消息
                if (msg != null) {
                    if (now < msg.when) { //当前时间小于msg执行的时间
                        // Message未到执行时间,计算线程需要堵塞的时间
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // 已经到执行时间,可以直接取出Message
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        //将msg行消息队列中剥离出来
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        //设置in-use状态
                        msg.markInUse();
                        //返回取出的消息,结束循环,结束next()方法
                        return msg;
                    }
                } else {
                    // 消息队列为空,,nextPollTimeoutMillis为-1,让线程一直阻塞
                    nextPollTimeoutMillis = -1;
                }

                // 如果队列已经退出了直接注销和结束方法
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new android.os.MessageQueue.IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final android.os.MessageQueue.IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

从代码上面可以看出,当MessageQueen中没有合适的Message出列,会堵塞线程直到有合适的Message出列。

Looper

Looper翻译过来就是循环器,我们上文已经分析了,消息载体(Message),消息队列(MessageQueen),那么如何从MessageQueen中取出消息并分发给MessageQueen?
这个部分工作由Looper完成。
初始化

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

Looper的构造函数是private的,外部无法通过构造函数直接创建Looper对象。Looper的对象是在prepare(boolean quitAllowed)方法中创建的

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {//判断是否已经存在Looper对象
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        //将Looper对象存储到ThreadLocal中
        sThreadLocal.set(new Looper(quitAllowed));
    }

从上面代码可以看出Looper有以下特点:
1.一个线程只能有一个Looper
2.Looper对象是放到ThreadLocal中。
在这里插入图片描述
我们平常在主线程中使用Handler时并没有初始化Looper,在主线程中,何时进行初始化操作?
主线程中Looper的初始化操作是在ActivityThread中进行的。

 public static void main(String[] args) {
 /**
 *省略代码
 **/
 Looper.prepareMainLooper();
  /**
 *省略代码
 **/
  Looper.loop();
 }

可以看到主线程是调用Looper.prepareMainLooper()创建了Looper,而且不需要用户手动创建,在Activity启动时就已经创建了,子线程是需要通过prepare()手动创建Looper,并会创建一个MessageQueen能够退出的消息队列。

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

Looper创建后,怎么启动Looper读取MessageQueen?ActivityThread中调用了Looper.loop(),loop()方法就是循环读取MessageQueue中的Message

    public static void loop() {
        //获取当前线程的Looper对象
        final Looper me = myLooper();
        //如果线程中的Looper还未初始化,抛出异常
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        //一个线程中只能调用一次loop()
        if (me.mInLoop) {
            Slog.w(TAG, "Loop again would have the queued messages be executed"
                    + " before this one completed.");
        }

        me.mInLoop = true;

        //得到当前线程的唯一标识(uid+pid),作用是下面每次循环时都检查一下线程有没有被切换
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        // Allow overriding a threshold with a system prop. e.g.
        // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow", 0);

        me.mSlowDeliveryDetected = false;
        //开启死循环
        for (;;) {
            if (!loopOnce(me, ident, thresholdOverride)) { //返回false,就结束循环
                return;
            }
        }
    }

loop() 方法主要是就是开启死循环,调用loopOnce方法

    private static boolean loopOnce(final Looper me,
                                    final long ident, final int thresholdOverride) {
        //从MessageQueue中获取需要处理的Message,获取过程中可能会阻塞线程
        android.os.Message msg = me.mQueue.next(); // might block
        //消息为空,说明队列已经退出了,直接结束循环.结束方法
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return false;
        }

        /**
         * 中间省略部分代码,只要是记录分发时间和进行性能追踪,防止分发时间过慢
         */
        //向Handler分发数据
        try {
            //调用Handler的dispatchMessage方法
            msg.target.dispatchMessage(msg);
            if (observer != null) {
                observer.messageDispatched(token, msg);
            }
            //通知Observer消息分发结束
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } catch (Exception exception) {
            if (observer != null) {
                // 通知Observer消息分发异常
                observer.dispatchingThrewException(token, msg, exception);
            }
            throw exception;
        } finally {
            // 恢复当前线程的Uid
            ThreadLocalWorkSource.restore(origWorkSource);
            if (traceTag != 0) {
                // 结束追踪
                Trace.traceEnd(traceTag);
            }
        }
        
        //如果本次循环所在的线程和最开始的不一样,打印日志
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                    + Long.toHexString(ident) + " to 0x"
                    + Long.toHexString(newIdent) + " while dispatching to "
                    + msg.target.getClass().getName() + " "
                    + msg.callback + " what=" + msg.what);
        }
        //消息分发完毕,回收消息到缓存池
        msg.recycleUnchecked();

        return true;
    }

Looper的功能很简单,核心方法Looper.loop()就是不断地从消息队列中分发对应的宿主Handler,它与对应的MessageQueue息息相关,一起创建,一起退出。
Looper中使用ThreadLocal保证每一个线程只有一个Looper的实例;

整体流程

在这里插入图片描述
Handler发送Message到MessageQueue,Looper通过loop从MessageQueue中获取到时间的Message发送给Handler处理。

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

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

相关文章

CSS中的transform属性有哪些值?并分别描述它们的作用。

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ translate()⭐ rotate()⭐ scale()⭐ skew()⭐ matrix()⭐ scaleX() 和 scaleY()⭐ rotateX()、rotateY() 和 rotateZ()⭐ translateX() 和 translateY()⭐ skewX() 和 skewY()⭐ perspective()⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&…

基于react-native-date-picker的日期选择组件

基于react-native-date-picker的日期选择组件 效果示例图安装依赖封装组件PickerDateComponent使用组件device.js 效果示例图 安装依赖 https://www.npmjs.com/package/react-native-date-picker?activeTabreadme npm install react-native-date-picker --save封装组件Picker…

Jmeter 二次开发 函数助手 AES加解密

Jmeter 二次开发 函数助手 AES加解密 1. 环境准备2. 关键技术说明2.1 离线导包2.2 示例代码 3. 代码包4. 结果演示 1. 环境准备 IDE &#xff1a;IntelliJ IDEA 2021.1.1 x64JAVA环境 &#xff1a;jdk1.8.0_251离线导包&#xff1a;导入Jmeter安装目录下lib/ext下的ApacheJmet…

Datawhale Django后端开发入门 TASK02 Admin管理员、外键的使用

1.Admin管理员的使用 先放一张成功的截图&#xff0c;记得自己创建时的账号和密码呀&#xff0c;如果忘了的话可以也是再重新创建管理员账号和密码的 &#xff0c;这个页面接下来就不用操作了,就要开始重要的 post 步骤。 二、外键的使用 我认为比较难的&#xff08;很不好操作…

可视化绘图技巧100篇进阶篇(九)-三维百分比堆积条形图(3D Stacked Percentage Bar Chart)

目录 前言 适用场景 绘图工具及代码实现 帆软 实现思路 方案一&#xff1a;使用计算指标 上传数据 添加组件 生成图表 添加计算字段 生成分区柱形图 生成百分比堆积条形图 美化图表 设置标签 设置颜色 效果查看 PC 端 移动端 方案二&#xff1a;使用自助数…

运动健身耳机什么的好、健身房运动耳机推荐

对于坚持长期运动的健身人士来说&#xff0c;除了健身器材之外&#xff0c;运动耳机是必备的装备之一。因为尽管运动对身体健康有益&#xff0c;但过程往往感到枯燥和无聊。然而&#xff0c;只要有音乐作伴&#xff0c;情况就会好上许多。那么&#xff0c;什么样的耳机更适合运…

Numpy浅学

Numpy01 不要使用rank为1的&#xff0c;在python中(5,)既不是行向量也不是列向量&#xff0c;they are rank 1 arrays 行向量和列向量一定有两个括号 向量外积&#xff1a; 使用assert和.shape帮助Debug 可以用reshape消除rank1array

[Go版]算法通关村第十一关青铜——理解位运算的规则

目录 数字在计算机中的表示&#xff1a;机器数、真值对机器数进一步细化&#xff1a;原码、反码、补码为何会有原码、反码和补码为何计算机中的按位运算使用的是补码&#xff1f;位运算规则与、或、异或和取反移位运算移位运算与乘除法的关系位运算常用技巧⭐️ 操作某个位的数…

opencv进阶06-基于K邻近算法识别手写数字示例

opencv 中 K 近邻模块的基本使用说明及示例 在 OpenCV 中&#xff0c;不需要自己编写复杂的函数实现 K 近邻算法&#xff0c;直接调用其自带的模块函数即可。本节通过一个简单的例子介绍如何使用 OpenCV 自带的 K 近邻模块。 本例中有两组位于不同位置的用于训练的数据集&…

php base64转图片保存本地

调用函数 public function base64(){$img $this->request->param(img);$img data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAkACQAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy…

详细介绍如何使用 Keras 构建生成对抗网络的源码实现

本文将演示如何使用 Keras 库构建生成对抗网络。使用的数据集是预加载到 Keras 中的CIFAR10 图像数据集。 第1步:导入所需的库 import numpy as npimport matplotlib.pyplot as plt import keras from keras.layers import Input, Dense, Reshape, Flatten, Dropout from kera…

深度学习从入门到实际项目资料汇总

图片来源于AiLake&#xff0c;如若侵权&#xff0c;请联系博主删除 文章目录 1. 介绍2. 深度学习相关学习资料2.1 [《动手学深度学习》](http://zh.d2l.ai/index.html)2.2 [导航文库](https://docs.apachecn.org/#1be32667e7914f03afb3c39239bd2525)2.3 [AI学习地图&#xff0c…

LeetCode算法心得——限制条件下元素之间的最小绝对差(TreeSet)

大家好&#xff0c;我是晴天学长&#xff0c;今天用到了Java一个非常实用的类TreeSet&#xff0c;能解决一些看起来棘手的问题。 1 &#xff09;限制条件下元素之间的最小绝对差 2) .算法思路 初始化变量&#xff1a;n为列表nums的大小。 min为整型最大值&#xff0c;用于记录…

图片懒加载指令-vueUse

基于Vue的自定义钩子集合 https://vueuse.org/ 适用于Vue 3和Vue2.7版本之后 基于vueUse定义懒加载指令

diffusion model (七) diffusion model是一个zero-shot 分类器

Paper: Your Diffusion Model is Secretly a Zero-Shot Classifier Website: diffusion-classifier.github.io/ 文章目录 相关阅读背景方法大意diffusion model的背景知识如何将diffusion model应用到zero-shot classification如何求解 实验参考文献 相关阅读 diffusion mode…

Linux--KVM虚拟机扩容

KVM虚拟机扩容 扩容流程 通过virsh list --all可以看到新clone的虚拟机名称 查看磁盘所在位置 virsh domblklist 虚拟机名称 查看虚拟机磁盘文件的大小 qemu-img info 上述hda 扩容之前先关闭虚拟机&#xff08;后面所有的命令都在虚拟机关闭的状态下运行&#xff09;&…

sd卡显示为空白怎么办?解析原因及分享解决方法

随着智能手机和数码相机的普及&#xff0c;SD卡已成为我们常用的存储媒体之一。然而&#xff0c;由于各种原因&#xff0c;SD卡有时会突然显示为空白&#xff0c;这意味着存储在卡上的数据不再可见。这对于用户来说可能造成困扰和焦虑&#xff0c;因为其中的重要照片、视频等文…

CSS中的calc()函数有什么作用?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ CSS中的calc()函数及其作用⭐ 作用⭐ 示例1. 动态计算宽度&#xff1a;2. 响应式布局&#xff1a;3. 自适应字体大小&#xff1a;4. 计算间距&#xff1a; ⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点…

激光里程计:fast-lio复现

文章目录 复现概要相关代码和数据下载环境搭建运行demo简要说明Velodyne Rosbag TEST 里程计话题发布参考 复现概要 按照GitHub上面的markdown文档&#xff0c;搭建FAST_LIO环境并进行Rosbag Example验证的整个过程的简单记录。 相关代码和数据下载 url: https://pan.baidu.…

【数据结构】二叉树链式结构的实现及其常见操作

目录 1.手搓二叉树 2.二叉树的遍历 2.1前序、中序以及后序遍历 2.2二叉树的层序遍历 3.二叉树的常见操作 3.1求二叉树节点数量 3.2求二叉树叶子节点数量 3.3求二叉树第k层节点个数 3.3求二叉树的深度 3.4二叉树查找值为x的节点 4.二叉树的销毁 1.手搓二叉树 在学习…