当前位置: 首页 > news >正文

Linux设备驱动模型之字符设备

Linux设备驱动模型之字符设备

前面我们有介绍到Linux的设备树,这一节我们来介绍一下字符设备驱动。字符设备是在IO传输过程中以字符为单位进行传输的设备,而字符设备驱动则是一段可以驱动字符设备驱动的代码,当前Linux中,字符设备驱动是怎样的呢,下面一起来探讨学习一下。

基础小知识

字符设备框架

如果让你来设计字符设备框架,你会怎么设计呢?不同的开发者会有不同的需求,但是每个人都需要注册字符设备,需要有个地方来保存、管理这些设备驱动信息,在代码中如何保存会更灵活,更合适呢?

注册字符设备

当前Linux内核是通过主设备号与次设备号来定义某一个驱动,其中主设备号从0到CHRDEV_MAJOR_MAX(512) - 1共512个主设备号,而次设备号则是从0到220 - 1(MINORMASK定义)。所以,字符设备驱动第一步就是先向内核注册一个设备号:

#define DAO_NAME        "dao"
static dev_t            dao_devt;// 注册字符设备函数调用
alloc_chrdev_region(&dao_devt, 0, MINORMASK + 1, DAO_NAME);	//注册字符设备号/** 注册字符设备函数声明,通过下面我们可以知道,* dev 是保存主设备号,* baseminor 则是代表可以从该索引开始查找可使用的次设备号* count 代表次设备号可搜索的范围* name 则是该字符设备的名称*/
/*** alloc_chrdev_region() - register a range of char device numbers* @dev: output parameter for first assigned number* @baseminor: first of the requested range of minor numbers* @count: the number of minor numbers required* @name: the name of the associated device or driver** Allocates a range of char device numbers.  The major number will be* chosen dynamically, and returned (along with the first minor number)* in @dev.  Returns zero or a negative error code.*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);

而 alloc_chrdev_region() 是怎么来注册字符设备号的呢?下面来看看它的实现。


/** Register a single major with a specified minor range.** If major == 0 this function will dynamically allocate an unused major.* If major > 0 this function will attempt to reserve the range of minors* with given major.**/
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,int minorct, const char *name)
{struct char_device_struct *cd, *curr, *prev = NULL;int ret;int i;/* 先检查需要申请的主设备号是否在合理范围之内:0 ~ CHRDEV_MAJOR_MAX(512) - 1 */if (major >= CHRDEV_MAJOR_MAX) {pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n",name, major, CHRDEV_MAJOR_MAX-1);return ERR_PTR(-EINVAL);}/* 再检查次设备号是否在合理范围之内:0 ~ MINORMASK */if (minorct > MINORMASK + 1 - baseminor) {pr_err("CHRDEV \"%s\" minor range requested (%u-%u) is out of range of maximum range (%u-%u) for a single major\n",name, baseminor, baseminor + minorct - 1, 0, MINORMASK);return ERR_PTR(-EINVAL);}/* 申请一块内存,该内存将是保存字符设备信息的,内核通过结构体(struct char_device_struct)来存储该信息*/cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);if (cd == NULL)return ERR_PTR(-ENOMEM);mutex_lock(&chrdevs_lock);/* 如果调用该函数时传递进来的主设备号为0,则代表内核动态分配主设备号,* 此时通过 find_dynamic_major() 搜索可用的主设备号*/if (major == 0) {ret = find_dynamic_major();	/* 动态搜索可用的主设备号 */if (ret < 0) {pr_err("CHRDEV \"%s\" dynamic allocation region is full\n",name);goto out;}major = ret;	/* 保存搜索到的主设备号到major */}ret = -EBUSY;i = major_to_index(major);/* 确认主、次设备号是否可用 */for (curr = chrdevs[i]; curr; prev = curr, curr = curr->next) {if (curr->major < major)continue;if (curr->major > major)break;if (curr->baseminor + curr->minorct <= baseminor)continue;if (curr->baseminor >= baseminor + minorct)break;goto out;}/* 保存字符设备的主、次设备号、设备名称 */cd->major = major;cd->baseminor = baseminor;cd->= minorct;strlcpy(cd->name, name, sizeof(cd->name));/* 将新注册的字符设备添加到chrdevs */if (!prev) {cd->next = curr;chrdevs[i] = cd;} else {cd->next = prev->next;prev->next = cd;}mutex_unlock(&chrdevs_lock);/* 返回配置了设备号的char_device_struct */return cd;
out:mutex_unlock(&chrdevs_lock);kfree(cd);return ERR_PTR(ret);
}

