Android MVC、MVP、MVVM三种架构的介绍和使用。

news2025/4/22 16:38:31

写在前面:现在随便出去面试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

    @Override
    protected 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);

        // 初始化 Controller
        controller = new UserController(this);

        // 按钮点击事件
        submitButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public 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; // View
    private UserModel model;  // Model

    public 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 {
    @Override
    public void login(String username, String password, LoginCallback callback) {
        // 模拟网络延迟2秒
        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public 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")
    @Override
    protected 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() {
            @Override
            public void onClick(View view) {
                String username = etUsername.getText().toString().trim();
                String password = etPassword.getText().toString().trim();
                presenter.performLogin(username, password);
            }
        });
    }

    @Override
    public void showLoading() {
        progressBar.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideLoading() {
        progressBar.setVisibility(View.GONE);
    }

    @Override
    public void showLoginSuccess(String message) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
    }

    @Override
    public 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() {
            @Override
            public void onSuccess(String user) {
                view.hideLoading();
                view.showLoginSuccess(user);
            }

            @Override
            public 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传统的开发也给我们提供了保存数据的方法,但是很简陋、不实用,只适合保存简单键值对,如下所示:以下示例为伪代码。

	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState != null) {
            // 恢复数据
            front = savedInstanceState.getInt("front", -1);
            rear = savedInstanceState.getInt("rear", -1);
        }
    }

	@Override
    protected 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: ActivityMainBinding

    override 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>
        <variable
            name="viewModel"
            type="com.htc.mvvm_kotlin.UserViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <!-- 双向绑定:EditTextViewModel 中的 userName 绑定 -->
        <EditText
            android:id="@+id/editTextName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={viewModel.userName}" />

        <!-- 单向绑定:TextView 显示 userName -->
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="@{viewModel.userName}" />

        <!-- 显示其他信息 -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="@{`User Name: ` + viewModel.user.name}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="@{`User ID: ` + viewModel.user.id}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="@{`Age: ` + viewModel.user.age}" />

        <TextView
            android: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: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        // 绑定 ViewModel
        binding.viewModel = userViewModel
        binding.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

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

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

相关文章

python使用django搭建图书管理系统

大家好,你们喜欢的梦幻编织者回来了 随着计算机网络和信息技术的不断发展&#xff0c;人类信息交流的方式从根本上发生了改变&#xff0c;计算机技术、信息化技术在各个领域都得到了广泛的应用。图书馆的规模和数量都在迅速增长&#xff0c;馆内藏书也越来越多&#xff0c;管理…

JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件

JavaScript 事件系统是构建交互式 Web 应用的核心。本文从原生 DOM 事件到 React 的合成事件&#xff0c;内容涵盖&#xff1a; JavaScript 事件基础&#xff1a;事件类型、事件注册、事件对象事件传播机制&#xff1a;捕获、目标和冒泡阶段高级事件技术&#xff1a;事件委托、…

大话机器学习三大门派:监督、无监督与强化学习

以武侠江湖为隐喻&#xff0c;系统阐述了机器学习的三大范式&#xff1a;​监督学习&#xff08;少林派&#xff09;​凭借标注数据精准建模&#xff0c;擅长图像分类等预测任务&#xff1b;无监督学习&#xff08;逍遥派&#xff09;​通过数据自组织发现隐藏规律&#xff0c;…

win11编译llama_cpp_python cuda128 RTX30/40/50版本

Geforce 50xx系显卡最低支持cuda128&#xff0c;llama_cpp_python官方源只有cpu版本&#xff0c;没有cuda版本&#xff0c;所以自己基于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介绍 风云三号气象卫星&#xff08;FY-3&#xff09;是我国自行研制的第二代极轨气象卫星&#xff0c;其有效载荷覆 盖了紫外、可见光、红外、微波等频段&#xff0c;其目标是实现全球全天候、多光谱、三维定量 探测&#xff0c;为中期数值天气预报提供卫星观测数…

Codeforces1929F Sasha and the Wedding Binary Search Tree

目录 tags中文题面输入格式输出格式样例输入样例输出说明 思路代码 tags 组合数 二叉搜索树 中文题面 定义一棵二叉搜索树满足&#xff0c;点有点权&#xff0c;左儿子的点权 ≤ \leq ≤ 根节点的点权&#xff0c;右儿子的点权 ≥ \geq ≥ 根节点的点权。 现在给定一棵 …

HBuilder X 使用 TortoiseSVN 设置快捷键方法

