Android Handler消息机制完全解析(二)

news2024/9/23 5:16:10

欢迎转载,转载请注明出处:https://blog.csdn.net/dmk877/article/details/141690289

在阅读本篇博客之前建议先阅读此篇 Android Handler消息机制完全解析(一)

在看Looper源码之前必须要理解ThreadLocal,ThreadLocal在Handler消息机制中起到了至关重要的作用,面试中也经常会被问到,所以我们先来了解下ThreadLocal,通过本篇博客你将学到

①ThreadLocal的作用以及原理

②Looper方法的源码

③Handler的dispatchMessage消息分发流程

④几个常见的面试题

1.ThreadLocal的作用

ThreadLocal主要有以下作用

(1)线程隔离:每个线程都可以独立的改变自己的副本,而不会影响其它线程的副本

(2)减少同步:由于每个线程操作的是属于自己的数据副本,因此可以避免使用同步机制(synchronized或Lock)来保护共享数据,从而提高性能

(3)简化编程:在某些情况下,使用 ThreadLocal 可以使代码更加简洁,因为它允许开发者以单线程的方式来编写多线程代码。

看到这你可能有点懵,看段代码你就会理解

static ThreadLocal<String> localVar = new ThreadLocal<String>();

public static void main(String[] args) {
    localVar.set("main");
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("thread1 localVar:" + localVar.get());
            localVar.set("thread1");
            System.out.println("thread1 localVar: " + localVar.get());
        }
    });

    Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("thread2 localVar:" + localVar.get());
            localVar.set("thread2");
            System.out.println("thread2 localVar: " + localVar.get());
        }
    });
    thread1.start();
    thread2.start();
    System.out.println(localVar.get());
}

输出

main
thread1 localVar:null
thread2 localVar:null
thread1 localVar: thread1
thread2 localVar: thread2

上述代码中在主线程中声明了一个ThreadLocal变量,然后分别在主线程和两个子线程中set值并取出进行打印,你会发现ThreadLocal变量在主线程中设置值后在子线程thread1和thread1中get获取时是null,main、thread1、thread2对localVar 的操作互不影响,对,这就是线程隔离,实现的原理就是每个线程中都会保存一个ThreadLocal的副本,每次在子线程中修改只是修改了当前线程对应的ThreadLocal对象,对其它线程没有影响。那么问题来了,它是怎么做到的呢?

2.ThreadLocal源码解析

在详细查看源码前,先来看下实现原理图,大概了解之后再看源码事半功倍

在这里插入图片描述

从图中可以看到每一个Thread都会持有一个ThreadLocalMap对象,这个ThreadLocalMap内部持有一个Entry类型的数组,Entry类型有key和value键值对其中key就是ThreadLocal对象,value就是每个线程需要隔离的变量。这样就实现了数据与线程的隔离,每个线程有自己独立的数据。另外需要说明的是一个ThreadLocal实例,在一个线程中只能存储一类数据,线程中的ThreadLocalMap是一个数组结构它可以存储多个ThreadLocal对象。有点懵?没关系,接下来我们看源码,源码结合上图相信你能很好的理解其中的奥妙。

首先从ThreadLocal的set方法开始

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

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

public class Thread implements Runnable {
    ......
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ......
}

在ThreadLocal的set方法中首先获取当前的线程对象t,然后以t为参数调用getMap方法,getMap返回的其实就是当前线程对应的threadLocals,它是声明在Thread类中的ThreadLocal.ThreadLocalMap对象,ThreadLocalMap是ThreadLocal的静态内部类

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

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;
    
    private void set(ThreadLocal<?> key, Object value) {
        	......
            tab[i] = new Entry(key, value);
            ......
        }
}

可以看到ThreadLocalMap中有一个Entry类型的数组,ThreadLocalMap的set方法的参数key就是ThreadLocal类型的参数,value就是我们要保存的数据

如下图所示

在这里插入图片描述

这就是set的大致流程,还是比较清晰的。接下来看看ThreadLocal的get方法源码

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