在alloc_chrdev_region中有搜索可用的主设备号以及将设备信息保存到chrdevs,它们究竟是什么呢?

上面我们就问,如果是你来设计这个字符设备驱动框架,你会怎么来设计,那么内核是怎么设计的呢?

// fs/char_dev.c#define CHRDEV_MAJOR_HASH_SIZE 255static struct char_device_struct {struct char_device_struct *next;unsigned int major;unsigned int baseminor;int minorct;char name[64];struct cdev *cdev;              /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

内核定义了一个叫chrdevs的指针数组来保存字符设备信息,这个指针指向的是struct char_device_struct结构体。chrdevs一共255个成员,它们是通过主设备号major来进行排序的。在find_dynamic_major()中,搜索可用主设备号先从chrdevs数组的末端开始查找到CHRDEV_MAJOR_DYN_END(234),如果有元素为空的,则直接返回索引,找到有效的主设备号。简单说就是从 234254,从后往前找,如果数组元素为空,则返回数组索引作为主设备号。如果动态分配(234254)的都找不到了,则从314~511开始查找,注意到chrdevs只有255个元素,所以内核通过major_to_index将主设备号转为cherdev的索引,实际上就是一个chrdevs的元素分别被两个字符设备共用,比如主设备号为59的设备与主设备号为314的公用一个chrdevs元素chrdevs[59]。此时检查chrdevs的元素是否为空,为空则直接返回,如果元素中的主设备已经被填充了,则查找下一次索引的chrdevs元素。

static int find_dynamic_major(void)
{int i;struct char_device_struct *cd;/* 234~254是否为空闲的,为空闲则返回 */for (i = ARRAY_SIZE(chrdevs)-1; i >= CHRDEV_MAJOR_DYN_END; i--) {if (chrdevs[i] == NULL)return i;}/*234~254已经被占用了,从314~511开始查找 */for (i = CHRDEV_MAJOR_DYN_EXT_START;i >= CHRDEV_MAJOR_DYN_EXT_END; i--) {for (cd = chrdevs[major_to_index(i)]; cd; cd = cd->next)/* 确认chrdevs元素对应的大于255的主设备号已经被占用了,* 则从再下一个主设备开始查找,该设备号已经被占用*/if (cd->major == i)	break;if (cd == NULL)return i;}return -EBUSY;
}

上述也就是说,每个chrdevs元素可以代表两个主设备号,一个是元素的索引i,一个是i+255,如果两个都被用了,则只能从下一个元素继续查找了。

总结

内核字符设备通过全局静态指针数组chrdevs来保存字符设备信息,数组一共255个元素,每个元素可保存两个不同主设备号的字符设备信息,所以内核一个可申请512个不同的主设备号字符设备。

当驱动调用alloc_chrdev_region()注册设备号之后,终端可以看到以下的信息:

root@root:/# cat /proc/devices
Character devices:1 mem4 /dev/vc/04 tty5 /dev/tty5 /dev/console5 /dev/ptmx7 vcs10 misc13 input89 i2c90 mtd
116 alsa
128 ptm
136 pts
153 spi
180 usb
189 usb_device
247 ubi0
248 ttyS
249 hidraw
250 rpmb
251 dao		// 这里就是我们注册的字符设备,可以看到它的主设备号是251,名字叫dao
252 watchdog
253 rtc
254 gpiochipBlock devices:8 sd31 mtdblock65 sd66 sd67 sd68 sd69 sd70 sd71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
254 ubiblock
259 blkext

但是此时我们并没有给这个字符设备增加什么操作,下面我们继续看如何添加操作。

字符设备初始化

上面我们向内核申请注册了一个字符设备号,但是该设备号我们还没有将其与字符设备建立关系,那么,又是怎么建立的呢?

