MVVM下的Jetpack核心组件

news2025/2/12 15:53:00

前言

Jetpack 架构组件及 “标准化开发模式” 确立,意味着Android 开发已步入成熟阶段,只有对 MVVM 确有深入理解,才能自然而然写出标准化、规范化代码。

本次笔者会浅入浅出的介绍以下内容,由于它是一个我的学习总结记录,所以比较适合对MVVM不是很熟悉,但又想了解下全貌的读者:

  • Jetpack MVVM
  • Jetpack Lifecycle
  • Jetpack LiveData
  • Jetpack ViewModel
  • Jetpack DataBinding

Jetpack MVVM

在正文开始前,先回顾下MVP

MVP,Model-View-Presenter,职责分类如下:

  • Model,数据模型层,用于获取和存储数据。
  • View,视图层,即Activity/Fragment
  • Presenter,控制层,负责业务逻辑。

我们知道,MVP是对MVC的改进,解决了MVC的两个问题:

  • View责任明确,逻辑不再写在Activity中,放到了Presenter中;
  • Model不再持有View

MVP最常用的实现方式是这样的:

View层接收到用户操作事件,通知到PresenterPresenter进行逻辑处理,然后通知Model更新数据,Model 把更新的数据给到PresenterPresenter再通知到View 更新界面。

MVP本质是面向接口编程,它也存在一些痛点:

  • 会引入大量的IViewIPresenter接口,增加实现的复杂度。
  • ViewPresenter相互持有,形成耦合。

随着发展,Jetpack MVVM 就应势而生,它是MVVM 模式在Android 开发中的一个具体实现,是Google 官方提供并推荐的MVVM实现方式。它的分层:

  • Model层:用于获取和存储数据
  • View层:即Activity/Fragment
  • ViewModel层:负责业务逻辑

MVVM的核心是 数据驱动,把解耦做的更彻底(ViewModel不持有view )。

View 产生事件,使用ViewModel进行逻辑处理后,通知Model更新数据,Model把更新的数据给ViewModelViewModel自动通知View更新界面

Jetpack Lifecycle

起源

在没有Lifecycle之前,生命周期的管理都是靠手工维持。比如我们经常会在ActivityonStart初始化某些成员(比如MVPPresenterMediaPlayer)等,然后在onStop中释放这些成员的内部资源。

class MyActivity extends AppCompatActivity {
    private MyPresenter presenter;
​
    public void onStart(...) {
        presenter= new MyPresenter ();
        presenter.start();
    }
​
    public void onStop() {
        super.onStop();
        presenter.stop();
    }
}
​
class MyPresenter{
    public MyPresenter() {
    }
  
    void start(){
       // 耗时操作
      checkUserStatus{
        if (result) {
          myLocationListener.start();
        }
      }
    }
​
    void stop() {
      // 释放资源
      myLocationListener.stop();
    }
}

上述的代码本身是没有太大问题的。它的缺点在于实际生产环境下,会有很多的页面和组件需要响应生命周期的状态变化,就得在生命周期方法中放置大量的代码,这样的方式就会导致代码(如 onStart()onStop())变得臃肿,难以维护。

除此之外还有一个问题就是:

MyPresenter类中onStart里的checkUserStatus是个耗时操作,如果耗时过长,Activity 销毁的时候,还没有执行过来,就已经stop了,然后等一会儿执行过来的时候,myLocationListenerstart,但后面不会再有myLocationListenerstop,这样这个组件的资源就不能正常释放了。如果它内部还持有Activity的引用,还会造成内存泄露。

Lifecycle

于是,Lifecycle就出来了,它通过 “模板方法模式” 和 “观察者模式”,将生命周期管理的复杂操作,放到LifecycleOwner(如 Activity、Fragment 等 “视图控制器” 基类)中封装好。

对于开发者来说,在 “视图控制器” 的类中只需一句 getLifecycle().addObserver(new MyObserver()) ,当Lifecycle的生命周期发生变化时,MyObserver就可以在自己内部感知到。

protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_lifecycle);
   // 使MyObserver感知生命周期
   getLifecycle().addObserver(new MyObserver());
}

