【Android】Handler消息机制

news2024/10/6 17:15:50

文章目录

  • 前言
  • 概述
    • 核心组件概述
    • Android消息机制概述
  • Android消息机制分析
    • ThreadLocal的工作原理
      • ThreadLocal基础
      • ThreadLocal实现原理
    • MessageQueue
    • Looper
    • Handler的工作原理
    • 总结

前言

本文用于记录Android的消息机制,主要是指Handler的运行机制。部分内容参考自《Android开发艺术探索》,感兴趣可以阅读原书。

概述

核心组件概述

Handler是Android消息机制的上层接口,通过它可以轻松将一个任务从一线程切换到Handler所在的线程执行,如由子线程切换到主线程更新UI。具体来说有时候我们需要在子线程中执行耗时操作,当耗时操作结束时希望将UI更新,这时候我们就需要将线程切换到主线程了,也就是UI线程(因为Android不允许在子线程更新UI)。

Handler的运行需要底层MessageQueue和Looper的支持。MessageQueue也就是消息队列,它可以存储消息并以队列的形式提供插入消息和删除消息操作(注意:虽然叫消息队列,但它内存存储结构并不是队列而是单链表)。Looper我们可以将它理解为消息循环,它会以无限循环的方式去消息队列中查找是否存在消息,有的话就对消息进行处理,没有的话就一直等待。

此外Looper中还有一个特殊的概念——ThreadLocal,ThreadLocal不是线程,它的作用是在每个线程中独立的存储数据。Handler在创建的时候,需要使用当前线程的Looper来构建消息循环系统,,那么如何获取当前线程的Looper呢?此时就要用到ThreadLocal,我们知道ThreadLocal可以在不同线程互不干扰的存储并提供数据,我们就可以通过ThreadLocal存储并获取每个线程的Looper了。需要注意的是除主线程外,其他线程默认是没有Looper的,想要使用Handler就必须为线程创建Looper。

Android消息机制概述

Android的消息机制主要指Handler的运行机制以及Handler附带的MessageQueue和Looper的工作过程。系统之所以提供Handler,就是为了解决在子线程中无法更新UI的矛盾。试想一下,当我们通过网络请求从服务端拉取了一些数据需要更新在UI上,为了避免造成ANR,我们需要在子线程中执行网络请求操作,那该怎么访问UI呢,这时候我们就可以使用Handler,因为它允许我们将一个任务由某个线程切换到另一个线程执行。

延伸

  • 系统为什么不允许在子线程更新UI?

Android的UI控件是线程不安全的,如果在多线程并发访问可能导致UI控件处于不可预期的状态。

  • 为什么不给UI控件的访问加上锁机制?

首先加锁会让UI访问的逻辑变得十分复杂,其次加锁会降低UI的访问效率,因为加锁会阻碍某些线程的执行。

接着概述一下Android的消息机制,首先我们需要创建得到Handler,Handler创建时获取当前线程的Looper来构建内部的消息循环系统(除主线程外都需要手动创建Looper)。Handler创建完毕后,此时内部的Looper和MessageQueue就可以和Handler协调工作了。

Android消息机制分析

ThreadLocal的工作原理

ThreadLocal基础

ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中才可以获取到存储到数据,对于其他线程数据是不可访问的。

当某些数据是以线程为作用域而且不同线程需要不同的数据副本时,就可以考虑使用ThreadLocal。如对于Handler来说,他需要获取当前线程的Looper,因为Looper的作用域是线程且不同线程有不同的Looper,此时我们就可以通过ThreadLocal实现Looper在线程内的存取。

ThreadLocal的另一个使用场景是实现复杂逻辑下的对象传递。比如监听器的传递,有时一个线程中的任务过于复杂——函数调用栈比较深或代码入口多样,我们需要监听器能够贯彻整个线程的执行过程,这时候就可以使用ThreadLocal让监听器作为一个全局变量而存在,在线程内部只需通过get方法获取监听器即可。

