android存储4--初始化.emulated设备的挂载

android版本:android-11.0.0_r21
http://aospxref.com/android-11.0.0_r21

android手机的挂载非常复杂。这篇文章针对emulated存储,介绍它的挂载过程。

一、为什么emulted存储要用很复杂的挂载方式

1, emulted存储是什么

android早期,手机内部flash容量比较小,为了能让手机存放更多的文件,需要通过外部存储(如SD卡)来扩展存储容量。随着技术的进步,大容量flash的价格变便宜了,手机内置大容量flash已经成常态。在大容量的flash中划出一块空间(图中绿色部分)模拟成外部存储,就可以兼容早期的flash+外置SD卡的方式了。emulated storage的容量随着用户使用动态增减,它是/data/路径下的一个目录,所以emulated storage可用空间取决于/data所在分区大小。注意,如果用户通过emulated storage占满了这个分区,那么系统启动时,一些更新、写重要系统文件的操作会因没有空间而失败,从而导致系统无法启动,所以需要限制一下emulated storage大小,解决方案可以google搜索LIMIT_SDCARD_SIZE宏。

2,为什么要用这么复杂的挂载

结论:为了实现存储的动态权限。默认情况下,应用没有权限访问共享空间,应用需要读写共享空间时,需要申请权限,征得用户同意后才能后访问文件。这就涉及到android的“运行时权限”,存储模块通过复杂的挂载来实现这个机制。大致的思想是这样的,对于共享空间,做4个bind mount,通过这几个挂载点访问共享空间的权限分别为:a无权限、b读权限、c写权限、d读写权限。应用启动的时候,按a挂载(假设没申请过权限),申请权限并经用户同意后,通过remount挂载b/c/d中的一个,比如需要写共享空间的文件,则采用“写权限”方式remount共享空间。

从android 11开始启用了分区存储(scoped storage,参考https://www.youtube.com/watch?v=UnJ3amzJM94),分区存储的目的我认为有2点:为了更好地管理外部存储空间,将同类型文件集中存储,避免各个应用在存储中乱放文件;还有就是在实现了分区存储的基础上,可以更集中统一地做权限管理。

android 11 权限设计规则如下:

  • 不需要任何权限,应用可以自由地读写自己的专属空间。比如在/data/data/包名/files目录中创建新的文件。
  • 不需要任何权限,应用可以自由地共享空间(/storage/emulated/0)中的内容,也就是可以列出共享空间中的文件。
  • 应用共享空间中的媒体文件,需要申请READ_EXTERNAL_STORAGE 。
  • 文件管理类应用申请MANAGE_EXTERNAL_STORAGE权限,可以读写共享存储空间中的所有文件
  • 非文件管理类应用,通过SAF(Storage Access Framework)访问非媒体文件或非媒体目录。

二、外部存储挂载视图

1,bind mount介绍

在正式介绍挂载视图前,需要讲一下bind mount。

绑定挂载命令:bindmount  --bind   olddir   newdir
1)将olddir绑定到newdir。绑定后,olddir和newdir看到的都是olddir中的内容(newdir中绑定前的内容变不可见)。
2)命令中的olddir对应内核mount函数的dev_name参数,newdir对应mount的dir_name参数。
3)务必注意,mount命令查看挂载详情,是看不到olddir的,取而代之的是olddir所在的device name。

比如:

cp:/data/media/0 # touch new/this-is-newdir
cp:/data/media/0 # touch old/this-is-olddir

//把/data/media/0/old绑定到/data/media/0/new
cp:/data/media/0 # mount --bind old new

//绑定后,只能看到old目录中的内容
cp:/data/media/0 # ls old/
this-is-olddir
cp:/data/media/0 # ls new/
this-is-olddir

