由浅入深,详解ViewModel的那些事

news2024/11/27 10:36:49

Hi,你好 😃

引言

关于 ViewModel ,Android 开发的小伙伴应该都非常熟悉,无论是新项目还是老项目,基本都会使用到。而 ViewModel 作为 JetPack 核心组件,其本身也更是承担着不可或缺的作用。

因此,了解 ViewModel 的设计思想更是每个应用层开发者必不可缺的基本功。

随着这两年 ViewModel 的逐步迭代,比如 SaveStateHandle 的加入等,ViewModel 也已经不是最初版本的样子。要完全理解其设计体系,往往也要伴随这其他组件的基础,所以并不是特别容易能被开发者吃透。

故本篇将以最新视角开始,与你一起,用力一瞥 ViewModel 的设计原理。

本文对应的组件版本:

  • Activity-ktx-1.5.1
  • ViewModel-ktx-2.5.1

本篇定位中等,将从背景与使用方式开始,再到源码解读。由浅入深,解析 ViewModel 的方方面面。

导航

学完本篇,你将了解或明白以下内容:

  • ViewModel 的使用方式;
  • SavedStateHandle 的使用方式;
  • ViewModel 创建与销毁流程;
  • SavedStateHandle 创建流程;

好了,让我们开始吧! 🐊

基础概念

在开始本篇前,我们先解释一些基础概念,以便更加清晰的了解后续的状态保存相关。

何谓配置变更?

配置变更指的是,应用在运行时,内置的配置参数变更从而触发的Activity重新创建

常见的场景有:旋转屏幕、深色模式切换、屏幕大小变化、更改了默认语言或者时区、更改字体大小或主题颜色等。

何谓异常重建?

异常重建指的是非配置变更情况下导致的 Activity 重新创建。

常见场景大多是因为 内存不足,从而导致后台应用被系统回收 ,当我们切换到前台时,从而触发的重建,这个机制在Android中为 Low Memory Killer 机制,简称 LMK

可以在开发者模式,限制后台任务数为1,从而测试该效果。

ViewModel存在之前的世界

ViewModel 出现之前,对于 View 逻辑与数据,我们往往都是直接存在 Activity 或者 Fragment 中,优雅一点,会细分到具体的单独类中去承载。当配置变更时,无可避免,会触发界面重绘。相应的,我们的数据在没有额外处理的情况下,往往也会被初始化,然后在界面重启时重新加载。

但如果当前页面需要维护某些状态不被丢失呢,比如 选择、上传状态 等等? 此时问题就变得棘手起来。

稍有经验同学会告诉你,在 onSaveInstanceState 中重写,使用bundle去存储相应的状态啊?➡️

但状态如果少点还可以,多一点就非常头痛,更别提包含继承关系的状态保存。☹️

所以,不出意外的话,我们 App 的 Activity-manifest 中通常默认都是下列写法:

android:configChanges="keyboard|orientation|uiMode|..."

这也是为啥Android程序普遍不支持屏幕旋转的一部分原因,从源头扼杀因部分配置变更导致的状态丢失问题。🐶保命

VideModel存在之后的世界

随着 ViewModel 组件推出之后,上述因配置变更而导致的状态丢失问题就迎刃而解。

ViewModel 可以做到在配置变更后依然持有状态。所以,在现在的开发中,我们开始将 View数据 与 逻辑 藏于 ViewModel 中,然后对外部暴漏观察者,比如我们常常会搭配 LiveData 一起使用,以此更容易的保持状态同步。

关于 ViewModel 的生命周期,具体如下图所示:

viewmodel-lifecycle

虽然 ViewModel 非常好用,但 ViewModel 也不是万能,其只能避免配置变更时避免状态丢失。比如如果我们的App是因为 内存不足 而被系统kill 掉,此时 ViewModel 也会被清除⚠️。

不过对于这种情况,仍然有以下三个方法可以依然保存我们的状态:

  • 重写 onSaveInstanceState()onRestoreInstanceState();
  • 使用 SavedState,本质上其实还是 onSaveInstanceState()
  • 使用 SavedStateHandle ,本质上是依托于 SaveState 的实现;

上述的后两种都是随着 JetPack 逐步被推出,可以理解为是对原有的onSavexx的封装简化,从而使其变得更易用。