看看它是怎么实现的:

# ComponentActivity
private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);   
public Lifecycle getLifecycle() {
    return mLifecycleRegistry;
}
​
# LifecycleRegistry
public LifecycleRegistry(@NonNull LifecycleOwner provider) {
    this(provider, true);
}
​
private FastSafeIterableMap<LifecycleObserver, ObserverWithState> mObserverMap =
            new FastSafeIterableMap<>();
public void addObserver(@NonNull LifecycleObserver observer) {
  mObserverMap.putIfAbsent(observer, statefulObserver);
  ...
}
public void removeObserver(@NonNull LifecycleObserver observer) {
   mObserverMap.remove(observer);
}
​
void dispatchEvent(LifecycleOwner owner, Event event) {
  State newState = event.getTargetState();
  mState = min(mState, newState);
  mLifecycleObserver.onStateChanged(owner, event);
  mState = newState;
}

正因为Activity实现了LifecycleOwner,所以才能直接使用getLifecycle()

# ComponentActivity
protected void onCreate(@Nullable Bundle savedInstanceState) {
    // 关键代码:通过ReportFragment完成生命周期事件分发
    ReportFragment.injectIfNeededIn(this); 
    if (mContentLayoutId != 0) {
        setContentView(mContentLayoutId);
    }
}
# ReportFragment
static void dispatch(@NonNull Activity activity, @NonNull Lifecycle.Event event) {
    if (activity instanceof LifecycleOwner) {
        Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
        if (lifecycle instanceof LifecycleRegistry) {
          // 处理生命周期事件,更新当前都状态并通知所有的注册的LifecycleObserver
          ((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
        }
    }
}
​
# LifecycleRegistry
public void handleLifecycleEvent(@NonNull Lifecycle.Event event) {
    enforceMainThreadIfNeeded("handleLifecycleEvent");
    moveToState(event.getTargetState());
}

小结

所以Lifecycle 的存在,是为了解决 “生命周期管理” 一致性的问题。

Jetpack LiveData

起源

在没有LiveData的时候,我们在网络请求回调、跨页面通信等场景分发消息,大多是通过EventBus、接口callback的方式去完成。

比如经常使用的EventBus等消息总线的方式会有问题:

它缺乏一种约束,当我们去使用时,很容易因为随处使用,最后追溯数据来源的难度就会很大。

另外,EventBus在处理生命周期上也很麻烦,由于需要手动去控制,会容易出现生命周期管理不一致的问题。

LiveData

先看下官方的介绍:

LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意味着它遵循其他应用组件(如 Activity/Fragment)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。

如果观察者的生命周期处于 STARTEDRESUMED状态,则 LiveData 会认为该观察者处于活跃状态,就会将更新通知给活跃的观察者,非活跃的观察者不会收到更改通知。

LiveData观察者模式 的体现,先从LiveDataobserve方法看起:

public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
  // LifecycleOwner是DESTROYED状态,直接忽略
  if (owner.getLifecycle().getCurrentState() == DESTROYED) {
      return;
  }
  // 绑定生命周期的Observer
  LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
  ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
  // 让该Observer可以感知生命周期
  owner.getLifecycle().addObserver(wrapper);
}

observeForeverobserve()类似,只不过它会认为观察者一直是活跃状态,不会自动移除观察者。

LiveData很重要的一部分就是数据更新:·

LiveData原生的API提供了2种方式供开发者更新数据, 分别是setValue()postValue(),调用它们都会 触发观察者并更新UI

setValue()方法必须在 主线程 进行调用,而postValue()方法更适合在 子线程 中进行调用。postValue()最终也会调用setValue,只需要看下setValue方法就可以了:

protected void setValue(T value) {
  assertMainThread("setValue");
  mVersion++;
  mData = value;
  dispatchingValue(null);
}
​
void dispatchingValue(@Nullable ObserverWrapper initiator) {
  ...
  for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
          mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
      considerNotify(iterator.next().getValue());
  }
}
​
private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    ...
    observer.mObserver.onChanged((T) mData);
}

小问题:我们在使用LiveData有一个优势是不会发生内存泄漏,是怎么做到的呢?