//mount命令只能看到old所属的/dev/block/dm-9挂载到了/data/media/0/new
cp:/data/media/0 # mount |grep new
/dev/block/dm-9 on /data/media/0/new type f2fs (rw,lazytime,seclabel,nosuid,nodev,noatime,background_gc=on,discard,no_heap,user_xattr,inline_xattr,acl,inline_data,inline_dentry,extent_cache,mode=adaptive,active_logs=6,reserve_root=56683,resuid=0,resgid=1065,inlinecrypt,alloc_mode=default,fsync_mode=nobarrier

再比如:

cp:/storage/emulated/0 # mkdir old new
cp:/storage/emulated/0 # mount --bind old new

//mount命令只能看到old目录所属的/dev/fuse挂载到了/data/media/0/new
cp:/storage/emulated/0 # mount |grep new
/dev/fuse on /storage/emulated/0/new type fuse (rw,lazytime,nosuid,nodev,noexec,noatime,user_id=0,group_id=0,allow_other)

注意,下文会用到这些结论:
1,newdir看到的是olddir中的内容。
2,,mount命令看不到bindmount命令的olddir。

2,挂载视图

预置条件:使能fuse、使能sdcardfs
Note:android挂载实在很复杂,图中没有完整地画出来,也没有按照mount namespace把各命名空间的挂载情况分开画出来,比如你会看到/storage有多个bind mount(③④⑤),但任何时刻一个进程只属于一个mount space,只能看到其中一个bind mount。普通的app(不是installer app、Mediaprovider类型的),通过③将/mnt/user/0 bind mount到  /storage;installer app通过⑤将/mnt/installer/0 bind mount到 /storage;MediaProvider将/mnt/pass_through/0 bind mount到 /storage。

注意:通常,→的起始端表示source(对应bindmount命令中的olddir),终端表示target(对应bindmount命令中的newdir),但是为了画图方便,图中的起始端表示bind mount的target,比如将/mnt/user/0(source)bind mount 到/storage(target),图中表示出来就是③,所以/storage看到的是/mnt/user/0中的内容,原/storage中的内容被隐藏

app访问/sdcard的过程是这样的(内核解析路径时,lookup过程是一级一级解析路径分量名的):
1)/sdcard是个软链接,通过①链接到/storage/self/primary,所以访问/sdcard就是访问/storage/self/primary目录
2)启动app时,zygote 为app fork子进程,接着设置app的挂载命名空间,并通过③将/mnt/user/0目录bind  mount到/storage目录(com_android_internal_os_Zygote_nativeForkSystemServer --> SpecializeCommon --> MountEmulatedStorage),所以访问/storage/self/primary就是访问/mnt/user/0/self/primary目录
3)/mnt/user/0/self/primary是个软链接,链接到/storage/emulated/0(图中未画),所以访问/mnt/user/0/self/primary就是访问/storage/emulated/0目录
4)从2可知,/mnt/user/0目录(olddir)bind  mount到/storage目录(newdir),所以
访问/storage/emulated/0目录就是访问/mnt/user/0/emulated/0目录。
5)/mnt/user/0/emulated是一个挂载点,设备是/dev/fuse,见图中⑥,所以访问/mnt/user/0/emulated开头的文件(比如上面的/mnt/user/0/emulated/0),就会交给fuse文件系统处理。
6)StorageSessionController::onVolumeMount挂载外部存储时,connection.startSession将/storage/emulated(上层目录)和/data/media(底层目录)通过/dev/fuse关联起来StorageUserConnection: StorageUserConnection::startSession task:StorageManagerService sessionId: emulated;0 upperPath:/storage/emulated lowerPath:/data/media

/storage/emulated/0的访问将转换成对/data/media/0的访问。/data/media/0就是底层文件系统(f2fs/ext4)上的一个目录,通过底层文件系统就可以获取真实的数据了。

上面各种链接,各种bind mount,绕来绕去了的,是为了兼容早期的版本,以用户0为例,兼容的效果是让访问外部存储的路径变成/storage/emulated/0目录。现在版本通过getExternalStorageDirectory()获取到的路径就是“/storage/emulated/0”,这个路径的解析过程,从上面4)开始,最终定向到/data/media/0目录。

3,动态权限

提交记录:https://android.googlesource.com/platform/system/core/+/f38f29c87d97cea45d04b783bddbd969234b1030%5E%21/#F1

动态权限是通过改变app的不同视图实现的(default、read、write、full):

127|cp:/ # mount |grep runtime
/data/media on /mnt/runtime/default/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,multiuser,mask=6,derive_gid,default_normal,unshared_obb)
/data/media on /mnt/runtime/read/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=23,derive_gid,default_normal,unshared_obb)
/data/media on /mnt/runtime/write/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=7,derive_gid,default_normal,unshared_obb)
/data/media on /mnt/runtime/full/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=7,derive_gid,default_normal,unshared_obb)

 mask:八进制表示ugo(user-group-other)权限,比如006表示other的权限掩码为0110,即可读可写无执行(rw-)。