关于这三种方法,我们会在 SavedStateHandle 流程解析中再进行具体叙述,这里先提出来,留个伏笔。

ViewModel使用方式

作为文章的开始,我们还是要先聊一聊 ViewModel 的使用方式,如下例所示:

image

当然,你也可以选择引入 activity-ktx ,从而以更简便的写法去写:

implementation 'androidx.activity:activity-ktx:1.5.1'

private val mainModel by viewModels<MainViewModel>()

示例比较简单,我们创建了一个 ViewModel ,如上所示,并在 MainActivity 的 onCreate() 中进行了初始化。

这也是我们日常的使用方式,具体我们这里就不再做阐述。

SavedStateHandle使用方式

我们知道,ViewModel 可以处理因为配置更改而导致的的状态丢失,但并不保证异常终止的情况,而官方的 SavedStateHandle 正是用于这种情况的解决方式。

SavedStateHandle ,如名所示,用于保存状态的手柄。再细化点就是,用于保存状态的工具,从而配合 ViewModel 而使用,其内部使用一个 map 保存我们要存储的状态,并且其本身使用 operator 重载了 set()get() 方法,所以对于我们来说,可以直接使用 **键值对 ** 的形式去操作我们要保存的状态,这也是官方为什么称 SavedStateHandle 是一个 具有键值映射Map 特性的原因。

在 Fragment1.2 及 Activity1.1.0 之后, SavedStateHandle 可以作为 ViewModel 的构造函数,从而反射创建带有 SavedStateHandle 的 ViewModel 。

具体使用方式如下:

SavedStateHandle

我们在 MainViewModel 构造函数中新增了一个参数 state:SavedStateHandle ,这个参数在 ViewModel 初始化时,会帮我们自动进行注入。从而我们可以利用 SavedStateHandle 以key-value的形式去保存一些 自定义状态 ,从而在进程异常终止,Act重建后,也能获取到之前保存的状态。

至于为什么能实现保存状态呢?

主要是因为 SavedStateHandle 内部默认有一个 SavedStateRegistry.SavedStateProvider 状态保存提供者对象,该对象会在我们创建ViewModel 时绑定到 SavedStateRegistry 中,从而在我们 Activity 异常重建时做到状态的 恢复 与 **绑定 ** (通过重写 onSavexx()onCreate() 方法监听)。

关于这部分内容,我们下面的源码解析部分也会再聊到,这里我们只需要知道是这么回事即可。

ViewModel源码解析

本章节,我们将从 ViewModelProvider() 开始,理清 ViewModel创建销毁 流程,从而理解其背后的 [魔法]。

不过 ViewModel 的源码其实并不是很复杂,所以别担心😉。

仔细想想,要解析ViewModel的源码,应该从哪里入手呢?

ViewModelProvider(this).get(MainViewModel::class.java)

最简单的方式还是初始化这里,所以我们直接从 ViewModelProvider() 初始化开始->

ViewModelProvider(this)

public constructor(owner: ViewModelStoreOwner)
	: this(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))

相应的,这里开始,我们就涉及到了三个方面,即 viewModelStoreFactoryExras 。所以接下来我们就顺藤摸瓜,分别看看这三处的实现细节。

owner.viewModelStore

viewmodelprovider-owner

ViewModelStoreOwner 顾名思义,用于保存 ViewModelStore 对象。

ViewModelStore 是负责维护我们 ViewModel 实例的具体类,内部有一个 map 的合集,用于保存我们创建的所有 ViewModel ,并对外提供了 clear() 方法,以 便于非配置变更时清除缓存


defaultFactory(owner)

viewmodelprovder-defaultFactory

该方法用于初始化 ViewModel 默认的创造工厂🏭 。默认有两个实现,前者是 HasDefaultViewModelProviderFactory ,也是我们 Fragment 或者 ComponentActivity 都默认实现的接口,而后者是是指全局 NewInstanceFactory

两者的不同点在于,后者只能创建 空构造函数ViewModel ,而前者没有这个限制。

示例源码:

HasDefaultViewModelProviderFactory 在 ComponentActivity 中的实现如下:

componentAct-HasDefaultViewModelProviderFactory