同样也是先获取当前所在线程t,然后以t为参数调用getMap方法获取与当前线程对应的ThreadLocalMap对象,然后调用map.getEntry(this)在set的时候说过,ThreadLocalMap.Entry中key是ThreadLocalMap对象,value就是我们要保存的值,这里也是将this传入然后拿到当前ThreadLocal对象中保存的值然后返回。到这里大家应该能很清楚的知道ThreadLocal是怎么实现线程隔离的了。其实就是每个线程独有一个ThreadLocalMap对象,当我们set值时会先获取当前的线程,然后得到当前线程的ThreadLocalMap对象,然后将我们要保存的数据存放到ThreadLocalMap中的Entry数组中,get值也是先获取ThreadLocalMap对象,然后根据key(当前的ThreadLocal对象)获取到我们保存的数据。

ok,了解ThreadLocal对Looper的源码会有很大的帮助,接下来我们就来分析下Handler机制中的Looper

3.Looper源码分析

Looper是Handler的核心组成部分之一,它是一个消息循环器,负责管理消息队列,每个线程都可以创建自己的Looper主线程默认就会创建一个Looper,Looper通过Looper.prepare()进行初始化,然后调用Looper.loop()开启消息循环,接下来我们看下Looper的核心代码

public final class Looper {
    ......
    // 保存当前线程的Looper对象,注意这个ThreadLocal对象保存是一个Looper对象,每个线程的Looper是独立的
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal();
    // 主线程Looper
    private static Looper sMainLooper;
    // 当前线程的MessageQueue对象
    final MessageQueue mQueue;
    // 创建Looper的线程
    final Thread mThread;
    ......
    
    public static Looper myLooper() {
        // 返回当前线程的looper
        return (Looper)sThreadLocal.get();
    }
    
    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");
        } else {
            sThreadLocal.set(new Looper(quitAllowed));
        }
    }
    ......
}
  • myLooper方法

首先看myLooper方法其实就是获取当前线程的looper对象,它是保存在ThreadLocal对象中的从刚才对ThreadLocal的讲解这一点应该很好理解。

  • prepare方法

prepare方法会调用prepare(boolean)方法,在prepare(quitAllowed)方法中会判断looper是否为空,如果不为空则会抛异常Only one Looper may be created per thread,这也是面试中经常被问到的,这也就是说一个线程只能创建一个Looper对象,否则会抛异常。

再看下Looper的构造方法

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

因为在Looper的prepare方法中会往ThreadLocal对象中set Looper对象即调用sThreadLocal.set(new Looper(quitAllowed)),而MessageQueue又是在Looper的构造函数中创建的因此一个线程只有一个Looper对象,同时也只有一个MessageQueue对象。

还有一个重要方法loop

public static void loop() {
        final Looper me = myLooper();
        ......
        final MessageQueue queue = me.mQueue;

        for (;;) {
            // 不停的从MessageQueue中取消息,当没消息时可能会阻塞
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            msg.target.dispatchMessage(msg);
			// 将消息进行回收放到消息池中,以便后续使用
            msg.recycleUnchecked();
        }
    }

为了更好的阅读我删除很多代码,只留下了最核心的代码,从loop的源码中可以看到调用loop方法后会开启一个死循环,这就相当于我们给传送带通电,此时它就获得了动力,在这个死循环里会不停的调用MessageQueue的next方法向MessageQueue中取消息,取到消息之后就会调用msg.target.dispatchMessage方法

4. Handler dispatchMessage消息分发流程

在上一篇中我们说到在MessageQueue的enqueueMessage方法中会将当前的handler赋值给msg.target因此这里的msg.target就是Handler对象,那我们来看下源码就看下Handler的dispatchMessage方法的源码

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (this.mCallback != null && this.mCallback.handleMessage(msg)) {
            return;
        }
        this.handleMessage(msg);
    }
}

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

考点来了,在上一篇文章的开头,我列举了几个大厂的面试题其中就有