挂载点信息用户权限
bind mount目录mask
 
gidgroup用户权限other用户权限
/mnt/runtime/default006AID_SDCARD_RW
(1023)
group掩码0,不去任何权限。即有完整权限other权限掩码6,去掉读写权限,即有执行
/mnt/runtime/read023AID_EVERYBODY
(9997)
group掩码010,拿到写权限,即可读、可执行other权限掩码3,去掉写、执行权限,即可读
/mnt/runtime/write007AID_EVERYBODY
(9997)
group掩码0,不去任何权限,即有完整权限other权限掩码7,去掉读、写、执行权限,即无任何权限
/mnt/runtime/full007AID_EVERYBODY
 (9997)
group掩码0,不去任何权限,即有完整权限other权限掩码7,去掉读、写、执行权限,即无任何权限

一般的应用程序属于AID_EVERYBODY组,可对照上面表格"group用户权限"一列查看各bind mount目录视图的权限。

三、代码分析

1,app挂载命名空间的创建

启动app时,zygote通过forkAndSpecialize为app创建主线程,并通过unshare(CLONE_NEWNS)为新创建的子进程设置新的命名空间。

 forkAndSpecialize  (Zygote.java)
 --> nativeForkAndSpecialize
	 --> com_android_internal_os_Zygote_nativeForkAndSpecialize (com_android_internal_os_Zygote.cpp)
		 --> SpecializeCommon 
			 --> MountEmulatedStorage
				 --> ensureInAppMountNamespace 
					  --> unshare(CLONE_NEWNS)

fork创建进程时,父子进程共享一些“execution context”(比如命名空间),unshare指定父子进程的“execution context”相互独立。

2,/storage的bind mount

在挂载视图一节,可以看到有很多目录bind mount到了/storage(图中的③④⑤,以及未画的/mnt/androidwritable),是不是很奇怪?其实是这样的,各app是在自己的mount name space中bind mount /storage目录的,所以对于app来说,只会挂载③④⑤中的一个,最终,各app进入/storage目录看到的内容是不一样的。

 /storage挂载是这样的:

 代码见MountEmulatedStorage(com_android_internal_os_Zygote.cpp)。