defaultCreationExtras(owner)

用于辅助 ViewModel 初始化时需要传入的参数,具体源码如下:

petterp-image

如上所示,默认有两个实现,前者是 HasDefaultViewModelProviderFactory ,也就是我们 ComponentActivity 实现的接口,具体的实现如下:

petterp-image

默认会帮我们注入 application 以及 intent 等,注意这里还默认使用了 getIntent().getExtras() 作为 ViewModel默认状态 ,如果我们 ViewModel 构造函数中有 SavedStateHandle 的话。

更多关于 CreationExtras 可以了解这篇 创建 ViewModel 的新方式,CreationExtras 了解一下?


get(ViewModel::xx)

从缓存中获取现有的 ViewModel 或者 反射创建 新的 ViewModel

示例源码如下:

petterp-image

当我们使用 get() 方法获取具体的 ViewModel 对象时,内部会先利用 当前包名+ViewModel类名 作为 key ,然后从 viewModelStore 中取。如果当前已创建,则直接使用;反之则调用我们的 ViewModel工厂 create() 方法创建新的 ViewModel。 创建完成后,并将其保存到 ViewModelStore 中。


create(modelClass,extras)

具体的创造逻辑里,这里的 factory 正是我们在 ViewModelProvider 初始化时,默认构造函数 defaultFactory() 方法中生成的SavedStateViewModelFactory ,所以我们直接去看这个工厂类即可。

具体源码如下:

petterp-image


create(key,modelClass)

兼容旧的版本以及用户操作行为。

petterp-image

相应的,这里我们还需要再提一下,LegacySavedStateHandleController.create() 方法:

petterp-image

当我们调用创建 ViewModel 时,内部会调用具体的 ViewModel 工厂去创建,如果当前 ViewModel 已创建,则直接返回,否则调用其 create() 方法创建新的 ViewModel 。在具体的创建方法中,需要判断当前构造函数是不是带 application 或者 SaveStateHandle ,从而调用合适的 newInstance() 方法,最后再将创建好的 ViewModel 添加到 ViewModelStore缓存 中。


销毁流程

在初始化 ViewModelProvider 时,还记得我们需要传递的 ViewModelStoreOwner 吗?

而这个接口正是被我们的 ComponentActivity 或者 Fragment 各自实现,相应的 ViewModelStore 也是存在于我们的 ComponentActivity 中,所以我们直接去看示例代码即可:

以ComponentActivity为例,具体的源码如下:

petterp-image

如上所示:在初始化Activity时,内部会使用 lifecycle 添加一个生命周期观察者,并监听 onDestory() 通知(Act销毁),如果当前销毁的原因非配置更改导致,则调用 ViewModeltore.clear() ,即清空我们的ViewModel缓存列表,从而这也是为什么 ViewModel 不支持非配置更改的实例保存。

你可能会惊讶,那还怎么借助SavedStateHandle保存状态,viewModel已经被清空了啊🤔?

如果你记得 Activity 传统处理状态的方式,此时也就能理解为什么了?因为源头都是一个地方,而 SavedStateHandle 仅仅只是一个更简便的封装而已。不过关于这个问题具体解析,我们将在下面继续进行探讨,从而理解 SavedStateHandle 的完整流程。

SavedStateHandle流程解析

关于 SavedStateHandle 的使用方法我们在上面已经叙述过了,其相关的 api 使用源码也不是我们所关注的重点,因为并不复杂,而我们主要要探讨的是其整个流程。

要摸清 SavedStateHandle 的流程,无非就两个方向,即 从何而来 ,又 **在哪里进行使用 ** 🤔。

在上面探索 ViewModel 创建流程时,我们发现,在 get(ViewModel:xx) 方法内部,最终的 create() 方法里,存在两个分支:

  1. 存在附加参数extras(viewModel2.5.0新增);
  2. 不存在附加参数extras(兼容历史版本或者用户自定义的行为);

相应的,如果 ViewModel 的构造函数中存在 SavedStateHandle ,则各自的流程如下所示:

  • CreationExtras.createSavedStateHandle()
  • LegacySavedStateHandleController.create(xx).handle

前者使用了 CreationExtras 的扩展函数 createSavedStateHandle()

petterp-image

