Android MVC、MVP、MVVM三种架构的介绍和使用。
写在前面:现在随便出去面试Android APP相关的工作,面试官基本上都会提问APP架构相关的问题,用Java、kotlin写APP的话,其实就三种架构MVC、MVP、MVVM,MVC和MVP高度相似,区别不大,MVVM则不同,引入了新的JetPack工具:ViewModel、LiveData、DataBinding,导入了“View和数据双向绑定的概念”。搞Android APP的必须把这三种架构搞清楚、搞透彻。
正式开始码字前,我们要先清楚两个词:高内聚、低耦合。相信计算机和相关专业的同学肯定都在老师的嘴里听说过这两个词,我们写Android APP为什么要用架构,其实就是为了实现这两个词代表的含义。
高内聚:Java、kotlin都是面向对象编程的语言,高内聚就是要求每个类的功能精简,类里面的每个方法精简。最好就是每个类、每个方法相对独立,对外的依赖少,功能明确。
低耦合:追求高内聚的结果必然就是低耦合,低耦合说的就是代码的不同功能模块之间,没有绝对的依赖关系,不是谁离开谁就运行不下去那种。不能出现搭积木拆了一块剩下的全塌了这种情况。
高内聚、低耦合的目的最终就是为了项目代码方便维护,后续方便功能扩展。代码架构就是为了更方便的实现高内聚、低耦合目标的代码组成方式,使用了之后你的代码就像用收纳盒规整过一样。当然高内聚、低耦合只是一个指导思想,在实际开发中我们不可能处处都能完美做到,但必须作为一个追求的目标。
下面依次介绍MVC、MVP、MVVM,搞清楚它们之间的联系和区别,以及为什么会演变出新的架构。
一、MVC
MVC主要分为三个部分,Model数据层、View视图层、Controller控制层,Model和View不直接联系,它们之间以Controller作为纽带。下面举一个简单的例子进行说明。
1.1 Model:负责数据处理
用Android Studio创建一个普通的项目。创建一个UserModel类用来模拟处理数据,当然写得很简单。
// Model - 保存用户数据
public class UserModel {private String name;// 设置用户名字public void setName(String name) {this.name = name;}// 获取用户名字public String getName() {return this.name;}
}
1.2 View:负责显示UI、更新UI和接收用户输入事件
View视图层简单来说就是Activity、Fragment、Dialog这些负责承载视图的组件,方便理解就直接把它看成Activity吧。MainActivity中我们这样写:代码都比较简单可以直接看懂。
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity {private EditText nameInput; // 输入框private Button submitButton; // 提交按钮private TextView welcomeMessage; // 显示欢迎消息private UserController controller; // Controller@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 初始化界面组件nameInput = findViewById(R.id.nameInput);submitButton = findViewById(R.id.submitButton);welcomeMessage = findViewById(R.id.welcomeMessage);// 初始化 Controllercontroller = new UserController(this);// 按钮点击事件submitButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// 获取用户输入并通过 Controller 处理controller.onSubmitButtonClicked();}});}// 显示欢迎消息public void showWelcomeMessage(String message) {welcomeMessage.setText(message);}// 获取用户输入的名字public String getUserInput() {return nameInput.getText().toString();}
}
1.3 Controller:负责将用户输入的数据传递给 Model,并通过 View 更新界面
创建一个Controller类:
// Controller - 处理业务逻辑
public class UserController {private MainActivity view; // Viewprivate UserModel model; // Modelpublic UserController(MainActivity view) {this.view = view;this.model = new UserModel();}// 处理提交按钮点击事件public void onSubmitButtonClicked() {// 从 View 获取用户输入String name = view.getUserInput();// 将输入保存到 Model 中model.setName(name);// 创建欢迎消息并通过 View 显示String welcomeMessage = "Hello, " + model.getName() + "!";view.showWelcomeMessage(welcomeMessage);}
}
1.4 总结
上述的代码我们实现了一个最简单的MVC架构,MVC的核心思想就是让View视图层和Model数据处理层完全解耦分离,中间通过Controller控制层链接,视图层只负责更新数据,不像以前把很多逻辑都塞到Activity中,导致Activity文件臃肿。可以看到Controller同时持有了Model、View的实例对象。MVC架构可以用下面这张图做一个形象的表示:
点击下载 Android MVC架构示例Demo https://github.com/xuhao120833/MVC
二、MVP
MVP包含三个部分:Model、View、Presenter。和MVC相比用Presenter替代了Controller,在MVC中Controller同时持有了Model、View的实例对象,起到中间人的作用,但是这种形式的缺点在于,换一个View就得新建一个Controller,Controller无法复用,大大增加了代码量,于是MVP更进一步,Presenter持有的不再是Model、View的实例对象,而是一个接口引用,这样一来Presenter就可以得到复用,进一步解耦了Model和Presenter的关系、View和Presenter的关系。
我们举一个模拟登录界面的例子来说明MVP架构。
2.1 创建接口
Android Studio新建一个名称为MVP的项目,之后新建如下三个Java接口:ILoginModel、LoginCallback、LoginView。
ILoginModel接口用于不同的Model实现。
package com.htc.mvp;public interface ILoginModel {// 定义登录方法void login(String username, String password, LoginCallback callback);
}
LoginCallback接口,在调用ILoginModel.login方法时实现的回调。
package com.htc.mvp;// 回调接口,用于通知登录结果
public interface LoginCallback {void onSuccess(String user);void onFailure(String error);
}
LoginView接口用于不同的View去实现。
package com.htc.mvp;public interface LoginView {// 显示加载动画void showLoading();// 隐藏加载动画void hideLoading();// 登录成功时显示消息void showLoginSuccess(String message);// 登录失败时显示错误void showLoginError(String error);
}
2.2 LoginModel实现ILoginModel接口
package com.htc.mvp;import android.os.Handler;
import android.os.Looper;public class LoginModel implements ILoginModel {@Overridepublic void login(String username, String password, LoginCallback callback) {// 模拟网络延迟2秒new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {@Overridepublic void run() {if ("admin".equals(username) && "123456".equals(password)) {callback.onSuccess("欢迎您, " + username + "!");} else {callback.onFailure("用户名或密码错误");}}}, 2000);}
}
2.3 MainActivity实现LoginView接口
package com.htc.mvp;import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity implements LoginView {private LoginPresenter presenter;private EditText etUsername;private EditText etPassword;private Button btnLogin;private ProgressBar progressBar;@SuppressLint("MissingInflatedId")@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);etUsername = findViewById(R.id.etUsername);etPassword = findViewById(R.id.etPassword);btnLogin = findViewById(R.id.btnLogin);progressBar = findViewById(R.id.progressBar);// 使用构造函数注入 LoginModel 实例presenter = new LoginPresenter(this, new LoginModel());btnLogin.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {String username = etUsername.getText().toString().trim();String password = etPassword.getText().toString().trim();presenter.performLogin(username, password);}});}@Overridepublic void showLoading() {progressBar.setVisibility(View.VISIBLE);}@Overridepublic void hideLoading() {progressBar.setVisibility(View.GONE);}@Overridepublic void showLoginSuccess(String message) {Toast.makeText(this, message, Toast.LENGTH_LONG).show();}@Overridepublic void showLoginError(String error) {Toast.makeText(this, error, Toast.LENGTH_LONG).show();}
}
2.4 实现LoginPresenter
package com.htc.mvp;public class LoginPresenter {private LoginView view;private ILoginModel model;// 通过构造方法传入 ILoginModel 的实例,可以在外部进行依赖注入public LoginPresenter(LoginView view, ILoginModel model) {this.view = view;this.model = model;}// 执行登录操作public void performLogin(String username, String password) {if (username == null || username.isEmpty() || password == null || password.isEmpty()) {view.showLoginError("用户名和密码不能为空");return;}view.showLoading();model.login(username, password, new LoginCallback() {@Overridepublic void onSuccess(String user) {view.hideLoading();view.showLoginSuccess(user);}@Overridepublic void onFailure(String error) {view.hideLoading();view.showLoginError(error);}});}
}
2.5 总结
可以看到MVP模式中,Model、View中用到的方法都被抽象到接口中了,而Presenter只持有了Model、View的接口引用,保证了Presenter可以得到复用。MVP架构可以用下面这张图做一个形象的表示:创建Presenter的时候传的是View、Model的实例,但是Presenter保存的却是View、Model的接口引用,这就保证了Presenter可以调用不同的View和Model,保证了Presenter的复用,这是MVP相较于MVC进步最大的地方。还有一个不常提的优势是,由于Presenter持有的是接口引用,就很方便进行单元测试,不用创建真的View、Model,可以使用Mockito或类似的库模拟传入Presenter进行测试。
注:图中的虚线表示通过接口进行方法调用。
点击下载Android MVP架构示例Demo:https://github.com/xuhao120833/MVP
三、MVVM
MVVM架构有三大要素:Model、View、ViewModel。相较于MVC、MVP,MVVM迎来了很大的变化,ViewModel不直接持有View的接口引用或者实例对象,而是通过DataBinding或者LiveData.observe来更新UI。MVVM的学习难度更高,引入了三个新的技术:ViewModel、DataBinding、LiveData。下面依次介绍三个新的小伙伴。
3.1 ViewModel:
ViewModel是JetPack androidx.lifecycle仓库中的组件,存在的意义主要是帮助开发者有效保存UI界面数据,看它所属于的仓库路径包含lifecycle,就知道它和“生命周期”强相关。上述解释很抽象,必须举个例子理解一下。
Android开发者都知道,当系统语言、屏幕方向(横屏)、主题等发生变化时,会触发当前Activity重建重新执行onCreate,如果你把和UI相关的数据放在Activity中,数据就会被重新赋值导致数据丢失。倘若你的UI界面复杂,数据很多,那么数据丢失带来的结果将是毁灭性的。Android传统的开发也给我们提供了保存数据的方法,但是很简陋、不实用,只适合保存简单键值对,如下所示:以下示例为伪代码。
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);if (savedInstanceState != null) {// 恢复数据front = savedInstanceState.getInt("front", -1);rear = savedInstanceState.getInt("rear", -1);}}@Overrideprotected void onSaveInstanceState(Bundle outState) {//onSaveInstanceState() → onPause() → onStop() → onDestroy()super.onSaveInstanceState(outState);//保存数据到 Bundle,避免系统设置如语言改变时,首页midlleApp显示的位置复位。if (circularQueue != null) {outState.putInt("front", circularQueue.front); // 保存头指针outState.putInt("rear", circularQueue.rear); // 保存尾指针Log.d(TAG,"onSaveInstanceState 保存头尾指针 front "+circularQueue.front+" rear"+circularQueue.rear);}}
注:Bundle savedInstanceState就是Android原生提供的保存数据的方法。用起来也很简单,就是当系统属性发生变化时,Activity被重建前会先回调到onSaveInstanceState方法,如果有需要保存的数据就用Bundle outState进行保存,Activity重新执行到onCreate时又通过Bundle savedInstanceState把数据取出来使用,达到防止数据丢失的作用。
但是通过Bundle来保存数据也有致命的缺陷:1、它只能保存下面的类型:简单的基本类型键值对;List<T>、ArrayList<T>实例对象,T必须实现Parcelable接口,也就是必须是可序列化、反序列化的类。2、Bundle最大只能保存1MB的数据,很小,超过限制会崩溃。就是因为有这些缺点,我们必须引入ViewModel,它可以保存大量数据、几乎所有类型数据都能保存、生命周期和Activity/Fragment无关。 Bundle 和ViewModel区别如下图:

总结:为什么要用ViewModel? ——》数据放在ViewModel中,ViewModel有独立的生命周期,Activity/Fragment意外被销毁时,和它没有关系,保存在其中的数据不会被重新赋值,它可以保存大量数据,囊括几乎所有类型,完美解决了Bundle savedInstanceState的致命缺陷。
3.2 LiveData
LiveData也是JetPack androidx.lifecycle仓库下提供的一个组件,和生命周期这个概念也是强相关,当然这次不是它自己的什么周期,而是观察者的生命周期。这话听起来很拗口,我们接下来慢慢说。
LiveData是用来保存数据的,它最核心的设计思想是“观察者模式”,也就是它可以被Activty/Fragment观察,当它保存的数据发生变化时自动通知所有的观察者更新UI。说到这里有同学就问了:“那这和普通的观察者模式也没区别呀?只不过代替码农封装了通知观察者的过程,把这个过程隐藏了而已。”LiveData不仅如此,它最牛的地方来了:可以多个Activity/Fragment同时观察一个LiveData数据,LiveData数据发生变化时,会根据传进来的LifecycleOwner,提前获悉所有观察者的生命周期状态,只有处在 “活跃期”的,它才会通知,已经销毁的还会自动解绑。 完全不用程序员自己操心,避免了Activity/Fragment已经被销毁,但是依然是观察者,数据改变依然需要通知导致的内存泄漏。下面我总结一下LiveData的特点:
<1> 观察者生命周期感知:上面说了,它只会通知“活跃期”的观察者更新UI,那么怎么定义活跃期?很简单,就是用户能用眼睛看到的就是处在活跃期的观察者。
<2>自动处理线程:处理耗时任务传统的写法是开一个线程后台处理,处理完之后如果数据需要用来更新UI,那么还得手动切换到主线程,比如runOnUiThread。LiveData就不需要这么麻烦,它提供了两个更新数据的方法,一个setValue用于在主线程直接更新数据,立马通知活跃观察者;一个postValue用于在其它线程更新数据,postValue的工作逻辑是,把数据更新的任务自动放到主线程的工作队列中,等到主线程执行到了这个任务再去更新数据——》然后通知活跃的观察者更新UI。
<3>避免内存泄:LiveData 是弱引用的,这意味着当 Activity 或 Fragment 被销毁时,它的观察者会自动被解除,不会导致内存泄漏。你不需要手动移除观察者。
<4>常见类型:常见的有LiveData和MutableLiveData两种,LiveData中的数据只读,MutableLiveData的数据可读可写。
<5>支持保存几乎所有数据类型
说了这么多,接下来用kotlin看看一个最简单的Livedata如何使用:
注:以下代码用伪代码展示//ViewModel类中使用MutableLiveData保存数据
class UserViewModel : ViewModel() {val user = MutableLiveData<User>()val userName = MutableLiveData<String>()fun update_user() {user.value = User(id = 1, name = userName.value.toString(), age = 30)//kotlin中的.value = 就是Java中setValue在主线程更新LiveData数据的意思,这里是简写}. . . . . .
}//MainActivity 中观察userName的变化
class MainActivity : AppCompatActivity() {companion object {private const val TAG = "MainActivity"}private val userViewModel: UserViewModel by viewModels() // ✅ 移到类内部private lateinit var binding: ActivityMainBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 观察 LiveData 数据变化userViewModel.userName.observe(this) { userName ->// 这里可以处理一些更新逻辑Log.d(TAG,"userViewModel.user.observe binding.livedata.setText"+userName)userViewModel.update_user()}}. . . . . .
}
总结:综合看下来LiveData更像是一个托管工具人,它够强大也很好用,专门用来更新UI,自动管理线程,避免内存泄漏,是一个半自动化工具。
3.3 DataBinding
如果说LiveData是一个半自动化工具的话,那么DataBinding就是全自动化工具。强如LiveData最后也需要通过binding.livedata.setText(userName+" Livedata使用")这种形式来更新UI,DataBinding直接把userViewModel.userName.observe(this)这种观察到变化再更新的模式直接抛弃了,它直接到layout布局文件给UI组件绑定数据。
其它特点和LiveData高度相似,也是只会更新活跃期的Activity/Fragment。我们需要注意的是,DataBinding的绑定方式分为两种:
<1> 双向绑定:数据变化会导致UI变化,UI变化也会导致数据变化,可以用EditText编辑文字来模拟。
<2> 单向绑定:只有数据变化才会导致UI变化,反过来却不行。下面举个kotlin例子介绍如何使用:
想要使用DataBinding,第一步需要在build.gradl.kts中打开databinding开关:
android {. . . . . .buildFeatures {dataBinding = true}
}
第二步,去layout布局文件中,结合ViewModel(用一般的数据类也行,这里只是用ViewModel结合举例)绑定UI和数据。@={ 双向绑定,@{ 单向绑定
<layout xmlns:android="http://schemas.android.com/apk/res/android"><data><variablename="viewModel"type="com.htc.mvvm_kotlin.UserViewModel" /></data><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><!-- 双向绑定:EditText 与 ViewModel 中的 userName 绑定 --><EditTextandroid:id="@+id/editTextName"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="@={viewModel.userName}" /><!-- 单向绑定:TextView 显示 userName --><EditTextandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="20dp"android:text="@{viewModel.userName}" /><!-- 显示其他信息 --><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="20dp"android:text="@{`User Name: ` + viewModel.user.name}" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="20dp"android:text="@{`User ID: ` + viewModel.user.id}" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="20dp"android:text="@{`Age: ` + viewModel.user.age}" /><TextViewandroid:id="@+id/livedata"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="20dp"android:text="LiveData" /></LinearLayout>
</layout>
第三步,Activty/Fragment中加载使用。
class MainActivity : AppCompatActivity() {companion object {private const val TAG = "MainActivity"}private val userViewModel: UserViewModel by viewModels() // ✅ 移到类内部private lateinit var binding: ActivityMainBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = DataBindingUtil.setContentView(this, R.layout.activity_main)// 绑定 ViewModelbinding.viewModel = userViewModelbinding.lifecycleOwner = this// 观察 LiveData 数据变化userViewModel.userName.observe(this) { userName ->// 这里可以处理一些更新逻辑Log.d(TAG,"userViewModel.user.observe binding.livedata.setText"+userName)binding.livedata.setText(userName+" Livedata使用")userViewModel.update_user()}}
}
总结:DataBinding是一个绑定UI、自动更新UI的工具,很省事,中间过程几乎都省略了,但是我个人是不推荐使用DataBinding双向绑定的,因为如果一旦UI更新出错,那么将是致命的,报错信息很少,几乎无法排错。使用起来也容易出错,导致编译不过。MVVM推荐的使用方式是:ViewModel保存数据 + LiveData绑定UI更新 + DataBinding单向绑定UI更新。LiveData.observe留给程序员更多自主可控的空间。
3.4 总结
MVVM引入了LiveData、DataBinding、ViewModel这些强大的工具,旨在进一步解耦代码,ViewModel不直接持有View层的实例对象或者接口引用,而是通过DataBinding绑定、LiveData.oberverve这些方式来更新UI。MVVM相较于MVC、MVP来说,带来了数据持久化、更加解耦、防止内存泄漏等诸多进步,可以用下面的图片来简单表示这种结构:ViewModel和Model之间即有双箭头实线又有双箭头虚线,意思是ViewModel即可以持有Model的引用(虚线),也可以持有Model的实例对象(实线)。
可以看到同一个ViewModel的LiveData可以被多个Activity/Fragment observe或者DataBinding。
特别注意:DataBinding绑定的不一定是ViewModel,普通的数据类也行;ViewModel中也不是只能用LiveData;LiveData离开了ViewModel也可以正常使用。它们三个是互相独立的,可单独使用,别混为一潭。它们各有特点,在MVC、MVP架构中也可以单独导入使用。只是它们结合起来共同构成了MVVM的完整形态。
3.5 Demo APP下载
MVVM架构推荐使用Kotlin编写,更简洁,和JetPack组件结合得更好。这里Koltin、Java的实现都一起给出。
3.5.1 Kotlin Demo APP下载
点击下载Kotlin Demo APP ,GitHub链接:https://github.com/xuhao120833/MVVM_Kotlin
3.5.2 Java Demo APP下载
点击下载Java Demo APP ,GitHub链接:https://github.com/xuhao120833/MVVM_Java
四、总结
总的看起来,MVVM比MVC、MVP先进得多,但是也较为复杂,容易出错。如果你写的APP对性能、内存这些要求没那么高,完全可以不用MVVM。最后还是一句话:没有最好的架构,只有最适合的架构。熟练掌握一种,就能工作了。但是对所有架构的持续学习是必须要做的。
注:还没来得及校对,如果有错别字和表达不清的地方还请见谅,后续会陆续改正的——2025.3.6
注:校对已完成——2025.3.7
相关文章:
Android MVC、MVP、MVVM三种架构的介绍和使用。
写在前面:现在随便出去面试Android APP相关的工作,面试官基本上都会提问APP架构相关的问题,用Java、kotlin写APP的话,其实就三种架构MVC、MVP、MVVM,MVC和MVP高度相似,区别不大,MVVM则不同&…...
python使用django搭建图书管理系统
大家好,你们喜欢的梦幻编织者回来了 随着计算机网络和信息技术的不断发展,人类信息交流的方式从根本上发生了改变,计算机技术、信息化技术在各个领域都得到了广泛的应用。图书馆的规模和数量都在迅速增长,馆内藏书也越来越多,管理…...
JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件
JavaScript 事件系统是构建交互式 Web 应用的核心。本文从原生 DOM 事件到 React 的合成事件,内容涵盖: JavaScript 事件基础:事件类型、事件注册、事件对象事件传播机制:捕获、目标和冒泡阶段高级事件技术:事件委托、…...
大话机器学习三大门派:监督、无监督与强化学习
以武侠江湖为隐喻,系统阐述了机器学习的三大范式:监督学习(少林派)凭借标注数据精准建模,擅长图像分类等预测任务;无监督学习(逍遥派)通过数据自组织发现隐藏规律,…...
win11编译llama_cpp_python cuda128 RTX30/40/50版本
Geforce 50xx系显卡最低支持cuda128,llama_cpp_python官方源只有cpu版本,没有cuda版本,所以自己基于0.3.5版本源码编译一个RTX 30xx/40xx/50xx版本。 1. 前置条件 1. 访问https://developer.download.nvidia.cn/compute/cuda/12.8.0/local_…...
FY-3D MWRI亮温绘制
1、FY-3D MWRI介绍 风云三号气象卫星(FY-3)是我国自行研制的第二代极轨气象卫星,其有效载荷覆 盖了紫外、可见光、红外、微波等频段,其目标是实现全球全天候、多光谱、三维定量 探测,为中期数值天气预报提供卫星观测数…...
Codeforces1929F Sasha and the Wedding Binary Search Tree
目录 tags中文题面输入格式输出格式样例输入样例输出说明 思路代码 tags 组合数 二叉搜索树 中文题面 定义一棵二叉搜索树满足,点有点权,左儿子的点权 ≤ \leq ≤ 根节点的点权,右儿子的点权 ≥ \geq ≥ 根节点的点权。 现在给定一棵 …...
HBuilder X 使用 TortoiseSVN 设置快捷键方法
HBuilder X 使用 TortoiseSVN 设置快捷键方法 单文件:(上锁,解锁,提交,更新) 安装好 TortoiseSVN ,或者 按图操作: 1,工具栏中 【自定义快捷键】 2,点击 默认的快捷键设置&…...
Java jar包后台运行方式详解
目录 一、打包成 jar 文件二、后台运行 jar 文件三、示例四、总结在 Java 开发中,我们经常需要将应用程序打包成可执行的 jar 文件,并在后台运行。这种方式对于部署长时间运行的任务或需要持续监听事件的应用程序非常重要。本文将详细介绍如何实现 Java jar 包的后台运行,并…...
Refreshtoken 前端 安全 前端安全方面
网络安全 前端不需要过硬的网络安全方面的知识,但是能够了解大多数的网络安全,并且可以进行简单的防御前两三个是需要的 介绍一下常见的安全问题,解决方式,和小的Demo,希望大家喜欢 网络安全汇总 XSSCSRF点击劫持SQL注入OS注入请求劫持DDOS 在我看来,前端可以了解并且防御前…...
Mysql5.7-yum安装和更改mysql数据存放路径-2020年记录
记录下官网里用yum rpm源安装mysql, 1 官网下载rpm https://dev.mysql.com/downloads/repo/yum/ https://dev.mysql.com/doc/refman/5.7/en/linux-installation-yum-repo.html(附官网操作手册) wget https://repo.mysql.com//mysql80-community-release…...
[项目]基于FreeRTOS的STM32四轴飞行器: 七.遥控器按键
基于FreeRTOS的STM32四轴飞行器: 七.遥控器 一.遥控器按键摇杆功能说明二.摇杆和按键的配置三.按键扫描 一.遥控器按键摇杆功能说明 两个手柄四个ADC。 左侧手柄: 前后推为飞控油门,左右推为控制飞机偏航角。 右侧手柄: 控制飞机飞行方向&a…...
Android15使用FFmpeg解码并播放MP4视频完整示例
效果: 1.编译FFmpeg库: 下载FFmpeg-kit的源码并编译生成安装平台库 2.复制生成的FFmpeg库so文件与包含目录到自己的Android下 如果没有prebuiltLibs目录,创建一个,然后复制 包含目录只复制arm64-v8a下...
numpy常用函数详解
在深度神经网络代码中经常用到numpy库的一些函数,很多看过之后很容易忘记,本文对经常使用的函数进行归纳总结。 np.arange arange是numpy一个常用的函数,该函数主要用于创建等差数列。它的使用方法如下所示: numpy.arange([star…...
安装树莓派3B+环境(嵌入式开发)
一、环境配置 1、下载树莓派镜像工具 点击进入下载连接 进入网站,点击下载即可。 2、配置wifi及ssh 将SD卡插入读卡器,再接入电脑,随后打开Raspberry Pi Imager下载工具, 选择Raspberry Pi 3 选择64位的操作系统 选择SD卡 选择…...
深度学习/强化学习调参技巧
深度调优策略 1. 学习率调整 技巧:学习率是最重要的超参数之一。过大可能导致训练不稳定,过小则收敛速度慢。可以使用学习率衰减(Learning Rate Decay)或自适应学习率方法(如Adam、RMSprop)来动态调整学习…...
p5.js:sound(音乐)可视化,动画显示音频高低变化
本文通过4个案例介绍了使用 p5.js 进行音乐可视化的实践,包括将音频振幅转化为图形、生成波形图。 承上一篇:vite:初学 p5.js demo 画圆圈 cd p5-demo copy .\node_modules\p5\lib\p5.min.js . copy .\node_modules\p5\lib\addons\p5.soun…...
Linux下安装elasticsearch(Elasticsearch 7.17.23)
Elasticsearch 是一个分布式的搜索和分析引擎,能够以近乎实时的速度存储、搜索和分析大量数据。它被广泛应用于日志分析、全文搜索、应用程序监控等场景。 本文将带你一步步在 Linux 系统上安装 Elasticsearch 7.17.23 版本,并完成基本的配置࿰…...
plt和cv2有不同的图像表示方式和颜色通道顺序
在处理图像时,matplotlib.pyplot (简称 plt) 和 OpenCV (简称 cv2) 有不同的图像表示方式和颜色通道顺序。了解这些区别对于正确处理和显示图像非常重要。 1. 图像形状和颜色通道顺序 matplotlib.pyplot (plt) 形状:plt 通常使用 (height, width, cha…...
【The Rap of China】2018
中国新说唱第一季,2018 2018年4月13日,该节目通过官方微博宣布,其第二季将更名为《中国新说唱》。 《中国新说唱2018》由张震岳、MC Hotdog、潘玮柏、邓紫棋、WYF 担任明星制作人; 艾热获得冠军、那吾克热玉素甫江获得亚军、ICE…...
通义万相2.1开源版本地化部署攻略,生成视频再填利器
2025 年 2 月 25 日晚上 11:00 通义万相 2.1 开源发布,前两周太忙没空搞它,这个周末,也来本地化部署一个,体验生成效果如何,总的来说,它在国内文生视频、图生视频的行列处于领先位置,…...
YOLOv10改进之MHAF(多分支辅助特征金字塔)
YOLOv10架构 YOLOv10的架构主要由 主干网络、特征金字塔和预测头 三部分组成。主干网络采用改进的Darknet结构,增强特征提取能力。特征金字塔模块使用多尺度特征融合技术,提高对不同大小目标的检测效果。预测头则负责生成最终的检测结果。这种结构设计使得YOLOv10在保持高效…...
好玩的谷歌浏览器插件-自定义谷歌浏览器光标皮肤插件-Chrome 的自定义光标
周末没有啥事 看到了一个非常有意思的插件 就是 在使用谷歌浏览器的时候,可以把鼠标的默认样式换一个皮肤。就像下面的这种样子。 实际谷歌浏览器插件开发对于有前端编程基础的小伙伴 还是比较容易的,实际也是写 html css js 。 所以这个插件使用的技术…...
svn删除所有隐藏.svn文件,文件夹脱离svn控制
新建一个文件,取名remove-svn-folders.reg,输入如下内容: Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSVN] "Delete SVN Folders" [HKEY_LOCAL_MACHINE\SOFTWARE\Class…...
六十天前端强化训练之第十二天之闭包深度解析
欢迎来到编程星辰海的博客讲解 目录 第一章:闭包的底层运行机制 1.1 词法环境(Lexical Environment)的构成JavaScript 引擎通过三个关键组件管理作用域: 1.2 作用域链的创建过程当函数被定义时: 1.3 闭包变量的生命…...
DeepSeek R1-32B医疗大模型的完整微调实战分析(全码版)
DeepSeek R1-32B微调实战指南 ├── 1. 环境准备 │ ├── 1.1 硬件配置 │ │ ├─ 全参数微调:4*A100 80GB │ │ └─ LoRA微调:单卡24GB │ ├── 1.2 软件依赖 │ │ ├─ PyTorch 2.1.2+CUDA │ │ └─ Unsloth/ColossalAI │ └── 1.3 模…...
10.2 继承与多态
文章目录 继承多态 继承 继承的作用是代码复用。派生类自动获得基类的除私有成员外的一切。基类描述一般特性,派生类提供更丰富的属性和行为。在构造派生类时,其基类构造函数先被调用,然后是派生类构造函数。在析构时顺序刚好相反。 // 基类…...
[网络爬虫] 动态网页抓取 — Selenium 元素定位
🌟想系统化学习爬虫技术?看看这个:[数据抓取] Python 网络爬虫 - 学习手册-CSDN博客 在使用 Selenium 时,往往需要先定位到指定元素,然后再执行相应的操作。例如,再向文本输入框中输入文字之前,…...
静态网页的爬虫(以电影天堂为例)
一、电影天堂的网址(url) 电影天堂_免费电影_迅雷电影下载_电影天堂网最好的迅雷电影下载网,分享最新电影,高清电影、综艺、动漫、电视剧等下载!https://dydytt.net/index.htm 我们要爬取这个页面上的内容 二、代码…...
将图片存储至阿里云 OSS
将图片存储至阿里云 OSS 一、概述 在项目开发中,我们常常需要处理用户上传的图片。本文将介绍如何使用前端的 el-upload 组件将照片上传到后端,后端再将照片存储到阿里云 OSS,并最终返回图片的 URL 给前端。 二、前端实现 1. 安装依赖 确…...