// Create a private mount namespace and bind mount appropriate emulated
// storage for the given user.
static void MountEmulatedStorage(uid_t uid, jint mount_mode,
        bool force_mount_namespace,
        fail_fn_t fail_fn) {

  /*
   * 确认app的mount name space已经创建了。
   * 如果没有创建挂载命名空间,则通过unshare创建。
   * 如果创建失败,则通过fail_fnye(也就是ZygoteFailure)终止进程并报告错误。
   */
  ensureInAppMountNamespace(fail_fn);

  /* 挂载模式指定不挂载外部存储,直接返回 */
  if (mount_mode == MOUNT_EXTERNAL_NONE) {
    return;
  }

  const userid_t user_id = multiuser_get_user_id(uid);
  const std::string user_source = StringPrintf("/mnt/user/%d", user_id);

  PrepareDir(user_source, 0710, user_id ? AID_ROOT : AID_SHELL,
             multiuser_get_uid(user_id, AID_EVERYBODY), fail_fn);
  bool isFuse = GetBoolProperty(kPropFuse, false);
  bool isAppDataIsolationEnabled = GetBoolProperty(kVoldAppDataIsolation, false);

  /* persist.sys.fuse属性是否enable了,可通过getprop persist.sys.fuse查看 */
  if (isFuse) {
    if (mount_mode == MOUNT_EXTERNAL_PASS_THROUGH) {
		/* MOUNT_EXTERNAL_PASS_THROUGH = 7,MediaProvider 进入此分支。
		 * 对于MediaProvider 进程,访问 /storage 就是访问 "/mnt/pass_through/[userid]"。
         * 务必要记住MediaProvider访问/storage跟普通app访问/storage是不一样的,
         * 普通app访问/storage,经过fuse兜兜转转,交给了MediaProvider,这个时候
         * MediaProvider访问/storage访问的是/mnt/pass_through/%d目录了。否则如果 
         * MediaProvider访问跟普通app一样的/storage,那就又回头死循环了。
         */
      const std::string pass_through_source = StringPrintf("/mnt/pass_through/%d", user_id);
      PrepareDir(pass_through_source, 0710, AID_ROOT, AID_MEDIA_RW, fail_fn);
      BindMount(pass_through_source, "/storage", fail_fn);

    } else if (mount_mode == MOUNT_EXTERNAL_INSTALLER) {
        /* MOUNT_EXTERNAL_INSTALLER = 5,packageinstall 进入此分支。
         * 调试的话,可通过下面方法触发执行该分支代码
         --------------------------------------------------------------------
		cp:/ # ps -AT |grep install
		u0_a59    1823  1823   554 5404912  55800 SyS_epoll_wait    0 S ackageinstaller
		u0_a168   4990  5898   555 2440460 635332 futex_wait_queue_me 0 S split_install_t
		u0_a60    6224  6224   554 5886452 169904 SyS_epoll_wait     0 S ageinstaller.ui
		cp:/ # kill -9 1823
		这个时候logcat 可以看到自己添加的调试信息
        --------------------------------------------------------------------
		 */
      const std::string installer_source = StringPrintf("/mnt/installer/%d", user_id);
      BindMount(installer_source, "/storage", fail_fn);

    } else if (isAppDataIsolationEnabled && mount_mode == MOUNT_EXTERNAL_ANDROID_WRITABLE) {
      /*
       * 有写权限的app走这个分支,等价于命令: mount --bind /mnt/androidwritable  /storage
       * mount命令看不到olddir,也即/mnt/androidwritable。前面说过原因。
       * nsenter -t app进程tid -m mount |grep "/storage" (nsenter进入app的命名空间)
       * tmpfs on /storage type tmpfs  ……(挂载参数)
       */
      const std::string writable_source = StringPrintf("/mnt/androidwritable/%d", user_id);
      BindMount(writable_source, "/storage", fail_fn);
  
    } else {
        /*
         * 普通app走这个分支,等价于命令: mount --bind /mnt/user/0  /storage
         * * mount命令看不到olddir,也即/mnt/user/0。前面说过原因。
         * nsenter -t app进程tid -m mount |grep "/storage" (nsenter进入app的命名空间)
         * tmpfs on /storage type tmpfs  ……(挂载参数)
         */
        BindMount(user_source, "/storage", fail_fn);
    }

  } else {
    /* persist.sys.fuse没有enable的场景,挂载比较简单 */
    const std::string& storage_source = ExternalStorageViews[mount_mode];
    BindMount(storage_source, "/storage", fail_fn);

    // Mount user-specific symlink helper into place
    BindMount(user_source, "/storage/self", fail_fn);
  }
}

3,外部存储挂载流程

上一篇文章android存储3--初始化.unlock事件的处理_geshifei的博客-CSDN博客,讲了用户解锁设备后,SystemServiceManager处理unlock事件,主要有3个存储相关的service要处理解锁事件:
1)StorageManagerService$Lifecycle.onUserUnlocking
2)mStorageSessionController.onUnlockUser
3)mStoraged.onUserStarted

但只讲了怎么触发存储相关service做各自初始化工作的,涉及到具体的emulated device的挂载就没有分析了。下图展示了mVold.onUserStarted挂载emulated device的流程(图中蓝色字体是函数)。

4,动态权限处理

4.1 没有使能fuse时的场景

app访问外部存储时,弹窗申请读写权限,点击允许,代码流程如下:

PackageManagerService::grantRuntimePermission
  -> PermissionManagerService::grantRuntimePermission
        -> PermissionManagerService::grantRuntimePermissionInternal
              -> StorageManagerService::onExternalStoragePolicyChanged
                     -> StorageManagerService::remountUidExternalStorage
                            -> mVold.remountUid(uid, mode),即VoldNativeService::remountUid
                                  -> VolumeManager::remountUid