Handler的callback存在但返回true,handleMessage是否会执行?看到这个问题是不是有点懵?没关系带着这个问题分析完dispatchMessage这个方法之后你就会非常清晰,这里面有两个callback一个是msg.callback一个是this.mCallback我们一个一个来看首先来看

  • msg.callbak

这个callback是从哪里来的呢?在上一篇文章中也提到Handler发送消息有两种方法一种是调用handler.send,一种是调用handler.post,再看下Handler的post方法的源码如下

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

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

可以看到在调用Handler的post方法时它传递的参数是一个Runnable对象,并且将这个Runnable对象转化为Messgage对象后调用send方法发出去,在转化的过程中有一个赋值m.callback=r即将当前的Runnable对象赋值个Message的callback,这就是msg.callback,也就是说只有调用handler.post方法时msg.callback才不为空。

  • this.mCallback

this.mCallback很明显是Handler的变量,它是怎么来的呢?它是通过new Handler时传递过来的,如下所示

在这里插入图片描述

public Handler(Looper looper, Handler.Callback callback, boolean async) {
    this.mLooper = looper;
    this.mQueue = looper.mQueue;
    this.mCallback = callback;
    this.mAsynchronous = async;
}

public interface Callback {
    boolean handleMessage(Message var1);
}

注意区分Handler.callback.handleMessage和this.handleMessage

  • this.handleMessage(msg)

这个就是我们重写的Handler的handleMessage方法

理清楚这些,再来个实例你肯定就会非常清楚,看个实例代码

mHandler = new Handler(getMainLooper(), new Handler.Callback() {
    @Override
    public boolean handleMessage(@NonNull Message msg) {
        Log.d(TAG, "Handler.Callback handleMessage is called");
        return true;
    }
}) {
    @Override
    public void handleMessage(@NonNull Message msg) {
        Log.d(TAG, "Handler handleMessage is called");
        super.handleMessage(msg);
    }
};

handlerPost.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "handler post onClick,send post msg");
            }
        });
    }
});

handlerSend.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d(TAG, "handler send onClick,send msg");
        Message msg = Message.obtain();
        mHandler.sendMessage(msg);
    }
});

可以看到在new Handler的时候传递了Handler.callback参数并且返回true,并且重写了Handler的handleMessage方法然后定义了两个按钮一个用来调用Handler的post方法,一个用来调用Handler的send方法,点击handlerPost打印结果

MainActivity: handler post onClick,send post msg

分析:根据Handler的dispatchMessage如果msg.callack!=null则直接调用handleCallback(msg)方法,handleCallback方法其实就是调用msg.callback.run(),此时就会执行hander.post(runnable)中的runnable任务,因此就是只打印了handler post onClick,send post msg。

点击handlerSend打印结果

MainActivity: handler send onClick,send msg
MainActivity: Handler.Callback handleMessage is called

这个就有点说法了,再看下dispatchMessage的代码

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (this.mCallback != null && this.mCallback.handleMessage(msg)) {
            return;
        }
        this.handleMessage(msg);
    }
}

首先第一个if不满足,send方法发送的消息msg.callback==null,进入else,因为我们在new Handler的时候传递了Handler.callback回调,所以this.callback!=null成立,而我们传递的回调中Handler.callback返回true因此执行this.mCallback.handleMessage它也返回了true,两个条件成立因此直接return,this.handleMessage不会执行到,在上述代码中做下修改把Handler.callback的返回值改为false打印结果是啥呢?大家可以自己尝试下。

因此Handler的callback存在但返回true,handleMessage是否会执行?的回答是不会执行。另外此处还涉及到一个面试题

面试题:Handler的post和send方法的区别,相信到此你也能回答出来,主要从发送和分发来回答

(1)post一类的方法发送的是Runnable对象,但是最后还是会被封装成Message对象,将Runnable对象赋值给Message对象中的callback字段,然后交由sendMessageAtTime()方法发送出去。

在处理消息时,会在dispatchMessage()方法里首先被handleCallback(msg)方法执行,实际上就是执行Message对象里面的Runnable对象的run方法。