static dev_t            dao_devt;	// 上面注册得到的主设备号
static struct cdev      dao_cdev;	// 字符设备结构体
static const struct file_operations dao_fops = {	// 字符设备操作集
}cdev_init(&dao_cdev, &dao_fops);	// 初始化字符设备结构体,并更新其文件字符操作集
cdev_add(&dao_cdev, dao_devt, MINORMASK + 1);	// 将主设备号devt与字符设备 cdev 建立关系,实际上就是更新cdev中的主设备号成员信息。同时在cdev_add中,cdev信息保存到指针数组cdev_map中。

struct file_operations

file_operations 是干什么的呢?Linux中任意一个设备都是文件,针对文件,我们会有各种各样的操作,比如打开、读、写、关闭等,不同的设备它们上述的操作都会不一样,所以设备驱动中需要自定义好设备的操作集,方便应用层可正确使用该设备。

经过上面的操作之后,我们的字符设备已经完成了基本的初始化,在/proc/devices中可以看到以注册的字符设备号,而针对该设备操作的file_operations也已经填充,此时用户空间已经可以对该设备进行操作。

如果完成上述进行操作时,会发现/dev目录下并没有相关的字符节点,此时只能通过mknod /dev/dao c 251 0这样的命令来完成创建/dev/dao节点,执行cat /dev/dao将会看到依次调用到 file_operations 的 open、read、release函数。

那么我们需要如何操作才会在/dev目录下完成节点的注册呢?

创建设备节点

在介绍创建节点之前,我们先来了解class。在内核中,经常会看到xxx_class,内核将设备分为字符设备、块设备、网络设备,同时也会分class。有相同特性的设备为同一个class,class可以自己创建,设备会属于某一个class。那么,上面我们完成字符设备的初始化,但我们并完成将其与某个class绑定在一起。所以,创建设备节点,我们先创建一个class:

#define DAO_CLASS_NAME  "dao"
static struct class     *dao_class;/* 创建一个class,此时在/sys/class目录下看到一个叫dao的文件夹,这个就是我们注册的class */
dao_class = class_create(THIS_MODULE, DAO_CLASS_NAME); 

接着创建设备:

static struct device    *dao_dev;dao_dev = device_create(dao_class, NULL, dao_devt, NULL, DAO_NAME);函数声明:
struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...);

通过device_create,我们将dao_devt这个字符设备号与dao_class绑定在一起并创建一个设备。通过device_create函数声明我们可以知道,创建一个设备时可传入该设备的class、父设备、设备号、设备私有数据以及设备名。

device_create主要完成下面的操作:

  • 完成struct device结构体的初始化;
  • 通知平台其他总线,系统有新设备加入;
  • 创建设备uevent节点;
  • 将设备与class建立链接;
  • 增加其他设备节点信息;
  • 如果bus则加入bus;
  • 创建设备attr_dev;
  • sys目录下创建设备节点;
  • dev目录下创建设备节点;
  • 触发bus的probe;

完成上述操作之后,设备完成相应的注册,用户空间可正常的操作该设备。

框架架构图

在这里插入图片描述

例程