int VolumeManager::remountUid(uid_t uid, int32_t mountMode) {

    /* 使能fuse的情况,直接返回 */
    if (GetBoolProperty(android::vold::kPropFuse, false)) {
        // TODO(135341433): Implement fuse specific logic.
        return 0;
    }

    /*
     * 遍历/proc下各个进程目录,根据uid进行查找,找到pid后,
     * fork子进程进行重新挂载/mnt/runtime/XX ,setns切换mount name space
     */
    return scanProcProcesses(uid, static_cast<userid_t>(-1),
            forkAndRemountChild, &mountMode) ? 0 : -1;
}

4.2 使能fuse时的场景

待补充。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RCU 使用及机制源码的一些分析

》内核新视界文章汇总《 文章目录 1 介绍2 使用方法2.1 经典 RCU2.2 不可抢占RCU2.3 加速版不可抢占RCU2.4 链表操作的RCU版本2.5 slab 缓存支持RCU 3 源码与实现机制的简单分析3.1 数据结构3.2 不可抢占RCU3.3 加速版不可抢占RCU3.4 可抢占RCU3.5 报告禁止状态3.6 宽限期的开…

Photoshop2023beta常见问题|ps 2023测试版智能AI功能不能用如何解决?

PS beta ai创成式填充用不了怎么办 生成图像出错解决方法&#xff1f;PS 2023最新版本更新了超强大的AI功能&#xff0c;可以一键生成或删除用户选中的内容&#xff0c;这可大大提高了生成图片的效率。生成出来的图片也被公认为质量超高&#xff0c;虽然偶尔可能有点小瑕疵&…

vue2中开发时通过template中的div等标签自动输出对应的less形式带层级的class,只显示带class的

1.写完静态不是要写less吗&#xff0c;自动生成一下实现 this.getLevelClass(domId); domId是自定义的class名称&#xff0c;跟根据自己的需要设置 //vue2中开发时通过template中的div等标签自动输出对应的less形式带层级的class,只显示带class的getLevelClass(name) {let dom…

flask的配置项

flask的配置项 为了使 Flask 应用程序正常运行&#xff0c;有多种配置选项需要考虑。下面是一些基本的 Flask 配置选项&#xff1a; DEBUG: 这个配置项决定 Flask 是否应该在调试模式下运行。如果这个值被设为 True&#xff0c;Flask 将会提供更详细的错误信息&#xff0c;并…

STM32 I2C OVR 错误

一、问题 STM32 I2C 用作从机时&#xff0c;开启如下中断并启用 callback 回调函数。 每一次复位后&#xff0c;从机都可以正常触发地址匹配中断ADDR&#xff0c;之后在该中断的回调函数中启用接收中断去收取数据时&#xff0c;却无法进入RXNE中断&#xff0c;而是触发了 OVR …

Exadata磁盘损坏导致磁盘组无法mount恢复(oracle一体机磁盘组异常恢复)---惜分飞

Oracle Exadata客户,在换盘过程中,cell节点又一块磁盘损坏,导致datac1磁盘组&#xff08;该磁盘组是normal方式冗余)无法mount Thu Jul 20 22:01:21 2023 SQL> alter diskgroup datac1 mount force NOTE: cache registered group DATAC1 number1 incarn0x0728ad12 NOTE: ca…

【Spring Boot丨序列化、反序列化】

序列化、反序列化 概述Jackson 序列化和反序列化简介自定义序列化器注册外部序列化程序&#xff1a; 指定类的 Json 序列化、反序列化 主页传送门&#xff1a;&#x1f4c0; 传送 概述 序列化是将对象转换为字节序列的过程&#xff0c;而反序列化则是将字节序列恢复为对象的过…

16.Netty源码之ChannelPipeline

highlight: arduino-light 服务编排层:ChannelPipeline协调ChannelHandlerHandler EventLoop可以说是 Netty 的调度中心&#xff0c;负责监听多种事件类型&#xff1a;I/O 事件、信号事件、定时事件等&#xff0c;然而实际的业务处理逻辑则是由 ChannelPipeline 中所定义的 Cha…

【数据分析专栏之Python篇】二、Jupyer Notebook安装配置及基本使用

文章目录 前言一、Jupter Notebook是什么1.1 简介1.2 组成部分1.3 Jupyter Notebook的主要特点 二、为什么使用Jupyter Notebook?三、安装四、Jupyter Notebok配置4.1 基本配置4.2 配置开机自启与后台运行4.3 开启代码自动补全 五、两种键盘输入模式5.1 编辑模式5.2 命令模式5…