(2)sendMessage一类方法发送的消息直接是Message对象,处理消息时,在dispatchMessage里优先级会低于handleCallback(msg)方法,是通过自己重写的handleMessage(msg)方法执行。

5. 子线程中如何正确创建Handler

好,到这里Handler相关的几个对象Handler、MessageQueue、Looper就介绍的差不多了,到这里可能有些同学还是心存疑惑,到底这个Handler机制是从哪里触发的?这里涉及到App的启动流程后面有时间的话会写一篇博客,在这里你只要知道,当我们的手指点击手机上的应用图标时,系统会创建一个ActivityThread负责管理所有Activity的线程,它负责处理Activity的生命周期、事件分发、消息处理等工作,在ActivityThread的main方法源码如下

public static void main(String[] args) {
    ......
    Looper.prepareMainLooper();
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    // 开启死循环,不停的取消息
    Looper.loop();
}

也就是说当我们手指点击应用图标应用启动的时候,系统会执行ActivityThread的main方法而在main方法中会调用

Looper.prepareMainLooper()和Looper.loop()

public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

系统会帮我们生成存有Looper的ThreadLocal对象并调用loop方法开启循环从MessageQueue中取消息,这样整个流程就通了,因为loop是一个死循环,所以当我们使用handler发送一条消息时,在loop方法中会把此消息取出,并调用Handler的dispatchMessage方法将此消息分发出去,执行对应的方法。注意这只针对主线程,在子线程中我们要手动调用Looper.prepare和Looper.loop,如下所示先把Looper.prepare注释掉运行看下下效果

new Thread(new Runnable() {
    @Override
    public void run() {
        // Looper.prepare();
        mHandler = new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                Log.d(TAG, "mHandler handleMessage is called:" + Thread.currentThread().getName());
            }
        };
        mHandler.sendMessage(Message.obtain());
        Looper.loop();
    }
}).start();

在这里插入图片描述
这个报错是不是很常见,很多人都知道调用下Looper.prepare就行了,为什么会报这个错呢?这是因为new Handler的时候会判断当前Handler所在的线程的Looper是否为空?如果为空就抛异常,所以在子线程中不调用Looper.prepare()直接new Handler就会抛异常,源码如下

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

看到没异常就是从这里抛出来的。

这篇就写到这里了,总结下:
(1) Handler是怎么进行线程间通讯的,原理是什么?
Handler之所以能够进行线程间通讯原理就是,线程间内存共享,在Handler机制中共享的就是MessageQueue,首先new Handler的这个线程中的Looper(ThreadLocal使得Looper被当前线程独有)、MessageQueue是与当前线程绑定的,其它的线程拿到这个Handler的引用发送消息时,会将此消息插入到这个MessageQueue中,从而就实现了两个不同线程间的通讯。就是说现在有两个线程A和B,在B中创建了Handler,此时Looper、MessageQueue都在线程B中,在线程A中拿到Handler的引用发送消息时这条消息就会被插入到线程B的MessageQueue中,这样就完成了从线程A到线程B的通讯。这一块建议大家好好理一下。

public Handler handler;
Thread A = new Thread(new Runnable() {
    @Override
    public void run() {
        // handler在线程B中初始化,此消息会插入到线程B中的MessageQueue中
        handler.sendMessage(Message.obtain());
    }
});

Thread B = new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();
        handler = new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                Log.d(TAG, "receive message");
            }
        };
        Looper.loop();
    }
});

B.start();
Thread.sleep(1000);
A.start();

(2)一个线程只有一个Looper并且只有一个MessageQueue
(3)对Handler的dispatchMessge方法的流程要掌握清晰

如果本篇对你有帮助欢迎点赞留言,若发现文中的错误欢迎批评指正,大家一起进步。

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

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

相关文章

信息安全--(五)物理与环境安全技术(二)机房安全分析与防护