这需要从上面提到的observe方法中寻找答案

public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
  LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
  owner.getLifecycle().addObserver(wrapper);
}

传递的第一个是 LifecycleOwner,第二个参数Obserser实际就是我们的观察后的回调。这两个参数被封装成了LifecycleBoundObserver对象。

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
        @NonNull Lifecycle.Event event) {
    Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
    if (currentState == DESTROYED) {
      // Destoryed状态下,自动移除mObserver,避免内存泄漏
      removeObserver(mObserver);
      return;
    }
    activeStateChanged(shouldBeActive());
    ...
 }

这里就解释了为什么LiveData能够 自动解除订阅而避免内存泄漏 了,因为它内部能够感应到Activity或者Fragment的生命周期。

PS:这种设计非常巧妙,给我们一个启发点:

在我们初识 Lifecycle 组件对它不是理解很透彻的时候,总是下意识认为它能够对大的对象进行有效生命周期的管理(比如Presenter),实际上,这种生命周期的管理我们完全可以应用到各个功能的基础组件中,比如大到吃内存的MediaPlayer、绘制设计复杂的自定义View,小到随处可见的LiveData,都可以通过实现LifecycleObserver接口达到感应生命周期的能力,并内部释放重资源的目的。

小结

LiveData在感知生命周期的能力下,让应用数据发生变化时通过观察者去更新界面,并且不会出现内存泄露的情况。

Jetpack ViewModel

起源

在没有ViewModel,我们用MVP开发的时候,我们为了实现数据在UI上的展示,往往会写很多UI层和Model层相互调用的代码,这些代码写起来繁琐且一定程度的模版化。另外,某些场景(例如屏幕旋转)销毁和重新创建界面,那么存储在其中的界面相关数据都会丢失,一般都需要手动存储和恢复。

为了解决这两个痛点,ViewModel就出场,用ViewModel用于代替MVP中的Presenter

ViewModel 的概念就是这样被提出来的,它就像一个 状态存储器 ,存储着UI中各种各样的状态。

ViewModel的好处

1.更规范化的抽象接口

Google官方建议ViewModel尽量保证 纯的业务代码,不要持有任何View层(Activity或者Fragment)或Lifecycle的引用,这样保证了ViewModel内部代码的可测试性,避免因为Context等相关的引用导致测试代码的难以编写(比如,MVPPresenter层代码的测试就需要额外成本,比如依赖注入或者Mock,以保证单元测试的进行)。

也正是这样的规范要求,ViewModel不能持有UI层引用,自然也就避免了可能发生的内存泄漏。

2.更便于保存数据

当组件被销毁并重建后,原来组件相关的数据也会丢失。最简单的例子就是屏幕的旋转,如果数据类型比较简单,同时数据量也不大,可以通过onSaveInstanceState()存储数据,组件重建之后通过onCreate(),从中读取Bundle恢复数据。但如果是大量数据,不方便序列化及反序列化,则上述方法将不适用。

ViewModel的扩展类则会在这种情况下自动保留其数据,如果Activity被重新创建了,它会收到被之前相同ViewModel实例。当所属Activity终止后,框架调用ViewModelonCleared()方法释放对应资源。

3.更方便UI组件之间的通信

一个Activity中的多个Fragment相互通讯是很常见的,如果ViewModel的实例化作用域为Activity的生命周期,则两个Fragment可以持有同一个ViewModel的实例,这也就意味着数据状态的共享

接下来,分析它的源码是怎么做到这些的:

我们可以通过ViewModelProvider注入ViewModelStoreOwner,从而为引用ViewModel 的页面(比如Activity)创建一个临时的、单独的 ViewModelProvider 实例。并通过这个ViewModelProvider可以获取到ViewModel

# this: ViewModelStoreOwner(interface)
ViewModelProvider(this).get(viewModelClass)

分创建、获取两步来看,先看创建ViewModelProvider做了什么:

# ViewModelProvider 
public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
  // owner.getViewModelStore(),比如:owner是ComponentActivity
  this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
          ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
          : NewInstanceFactory.getInstance());
}
​
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
    mFactory = factory;
    mViewModelStore = store;
}
​
public interface ViewModelStoreOwner {
    ViewModelStore getViewModelStore();
}
​
# ComponentActivity implements ViewModelStoreOwner
public ViewModelStore getViewModelStore() {
    // 为空就创建
    ensureViewModelStore();
    return mViewModelStore;
}
​
void ensureViewModelStore() {
 if (mViewModelStore == null) {
     mViewModelStore = new ViewModelStore();
  }
}

这一步是基石:把ViewModelStoreOwnermViewModelStore绑定到了ViewModelProvider中。简单点说就是同一个ViewModelStoreOwner拿到的是同一个mViewModelStore

如何获取对应的ViewModel

# ViewModelProvider
private final ViewModelStore mViewModelStore;
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
    String canonicalName = modelClass.getCanonicalName();
    return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}
​
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
    ViewModel viewModel = mViewModelStore.get(key);
    // 直接返回已存在的viewModel
    if (modelClass.isInstance(viewModel)) {
        return (T) viewModel;
    }
    if (mFactory instanceof KeyedFactory) {
        viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
    } else {
        viewModel = mFactory.create(modelClass);
    }
    // 存储viewModel
    mViewModelStore.put(key, viewModel);
    return (T) viewModel;
}
​
# ViewModelStore
public class ViewModelStore {
  private final HashMap<String, ViewModel> mMap = new HashMap<>();
  final void put(String key, ViewModel viewModel) {
    ViewModel oldViewModel = mMap.put(key, viewModel);
    if (oldViewModel != null) {
        oldViewModel.onCleared();
    }
  }
}

即通过这样的设计,来实现类似于单例的效果:每个页面都可以通过ViewModelProvider 注入Activity 这个ViewModelStoreOwner,来共享跨页面的状态;

同时,又不至于完全沦为简单粗暴的单例:每个页面都可以通过 ViewModelProvider 注入this,来管理私有的状态。

比如下面这个具体的例子:

当应用中某个ViewModel 存在既被ViewModelProvider 传入过 Activity,又被传入过某个 Fragmentthis 情况,实际上是生成了两个不同的 ViewModel实例,属于不同的 ViewModelStoreOwner。当引用被this 持有的ViewModel 的 页面destory 时,被Activity 持有的ViewModel 的页面并不受影响。

小结

ViewModel是为了解决 “状态管理” 和 “页面通信” 问题。有了ViewModel,我们在开发的时候,可以大幅减少UI层和Model层相互调用的代码,将更多的重心投入到业务代码的编写

Jetpack DataBinding

起源

DataBinding 出现以前,想要更新视图就要引用该视图,然后调用setxxx方法:

TextView textView = findViewById(R.id.sample_text);
if (textView != null && viewModel != null) {
    textView.setText(viewModel.getUserName());
}

这种方式有几个不好的地方:

  • 容易出现空指针(存在差异的横、竖两种布局,如横屏存在此 textView 控件,而竖屏没有),引用该视图一般要先判空
  • 需要写模板代码 findViewById
  • 业务复杂的话,一个控件会在多处调用

DataBinding

DataBinding是个受争议比较大的组件。很多人对 DataBinding 的认知就是在xml中写逻辑:

  • xml中写表达式逻辑,出错了debug不了
  • 逻辑写在xml里面的话 xml 就承担了 Presenter/ViewModel 的职责,职责变得混乱了

当然如果站在把逻辑写在xml中的角度看,确实会造成xml中是不能调试的、职责混乱。

但这不是DataBinding的本质。DataBinding,含义是 数据绑定,即 布局中的控件可观察的数据 进行绑定。

<TextView
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="@{user.name}"/>

user.nameset 新值时,被绑定了该数据的控件即可获得通知和刷新。就是说,在使用DataBinding 后,唯一的改变是,你无需手动调用视图来 set 新状态,你只需 set 数据本身。

所以,DataBinding 并非是将 UI 逻辑搬到 XML 中写导致而难以调试 ,它只负责绑定数据,将 UI 控件与其需要的终态数据进行绑定。

双向绑定

上面介绍的例子,数据的流向是单向的,只需要监听到数据的变更然后展示到UI上,是个单向绑定。