而后者使用了 LegacySavedStateHandleController 控制器去创建:

petterp-image

总结:

上述流程中,两者大致是一样的,都需要先调用 consumeRestoredStateForKey(key) 拿到要还原的 Bundle , 再调用 SavedStateHandle.createHandle() 去创建 SavedStateHandle

SavedStateRegistry 又是什么呢?

我们的插入点也就在于此开始。

我们暂时先不关注如何还原状态,而是先搞清楚 SavedStateRegistry 是什么,它又是从哪来而传递来的。然后再来看 状态如何被还原,以及 SavedStateHandle 的创建流程,最后再搞清与 SavedStateRegistry 又是如何进行关联。


SavedStateRegistry

其是一个用于保存状态的注册表,往往由 SavedStateRegistryOwner 接口所提供实现,从而以便与拥有生命周期的组件相关联。

比如我们常用的 ComponentActivity 或者 Fragment 默认都实现了该接口。

源码如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H39nBBAk-1673327755787)(null)]

petterp-image

分析上面的代码不难发现,SavedStateRegistry 本身提供了状态 还原保存 的具体能力,并使用一个 map 保存当前所有的状态提供者,具体的状态提供者由 SavedStateProvider 接口实现。


SavedStateRegistryOwner

相当于是拥有 SavedStateRegistry 的具体类,因为本身继承了 LifecycleOwner 接口,故其也具备 生命感知 能力,如下所示:

interface SavedStateRegistryOwner : LifecycleOwner {
    val savedStateRegistry: SavedStateRegistry
}

ComponentActivity 为例,我们会发现,ComponentActivity 默认实现 SavedStateRegistryOwner 接口。即 SavedStateRegistry 的创造以及状态的保存,肯定也是 经过我们Activity转发处理(不然它自己怎么处理呢😅)。

而在上面探索 ViewModel 初始化时,我们了解到,ComponentActivity 默认实现了 HasDefaultViewModelProviderFactory 接口,用于创建ViewModel工厂 。相应的,其接口方法 getDefaultViewModelProviderFactory() 默认返回的是 SavedStateViewModelFactory ,即支持状态保存的ViewModel工厂。而该工厂构造函数中正是需要接受一个 SavedStateRegistry 变量,也正是我们 ComponentActivity 中默认保存的实例,所以也不难猜测 ViewModel工厂 是如何与 SavedStateRegistry 如何关联的。

ComponentActivity 的实现为例,源码如下:

petterp-image

ComponentActivity 初始化时,会创建一个 用于保存状态注册表的控制器 SavedStateRegistryController 对象,见面知意,不难猜出,其是用于控制 SavedStateRegistry 的具体类。并且该控制器对象会在 onCreate() 中调用 performRestore() 还原状态,并在onSaveInstanceState() 中去保存状态,此时也就解释了为什么 SavedStateRegistry 能做到状态保存。

相应的,我们还是要再去看看 SavedStateRegistryController ,以便更好的理解。


SavedStateRegistryController

用于控制 SavedStateRegistry ,对外提供了 初始化 ,状态 还原保存 等方法,如下所示:

petterp-image

简而言之,其主要用于辅助 SavedStateRegistry 进行状态保存与还原。

小结

我们再回顾一下上面的步骤,在只关心 SavedStateHandle 如何被创建这样一个大背景下,我们大致可以梳理出这样的流程:

因为我们的 ComponentActivity 或者 Fragment 默认已经实现了 SavedStateRegistryOwner 接口,而且默认是由 SavedStateRegistryController 作为 SavedStateRegistry 的具体控制,因此具体的状态保存与还原都由该控制器去操作。

当我们的 Activity 因为异常生命周期重建时,此时会回调 onSaveInstanceState() 去保存状态,此时 SavedStateRegistryController 就会调用 performSave() 去保存当前状态(即将我们ViewModel的状态保存到bundle里),然后在 Activity 重建时,在 onCreate() 方法里进行还原(即从bundle里取出我们保存的状态)。

当我们创建 ViewModel 时,默认使用的 ViewModel 工厂是支持保存状态的 SavedStateViewModelFactory 。在初始化该工厂时,需要显式传递 SavedStateRegistryOwner 接口对象到该工厂中,而该工厂的构造函数内,会将 SavedStateRegistry 自行保存起来。