// SPDX-License-Identifier: GPL-2.0+
/** dao char device test code** Copyright (c) 2022, dao. All rights reserved.*/#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/cdev.h>
#include <linux/sysfs.h>
#include <linux/fs.h>#define DAO_NAME	"dao"
#define DAO_CLASS_NAME	"dao"static dev_t		dao_devt;
static struct class	*dao_class;
static struct cdev	dao_cdev;
static struct device	*dao_dev;static int dao_dev_open(struct inode *inode, struct file *file)
{pr_info("%s\n", __func__);return 0;
}static int dao_dev_release(struct inode *inode, struct file *file)
{pr_info("%s\n", __func__);return 0;
}static ssize_t dao_dev_read(struct file *file, char __user *buf, size_t count, loff_t *ptr)
{pr_info("%s\n", __func__);return 0;
}static long dao_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{pr_info("%s\n", __func__);return 0;
}static const struct file_operations dao_fops = {.owner          = THIS_MODULE,.open           = dao_dev_open,.release        = dao_dev_release,.read           = dao_dev_read,.unlocked_ioctl = dao_dev_ioctl,
};static int __init dao_dev_init(void)
{int ret = 0;ret = alloc_chrdev_region(&dao_devt, 0, MINORMASK + 1, DAO_NAME);if (ret < 0) {pr_err("Error: failed to register dao_dev, err: %d\n", ret);return ret;}cdev_init(&dao_cdev, &dao_fops);cdev_add(&dao_cdev, dao_devt, MINORMASK + 1);pr_info("%s: major %d\n", __func__, MAJOR(dao_devt));dao_class = class_create(THIS_MODULE, DAO_CLASS_NAME);if (IS_ERR(dao_class)) {pr_err("Error: failed to register dao_dev class\n");ret = PTR_ERR(dao_class);goto failed1;}dao_dev = device_create(dao_class, NULL, dao_devt, NULL, DAO_NAME);if (!dao_dev)goto failed2;return 0;failed2:class_destroy(dao_class);
failed1:cdev_del(&dao_cdev);unregister_chrdev_region(dao_devt, MINORMASK + 1);return ret;
}static void __exit dao_dev_exit(void)
{device_destroy(dao_class, dao_devt);class_destroy(dao_class);cdev_del(&dao_cdev);unregister_chrdev_region(dao_devt, MINORMASK + 1);
}module_init(dao_dev_init)
module_exit(dao_dev_exit)

驱动注册

上面介绍了设备的注册,但是驱动的注册流程又是怎样的呢?下面介绍来介绍一下,驱动注册通过driver_register()函数完成,而它主要进行下面几个事情:

  1. 驱动都是挂载在总线上的,同时驱动也是使用名字进行区分的,所以需要确认该总线上没有同样名字的驱动;
  2. 将该驱动挂载到总线上,将驱动添加到klist_drivers这个链表,然后和device进行匹配操作;
  3. 创建驱动的属性配置节点;

具体的device与driver匹配操作我们下一章节进行介绍。

相关文章:

Linux设备驱动模型之字符设备

Linux设备驱动模型之字符设备 前面我们有介绍到Linux的设备树&#xff0c;这一节我们来介绍一下字符设备驱动。字符设备是在IO传输过程中以字符为单位进行传输的设备&#xff0c;而字符设备驱动则是一段可以驱动字符设备驱动的代码&#xff0c;当前Linux中&#xff0c;字符设备…...

Kafka3.0.0版本——消费者(自动提交 offset)

目录 一、自动提交offset的相关参数二、消费者&#xff08;自动提交 offset&#xff09;代码示例 一、自动提交offset的相关参数 官网文档 参数解释 参数描述enable.auto.commi默认值为 true&#xff0c;消费者会自动周期性地向服务器提交偏移量。auto.commit.interval.ms如果…...

【业务功能116】微服务-springcloud-springboot-Kubernetes集群-k8s集群-KubeSphere-公共服务 DNS

kubernetes集群公共服务 DNS 一、软件安装 # yum -y install bind二、软件配置 # vim /etc/named.conf # cat -n /etc/named.conf1 //2 // named.conf3 //4 // Provided by Red Hat bind package to configure the ISC BIND named(8) DNS5 // server as a caching only…...

马斯洛的动机与人格、需求层次理论

马斯洛是在研究动机&#xff08;Motivation&#xff09;时&#xff0c;才提出需求层次作为理论基础来支持动机理论的。所谓动机&#xff0c;就是人类的行为到底是由什么驱动&#xff0c;其实是对人类行为的当下原动力&#xff0c;区别于过去、未来或者是有可能起作用的动力。 …...

TCP/IP网络传输模型及协议

文章目录 前言一、TCP/IP协议二、协议层报文间的封装与拆封1.发送数据2.接收数据前言 TCP/IP模型由OSI七层模型演变而来: 国际标准化组织 1984年提出了模型标准,简称 OSI(Open Systems Interconnection Model)七层模型: 物理层(Physics) :提供机械、电气、功能和过程特性…...

git 推送出现fatal: The remote end hung up unexpectedly解决方案

在使用git更新或提交项目时候出现 "fatal: The remote end hung up unexpectedly " 的报错&#xff1b; 报错的原因原因是推送的文件太大。 下面给出解决方法 方法一&#xff1a; 修改提交缓存大小为500M&#xff0c;或者更大的数字 git config --global http.po…...