但有些场景,UI的变化需要影响到ViewModel层的数据状态,比如UI层的EditText,对它进行编辑并需要更新LiveData的数据。这时就需要 双向绑定

Android原生控件中,绝大多数的双向绑定使用场景,DataBinding都已经帮我们实现好了,比如EditText

<EditText
  android:id="@+id/etPassword"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:text="@={fragment.viewModel.password }" />

相比单向绑定,只需要多一个=符号,就能保证View层和ViewModel层的 状态同步

双向绑定使用起来很简单,但定义却稍微比单向绑定麻烦一些,即使原生的控件DataBinding已经帮助我们实现好了,对于三方的控件或者自定义控件,还需要我们自己实现

举个栗子

这里举个下拉刷新SwipeRefreshLayout的例子,来看看双向绑定是怎么实现的:

我们的需求时:当我们为LiveData手动设置值时,SwipeRefreshLayout的UI也会发生对应的变更;反之,当用户手动下拉执行刷新操作时,LiveData的值也会对应的变成为true(代表刷新中的状态):

// refreshing实际是一个LiveData:
val refreshing: MutableLiveData<Boolean> = MutableLiveData()
​
object SwipeRefreshLayoutBinding {
  // 1.@BindingAdapter 在数据发生更改时要执行的操作:
  // 每当LiveData的状态发生了变更,SwipeRefreshLayout的刷新状态也会发生对应的更新。
  @JvmStatic
  @BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
  fun setSwipeRefreshLayoutRefreshing(
          swipeRefreshLayout: SwipeRefreshLayout,
          newValue: Boolean
  ) {
      // 判断值是否变化了,避免无限循环
      if (swipeRefreshLayout.isRefreshing != newValue)
          swipeRefreshLayout.isRefreshing = newValue
  }
  
  // 2.@InverseBindingAdapter: view视图发生更改时要调用的内容
  // 但是它不知道特性何时或如何更改,所以还需要设置视图监听器
  @JvmStatic
  @InverseBindingAdapter(
          attribute = "app:bind_swipeRefreshLayout_refreshing",  
          event = "app:bind_swipeRefreshLayout_refreshingAttrChanged"    // tag
  )
  fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =
          swipeRefreshLayout.isRefreshing
 }
  
  // 3. @BindingAdapter: 事件监听器与相应的 View 实例相关联
  // 观察view的状态变化,每当swipeRefreshLayout刷新状态被用户的操作改变
  @JvmStatic
  @BindingAdapter(
          "app:bind_swipeRefreshLayout_refreshingAttrChanged",     // tag
          requireAll = false
  )
  fun setOnRefreshListener(
          swipeRefreshLayout: SwipeRefreshLayout,
          bindingListener: InverseBindingListener?
  ) {
      if (bindingListener != null)
          // 监听下拉刷新
          swipeRefreshLayout.setOnRefreshListener {
              bindingListener.onChange()
          }
  }

双向绑定将SwipeRefreshLayout的刷新状态抽象成为了一个LiveData<Boolean>,我们只需要在xml中定义好,之后就可以在ViewModel中围绕这个状态进行代码的编写。

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:bind_swipeRefreshLayout_refreshing="@={fragment.viewModel.refreshing}">
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

注意事项:避免死循环

双向绑定有一个致命的问题,那就是无限循环会导致的ANR异常。

View层UI状态被改变,ViewModel对应发生更新,同时,这个更新又回通知View层去刷新UI,这个刷新UI的操作又会通知ViewModel去更新…

因此,为了保证不会无限的死循环导致AppANR异常的发生,我们需要在最初的代码块中加一个判断,保证只有View状态发生了变更,才会去更新UI。

小结

DataBinding通过让 “控件” 与 “可观察数据” 发生绑定,它的本质是将终态数据 绑定到View ,而不是在xml写逻辑,当该数据被 set 新内容时,被绑定该数据的控件即可被通知和刷新。


为了帮助大家更好的熟知Jetpack 这一套体系的知识点,这里记录比较全比较细致的《Jetpack 入门到精通》(内含Compose) 学习笔记!!! 对Jetpose Compose这块感兴趣的小伙伴可以参考学习下……