上面介绍了这么多理论知识,接下来我们通过一个具体的代码实例来加深理解,在这个实例中我们会创建一个Integer类型的ThreadLocal对象,接着在主线程和三个子线程中分别设置并输出它的值:

		//创建ThreadLocal
        ThreadLocal<Integer> booleanThreadLocal=new ThreadLocal<>();
        booleanThreadLocal.set(1);//设置主线程的值为1
        System.out.println("ThreadLocal的值为:"+booleanThreadLocal.get());

        //子线程1
        new Thread(new Runnable() {
            @Override
            public void run() {
                booleanThreadLocal.set(2);//设置子线程1的值为2
                System.out.println("ThreadLocal的值为:"+booleanThreadLocal.get());
            }
        }).start();

        //子线程2
        new Thread(new Runnable() {
            @Override
            public void run() {
                booleanThreadLocal.set(3);//设置子线程2的值为3
                System.out.println("ThreadLocal的值为:"+booleanThreadLocal.get());
            }
        }).start();

        //子线程3
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("ThreadLocal的值为:"+booleanThreadLocal.get());
            }
        }).start();

打印结果如下:

在这里插入图片描述

可以看到每个线程打印的都是我们设置的值,因为在子线程3我们没有设置值,所以打印的是null。

ThreadLocal之所以能实现上述的效果,是因为不同线程访问同一个ThreadLocal的get方法,ThreadLocal内部会从各自线程中取出一个数组,然后根据当前ThreadLocal的索引去查找对应的value。显然每个线程的数组是不同的,所以通过ThreadLocal可以在指定线程存储数据,其他线程无法访问,不同线程间彼此互不干扰。

ThreadLocal实现原理

想弄清ThreadLocal的实现原理,需要弄清它的set方法和get方法

  • set方法

这里先介绍一下set方法执行的大概流程,再来介绍一下源码:

1.首先我们会获取当前的线程,然后根据当前的线程获取当前线程的ThreadLocalMap对象(ThreadLocalMap使用一个数组作为存储结构,数组元素为Entry,传入的的ThreadLocal对象作为Entry的key,要存入的值为value

2.如果获取到了当前线程的ThreadLocalMap对象,那么就将数据存进去

3.如果获取的ThreadLocalMap对象是null,那么就为当前线程创建一个ThreadLocalMap对象

源码分析

打开ThreadLocal的原码找到set方法:

public void set(T value) {
        Thread t = Thread.currentThread();//获取当前线程
        ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMap对象
        if (map != null) {
            map.set(this, value);//这里的this就是当前线程的ThreadLocal对象
        } else {
            createMap(t, value);
        }
    }

这个set方法接受一个value参数,这个参数可以指定为任何类型,我们先获取当前线程接着获取当前线程的ThreadLocalMap对象,如果当前线程有ThreadLocalMap对象,那么就将数据直接存进去(存进一个Entry对象,当前线程的ThreadLocal对象作为key,要存储的数据作为value);如果当前线程没有ThreadLocalMap对象,那么我们就需要去创建一个ThreadLocalMap对象:

	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }


private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //初始化一个数组
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//计算key的存储位置
    table[i] = new Entry(firstKey, firstValue);//将数据放到计算出的位置
    size = 1;
    setThreshold(INITIAL_CAPACITY);
    }

可以看到当调用createMap方法后回去创建一个ThreadLocalMap实例,在创建ThreadLocalMap实例时,会初始化一个数组,这个数组就是我们真正用于存取数据的数据结构,这个数组存储数据的数据类型是Entry,也就是一个key-value的数据结构。接着我们会通过传给我们的ThreadLocal参数计算出这个key的储存位置并将数据存到计算出的位置。

小结一下这个过程:我们的每个Thread(线程)都有唯一的一个ThreadLocalMap对象,这个ThreadLocalMap对象使用Enrty[]数组存储数据,Entry[]数组的每个元素都是以当前线程的ThreadLocal对象作为key,要存储的数据作为value的数据结构。

另外提一下使用数组是因为同一线程中可能有多个不同类型的ThreadLocal对象,它们计算出的key是不同的,就对应了在数组上不同的存储位置。

  • get方法