Hive内置函数字典

写在前面&#xff1a;HQL同SQL有很多的类似语法&#xff0c;同学熟悉SQL后一般学习起来非常轻松&#xff0c;写一篇文章列举常用函数&#xff0c;方便查找和学习。 1. 执行模式 1.1 Batch Mode 批处理模式 当使用-e或-f选项运行$ HIVE_HOME / bin / hive时&#xff0c;它将以…...

svg 知识点总结

1. 引用 svg&#xff0c;直接用 img 标签 <img src"帐篷.svg" alt"露营">2. 画 svg 各种图形。 矩形 rect圆角矩形 rect圆圈 circle椭圆 ellipse线段 line折线 polyline多边形 polygon路径 path <svg width"200" height"250&qu…...

开源库源码分析:OkHttp源码分析(二)

开源库源码分析&#xff1a;OkHttp源码分析&#xff08;二&#xff09; 导言 上一篇文章中我们已经分析到了OkHttp对于网络请求采取了责任链模式&#xff0c;所谓责任链模式就是有多个对象都有机会处理请求&#xff0c;从而避免请求发送者和接收者之间的紧密耦合关系。这篇文章…...

校园地理信息系统的设计与实现

校园地理信息系统的设计与实现 摘 要 与传统的地图相比较&#xff0c;地理信息系统有着不可比拟的优势&#xff0c;信息量大&#xff0c;切换方便&#xff0c;可扩展性强。本文阐述了研究地理信息系统的背景、目的、方法&#xff0c;介绍了一个实用的、方便可靠的校园地理信息…...

Vulnhub实战-prime1

前言 VulnHub 是一个面向信息安全爱好者和专业人士的虚拟机&#xff08;VM&#xff09;漏洞测试平台。它提供了一系列特制的漏洞测试虚拟机镜像&#xff0c;供用户通过攻击和漏洞利用的练习来提升自己的安全技能。本次&#xff0c;我们本次测试的是prime1。 一、主机发现和端…...

Scala学习笔记

Scala学习笔记 Scala笔记一、学习Scala的目的二、Scala的基本概念2.1 JDK1.8版本的新特性2.2 Scala的运行机制 三、Scala的基本语法3.1 Scala中输出语句、键盘输入、注释语法3.1.1 Scala注释三种&#xff0c;和Java一模一样的3.1.2 Scala键盘输入3.1.3 Scala输出 3.2 Scala变量…...

虹科分享 | 软件供应链攻击如何工作?如何评估软件供应链安全?

说到应用程序和软件&#xff0c;关键词是“更多”。在数字经济需求的推动下&#xff0c;从简化业务运营到创造创新的新收入机会&#xff0c;企业越来越依赖应用程序。云本地应用程序开发更是火上浇油。然而&#xff0c;情况是双向的&#xff1a;这些应用程序通常更复杂&#xf…...

gRpc入门和springboot整合

gRpc入门和springboot整合 一、简介 1、gprc概念 gRpc是有google开源的一个高性能的pc框架&#xff0c;Stubby google内部的rpc,2015年正式开源&#xff0c;云原生时代一个RPC标准。 tips:异构系统&#xff0c;就是不同编程语言的系统。 2、grpc核心设计思路 grpc核心设计…...

基于FPGA点阵显示屏设计-毕设

本设计是一1616点阵LED电子显示屏的设计。整机以EP2C5T144C8N为主控芯片,介绍了以它为控制系统的LED点阵电子显示屏的动态设计和开发过程。通过该芯片控制一个行驱动器74HC154和两个列驱动器74HC595来驱动显示屏显示。该电子显示屏可以显示各种文字或单色图像,采用4块8 x 8点…...

Rocky9.2基于http方式搭建局域网yum源

当前负责的项目有几十台Linux服务器,在安装各类软件的时候需要大量依赖包,而项目部署的环境属于内网环境,与Internet网完全隔离,无法采用配置网络yum源的方式安装rpm包,直接在每台linux服务器上配置本地yum源也比较麻烦,而采用直接下载rpm包用rpm命令安装更是费时费力。所…...

Android 串口通讯