最后,如果要创建的 ViewModel 需要保存状态(即构造函数中存在SavedStateHadnle),则使用保存的 SavedStateRegistry 变量去获取我们将要还原的状态,然后再调用 SavedStateHandle.createHandle() 去创建具体的 SavedStateHadnle

由此结合 ViewModel 创建的流程,我们可以总结 SavedStateRegistry 的传递流程伪代码如下:

petterp-image


SavedStateHandle如何创建

在上面,我们聊完了 SavedStateRegistry 是如何被创建以及被传递给我们的 ViewModel工厂 ,而这一小节,我们将要聊聊 SavedStateHandle 如何被创建,以及状态是如何被还原的。

我们知道,当创建 SavedStateHandle 前,需要先获取已保存的状态,也即 consumeRestoredStateForKey() 方法,所以我们本章节的插入点也就是从这里开始。

而与 consumeRestoredStateForKey() 关联的类有两个, SavedStateHandlesProviderSavedStateRegistry

前者是 viewModel(2.5.0) 新提供的 创建SavedStateHandle 的方式,后者则是用于 适配 2.5.0 之前的方式。

SavedStateHandlesProvider 为例,源码如下:

petterp-image

当我们调用 consumeRestoredStateForKey() 获取具体状态时,内部先会调用 performRestore()SavedStateRegistry 获取我们保存的状态集,然后将其保存到 provider 中。再从这个总的 状态bundle 中获取我们当前 viewModel 所对应的状态。

相应的,我们再去看看 SavedStateHandle.createHandle() 方法,即 SavedStateHandle 最终被怎么创建出来。

源码如下:

petterp-image

上述的逻辑也比较简单,具体如源码中所示,当我们创建 SavedStateHandle 时,需要先从 SavedStateRegistry 获取我们的状态Bundle,然后再调用 createHandle() 方法创建具体的 SavedStateHandle。并在其 createHandle() 内将我们传入的 bundle 转为 Map 形式,从而传入 SavedStateHandle 的构造函数中用于初始化。

总结

在这一章节,我们主要探讨的是 SavedStateHandle 的创建流程,以 ComponentActivity 为例:

我们知道 Android 中关于状态的保存与还原,官方建议使用 onSaveInstanceState()onRestoreInstanceState() ,但随着JetPack组件库的完善,官方在这两个方法的基础上新增了 SavedState ,目的是简化状态保存的成本。从原理上,其创建了一个 状态保存的的注册表 SavedStateRegistry ,内部缓存着具体的 状态提供者合集(key为string,value为SavedStateProvider)。

当我们 Activity 因为配置更改或者不可控原因需要重建时,系统此时会主动调用 onSaveInstanceState() 方法,从而触发调用 savedStateRegistry.performSave() 去保存状态。该方法内部会创建一个新的 Bundle 对象,用于保存所有状态,然后再调用所有缓存的状态提供者(SavedStateProvider)的 saveState() 方法,从而将所有需要需要保存的状态以 key-value 的方式存到 Bundle 中去。最后再将这个整体的 bundle 存入 onSaveInstanceState() 方法参数提供的 bundle 中。

当我们的 Activity 重建完成后,在 onCreate() 方法中,再使用 SavedStateRegistry 还原我们自己保存的状态 restoredState

最后当我们创建 ViewModel 时,因为我们的 ViewModel工厂(SavedStateViewModelFactory) 持有了 SavedStateRegistry ,也即持有着我们要还原的状态(如果有)。在创建具体的 ViewModel 时,如果我们要创建的 ViewModel 构造函数中存在 SavedStateHandle 参数,则该 ViewModel 支持保存状态,所以需要先去使用 SavedStateRegistry 获取我们保存的状态,最后再调用 SavedStateHandle.create() 去创建具体 SaveStateHandle ,从而创建出支持保存状态 ViewModel

结语

在本篇中,我们从 ViewModel 的背景开始,再到 ViewModelSavedStateHandle 的使用方式,最后又从源码层级分析了两者的具体流程,从而较完整的解析了 ViewModel 的底层实现与 SavedStateHandle 的整体创建流程。