在使用本博客提供的学习笔记及相关内容时&#xff0c;请注意以下免责声明&#xff1a;信息准确性&#xff1a;本博客的内容是基于作者的个人理解和经验&#xff0c;尽力确保信息的准确性和时效性&#xff0c;但不保证所有信息都完全正确或最新。非专业建议&#xff1a;博客中的…

FlowUs 小程序:开启高效之旅,订阅内容超精彩

一、丰富多样的订阅选择 FlowUs 小程序提供了极为丰富的订阅内容。无论你是对知识管理、项目管理感兴趣&#xff0c;还是专注于创意写作、时间规划&#xff0c;都能在这里找到适合自己的订阅选项。从专业的行业资讯到实用的技巧指南&#xff0c;从激发灵感的创意案例到深入的学…

业务解耦-Spring事件监听的三种实现方式

实现ApplicationListener 步骤如下&#xff1a; 1.写Event类&#xff0c;需要继承Spring的ApplicationEvent类 2.写监听类&#xff0c;需要实现Spring的ApplicationListener接口&#xff0c;加上Component注解 3.监听类实现onApplicationEvent方法 4.通过ApplicationContext.p…

开放大世界的全局寻路

开放大世界的寻路一直是很困扰我的一个点&#xff0c;地图大、还是动态可变的&#xff0c;所以寻路会有很多要求。就我们项目来讲&#xff0c;有这几个要求&#xff1a; 能满足极大范围的地图&#xff1b; 地图寻路数据能实时构建&#xff0c;且重建代价很小&#xff1b; 寻路的…

地质灾害监测预警系统的作用

在地球的广阔舞台上&#xff0c;自然灾害如同不可预测的演员&#xff0c;时常上演着惊心动魄的剧目。地震的震撼、滑坡的肆虐、泥石流的咆哮&#xff0c;这些地质灾害不仅给人类生命财产带来巨大威胁&#xff0c;也考验着社会的防灾减灾能力。为了应对这一挑战&#xff0c;地质…

【Linux】在 bash shell 环境下,当一命令正在执行时,按下 control-Z 会?

目录 题目分析答案 题目 分析 ctrl-c&#xff1a; 发送 SIGINT 信号给前台进程组中的所有进程。常用于终止正在运行的程序&#xff1b;ctrl-z&#xff1a; 发送 SIGTSTP信号给前台进程组中的所有进程&#xff0c;常用于挂起一个进程&#xff1b;ctrl-d&#xff1a; 不是发送信…

乐城堡 JoyCastle Unity岗位笔试题

1)实现 move(GameObjct gameObject, Vector3 begin, Vector3 end, float time, bool pingpong){ } 使 gameObject 在 time 秒内&#xff0c;从 begin 移动到 end&#xff0c;若 pingpong 为 true&#xff0c;则在结束时 使 gameObject 在 time 秒内从 end 移动到 begin&#xf…

机器学习中的增量学习(Incremental Learning,IL)策略是什么?

机器学习中的增量学习&#xff08;Incremental Learning&#xff0c;IL&#xff09;策略是什么&#xff1f; 在当今快速发展的数据驱动世界中&#xff0c;传统的静态机器学习模型逐渐显露出局限性。随着数据量的增长和分布的变化&#xff0c;模型需要不断更新&#xff0c;以保…

opc da 服务器数据 转IEC61850项目案例

目录 1 案例说明 1 2 VFBOX网关工作原理 1 3 应用条件 2 4 查看OPC DA服务器的相关参数 2 5 配置网关采集opc da数据 4 6 用IEC61850协议转发数据 6 7 网关使用多个逻辑设备和逻辑节点的方法 9 8 在服务器上运行仰科OPC DA采集软件 10 9 案例总结 12 1 案例说明 在OPC DA服务…

使用 Vue3 Element Plus 实现el-table中的特定单元格编辑,下拉选择等

效果预览 完整代码(后面有解析) <template><div style="display: flex;align-items: center;justify-co

磁性齿轮箱市场报告:前三大厂商占有大约79.0%的市场份额