智安网络|常见的网络安全陷阱:你是否掉入了其中?

在数字化时代&#xff0c;网络安全成为了一个重要的议题。随着我们越来越多地在互联网上进行各种活动&#xff0c;诸如在线银行交易、社交媒体分享和在线购物等&#xff0c;我们的个人信息也更容易受到攻击和滥用。虽然有许多关于网络安全的指导和建议&#xff0c;但仍然有许多…

深度学习实战44-Keras框架下实现高中数学题目的智能分类功能应用

大家好,我是微学AI ,今天给大家介绍一下深度学习实战44-Keras框架实现高中数学题目的智能分类功能应用,该功能是基于人工智能技术的创新应用,通过对数学题目进行智能分类,提供个性化的学习辅助和教学支持。该功能的实现可以通过以下步骤:首先,采集大量的高中数学题目数据…

练习时长两年半的双机热备

1.双机热备技术产生的背景 传统的组网方式如下左图所示&#xff0c;内部用户和外部用户的交互报文全部通过防火墙A。如果防火墙A出现故障&#xff0c;内部网络中所有以防火墙A作为默认网关的主机与外部网络之间的通讯将中断&#xff0c;通讯可靠性无法保证。防火墙作为安全设备…

LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443

1、问题&#xff1a; https://github.com/CocoaPods/Specs.git/&#xff1a;LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443的解决办法 出现这个问题的原因基本都是代理的问题&#xff1a; 只需要加上代理就可以了&#xff1a; #http代理 git conf…

机器学习03-数据理解(小白快速理解分析Pima Indians数据集)

机器学习数据理解是指对数据集进行详细的分析和探索&#xff0c;以了解数据的结构、特征、分布和质量。数据理解是进行机器学习项目的重要第一步&#xff0c;它有助于我们对数据的基本属性有全面的了解&#xff0c;并为后续的数据预处理、特征工程和模型选择提供指导。 数据理解…

Rman配置参数详解

using target database control file instead of recovery catalog指的是使用目标数据库控制文件代替恢复目录 1、CONFIGURE RETENTION POLICY TO REDUNDANCY 1; # default 设置rman备份过期条件&#xff1a;是用来决定那些备份不再需要了&#xff0c;它一共有三种可选项&…

Pytorch(一)

目录 一、基本操作 二、自动求导机制 三、线性回归DEMO 3.1模型的读取与保存 3.2利用GPU训练时 四、常见的Tensor形式 五、Hub模块 一、基本操作 操作代码如下: import torch import numpy as np#创建一个矩阵 x1 torch.empty(5,3)# 随机值 x2 torch.rand(5,3)# 初始化…

合作客户销售数据可视化分析

以一个案例进行实际分析&#xff1a; 数据来源&#xff1a;【地区数据分析】 以此数据来制作报表。 技巧一&#xff1a;词云图 以城市名称来显示合同金额的分布&#xff0c;合同金额越大&#xff0c;则城市文字显示越大。 技巧二&#xff1a;饼图 下面制定一个&#xff0c;合…

力扣 738. 单调递增的数字

题目来源&#xff1a;https://leetcode.cn/problems/monotone-increasing-digits/description/ C题解&#xff1a;像1234就可以直接返回1234&#xff0c;像120需要从个位往高位遍历&#xff0c;2比0大&#xff0c;那么2减一成为1&#xff0c;0变成9&#xff0c;变成119。 clas…

【图像分类】CNN + Transformer 结合系列.1

介绍三篇结合使用CNNTransformer进行学习的论文&#xff1a;CvT&#xff08;ICCV2021&#xff09;&#xff0c;Mobile-Former&#xff08;CVPR2022&#xff09;&#xff0c;SegNetr&#xff08;arXiv2307&#xff09;. CvT: Introducing Convolutions to Vision Transformers, …

SpringMVC 拦截器详解

目录 一、介绍 二、过滤器与拦截器的简单对比 三、自定义拦截器 四、注册拦截器 五、案例演示-登录拦截器 5.1 自定义拦截器 5.2 注册拦截器 编写的初衷是为了自己巩固复习&#xff0c;如果能帮到你将是我的荣幸❣️ 一、介绍 SpringMVC提供的拦截器类似于JavaWeb中的过…