Jetpack 全家桶(Compose)

Jetpack 部分

  1. Jetpack之Lifecycle
  2. Jetpack之ViewModel
  3. Jetpack之DataBinding
  4. Jetpack之Navigation
  5. Jetpack之LiveData

Compose 部分
1.Jetpack Compose入门详解
2.Compose学习笔记
3.Compose 动画使用详解

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

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

相关文章

动力节点|老杜Vue完整版教程,轻松掌握前端火爆框架

Vue拥有非常好的可用性和可组合性、试图像用户提供最少的API和尽可能的自然行为。 Vue之所以如此受欢迎和火爆&#xff0c;主要是由于以下几个原因&#xff1a; 1. 更简单的使用方式&#xff1a;Vue的API设计易于学习和使用&#xff0c;他的响应式系统可以自动追踪依赖关系&am…

[Idea热部署]两秒钟学会热部署

两者同时适配好,保证没有问题 哈&#xff0c;谢谢各位同志的阅读&#xff0c;然后呢如果觉得本文对您有所帮助的话&#xff0c;还给个免费的赞捏 Thanks♪(&#xff65;ω&#xff65;)&#xff89;

信息安全:防火墙技术原理与应用.

信息安全&#xff1a;防火墙技术原理与应用. 防火墙是网络安全区域边界保护的重要技术。为了应对网络威胁&#xff0c;联网的机构或公司将自己的网络与公共的不可信任的网络进行隔离&#xff0c;其方法是根据网络的安全信任程度和需要保护的对象&#xff0c;人为地划分若干安全…

安装Qt选择组件

最近在做Qt相关的开发&#xff0c;首先搭建开发环境&#xff0c;刚开始对组件这块不是很熟悉&#xff0c;需要了解这方面的知识&#xff0c;写下来主要是方便记住关于选择组件的说明&#xff0c;Qt版本是最新的长期维护版本&#xff0c;版本号&#xff1a;6.5.2 一、选择要安装…

辽宁线上3D三维虚拟工厂生产仿真系统应用场景及优势

工厂虚拟仿真是一种基于计算机技术和虚拟现实技术的数字化解决方案&#xff0c;它可以通过模拟工厂中的设备、流程和操作&#xff0c;来为工程师和操作人员提供了一个沉浸式的虚拟环境&#xff0c;帮助他们更好地了解和优化工厂生产过程。 工厂VR三维可视化技术为工业生产提供了…

warning: remember to run ‘libtool --finish /usr/local/1/php-7.4.29/libs

ubuntu上php7.4.33编译安装完成后警告报错&#xff0c;如下所示 # /usr/local/apache2/apr/build-1/libtool --finish /usr/local/soft/php-7.4.33/libs # vim /etc/ld.so.conf.d/local.conf /usr/local/lib /usr/lib64 # ldconfig 或者安装依赖服务&#xff0c;重新编译 #…

typecho 全站开启Https证书访问

原文地址:https://zhuoyue360.com/jyjl/107.html typecho 全站开启Https证书访问 https://zhuoyue360.com/ 网站已经很久没更新了, 最近决定重新把博客捡起来. 今天把ssl证书倒腾了一下,做个小记录! 1. 前提步骤 拥有SSL证书已将域名解析到服务器上&#xff0c;并配置了 Ngi…

电子企业MES管理系统的选型要素有哪些

随着全球电子行业的快速发展&#xff0c;电子企业面临着日益激烈的竞争和不断变化的市场需求。为了应对这种挑战&#xff0c;许多电子企业开始考虑引入MES管理系统解决方案来提高生产效率和管理水平。然而&#xff0c;在选择适合的MES生产管理系统之前&#xff0c;电子企业需要…

CTF之流量分析之密码文件

题目地址&#xff1a;BUUCTF在线评测 题目&#xff1a; 深夜里&#xff0c;Hack偷偷的潜入了某公司的内网&#xff0c;趁着深夜偷走了公司的秘密文件&#xff0c;公司的网络管理员通过通过监控工具成功的截取Hack入侵时数据流量&#xff0c;但是却无法分析出Hack到底偷走了什…