磁性齿轮箱是一种用于扭矩和速度转换的非接触式机构。它们无磨损、无摩擦、无疲劳。它们不需要润滑剂&#xff0c;并且可以针对其他机械特性&#xff08;如刚度或阻尼&#xff09;进行定制。 一、全球磁性齿轮箱行业现状与洞察 据 QYResearch 调研团队最新发布的“全球磁性齿轮…

成都高温限电:当电动汽车「无电可充」

8月末的成都&#xff0c;因为高温限电了。 近几日&#xff0c;成都市气象台连续发布了高温红色预警信号。据新华社报道&#xff0c;8月21日&#xff0c;四川电网用电负荷两次创下历史新高&#xff0c;最高达6797万千瓦&#xff0c;较去年最大用电负荷增长近13%&#xff0c;电力…

Golang | Leetcode Golang题解之第385题迷你语法分析器

题目&#xff1a; 题解&#xff1a; func deserialize(s string) *NestedInteger {index : 0var dfs func() *NestedIntegerdfs func() *NestedInteger {ni : &NestedInteger{}if s[index] [ {indexfor s[index] ! ] {ni.Add(*dfs())if s[index] , {index}}indexreturn…

HarmonyOS鸿蒙开发:在线短视频流畅切换最佳实践

简介 为了帮助开发者解决在应用中在线短视频快速切换时容易出现快速切换播放时延过长的问题&#xff0c;将提供对应场景的解决方案。 该解决方案使用&#xff1a; 视频播放框架AVPlayer和滑块视图容器Swiper进行短视频滑动轮播切换。绘制组件XComponent的Surface类型动态渲染…

挂载5T大容量外接硬盘到ubuntu

挂载5T大容量外接硬盘到ubuntu S1&#xff1a;查看硬盘 使用 $ sudo fdisk -l找到对应盘&#xff0c;例如下图所示 /dev/sdc S2: 创建分区 使用 $ sudo fdisk /dev/sdc对上硬盘进行创建分区&#xff1b;可以依次使用以下指令 m &#xff1a;查看命令&#xff1b; g &…

前端篇-html

day1: 超文本标记语言&#xff08;英语&#xff1a;HyperText Markup Language&#xff0c;简称&#xff1a;HTML&#xff09;是一种用于创建网页的标准标记语言。 作用&#xff1a;可以使用 HTML 来建立自己的 WEB 站点&#xff0c;HTML 运行在浏览器上&#xff0c;由浏览器…

基于贝叶斯优化CNN-LSTM网络的数据分类识别算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 卷积神经网络&#xff08;CNN&#xff09; 4.2 长短期记忆网络&#xff08;LSTM&#xff09; 4.3 BO-CNN-LSTM 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) B…

基于物联网的低成本便携式传感器节点用于火灾和空气污染的检测与报警

目录 摘要 引言 材料和方法 传感器节点 IoT 微控制器 颗粒物传感器 环境和气体传感器 MQTT代理 Node-Red监控平台 系统结构 数据存储 工作描述 实验结果 讨论 结论 致谢 参考文献 这篇论文的标题是《Low-cost IoT-based Portable Sensor Node for Fire and Air…

区块链媒体套餐发稿:世媒讯引领项目推广新风潮

在区块链技术迅猛发展的今天&#xff0c;越来越多的企业和项目涌现出来&#xff0c;希望通过区块链技术改变传统行业&#xff0c;并在全球范围内获得更多关注和支持。然而&#xff0c;在这个竞争激烈的市场中&#xff0c;如何快速有效地推广和传播项目变得尤为重要。选择合适的…

disk manager操作教程 如何使用Disk Manager组件 Mac如何打开ntfs格式文件

macOS系统有一个特别明显的弱点&#xff0c;即不能对NTFS格式磁盘写入数据。想要适合Mac系统使用来回转换磁盘格式又十分麻烦&#xff0c;这该怎么办呢&#xff1f;Tuxera ntfs for mac作为一款Mac完全读写软件&#xff0c;大家在安装该软件后&#xff0c;能充分使用它的磁盘管…