源码分析

找到get方法

public T get() {
        Thread t = Thread.currentThread();//获取当前线程
        ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMap对象
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();//表示当前线程都没有ThreadLocalMap对象或者有ThreadLocalMap对象当不存在ThreadLocal关联的Entry对象,去做一些初始化的工作
    }

同样先获取当前线程再获取当前线程的ThreadLocalMap对象,如果对象不为空,那么就根据key也就是ThreadLocal对象去获取Entry对象,最后获取到的Entry对象不为空时将数据返回,获取Entry对象的方法如下:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);//计算出存储的位置
    Entry e = table[i];
    if (e != null && e.refersTo(key))
        return e;
    else
        return getEntryAfterMiss(key, i, e);
    }

根据传入的key计算出key的存储位置,这个过程在我们set()方法存储数据时是相对应的,都是根据key(ThreadLocal对象)计算出在数组上的存储位置,接着直接存储位置直接获得Entry对象返回。

通过ThreadLocal的get方法和set方法可以看出,不同线程操作的都是各自线程中的ThreadLocalMap对象的Entry[]数组,所以ThreadLocal可以实现线程间存取数据互不干扰。

MessageQueue

MessageQueue也就是消息队列,虽然它叫做消息队列,但它的实现方法并不是队列,而是使用一个单链表来实现消息列表

MessageQueue主要有两个方法:enqueueMessage()插入消息和next()读取消息,需要注意的是读取消息时会伴随着消息的删除操作,也就是在读取完这条消息后将其从消息队列中移除。

Looper

Looper在Android消息机制中扮演循环者的角色,它会不停在MessageQueue中查看是否有新消息,如果有新消息就立即处理,否则就一直阻塞在那。在创建Looper时会创建一个MessageQueue对象并获取当前的线程,源码如下:

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

上面我们介绍过,Handler的工作需要Looper,如果一个线程没有Looper对象使用Handler对象就会报错,那么我们怎么获取Looper对象呢,答案是直接调用Looper.prepear()方法即可创建一个Looper对象,然后通过Looper.loop()来开启消息循环,如下:

		//子线程1
        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                Handler hanlder=new Handler();
                Looper.loop();
            }
        }).start();

Looper也是可以退出的,有quit()和quitSafely()两种方法,使用quit()会直接退出Looper,而quitSafely()会设置一个退出标记,接着把消息队列中已有的消息处理完毕才能安全退出。当Looper退出后,使用Handler的send方法发消息会返回false。

如果我们在子线程中创建了Looper,那么我们就应该在所有事做完之后调用上述两个方法退出Looper。

上面我们还提到了一个loop()方法,只有调用Looper.loop()方法后,消息循环系统才会真正的起作用,我们可以看看它的源码:

打开loop()方法的源码我们可以看到有下面一段代码:

for (;;) {
	if (!loopOnce(me, ident, thresholdOverride)) {
		return;
	}
}

首先我们可以知道loop方法是一个死循环,只有当!loopOnce(me, ident, thresholdOverride条件满足时才会跳出循环,接着我们打开loopOnce方法看看,首先里面有段代码:

Message msg = me.mQueue.next(); // might block
if (msg == null) {
	// No message indicates that the message queue is quitting.
	return false;
}

loop()方法会调用MessageQueue的next()方法一直获取消息,此时一直会处于循环状态。只有当MessageQueue的next()方法返回了null循环才会结束,那么什么情况下MessageQueue的next()方法才会返回null呢。答案是当Looper调用了quit()方法或quitSafely()方法时,我们看看这两个方法:

public void quit() {
   mQueue.quit(false);
}
public void quitSafely() {
   mQueue.quit(true);
}

可以看到Looper的quit()方法和quitSefely()方法这两个方法调用了MessageQueue的quit()方法,我们打开MessageQueue来看看它的quit()方法:

	void quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }

        synchronized (this) {
            if (mQuitting) {
                return;
            }
            mQuitting = true;

            if (safe) {
                removeAllFutureMessagesLocked();
            } else {
                removeAllMessagesLocked();
            }

            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr);
        }
    }