HBuilder X 使用 TortoiseSVN 设置快捷键方法 单文件&#xff1a;(上锁&#xff0c;解锁&#xff0c;提交&#xff0c;更新) 安装好 TortoiseSVN &#xff0c;或者 按图操作&#xff1a; 1&#xff0c;工具栏中 【自定义快捷键】 2&#xff0c;点击 默认的快捷键设置&…

Java jar包后台运行方式详解

目录 一、打包成 jar 文件二、后台运行 jar 文件三、示例四、总结在 Java 开发中,我们经常需要将应用程序打包成可执行的 jar 文件,并在后台运行。这种方式对于部署长时间运行的任务或需要持续监听事件的应用程序非常重要。本文将详细介绍如何实现 Java jar 包的后台运行,并…

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&#xff08;附官网操作手册&#xff09; wget https://repo.mysql.com//mysql80-community-release…

[项目]基于FreeRTOS的STM32四轴飞行器: 七.遥控器按键

基于FreeRTOS的STM32四轴飞行器: 七.遥控器 一.遥控器按键摇杆功能说明二.摇杆和按键的配置三.按键扫描 一.遥控器按键摇杆功能说明 两个手柄四个ADC。 左侧手柄&#xff1a; 前后推为飞控油门&#xff0c;左右推为控制飞机偏航角。 右侧手柄&#xff1a; 控制飞机飞行方向&a…

Android15使用FFmpeg解码并播放MP4视频完整示例

效果: 1.编译FFmpeg库: 下载FFmpeg-kit的源码并编译生成安装平台库 2.复制生成的FFmpeg库so文件与包含目录到自己的Android下 如果没有prebuiltLibs目录,创建一个,然后复制 包含目录只复制arm64-v8a下

安装树莓派3B+环境(嵌入式开发)

一、环境配置 1、下载树莓派镜像工具 点击进入下载连接 进入网站&#xff0c;点击下载即可。 2、配置wifi及ssh 将SD卡插入读卡器&#xff0c;再接入电脑&#xff0c;随后打开Raspberry Pi Imager下载工具&#xff0c; 选择Raspberry Pi 3 选择64位的操作系统 选择SD卡 选择…

p5.js:sound(音乐)可视化,动画显示音频高低变化

本文通过4个案例介绍了使用 p5.js 进行音乐可视化的实践&#xff0c;包括将音频振幅转化为图形、生成波形图。 承上一篇&#xff1a;vite&#xff1a;初学 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 是一个分布式的搜索和分析引擎&#xff0c;能够以近乎实时的速度存储、搜索和分析大量数据。它被广泛应用于日志分析、全文搜索、应用程序监控等场景。 本文将带你一步步在 Linux 系统上安装 Elasticsearch 7.17.23 版本&#xff0c;并完成基本的配置&#xff0…

【The Rap of China】2018

中国新说唱第一季&#xff0c;2018 2018年4月13日&#xff0c;该节目通过官方微博宣布&#xff0c;其第二季将更名为《中国新说唱》。 《中国新说唱2018》由张震岳、MC Hotdog、潘玮柏、邓紫棋、WYF 担任明星制作人&#xff1b; 艾热获得冠军、那吾克热玉素甫江获得亚军、ICE…

通义万相2.1开源版本地化部署攻略,生成视频再填利器

2025 年 2 月 25 日晚上 11&#xff1a;00 通义万相 2.1 开源发布&#xff0c;前两周太忙没空搞它&#xff0c;这个周末&#xff0c;也来本地化部署一个&#xff0c;体验生成效果如何&#xff0c;总的来说&#xff0c;它在国内文生视频、图生视频的行列处于领先位置&#xff0c…

好玩的谷歌浏览器插件-自定义谷歌浏览器光标皮肤插件-Chrome 的自定义光标

周末没有啥事 看到了一个非常有意思的插件 就是 在使用谷歌浏览器的时候&#xff0c;可以把鼠标的默认样式换一个皮肤。就像下面的这种样子。 实际谷歌浏览器插件开发对于有前端编程基础的小伙伴 还是比较容易的&#xff0c;实际也是写 html css js 。 所以这个插件使用的技术…

svn删除所有隐藏.svn文件,文件夹脱离svn控制

新建一个文件&#xff0c;取名remove-svn-folders.reg&#xff0c;输入如下内容&#xff1a; Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSVN] "Delete SVN Folders" [HKEY_LOCAL_MACHINE\SOFTWARE\Class…

六十天前端强化训练之第十二天之闭包深度解析

欢迎来到编程星辰海的博客讲解 目录 第一章&#xff1a;闭包的底层运行机制 1.1 词法环境&#xff08;Lexical Environment&#xff09;的构成JavaScript 引擎通过三个关键组件管理作用域&#xff1a; 1.2 作用域链的创建过程当函数被定义时&#xff1a; 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 模…