java面试总结(一)SnailClimb/JavaGuide

晚上标题党太多&#xff0c;拿着各种免费资料来收费&#xff0c;各种加微信购买解密密码的充斥百度搜索。 博主无套路分享&#xff1a; 阿里10w字Java面试手册 JAVA核心面试知识整理 1000道专题Java面试题手册 Java 基础 知识点/面试题总结 : (必看 Java 基础常见知识点…

c语言每日一练(5)

前言&#xff1a;每日一练系列&#xff0c;每一期都包含5道选择题&#xff0c;2道编程题&#xff0c;博主会尽可能详细地进行讲解&#xff0c;令初学者也能听的清晰。每日一练系列会持续更新&#xff0c;暑假时三天之内必有一更&#xff0c;到了开学之后&#xff0c;将看学业情…

创建和使用角色(RHCE)

题目&#xff1a; 创建和使用角色 根据下列要求&#xff0c;在 /home/curtis/ansible/roles 中创建名为 apache 的角色&#xff1a; httpd 软件包已安装&#xff0c;设为在系统启动时启用并启动 防火墙已启用并正在运行&#xff0c;并使用允许访问 Web 服务器的规则 模板文件 i…

【办公自动化】使用Python一键提取PDF中的表格到Excel

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

【调整奇数偶数顺序】

调整奇数偶数顺序 1.题目 输入一个整数数组&#xff0c;实现一个函数&#xff0c; 来调整该数组中数字的顺序使得数组中所有的奇数位于数组的前半部分&#xff0c; 所有偶数位于数组的后半部分。 2.题目分析 这道题首先用到的方法是冒泡排序的思想&#xff0c;首先通过冒泡排序…

Redis安装配置远程连接

1. yum 安装 redis&#xff1a; 直接使用命令&#xff0c;将 redis 安装到 linux 服务器中&#xff1a; yum -y install redis 2. 启动 redis&#xff1a; 在 xshell 里&#xff0c;可以使用下面命令&#xff0c;以后台方式启动 redis&#xff1a; [rootVM-8-17-centos /]…

Linux网络编程套接字(上)

目录 预备知识 理解源IP地址和目的IP地址 &#xff1a; 认识端口号&#xff1a; 理解"端口号"和"进程ID" 认识TCP/UDP协议 TCP: UDP : 网络字节序 Socket编程接口 Socket常见API&#xff1a; Sockaddr结构: 简单的UDP网络程序 实现一个简单…

全面讲解|DCMM数据管理能力成熟度及各地政策汇总

信息技术与经济社会的交汇融合引发了数据爆发式增长。数据蕴含着重要的价值&#xff0c;已成为国家基础性战略资源&#xff0c;正日益对全球生产、流通、分配、消费活动以及经济运行机制、社会生活方式和国家治理能力产生重要影响。数据价值发挥的前提是管理好数据&#xff0c;…

断点重传、错误自动重传,优秀的文件传输工具应该具备这些特性

在当今的信息时代&#xff0c;文件传输是我们日常工作和生活中不可或缺的一项功能。无论是发送照片、视频、文档等个人文件&#xff0c;还是交换设计图、合同、报告等商业文件&#xff0c;我们都需要依靠各种文件传输工具来实现。但是&#xff0c;文件传输并不总是一帆风顺的&a…

innovus 报告多边形floorplan的boundary坐标

我正在「拾陆楼」和朋友们讨论有趣的话题&#xff0c;你⼀起来吧&#xff1f; 拾陆楼知识星球 历史文章: 常用dbGet命令 dbGet快速入门 使用"Cut Rectilinear"功能可以​调整floorplan形状&#xff0c;使其变成非矩形多边形&#xff08;polygon&#xff09;。​ …

干货文章|特殊区域在OSPF协议中的分析

我们都知道OSPF路由器协议是基于SPF算法计算最优路径&#xff0c;当用于SPF算法的LSDB的越大&#xff0c;那么路由器的计算压力就越大&#xff0c;对于一些性能不佳的OSPF路由器来说&#xff0c;如何尽可能地减小LSDB规模也就成了OSPF设计时要考虑到的问题&#xff0c;为了进一…