MessageQueue的quit()方法接收一个布尔型参数用来表示是不是安全的,也就是区分Looper的quit()方法和quitSafely()方法。MessageQueue的quit()方法中有段代码,用来真正进行处理:

if (safe) {//quitSafely()安全退出
	removeAllFutureMessagesLocked();
} else {//quit()
	removeAllMessagesLocked();
}

点进这两个方法你会发现,这两个方法都是将MessageQueue.next()方法返回的值设为null,这样我们的Looper.quit()和Looper.quitSafely()方法就会执行,Looper退出。

如果MessageQueue的next()方法返回的值不为null,那么就会调用loopOnce()方法里的msg.target.dispatchMessage(msg);方法来处理这条消息,msg.target表示发送这条消息的Handler对象,也就是说Handler发送完消息,这条消息最终又交给Handler的dispatchMessage()方法来处理了,但dispatchMessage()方法是在Looper.loop()方法中执行的。这样我们就可以实现在子线程执行耗时操作,然后在子线程调用Looper.loop()方法执行Handler的dispatchMessage()方法,通知Handler来处理消息,最后到主线程(也就是Handler所在的线程)进行处理消息的操作了。

Handler的工作原理

Handler主要包括消息的发送和接收。发送就是通过post的一系列方法以及send的一系列方法来完成的,其实post的一系列方法最终也是通过send的一系列方法来实现的。

Handler的sendMessage()方法经过层层调用最终会调用到一个enqueueMessage()方法,是不是有点眼熟,在MessageQueue中我们就是通过这个方法插入消息,在Handler的enqueueMessage()方法就是调用MessageQueue.enqueueMessage()方法来向消息队列中添加一条消息,Handler的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);//调用MessageQueue的enqueueMessage()方法插入一条消息
    }

当调用Handler的send()方法时,MessageQueue的enqueueMessage()方法执行向消息队列中插入一条消息,同时MessageQueue的next()方法执行将这条消息返回给Looper(Looper通过loop()方法一直在MessageQueue中获取消息),Looper接收到消息就开始处理,最终消息交给Handler处理,即Handler.dispatchMessage()方法被调用,此时Handler进入处理消息阶段,我们可以打开Handler的源码看看dispatchMessage()方法:

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

这个方法中首先检查Message的callback对象(一个Runnable对象,就是Handler的post()方法传递的Runnable参数),不为空使用handleCallback(msg)方法处理消息。如果没有传递Runnable参数,也就是msg.callback为空,那么就接着判断mCallback是否为空,Callback是一个空接口,其源码定义如下:

public interface Callback {
        /**
         * @param msg A {@link android.os.Message Message} object
         * @return True if no further handling is desired
         */
        boolean handleMessage(@NonNull Message msg);
    }

通常我们在开发过程中,创建Handler的方式是派生一个Handler的子类并重写其handlerMessage()方法来进行具体的消息处理。当我们不想派生子类时,就可以通过Callback这样创建一个Handler对象Handler handler=new Handler(callback),这样我们就不需要派生Handler的子类。

其实上面代码不是特别重要,重要的是handleMessage(msg)这个方法。Handler的dispatchMessage()方法最终,调用handleMessage()方法来进行具体的消息处理。

总结

最后我们总结一下上面讲到的几个方法的相互之间的调用:

  • Handler通过sendMessage()方法向MessageQueue中添加Message(MessageQueue的enqueueMessage()方法执行)
  • Looper通过loop()方法,不停检查MessageQueue中是否有新消息(除非调用了Looper的quit()方法或quitSafely()方法),有就执行消息(MessageQueue的next()方法执行)
  • 处理消息时Looper调用Handler的dispatchMessage()方法,将消息交给Handler的HandleMessage()方法执行

在这里插入图片描述

最终我们就可以实现在子线程执行耗时操作,再在子线程通过dispatchMessage()方法通知主线程执行handleMessage()方法,在主线程更新UI。
分享到此结束,共勉!

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

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