至于更加详细的使用方式,这也非本篇要深入探索的细节,具体可参照其他同学的教程即可。至此,关于 ViewModel 设计思想 以及 状态保存原理 ,相信读过本篇的你也将不会有所疑问。

参阅

  • Android开发者-ViewModel
  • 创建 ViewModel 的新方式,CreationExtras 了解一下?

关于我

我是 Petterp ,一个 Android工程师 ,如果本文对你有所帮助,欢迎点赞支持,你的支持是我持续创作的最大鼓励!

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

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

相关文章

STL list 模拟实现

list 概述 相比于 vector 的连续线性空间&#xff0c;list 采用的是零散的空间&#xff0c;它的好处是每次插入或删除一个元素&#xff0c;就配置或释放一个元素空间。 list 是支持常数时间从容器任何位置插入和移除元素容器&#xff0c;但不支持快速随机访问。list 通常实现…

Linux操作系统之进程间通讯—共享内存与消息队列

文章目录一、共享内存1、共享内存的原理2、共享内存的实现三、消息队列1、消息队列原理2、消息队列实现一、共享内存 1、共享内存的原理 共享内存为多个进程之间共享和传递数据提供了一种有效的方式。共享内存是先在物理内存上申请一块空间&#xff0c;多个进程可以将其映射到…

dp (四) 打家劫舍

打家劫舍(一)_牛客题霸_牛客网 描述 你是一个经验丰富的小偷&#xff0c;准备偷沿街的一排房间&#xff0c;每个房间都存有一定的现金&#xff0c;为了防止被发现&#xff0c;你不能偷相邻的两家&#xff0c;即&#xff0c;如果偷了第一家&#xff0c;就不能再偷第二家&#…

离线和实时

离线和实时 一、数仓基本概念 1. 数据仓库架构 我们在谈数仓之前&#xff0c;为了让大家有直观的认识&#xff0c;先来谈数仓架构&#xff0c;“架构”是什么&#xff1f;这个问题从来就没有一个准确的答案。这里我们引用一段话&#xff1a;在软件行业&#xff0c;一种被普遍…

8种将pdf转化成excel的方法,亲测实用又有效!

PDF 到 Excel 的在线或离线转换工具可帮助您将原始或扫描的 PDF 文件转换为 Excel 格式。将 PDF 转换为 Excel 主要是为了获得可编辑的 Excel 文件或满足其他目标&#xff1b; 通过消除容易出错的手动复制粘贴来保持数据准确性。在需要使用 Excel 格式的大量 PDF 数据时节省时…

药物临床试验数据分析(靶点|适应症|企业|登记信息)

临床试验相关工作者在对药物进行系统性研究时都需要对药物做临床试验&#xff0c;且在对药物进行临床试验前和临床试验期间都需要对相关药物临床试验数据信息进行全面的分析及了解&#xff0c;有助于目标药物临床试验的顺利开展。药物临床试验数据覆盖非常宽泛&#xff0c;包含…

【进阶C语言】自定义类型——结构体+枚举+联合体

文章目录一.结构体1.内存对齐存在的原因规则举例2.位段二.枚举定义枚举的优点三.联合体定义特点内存计算一.结构体 1.内存对齐 存在的原因 平台原因(移植原因)&#xff1a; 不是所有的硬件平台都能访问任意地址上的任意数据的&#xff1b;某些硬件平台只能在某些地址处取某些…

【PHPWord】使用PHPWord自动生成TOC根据内容的目录完整示例 | table of contents (TOC)

目录 一、什么是Word中的目录二、目录的生成在Word中是如何操作的三、PHPWord中目录的生成1. 插入目录2.添加页码3.修改目录的字体样式4.修改目录的目录样式5.修改生成目录的标题等级四、完整示例代码和效果图一、什么是Word中的目录 在我们日常使用中,经常需要在文档中插入目…

微信小程序项目实例——心情记事本

微信小程序项目实例——心情记事本 文章目录微信小程序项目实例——心情记事本一、项目展示二、首页三、效果图文末项目代码见文字底部&#xff0c;点赞关注有惊喜 一、项目展示 心情记事本是一款可以记录当前心情和生活的记事本 用户可以选择当前的心情&#xff08;开心、平淡…

