Android MVI框架搭建与使用
MVI框架搭建与使用
- 前言
- 正文
- 一、创建项目
- ① 配置AndroidManifest.xml
- ② 配置app的build.gradle
- 二、网络请求
- ① 生成数据类
- ② 接口类
- ③ 网络请求工具类
- 三、意图与状态
- ① 创建意图
- ② 创建状态
- 四、ViewModel
- ① 创建存储库
- ② 创建ViewModel
- ③ 创建ViewModel工厂
- 五、UI
- ① 列表适配器
- ② 数据渲染
- 六、源码
前言
有一段时间没有去写过框架了,最近新的框架MVI,其实出来有一段时间了,只不过大部分项目还没有切换过去,对于公司的老项目来说,之前的MVC、MVP也能用,没有替换的必要,而对于新建的项目来说还是可以替换成功MVVM、MVI等框架的。本文完成后的效果图:

正文
每当一个新的框架出来,都会解决掉上一个框架所存在的问题,但同时也会产生新的问题,瑕不掩瑜,可以在实际开发中,解决掉产生的问题,就能够更好的使用框架,那么MVI解决了MVVM的什么问题呢?
MVI同样是基于观察者模式,只不过数据通信方面是单向的,解决了MVVM双向通信所带来的问题,实际上MVVM也能做成单向通讯,但是这样就不是纯粹的MVVM,当然了,仁者见仁,智者见智。MVI框架适用于UI变化很多的项目,通过数据去驱动UI,MVI就是Model、View、Intent。
- Model 这里的Model有所不同,里面还包含UI的状态。
- View 还是视图,例如Activity、Fragment等。
- Intent 意图,这个和Activity的意图要区分开,我觉得说成是行为可能更妥当,表示去做什么。
多说无益,我们还是进入实操环节吧。
一、创建项目
首先创建一个名为MviDemo的项目

项目创建好了,下面我们需要先进行项目的基本配置。
① 配置AndroidManifest.xml
文章中会通过一个网络API接口,拿到数据来进行MVI框架的搭建与使用,接口地址如下:
http://service.picasso.adesk.com/v1/vertical/vertical?limit=30&skip=180&adult=false&first=0&order=hot
通过浏览器打开可以得到很多数据,如图所示:

这些数据都是JSON格式的,后面我们还会用到这些数据。因为接口使用的是http,而不是https,所以在xml文件夹下新建一个network_security_config.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config><base-config cleartextTrafficPermitted="true" />
</network-security-config>
然后在AndroidManifest.xml中的application标签中配置它,如图所示:

从Android 9.0起,默认使用https进行网络访问,如果要进行http访问则需要添加这个配置。还需要添加一个网络访问静态权限:
<uses-permission android:name="android.permission.INTERNET"/>
添加位置如下图所示:

项目正常搭建还需要一些依赖库和其他的一些设置,下面我们配置app模块下的build.gradle。
② 配置app的build.gradle
请注意,这里是配置app的build.gradle,而不是项目的build.gradle,很多人会配置错误,所以我再次强调一下,将你的项目切换到Android模式,如下图所示:

这里我标注了一下,你看到有两个build.gradle文件,两个文件的后面有灰色的文字说明,就很清楚的知道这两个build.gradle分别是项目和模块的。下面打开app模块下的build.gradle,在里面找到dependencies{}闭包,闭包中添加如下依赖:
// lifecycleimplementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'//glideimplementation 'com.github.bumptech.glide:glide:4.14.2'//retrofitimplementation 'com.squareup.retrofit2:retrofit:2.9.0'//retrofit moshiimplementation "com.squareup.retrofit2:converter-moshi:2.6.2"//moshi used KotlinJsonAdapterFactoryimplementation "com.squareup.moshi:moshi-kotlin:1.9.3"//Coroutineimplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"
添加位置如下图所示:

然后再打开viewBinding,在android{}闭包下添加如下代码:
buildFeatures {viewBinding true}
添加位置如下图所示:

添加之后你会看到右上角有一个Sync Now,点击它进行依赖的载入配置,配置好之后进入下一步,为了确保你的项目没有问题,你可以现在运行一下看看。
二、网络请求
当我们使用Kotlin时,网络访问就变得更简单了,只需要Retrofit和协程即可,首先我们在com.llw.mvidemo包下新建一个data包,然后在data包下新建一个model包,model包下我们可以通过刚才使用网页访问API拿到的JSON数据来生成一个数据类。
① 生成数据类
生成数据类,这里我们可以使用一个插件,搜索JSON To Kotlin Class,如下图所示:

下载安装之后,如果需要重启,你就重启AS,重启之后,右键点击model → New → Kotlin data class File from JSON,如图所示:

在出现的弹窗中复制通过网页请求得到的JSON数据字符串,如图所示:

这里如果觉得看起来不舒服,点击 Format 进行JSON数据格式化,然后我们需要设置数据类的名称,这里输入Wallpaper,因为我们需要使用Moshi,将JSON数据直接转成数据类,所以这里我们点击Advanced,如图所示:

这里默认是None,选择MoShi(Reflect),其他的不用更改,点击OK,此弹窗关闭,回到之前的弹窗,然后点击 Generate 生成数据类,你会发现有三个数据类,分别是Wallpaper、Res和Vertical,我们看一下Wallpaper的代码:
package com.llw.mvidemo.data.modelimport com.squareup.moshi.Jsondata class Wallpaper(@Json(name = "code")val code: Int,@Json(name = "msg")val msg: String,@Json(name = "res")val res: Res
)
这里每一个字段上都有一个@Json注解,这里是MoShi依赖库的注解,主要检查一下导包的问题,这里还有一个小故事,Google 的Gson库,算是推出比较早的,从事Gson库的开发人员,后面离职去了Square,也就是OkHttp、Retrofit的开发者。Retrofit一开始是支持Gson转换的,后面增加了MoShi的转换,Moshi拥有出色的Kotlin支持以及编译时代码生成功能,可以使应用程序更快更小。这个故事我也是听说的,你可以自己去求证,下面继续。
② 接口类
现在数据类有了,那么我们就需要根据这个数据类来写一个接口类,在com.llw.mvidemo包下新建一个network包,network包下创建一个接口类ApiService,代码如下所示:
interface ApiService {/*** 获取壁纸*/@GET("v1/vertical/vertical?limit=30&skip=180&adult=false&first=0&order=hot")suspend fun getWallPaper(): Wallpaper
}
这里属于Retrofit的使用方式,增加了协程的使用而已,就取代了RxJava的线程调度。
③ 网络请求工具类
现在有接口,下面我们来做网络请求,在network包下新建一个NetworkUtils类,代码如下:
package com.llw.mvidemo.networkimport com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory/*** 网络工具类*/
object NetworkUtils {private const val BASE_URL = "http://service.picasso.adesk.com/"/*** 通过Moshi 将JSON转为为 Kotlin 的Data class*/private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()/*** 构建Retrofit*/private fun getRetrofit() = Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(MoshiConverterFactory.create(moshi)).build()/*** 创建Api网络请求服务*/val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
由于担心你看的时候导错包,现在贴代码我会将导包的信息也贴出来,这样你总不会再导错包了吧。下面简单说明一下这个类,首先我定义了一个常量BASE_URL。作为网络接口请求的地址头,然后构建了MoShi,通过MoShi去进行JSON转Kotlin数据类的处理,之后就是构建Retrofit,将MoShi设置进去,最后就是通过Retrofit创建一个网络请求服务。
三、意图与状态
之前我们说MVI的I 是Intent,表示意图或行为,和ViewModel一样,我们在使用Intent的时候,也是一个Intent对应一个Activity/Fragment。
① 创建意图
在data包下创建一个intent包,intent包下新建一个MainIntent类,代码如下所示:
package com.llw.mvidemo.data.intent/*** 页面意图*/
sealed class MainIntent {/*** 获取壁纸*/object GetWallpaper : MainIntent()
}
这里只有一个GetWallpaper,表示获取壁纸的动作,你还可以添加其他的,例如保存图片、下载图片等,现在意图有了,下面来创建状态,一个意图有用多个状态。
② 创建状态
在data包下创建一个state包,state包下新建一个MainState类,代码如下:
package com.llw.mvidemo.data.stateimport com.llw.mvidemo.data.model.Wallpaper/*** 页面状态*/
sealed class MainState {/*** 空闲*/object Idle : MainState()/*** 加载*/object Loading : MainState()/*** 获取壁纸*/data class Wallpapers(val wallpaper: Wallpaper) : MainState()/*** 错误信息*/data class Error(val error: String) : MainState()
}
这里可以看到四个状态,获取壁纸属于其中的一个状态,通过状态可以去更改页面中的UI,后面我们会看到这一点,这里的状态你还可以再进行细分,例如每一个网络请求你可以增加一个请求中、请求成功、请求失败。
四、ViewModel
在MVI模式中,ViewModel的重要性又提高了,不过我们同样要添加Repository,作为数据存储库。
① 创建存储库
在data包下创建一个repository包,repository包下新建一个MainRepository类,代码如下:
package com.llw.mvidemo.data.repositoryimport com.llw.mvidemo.network.ApiService/*** 数据存储库*/
class MainRepository(private val apiService: ApiService) {/*** 获取壁纸*/suspend fun getWallPaper() = apiService.getWallPaper()
}
这里的代码就没什么好说的,下面我们写ViewModel,和MVVM模式中没什么两样的。
② 创建ViewModel
下面在com.llw.mvidemo包下新建一个ui包,ui包下新建一个adapter包,adapter包下新建一个MainViewModel类,代码如下:
package com.llw.mvidemo.ui.viewmodelimport androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.llw.mvidemo.data.repository.MainRepository
import com.llw.mvidemo.data.intent.MainIntent
import com.llw.mvidemo.data.state.MainState
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch/*** @link MainActivity*/
class MainViewModel(private val repository: MainRepository) : ViewModel() {//创建意图管道,容量无限大val mainIntentChannel = Channel<MainIntent>(Channel.UNLIMITED)//可变状态数据流private val _state = MutableStateFlow<MainState>(MainState.Idle)//可观察状态数据流val state: StateFlow<MainState> get() = _stateinit {viewModelScope.launch {//收集意图mainIntentChannel.consumeAsFlow().collect {when (it) {//发现意图为获取壁纸is MainIntent.GetWallpaper -> getWallpaper()}}}}/*** 获取壁纸*/private fun getWallpaper() {viewModelScope.launch {//修改状态为加载中_state.value = MainState.Loading//网络请求状态_state.value = try {//请求成功MainState.Wallpapers(repository.getWallPaper())} catch (e: Exception) {//请求失败MainState.Error(e.localizedMessage ?: "UnKnown Error")}}}
}
这里首先创建一个意图管道,然后是一个可变的状态数据流和一个不可变观察状态数据流,观察者模式。在初始化的时候就进行意图的收集,你可以理解为监听,当收集到目标意图MainIntent.GetWallpaper时就进行相应的意图处理,调用getWallpaper()函数,这里面修改可变的状态_state,而当_state发生变化,state就观察到了,就会进行相应的动作,这个通过是在View中进行,也就是Activity/Fragment中进行。这里对_state首先赋值为Loading,表示加载中,然后进行一个网络请求,结果就是成功或者失败,如果成功,则赋值Wallpapers,View中收集到这个状态后就可以进行页面数据的渲染了,请求失败,也要更改状态。
③ 创建ViewModel工厂
在viewmodel包下新建一个ViewModelFactory类,代码如下:
package com.llw.mvidemo.ui.viewmodelimport androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.llw.mvidemo.network.ApiService
import com.llw.mvidemo.data.repository.MainRepository/*** ViewModel工厂*/
class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory {override fun <T : ViewModel> create(modelClass: Class<T>): T {// 判断 MainViewModel 是不是 modelClass 的父类或接口if (modelClass.isAssignableFrom(MainViewModel::class.java)) {return MainViewModel(MainRepository(apiService)) as T}throw IllegalArgumentException("UnKnown class")}
}
五、UI
前面我们写好基本的框架内容,下面来进行使用,简单来说,请求数据然后渲染出来,因为这里请求的是壁纸数据,所以我需要写一个适配器。
① 列表适配器
在创建适配器之前首先我们需要创建一个适配器所对应的item布局,在layout下新建一个item_wallpaper_rv.xml,代码如下图所示:
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.imageview.ShapeableImageView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:id="@+id/iv_wall_paper"android:layout_width="match_parent"android:layout_height="300dp"android:layout_margin="4dp"android:scaleType="centerCrop"app:shapeAppearanceOverlay="@style/roundedImageStyle" />
这里使用了ShapeableImageView,这个控件的优势就在于可以自己设置圆角,在themes.xml中添加如下代码:
<!-- 圆角图片 --><style name="roundedImageStyle"><item name="cornerFamily">rounded</item><item name="cornerSize">24dp</item></style>
添加位置如下图所示:

下面进行我们在ui包下新建一个adapter包,adapter包下新建一个WallpaperAdapter类,里面的代码如下所示:
package com.llw.mvidemo.ui.adapterimport android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.llw.mvidemo.data.model.Vertical
import com.llw.mvidemo.databinding.ItemWallpaperRvBinding/*** 壁纸适配器*/
class WallpaperAdapter(private val verticals: ArrayList<Vertical>) :RecyclerView.Adapter<WallpaperAdapter.ViewHolder>() {fun addData(data: List<Vertical>) {verticals.addAll(data)}class ViewHolder(itemWallPaperRvBinding: ItemWallpaperRvBinding) :RecyclerView.ViewHolder(itemWallPaperRvBinding.root) {var binding: ItemWallpaperRvBindinginit {binding = itemWallPaperRvBinding}}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =ViewHolder(ItemWallpaperRvBinding.inflate(LayoutInflater.from(parent.context), parent, false))override fun getItemCount() = verticals.sizeoverride fun onBindViewHolder(holder: ViewHolder, position: Int) {//加载图片verticals[position].img.let {Glide.with(holder.itemView.context).load(it).into(holder.binding.ivWallPaper)}}
}
这里的代码相对比较简单,就不做说明了,属于适配器的基本操作了。
② 数据渲染
适配器写好之后,我们需要修改一下activity_main.xml中的内容,修改后代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".ui.MainActivity"><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/rv_wallpaper"android:layout_width="match_parent"android:layout_height="match_parent"android:paddingStart="2dp"android:paddingEnd="2dp"android:visibility="gone" /><ProgressBarandroid:id="@+id/pb_loading"android:layout_width="wrap_content"android:layout_height="wrap_content"android:visibility="gone"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><Buttonandroid:id="@+id/btn_get_wallpaper"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="获取壁纸"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>
下面我们进入MainActivity,修改里面的代码如下所示:
package com.llw.mvidemo.uiimport androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.llw.mvidemo.network.NetworkUtils
import com.llw.mvidemo.databinding.ActivityMainBinding
import com.llw.mvidemo.data.intent.MainIntent
import com.llw.mvidemo.data.state.MainState
import com.llw.mvidemo.ui.adapter.WallpaperAdapter
import com.llw.mvidemo.ui.viewmodel.MainViewModel
import com.llw.mvidemo.ui.viewmodel.ViewModelFactory
import kotlinx.coroutines.launchclass MainActivity : AppCompatActivity() {private lateinit var binding: ActivityMainBindingprivate lateinit var mainViewModel: MainViewModelprivate var wallPaperAdapter = WallpaperAdapter(arrayListOf())override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)//使用ViewBindingbinding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)//绑定ViewModelmainViewModel = ViewModelProvider(this, ViewModelFactory(NetworkUtils.apiService))[MainViewModel::class.java]//初始化initView()//观察ViewModelobserveViewModel()}/*** 观察ViewModel*/private fun observeViewModel() {lifecycleScope.launch {//状态收集mainViewModel.state.collect {when(it) {is MainState.Idle -> {}is MainState.Loading -> {binding.btnGetWallpaper.visibility = View.GONEbinding.pbLoading.visibility = View.VISIBLE}is MainState.Wallpapers -> { //数据返回binding.btnGetWallpaper.visibility = View.GONEbinding.pbLoading.visibility = View.GONEbinding.rvWallpaper.visibility = View.VISIBLEit.wallpaper.let { paper ->wallPaperAdapter.addData(paper.res.vertical)}wallPaperAdapter.notifyDataSetChanged()}is MainState.Error -> {binding.pbLoading.visibility = View.GONEbinding.btnGetWallpaper.visibility = View.VISIBLELog.d("TAG", "observeViewModel: $it.error")Toast.makeText(this@MainActivity, it.error, Toast.LENGTH_LONG).show()}}}}}/*** 初始化*/private fun initView() {//RV配置binding.rvWallpaper.apply {layoutManager = GridLayoutManager(this@MainActivity, 2)adapter = wallPaperAdapter}//按钮点击binding.btnGetWallpaper.setOnClickListener {lifecycleScope.launch{//发送意图mainViewModel.mainIntentChannel.send(MainIntent.GetWallpaper)}}}
}
说明一下,首先声明变量并在onCreate()中进行初始化,这里绑定ViewModel采用的是ViewModelProvider(),而不是ViewModelProviders.of,这是因为这个API已经被移除了,在之前的版本中是过时弃用,在最新的版本中你都找不到这个API了,所以使用ViewModelProvider(),然后通过ViewModelFactory去创建对应的MainViewModel。
initView()函数中是控件的一些配置,比如给RecyclerView添加布局管理器和设置适配器,给按钮添加点击事件,在点击的时候发送意图,发送的意图被MainViewModel中mainIntentChannel收集到,然后执行网络请求操作,此时意图的状态为Loading。
observeViewModel()函数中是对状态的收集,在状态为Loading,隐藏按钮,显示加载条,然后网络请求会有结果,如果是成功,则在UI上隐藏按钮和加载条,显示列表控件,并添加数据到适配器中,然后刷新适配器,数据就会渲染出来;如果是失败则显示按钮,隐藏加载条,打印错误信息并提示一下。这样就完成了通过状态更新UI的环节,MVI的框架就是这样设计的。
页面UI(点击事件发送意图) → ViewModel收集意图(确定内容) →ViewModel更新状态(修改_state) → 页面观察ViewModel状态(收集state,执行相关的UI)
这是一个环,从UI页面出发,最终回到UI页面中进行数据渲染,我们看看效果。

六、源码
欢迎Star 或 Fork,山高水长,后会有期~
源码地址:MviDemo
相关文章:
Android MVI框架搭建与使用
MVI框架搭建与使用前言正文一、创建项目① 配置AndroidManifest.xml② 配置app的build.gradle二、网络请求① 生成数据类② 接口类③ 网络请求工具类三、意图与状态① 创建意图② 创建状态四、ViewModel① 创建存储库② 创建ViewModel③ 创建ViewModel工厂五、UI① 列表适配器②…...
第九节 使用设备树实现RGB 灯驱动
通过上一小节的学习,我们已经能够编写简单的设备树节点,并且使用常用的of 函数从设备树中获取我们想要的节点资源。这一小节我们带领大家使用设备树编写一个简单的RGB 灯驱动程序,加深对设备树的理解。 实验说明 本节实验使用到STM32MP1 开…...
Ubuntu 系统下Docker安装与使用
Ubuntu 系统下Docker安装与使用Docker安装与使用Docker安装安装环境准备工作系统要求卸载旧版本Ubuntu 14.04 可选内核模块Ubuntu 16.04 使用 APT 安装安装 Docker CE使用脚本自动安装启动 Docker CE建立 docker 用户组测试 Docker 是否安装正确镜像加速Docker使用拉取镜像创建…...
DHCP安全及防范
DHCP安全及防范DHCP面临的威胁DHCP饿死攻击仿冒DHCP Server攻击DHCP中间人攻击DHCP Snooping技术的出现DHCP Snooping防饿死攻击DHCP Snooping防止仿冒DHCP Server攻击DHCP Snooping防止中间人攻击DHCP Snooping防止仿冒DHCP报文攻击DHCP面临的威胁 网络攻击无处不在ÿ…...
【流畅的python】第一章 Python数据模型
文章目录第一章 Python 数据模型1.1 python风格的纸牌1.2 如何使用特殊方法-通过创建一个向量类的例子1.3 特殊方法汇总第一章 Python 数据模型 python最好的品质是一致性 python解释器碰到特殊句法时,会使用特殊方法去激活一些基本的对象操作 这些特殊的方法以两个…...
from文件突然全部变为类cs右击无法显示设计界面
右击也不显示查看设计器 工程文件 .csproj中将 <Compile Include"OperatorWindows\Connection.cs" /> <Compile Include"OperatorWindows\Connection.Designer.cs"> <DependentUpon>Connection.cs</DependentUpon> &…...
使用arthas中vmtool命令查看spring容器中对象的某个属性
场景: 线上环境我想查看spring中容器某个对象的属性值 vmtool命令 方式一: vmtool --action getInstances -c [类加载器的hash] --className [目标类全路径] --limit 10 -x 2 实例:查询该类的全部属性情况(该类是一个spri…...
四种幂等性解决方案
什么是幂等性? 幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。 在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。 幂等…...
【Nacos】Nacos配置中心客户端配置更新源码分析
上文我们说了服务启动的时候从远程Nacos服务端拉取配置,这节我们来说下Nacos服务端配置的变动怎么实时通知到客户端,首先需要注册监听器。 注册监听器 NacosContextRefresher类会监听应用启动发布的ApplicationReadyEvent事件,然后进行配置…...
按钮防抖与节流-vue2
防抖与节流,应用场景有很多,例如:禁止重复提交数据的场景、搜索框输入搜索条件,待输入停止后再开始搜索。 防抖 点击button按钮,设置定时器,在规定的时间内再次点击会重置定时器重新计时,在规定…...
PyTorch学习笔记:nn.SmoothL1Loss——平滑L1损失
PyTorch学习笔记:nn.SmoothL1Loss——平滑L1损失 torch.nn.SmoothL1Loss(size_averageNone, reduceNone, reductionmean, beta1.0)功能:创建一个平滑后的L1L_1L1损失函数,即Smooth L1: l(x,y)L{l1,…,lN}Tl(x,y)L\{l_1,\dots,l…...
2年时间,涨薪20k,想拿高薪还真不能老老实实的工作...
2016年开始了我的测试生活。 2016年刚到公司的时候,我做的是测试工程师。做测试工程师是我对自己的职业规划。说实话,我能得到这份工作真的很高兴。 来公司的第一个星期,因为有一个项目缺人,所以部门经理提前结束了我的考核期&a…...
Spark - Spark SQL中RBO, CBO与AQE简单介绍
Spark SQL核心是Catalyst, Catalyst执行流程主要分4个阶段, 语句解析, 逻辑计划与优化, 物理计划与优化, 代码生成 前三个阶段都由Catalyst负责, 其中, 逻辑计划的优化采用RBO思路, 物理计划的优化采用CBO思路 RBO (Rule Based Optimization) 基于规则优化, 通过一系列预定好…...
NeurIPS/ICLR/ICML AI三大会国内高校和企业近年中稿量完整统计
点击文末公众号卡片,找对地方,轻松参会。 近日,有群友转发了一张网图,统计了近年来中国所有单位在NeurIPS、ICLR、ICML论文情况。原图如下: 中稿数100: 清华(1) 北大(2) 占比:22.6%。 累计数…...
Android IO 框架 Okio 的实现原理,到底哪里 OK?
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。 前言 大家好,我是小彭。 今天,我们来讨论一个 Square 开源的 I/O 框架 Okio,我们最开始接触到 Okio 框架还是源于 Square 家的 OkHttp 网络…...
一文讲解Linux 设备模型 kobject,kset
设备驱动模型 面试的时候,有面试官会问,什么是Linux 设备驱动模型?你要怎么回答? 这个问题,突然这么一问,可能你会愣住不知道怎么回答,因为Linux 设备驱动模型是一个比较整体的概念࿰…...
linux配置密码过期的安全策略(/etc/login.defs的解读)
长期不更换密码很容易导致密码被破解,而linux的密码过期安全策略主要在/etc/login.defs中配置。一、/etc/login.defs文件的参数解读1、/etc/login.defs文件的内容示例[rootlocalhost ~]# cat /etc/login.defs # # Please note that the parameters in this configur…...
c_character_string 字符串----我认真的弄明白了,也希望你们也是。
字符串 1. 字符串长度strlen 1.1strlen 函数介绍 size_t strlen ( const char * str );strlen ——string length strlen 的头文件是 #include <string.h> 参数指向的字符串必须要以 ‘\0’ 结束。 strlen 是求字符串长度的函数,统计的是字符串中\0之前出现…...
spring面试题 一
一、为了降低Java开发的复杂性,Spring采取了那4种关键策略 基于POJO的轻量级和最小侵入性编程; 通过依赖注入和面向接口实现松耦合; 基于切面和惯例进行声明式编程; 通过切面和模板减少样板式代码。 二、Spring框架的核心&am…...
C++中char *,char a[ ]的特殊应用
1.数组的本质 数组是多个元素的集合,在内存中分布在地址相连的单元中,所以可以通过其下标访问不同单元的元素。 2.指针 指针也是一种变量,只不过它的内存单元中保存的是一个标识其他位置的地址。 3.字符串常量的本质是它的第一个字符的地…...
业务系统对接大模型的基础方案:架构设计与关键步骤
业务系统对接大模型:架构设计与关键步骤 在当今数字化转型的浪潮中,大语言模型(LLM)已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中,不仅可以优化用户体验,还能为业务决策提供…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
React第五十七节 Router中RouterProvider使用详解及注意事项
前言 在 React Router v6.4 中,RouterProvider 是一个核心组件,用于提供基于数据路由(data routers)的新型路由方案。 它替代了传统的 <BrowserRouter>,支持更强大的数据加载和操作功能(如 loader 和…...
Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
基于当前项目通过npm包形式暴露公共组件
1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...
uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...
USB Over IP专用硬件的5个特点
USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中,从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备(如专用硬件设备),从而消除了直接物理连接的需要。USB over IP的…...
免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...
MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...
云安全与网络安全:核心区别与协同作用解析
在数字化转型的浪潮中,云安全与网络安全作为信息安全的两大支柱,常被混淆但本质不同。本文将从概念、责任分工、技术手段、威胁类型等维度深入解析两者的差异,并探讨它们的协同作用。 一、核心区别 定义与范围 网络安全:聚焦于保…...