相关文章

产品经理都会的ComfyUI搭建指南

最近准备参加一个ComfyUI的活动&#xff0c;发现还没有上手过ComfyUI&#xff0c;于是先部署起来。ComfyUI是一个基于Stable Diffusion开发的UI。比起WebUI表单式交互的简单&#xff0c;ComfyUI主打灵活&#xff0c;Diffusion Model管线中的各个模块如&#xff1a;VAE、Control…

DINOv2: Learning Robust Visual Featureswithout Supervision

Abstract 在自然语言处理方面的模型&#xff0c;可以产生通用视觉特征&#xff08;即无需微调即可跨图像分布和任务工作的特征&#xff09;来极大地简化任何系统中图像的使用。这些模型能够提取出一些可以在不同类型的图像和任务中通用的视觉特征。这意味着不管图像的来源&…

电脑断网或者经常断网怎么办?

1、首先&#xff0c;按一下键盘的win R &#xff0c; 在打开的运行框内输入&#xff1a;cmd 然后按一下回车 或者 点击一下【确定】 2、在命令窗口输入&#xff1a;ipconfig/release , 然后按一下回车 作用&#xff1a;IP释放&#xff0c;相当于把网线拔了重新插上 3、接着…

【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第一部分 D3.js 基础知识 第一章 D3.js 简介&#xff08;已完结&#xff09; 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践&#xff08;上&#xff09;1.3 数据可…

C++11之线程

编译环境&#xff1a;Qt join&#xff1a;阻塞当前线程&#xff0c;直到线程函数退出 detach&#xff1a;将线程对象与线程函数分离&#xff0c;线程不依赖线程对象管理 注&#xff1a;join和detach两者必选其一&#xff0c;否则线程对象的回收会影响线程的回收&#xff0c;导致…

MATLAB下的RSSI定位程序,二维平面上的定位,基站数量可自适应

文章目录 引言程序概述程序代码运行结果待定位点、锚点、计算结果显示待定位点和计算结果坐标 引言 随着无线通信技术的发展&#xff0c;基于 R S S I RSSI RSSI&#xff08;接收信号强度指示&#xff09;的方法在定位系统中变得越来越流行。 R S S I RSSI RSSI定位技术特别适…

Vue 插槽全攻略:重塑组件灵活性

前言 &#x1f4eb; 大家好&#xff0c;我是南木元元&#xff0c;热爱技术和分享&#xff0c;欢迎大家交流&#xff0c;一起学习进步&#xff01; &#x1f345; 个人主页&#xff1a;南木元元 目录 什么是slot插槽 默认插槽 编译作用域 后备内容 具名插槽 作用域插槽 应…

医药行业的智能合同审查:大模型与AI赋能合规管理

随着医药行业的快速发展&#xff0c;尤其是在全球化背景下&#xff0c;企业在业务拓展、合作协议签订中需要处理大量复杂的合同。合同不仅是业务的法律保障&#xff0c;更是风险管理的重要工具。医药行业合同审查的复杂性源于其严格的合规性要求&#xff0c;包括与政府机构、研…

学会这几个简单的bat代码,轻松在朋友面前装一波13[通俗易懂]

大家好&#xff0c;又见面了&#xff0c;我是你们的朋友全栈君。 这个标题是干什么用的? 最近看晚上某些人耍cmd耍的十分开心&#xff0c;还自称为“黑客”&#xff0c;着实比较搞笑.他们那些花里胡哨的东西在外行看来十分nb,但只要略懂一些&#xff0c;就会发现他们的那些十…

数据库(MySQL):使用命令从零开始在Navicat创建一个数据库及其数据表(三),单表查询

前言 Navicat Premium 17 数据表需要经常清缓存&#xff0c;不然之前的自增的数据可能会一直存在&#xff0c;所以把之前的表删除重新创建是对练习数据库最简单的办法。新建数据库的命令如下&#xff1a; /* 创建有 自增主键的属性id&#xff0c;非空的属性name&#xff0c;唯…