自己写一个简单的工作流引擎V1

1.需求 市面上常见的工作流组件一般都是前端通过拖拉拽配置流程图&#xff0c;后端流程引擎解析流程配置&#xff0c;这里我们手写一个简单的流程引擎&#xff0c;先实现串行流程&#xff0c;例如下&#xff1a; 小明提交了一个申请单&#xff0c;然后经过经理审批&#xff0…

【学习】Meta Learning、

文章目录一、Meta Learning什么是元学习&#xff1f;元学习–第1步元学习–第2步元学习–步骤3架构ML和Meta回顾GD学习好的初始化参数学习学习率NAS寻找网络结构data augmentationSample ReweightingFew-shot Image Classification元学习与自我监督学习元学习和知识蒸馏元学习和…

语音识别综述

语音识别的基本单位 Phoneme&#xff1a; 音位&#xff0c;音素 a unit of sound 是声音的最基本单位**&#xff0c;每个词语token的声音由多个 phoneme 组成** Grapheme&#xff08;字位&#xff09; smallest unot of a writing system 每个单词书写最基本的单位&#xff…

Vue初识系之Webpack

文章目录一 Webpack简介二 Webpack的安装和使用2.1 安装Webpack2.2 配置参数初识2.3 使用webpack一 Webpack简介 webpack本质上是一个现代JavaScript应用程序的静态模块打包器&#xff08;modulebundler&#xff09;。当webpack处理应用程序时&#xff0c;它会递归地构建一个依…

LeetCode(String)2194. Cells in a Range on an Excel Sheet

1.问题 A cell (r, c) of an excel sheet is represented as a string “” where: denotes the column number c of the cell. It is represented by alphabetical letters. For example, the 1st column is denoted by A, the 2nd by B, the 3rd by C, and so on. is the ro…

Java抽象类:概述

1.抽象类 在Java中abstract是抽象的意思&#xff0c;可以修饰类、成员方法。 abstract修饰类&#xff1a;这个类就是抽象类。 abstract修饰方法&#xff1a;这个方法就是抽象方法。 修饰符 abstract class 类名{修饰符 abstract 返回值类型 方法名(形参列表); } public ab…

助力旅游业复苏,IPIDEA让旅游资源聚合更简单

目前我国疫情防控政策的优化&#xff0c;极大的简化了出境手续&#xff0c;对于深受疫情影响的旅游业来说&#xff0c;这无疑是一个好消息。随着旅游消费需求持续的增长&#xff0c;旅游业将会逐渐进入全面复苏的新进程&#xff0c;焕发新的活力。 全球旅游市场都在关注着中国…

ABAP 内表的定义,与PERFORM传值的定义<转载>

很早之前就想总结一下内表和定义和perform的传值定义&#xff0c;结果要么没时间&#xff0c;要么有时间忘了。 今天在网上看到一个博文写的还比较清楚&#xff0c;故读书人窃来一用 ^ - ^ 原文链接&#xff1a;https://blog.csdn.net/lmf496891416/article/details/117702217 …

5 UML views and the 9+4 UML Diagrams 关系

Refer&#xff1a;UML2.5图概述-Lib教程 UML旨在通过的建模图形Diagram&#xff0c;可视化 5 种不同的视图View。 这五个视图是&#xff1a; 一、Users View : 用户视图 1. Use case Diagram&#xff1a;用例图性 二、Structural Views : 结构视图 2. Class Diagrams&#xf…

数码钢琴行业市场运行态势及投资战略规划分析

2023-2029年中国数码钢琴行业市场运行态势及投资战略规划报告 报告编号&#xff1a;1691312 免费目录下载&#xff1a;http://www.cninfo360.com/yjbg/jdhy/sxjd/20230109/1691312.html 本报告著作权归博研咨询所有&#xff0c;未经书面许可&#xff0c;任何组织和个人不得以…

C语言进阶(5)——内存操作函数的解析

1.memcpy函数 void * memcpy ( void * destination, const void * source, size_t num ); 用途&#xff1a;各种数据类型&#xff0c;从源数组拷贝num个字节到指定目标空间里面。 要点&#xff1a; &#xff08;1&#xff09;函数memcpy从source的位置开始向后复制num个字节的数…