Serial Port Android 串口通讯 arm64-v8a、armeabi-v7a、x86、x86_64 AAR 名称操作serial.jar下载arm64-v8a下载armeabi-v7a下载x86下载x86_64下载arm-zip下载x86-zip下载 Maven 1.build.grade | setting.grade repositories {...maven { url https://jitpack.io } }2./a…...

论如何在Android中还原设计稿中的阴影

每当设计稿上注明需要添加阴影时&#xff0c;Android上总是显得比较棘手&#xff0c;因为Android的阴影实现方式与Web和iOS有所区别。 一般来说阴影通常格式是有&#xff1a; X: 在X轴的偏移度 Y: 在Y轴偏移度 Blur: 阴影的模糊半径 Color: 阴影的颜色 何为阴影 但是在A…...

Hadoop生态圈中的Flume数据日志采集工具

Hadoop生态圈中的Flume数据日志采集工具 一、数据采集的问题二、数据采集一般使用的技术三、扩展&#xff1a;通过爬虫技术采集第三方网站数据四、Flume日志采集工具概述五、Flume采集数据的时候&#xff0c;核心是编写Flume的采集脚本xxx.conf六、Flume案例实操1、采集一个网络…...

FFmpeg获取媒体文件的视频信息

视频包标志位 代码 printf("index:%d\n", in_stream->index);结果 index:0视频帧率 // avg_frame_rate: 视频帧率,单位为fps&#xff0c;表示每秒出现多少帧 printf("fps:%lffps\n", av_q2d(in_stream->avg_frame_rate));结果 fps:29.970070fps…...

大话软工笔记—需求分析概述

需求分析&#xff0c;就是要对需求调研收集到的资料信息逐个地进行拆分、研究&#xff0c;从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要&#xff0c;后续设计的依据主要来自于需求分析的成果&#xff0c;包括: 项目的目的…...

Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)

目录 1.TCP的连接管理机制&#xff08;1&#xff09;三次握手①握手过程②对握手过程的理解 &#xff08;2&#xff09;四次挥手&#xff08;3&#xff09;握手和挥手的触发&#xff08;4&#xff09;状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...

Java 加密常用的各种算法及其选择

在数字化时代&#xff0c;数据安全至关重要&#xff0c;Java 作为广泛应用的编程语言&#xff0c;提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景&#xff0c;有助于开发者在不同的业务需求中做出正确的选择。​ 一、对称加密算法…...

鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序

一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...

大模型多显卡多服务器并行计算方法与实践指南

一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...

Java入门学习详细版(一)

大家好&#xff0c;Java 学习是一个系统学习的过程&#xff0c;核心原则就是“理论 实践 坚持”&#xff0c;并且需循序渐进&#xff0c;不可过于着急&#xff0c;本篇文章推出的这份详细入门学习资料将带大家从零基础开始&#xff0c;逐步掌握 Java 的核心概念和编程技能。 …...

实现弹窗随键盘上移居中

实现弹窗随键盘上移的核心思路 在Android中&#xff0c;可以通过监听键盘的显示和隐藏事件&#xff0c;动态调整弹窗的位置。关键点在于获取键盘高度&#xff0c;并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...

ios苹果系统,js 滑动屏幕、锚定无效

现象&#xff1a;window.addEventListener监听touch无效&#xff0c;划不动屏幕&#xff0c;但是代码逻辑都有执行到。 scrollIntoView也无效。 原因&#xff1a;这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作&#xff0c;从而会影响…...

图表类系列各种样式PPT模版分享

图标图表系列PPT模版&#xff0c;柱状图PPT模版&#xff0c;线状图PPT模版&#xff0c;折线图PPT模版&#xff0c;饼状图PPT模版&#xff0c;雷达图PPT模版&#xff0c;树状图PPT模版 图表类系列各种样式PPT模版分享&#xff1a;图表系列PPT模板https://pan.quark.cn/s/20d40aa…...

精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南

精益数据分析&#xff08;97/126&#xff09;&#xff1a;邮件营销与用户参与度的关键指标优化指南 在数字化营销时代&#xff0c;邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天&#xff0c;我们将深入解析邮件打开率、网站可用性、页面参与时…...