如何使用ssm实现基于BS的超市商品管理系统的设计与实现+vue

TOC ssm787基于BS的超市商品管理系统的设计与实现vue 研究背景与现状 时代的进步使人们的生活实现了部分自动化&#xff0c;由最初的全手动办公已转向手动自动相结合的方式。比如各种办公系统、智能电子电器的出现&#xff0c;都为人们生活的享受提供帮助。采用新型的自动化…

TypeScript面向对象 02

抽象类 以abstract开头的类是抽象类。抽象类和其他类区别不大&#xff0c;只是不能用来创建对象。抽象类就是专门用来被继承的类。 抽象类中可以添加抽象方法。定义一个抽象方法使用abstract&#xff0c;没有方法体。抽象方法只能定义在抽象类中&#xff0c;子类必须对抽象方…

一些硬件知识(二十七)

单片机一般使用NOR FLASH &#xff0c;这是因为NOR FLASH支持字节级的随机读取&#xff0c;可以直接运行存贮其中的程序&#xff0c;NOR FLASH支持读取和执行存储其中的指令&#xff0c;而无需将程序拷贝到RAM中才可执行。NAND FLASH适用于大容量的数据存储&#xff0c;他的读写…

【Canvas与标志】灰座橙底红芯辐射标志

【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>灰座橙底红芯辐射标志</title><style type"text/css&q…

msvcp140.dll丢失的解决方法,详细解读6种解决方法

在使用电脑时&#xff0c;我们可能会遇到提示缺少msvcp140.dll的错误信息。这个提示意味着我们的电脑中缺少MSVCP140.dll这个文件&#xff0c;它是某些程序运行所必需的。如果我们遇到这个问题&#xff0c;应该如何解决呢&#xff1f;本文将详细解析如何解决msvcp140.dll丢失的…

qemu模拟arm64环境-构建6.1内核以及debian12

一、背景 手头没有合适的arm64开发板&#xff0c;但是需要arm的环境&#xff0c;于是想到qemu模拟一个。除了硬件交互以外&#xff0c;软件层面的开发还是都可以实现的。 虚拟机还能自定义内存大小和镜像大小&#xff0c;非常适合上板前的验证&#xff0c;合适的话再买也不迟。…

深度学习:5种经典神经网络模型介绍

目录 1. LeNet&#xff1a;CNN的鼻祖 2. AlexNet&#xff1a;深度学习的开山之作 3. VGGNet&#xff1a;深度与简洁的结合 4. GoogLeNet&#xff1a;Inception模块的创新 5. ResNet&#xff1a;残差学习的革命 卷积神经网络&#xff08;CNN&#xff09;已经发展为图像识别…

张雪峰谈人工智能技术应用专业的就业前景!

一、张雪峰谈人工智能技术应用专业 在教育咨询领域&#xff0c;张雪峰老师以其深入浅出的讲解和前瞻性的视角&#xff0c;为广大学子提供了宝贵的专业选择建议。对于人工智能技术应用专业&#xff0c;张雪峰老师通常给予高度评价&#xff0c;认为这是一个充满无限可能且就业前…

DELL SC compellent存储的四种访问方式

DELL SC存储&#xff08;国内翻译为 康贝存储&#xff0c;英文是compellent&#xff09;, compellent存储是dell在大概10多年前收购的一家存储&#xff0c;原来这个公司就叫做compellent。 本文的阅读对象是第一次接触SC存储的技术朋友们&#xff0c;如何访问和管理SC存储。总…

陀螺仪LSM6DSV16X与AI集成(13)----中断获取SFLP四元数

陀螺仪LSM6DSV16X与AI集成.13--中断获取SFLP四元数 概述视频教学样品申请源码下载硬件准备SFLP开启INT中断中断读取传感器数据主程序演示 概述 本文将介绍如何通过中断机制获取 LSM6DSV16X 传感器的 SFLP&#xff08;Sensor Fusion Low Power&#xff09;四元数数据。LSM6DSV1…