当前位置: 首页 > news >正文

彻底掌握Android中的ViewModel

彻底掌握Android中的ViewModel

ViewModel 属于Android Jetpack库的一部分,是一种业务逻辑或屏幕状态容器。它提供了在配置更改(如屏幕旋转)后依旧保留相应状态的特性,帮助开发者以更加清晰和可维护的方式处理UI相关的数据,从而避免了在 Activity 或 Fragment 中直接处理数据持久化的问题。

ViewModel的使用

创建

日常开发中,ViewModel 经常充当 MVVM 架构的 VM 层,分担 Activity/Fragment 的部分逻辑,充当页面的数据存储容器。ViewModel 的创建方式有好几种,官方的 API 也改了几版,ViewModelProviders 已标为废弃,目前创建 ViewModel 统一使用 ViewModelProvider,代码实现有如下几种方式:

//无参构造函数ViewModel
class MyViewModel() : ViewModel() {//...
}

1.通过ViewModelProdiver

Activity 中:

private val viewModel by lazy {ViewModelProvider(this).get(MyViewModel::class.java)
}

Fragment 中:

private val viewModel by lazy {ViewModelProvider(this).get(MyViewModel::class.java)	//关联的是Fragment
}private val viewModel by lazy {ViewModelProvider(requireActivity()).get(MyViewModel::class.java)	//关联的是Activity
}

2.通过Android KTX

KTX 扩展库提供了很多常用功能的简洁实现,KTX 分为若干模块,开发者需要按需引用,这里需要用到 Fragment KTX 模块,首先将该模块代码依赖到工程:

implementation "androidx.fragment:fragment-ktx:1.6.2"

然后就可以用以下方式进行 ViewModel 的创建了,代码非常简洁:

Activity 中:

private val viewModel by viewModels<MyViewModel>()

Fragment 中:

private val viewModel1 by viewModels<MyViewModel>()	 //关联的是Fragment
private val viewModel2 by activityViewModels<MyViewModel>()  //关联的是Activity

3.有参数的ViewModel创建方式

上面两种创建的 ViewModel 构造器都是无参数的,但 ViewModel 有时候也需要依赖注入外部对象,这时 ViewModel 就需要提供有参数的构造器,重点是创建自定义的 ViewModel 的创建工厂。

先看下 UserViewModel 的定义:

//数据仓库层
object UserRepo {//网络逻辑...
}//构造器有参的ViewModel
class UserViewModel(val repo: UserRepo) : ViewModel() {//...
}

一般情况下,重写 ViewModelProvider.Factory 一个参数的 create 方法即可:

class Factory1(val repo: UserRepo): ViewModelProvider.Factory {override fun <T : ViewModel> create(modelClass: Class<T>): T {return UserViewModel(repo) as T}
}

创建代码:

//通过ViewModelProvider方式:
private val viewModel by lazy {ViewModelProvider(this, UserViewModel.Factory1(UserRepo)).get(UserViewModel::class.java)
}//通过KTX方式:
private val viewModel by viewModels<UserViewModel>(factoryProducer = { UserViewModel.Factory1(UserRepo) })

ViewModelProvider 中还提供了几个默认工厂:

  1. NewInstanceFactory:用来创建无参的 ViewModel,也是 ViewModelProvider 的默认工厂。
  2. AndroidViewModelFactory:继承自 NewInstanceFactory ,用来创建构造函数需要 Application 参数的 ViewModel 实例,特殊情况下会调用 NewInstanceFactory 创建无参的 ViewModel。

其实 KTX 最后也是通过 ViewModelProdiver 进行创建的,只不过通过 Kotlin 的属性委托机制将语法简化了,源码如下:

//ViewModel会通过by关键字委托到该类,每次使用该属性时,都会走到get方法中
public class ViewModelLazy<VM : ViewModel> @JvmOverloads constructor(private val viewModelClass: KClass<VM>,private val storeProducer: () -> ViewModelStore,private val factoryProducer: () -> ViewModelProvider.Factory,private val extrasProducer: () -> CreationExtras = { CreationExtras.Empty }
) : Lazy<VM> {private var cached: VM? = nulloverride val value: VMget() {val viewModel = cachedreturn if (viewModel == null) {val factory = factoryProducer()val store = storeProducer()//最终还是通过ViewModelProvider进行创建的ViewModelProvider(store,factory,extrasProducer()).get(viewModelClass.java).also {cached = it}} else {viewModel}}override fun isInitialized(): Boolean = cached != null
}
使用

ViewModel 一般作为 MVVM 架构的 VM 层,可以将 Activity/Fragment 的业务逻辑都封装到 ViewModel 中,比较常见的就是网络请求了。Google 推荐如下方式实现:

class MyViewModel : ViewModel() {private val _userLiveData: MutableLiveData<User> = MutableLiveData<User>()val userData: LiveData<User>    //外部获取的类型是LiveData,不可变的,防止外部随意修改get() = _userLiveDatafun doAction() {//...比如请求网络,并更新user_userLiveData.postValue(User("白泽..."))}
}
class ViewModelActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)viewModel.userData.observe(this) {  //1.订阅数据变化//update UI}findViewById<Button>(R.id.button).setOnClickListener {viewModel.doAction()	//2.触发数据更新}}
}

ViewModel是如何存储的

首先来看看 ViewModel 的几个重要类,分别是 ViewMdoel、ViewModelProvider、ViewModelStore 和 ViewModelStoreOwner,关系类图如下:
在这里插入图片描述

这不是严格的UML图,只需大概理解即可,下面介绍下每个类的职责:

  • ViewModelProvider:只负责 ViewModel 的创建。无参构造器的 ViewModel 可以直接用其内部提供的 NewInstanceFactory 工厂创建,如果ViewModel 需要构造器参数,则需要实现 ViewModelProvider.Factory 接口并完善创建逻辑。

  • ViewModelStore:负责 ViewModel 实例的存储,内部通过 HashMap 实现,map 的 value 就是 ViewMode 的实例,key 的生成规则如下:

    private static final String DEFAULT_KEY = "androidx.lifecycle.ViewModelProvider.DefaultKey"//...String canonicalName = modelClass.getCanonicalName(); //返回此类的规范名称,如com.xx.x.MyViewModelget(DEFAULT_KEY + ":" + canonicalName, modelClass);//...
    

    所以同一 ViewModelStore 中,同一个类型的 ViewModel 并不会重复创建。

  • ViewModelStoreOwner:负责提供 ViewModelStore,常见的 ViewModelStoreOwner 有 ComponentActivity、Fragment 等,它们的内部会对 ViewModelStore 进行管理,在适当的时机进行创建和回收。以 ComponentActivity 为例,其内部会监听生命周期,并在生命周期变动时调用如下代码,确保 mViewModelStore 的存在:

    //1.Activity销毁时调用该方法临时保存ViewModelStore
    public final Object onRetainNonConfigurationInstance() {// Maintain backward compatibility.Object custom = onRetainCustomNonConfigurationInstance();ViewModelStore viewModelStore = mViewModelStore;if (viewModelStore == null) {// No one called getViewModelStore(), so see if there was an existing// ViewModelStore from our last NonConfigurationInstanceNonConfigurationInstances nc =(NonConfigurationInstances) getLastNonConfigurationInstance();if (nc != null) {viewModelStore = nc.viewModelStore;}}if (viewModelStore == null && custom == null) {return null;}NonConfigurationInstances nci = new NonConfigurationInstances();nci.custom = custom;nci.viewModelStore = viewModelStore;return nci;
    }//2.Activity创建时恢复上次保存的ViewModelStore
    void ensureViewModelStore() {if (mViewModelStore == null) {NonConfigurationInstances nc =(NonConfigurationInstances) getLastNonConfigurationInstance();if (nc != null) {// Restore the ViewModelStore from NonConfigurationInstancesmViewModelStore = nc.viewModelStore;  //拿到上次保存的ViewModelStore}if (mViewModelStore == null) {mViewModelStore = new ViewModelStore();  //创建新的ViewModelStore}}
    }

    ViewModelStore 的销毁时机:Activity 走到 Destroy 并且使非配置更改(如正常finish)。

    getLifecycle().addObserver(new LifecycleEventObserver() {@Overridepublic void onStateChanged(@NonNull LifecycleOwner source,@NonNull Lifecycle.Event event) {if (event == Lifecycle.Event.ON_DESTROY) {// Clear out the available contextmContextAwareHelper.clearAvailableContext();// And clear the ViewModelStoreif (!isChangingConfigurations()) {getViewModelStore().clear();}}}
    });
    

    面试常问的问题:

    为什么 Activity 在旋转屏幕时,Activity 对象都发生重建了,但 ViewModel 却还是原来的对象?

    ViewModel 是怎么保存和恢复的?

    上面两个问题问的其实是 mViewModelStore 的保存和恢复,因为它是持有 ViewModel 实例的仓库。而 mViewModelStore 的存储和恢复是通过 onRetainNonConfigurationInstancegetLastNonConfigurationInstance来实现的。

    在配置更改时会调用 Activity#onRetainNonConfigurationInstance() 保存 mViewModelStore 对象,并在 Activity 重建后通过 getLastNonConfigurationInstance 方法获取上次保存的 ViewModelStore 对象,如果有则直接使用,否则创建新的实例对象。

    状态保存:onRetainNonCongigurationInstance

    该方法是 Android 提供的在配置更改时,临时保存 Activity 数据的 API。onRetainNonConfigurationInstance() 允许 Activity 在配置改变之前返回一个对象,这个对象随后可以在 Activity 重新创建后的getLastNonConfigurationInstance()方法中被检索到。

    下面是该机制的源码,在设备配置发生更改时(如旋转屏幕),会调用到 ActivityThread#handleRelaunchActivity 方法:

    //看方法名字可以知道是处理Activity重建逻辑的
    public void handleRelaunchActivity(ActivityClientRecord tmp, PendingTransactionActions pendingActions) {//...handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");//...
    }private void handleRelaunchActivityInner(ActivityClientRecord r, int configChanges,List<ResultInfo> pendingResults, List<ReferrerIntent> pendingIntents,PendingTransactionActions pendingActions, boolean startsNotResumed,Configuration overrideConfig, String reason) {//1. 处理旧Activity的销毁,注意第三个参数是getNonConfigInstance,传入的是truehandleDestroyActivity(r, false, configChanges, true, reason);//2. 处理Activity新建逻辑handleLaunchActivity(r, pendingActions, customIntent);
    }
    

    这个方法处理了两件事,一是旧 Activity 的回收,二是 Activity 的新建。先从 Activity 销毁开始看:

    @Override
    public void handleDestroyActivity(ActivityClientRecord r, boolean finishing, int configChanges,boolean getNonConfigInstance, String reason) {performDestroyActivity(r, finishing, configChanges, getNonConfigInstance, reason);//...
    }void performDestroyActivity(ActivityClientRecord r, boolean finishing,int configChanges, boolean getNonConfigInstance, String reason) {//...注意:getNonConfigInstance为trueif (getNonConfigInstance) {try {//调用旧activity的方法,并保存到ActivityClientRecord对象中r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();} catch (Exception e) {//...}}//...
    }
    
    NonConfigurationInstances retainNonConfigurationInstances() {Object activity = onRetainNonConfigurationInstance();  //1.调用了onRetainNonConfigurationInstance方法HashMap<String, Object> children = onRetainNonConfigurationChildInstances(); //2.可以缓存一些自定义数据FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();  //3.Fragment状态//...NonConfigurationInstances nci = new NonConfigurationInstances();nci.activity = activity;nci.children = children;nci.fragments = fragments;nci.loaders = loaders;if (mVoiceInteractor != null) {mVoiceInteractor.retainInstance();nci.voiceInteractor = mVoiceInteractor;}return nci;
    }
    

    可以看到,Activity 销毁做了两件事:

    1. 调用了 Activity 对象的 onRetainNonConfigurationInstance 方法拿到临时对象,并赋值给 ActivityClientRecord#lastNonConfigurationInstances变量。
    2. 调用 Activity 的 pause、stop、destroy 等生命周期方法。

    接下来看 Activity 新建逻辑:

    public Activity handleLaunchActivity(ActivityClientRecord r,PendingTransactionActions pendingActions, Intent customIntent) {//...final Activity a = performLaunchActivity(r, customIntent);//...
    }private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {//...Activity activity = null;try {java.lang.ClassLoader cl = appContext.getClassLoader();//1.通过反射创建Activity对象activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);}//...try {if (activity != null) {//2.调用 attach 方法,将缓存信息传入activity.attach(appContext, this, getInstrumentation(), r.token,r.ident, app, r.intent, r.activityInfo, title, r.parent,r.embeddedID, r.lastNonConfigurationInstances, config,r.referrer, r.voiceInteractor, window, r.activityConfigCallback,r.assistToken, r.shareableActivityToken);//...return activity;
    }
    

    创建 Activity 流程同样做了两件事:

    1. 通过反射创建 Activity 实例对象
    2. 调用 attach 方法,将销毁时缓存在 ActivityClientRecord#lastNonConfigurationInstances 变量中的临时变量关联到新的 Activity 对象
    final void attach(Context context, ActivityThread aThread,Instrumentation instr, IBinder token, int ident,Application application, Intent intent, ActivityInfo info,CharSequence title, Activity parent, String id,NonConfigurationInstances lastNonConfigurationInstances,Configuration config, String referrer, IVoiceInteractor voiceInteractor,Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,IBinder shareableActivityToken) {//赋值给了mLastNonConfigurationInstancesmLastNonConfigurationInstances = lastNonConfigurationInstances;
    }public Object getLastNonConfigurationInstance() {return mLastNonConfigurationInstances != null? mLastNonConfigurationInstances.activity : null;
    }
    

    执行完 Activity 的 attach 方法后,就可以通过 getLastNonConfigurationInstance 方法获取之前 Activity 销毁时保存的状态数据了,到此 Activity 保存和恢复数据的链路就通了。

    onRetainNonConfigurationInstance 方法用 final 修饰,并已标为废弃了,其实改保存数据方案在 Android 3.0 就已经废弃了,Google 不希望我们用这个机制去进行 Activity 状态保存,而是推荐用基于该机制上衍生的 ViewModel 进行状态保存,使用起来更简单、更安全。

    onRetainNonCongigurationInstance 和 onSaveInstanceState区别:

    onSaveInstanceState 同样是用于处理 Activity 状态保存和恢复的方法,它与 onRetainNonCongigurationInstance 方式区别如下:

    1. 使用场景不同
      • onRetainNonCongigurationInstance 用于在设备配置更改时(如屏幕旋转)临时保存 Activity 的状态或数据。
      • onSaveInstanceState 用于Activity 即将被销毁时(无论是由于用户离开、配置更改还是系统回收资源),保存 Activity 的状态或数据,是一个更通用、更灵活的状态保存机制。。
    2. 支持数据类型不同
      • onRetainNonCongigurationInstance 返回 Object 类型对象,可以是任何类型,包括 Activity 实例本身或大型数据结构,如果使用不当容易造成内存泄漏。
      • onSaveInstanceState 通过 Bundle 对象来保存状态,只能存储基本数据类型、可序列化的对象或实现了 Parcelable 接口的对象,确保数据的安全性和可恢复性。
    3. 恢复数据方式不同
      • onRetainNonConfigurationInstance 在Activity重新创建后,可以通过调用getLastNonConfigurationInstance()方法来检索之前保存的数据。
      • onSaveInstanceState 在Activity重新创建时,系统会将之前保存的Bundle对象传递给onCreate(Bundle savedInstanceState)onRestoreInstanceState(Bundle savedInstanceState)方法。

ViewModel的协程作用域

协程是 Kotlin 的又一高效编程利器,使用协程可以非常简单的进行多线程协作。ViewModel 提供了和其生命周期一致协程作用域,可以引入 KTX 的 ViewModel 模块,让使用更简单:

dependencies {implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6"
}

使用时:

viewModel.viewModelScope.launch {if (isActive) {  //判断协程是否被取消了//doAction...}
}

下面来探索一下 ViewModel 协程作用域是如何管理的,首先看 viewModelScope 源码:

public val ViewModel.viewModelScope: CoroutineScopeget() {val scope: CoroutineScope? = this.getTag(JOB_KEY)  //1.缓存的协程作用域对象if (scope != null) {return scope}//2.创建协程return setTagIfAbsent(JOB_KEY,CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))}

可以看到,如果没从缓存获取到则直接通过 setTagIfAbsent 方法创建,协程上下文类型是 SupervisorJobDispatchers.Main,可以让发生异常时不影响其他协程,并且代码运行在主线程。

private final Map<String, Object> mBagOfTags = new HashMap<>();//1.setIfAbsend 系列方法一般表示,如果存在旧值就不进行赋值了,防止多次创建
<T> T setTagIfAbsent(String key, T newValue) {T previous;//2.通过同步锁,防止多线程场景错误synchronized (mBagOfTags) {previous = (T) mBagOfTags.get(key);if (previous == null) {mBagOfTags.put(key, newValue);}}T result = previous == null ? newValue : previous;if (mCleared) {closeWithRuntimeException(result);}return result;
}

通过该方法可以看到,ViewModel 的协程作用域创建后会缓存在 ViewModel 中的 mBagOfTags 中,这是一个 map 结构,key 为 JOB_KEY ,value 为协程作用域对象。

知道了 ViewModel 的协程作用域是如何创建和保存的,下面看协程是如何取消的。

还记得上面 ViewModel 对象的保存逻辑吗,ComponentActivity 会监听 DESTROY 生命周期,Activity 正常销毁时,会执行 ViewModelStore 的 clear 方法,ViewModelStore 又会遍历所有保存的 ViewModel 对象,并调用其 clear 方法。

final void clear() {mCleared = true;//1.加锁,进行协程作用域的取消if (mBagOfTags != null) {synchronized (mBagOfTags) {for (Object value : mBagOfTags.values()) {//2.取消协程closeWithRuntimeException(value);}}}//2.ViewModel销毁时会调用该方法,可以重写它进行长时间任务的清理工作onCleared();
}

可见最终会调用到 closeWithRuntimeException 方法进行协程取消,内部其实是 coroutineContext.cancel()

ViewModel 的协程作用域会在获取时进行创建,并缓存在 ViewModel 的 mBagOfTags 映射表内部,在 ViewModel 销毁时取消。协程取消并不会强制终端代码逻辑,使用 ViewModel 协程作用域进行长时间任务时,注意使用 isActive 方法适时判断协程是否被取消了。

ViewModel 的协程作用域和 Lifecycle 的协程作用域有何区别?

Android 中除了 ViewModel 提供了协程作用域外,Lifecycle 也提供了 lifecycleScope 协程作用域,首先看下 Lifecycle 的协程作用域是如何创建、保存和销毁的:

//Lifecycle:
//1.AtomicReference让对象读,写都是原子操作,保证修改对象引用时的线程安全
public var internalScopeRef: AtomicReference<Any> = AtomicReference<Any>()public val Lifecycle.coroutineScope: LifecycleCoroutineScopeget() {while (true) {val existing = internalScopeRef.get() as LifecycleCoroutineScopeImpl?if (existing != null) {return existing}//2.创建协程作用域val newScope = LifecycleCoroutineScopeImpl(this,SupervisorJob() + Dispatchers.Main.immediate)//3.通过CAS机制设置对象if (internalScopeRef.compareAndSet(null, newScope)) {newScope.register()return newScope}}}
internal class LifecycleCoroutineScopeImpl(override val lifecycle: Lifecycle,override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {init {if (lifecycle.currentState == Lifecycle.State.DESTROYED) {coroutineContext.cancel() //1.取消协程}}fun register() {launch(Dispatchers.Main.immediate) {if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)} else {coroutineContext.cancel()}}}override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {lifecycle.removeObserver(this)coroutineContext.cancel()  //2.取消协程}}
}

Lifecycle 的代码非常简单,同样在获取时创建协程作用域,并通过CAS机制保存到 internalScopeRef 对象引用,在 DESTROY 时进行协程的销毁操作。

通过上面分析 ViewModel 和 Lifecycle 的协程作用域相关代码,可以分析出以下几点区别:

  • ViewModel 协程作用域创建时通过 synchronized 同步锁保证线程安全;Lifecycle 协程作用域创建时通过 while 循环 + CAS 机制保证线程安全。相对来说 CAS 机制更能保证效率,ViewModel 使用 synchronized,主要还是因为其中的 mBagOfTags,它是一个Map,Android 官方因为一些旧系统的限制,导致无法使用ConcurrentHashMap,所以才出此下策。
  • ViewModel 和 Lifecycle 的协程作用域生命周期不同,因为销毁时机不一样,就像刚开始 ViewModel 的生命周期和 Activity 生命周期一样。

总结

Android 的 ViewModel 是一个强大的架构组件,它通过提供数据、管理状态以及生命周期感知等能力,帮助开发者构建更加健壮、易于维护和测试的应用。在 MVVM 代码架构中,ViewModel 是视图(View)与数据(Model)之间的桥梁,它负责为 UI组件提供数据,并管理 UI组件的状态,UI状态与业务逻辑分离,使得代码耦合性更低,更易于测试。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 14张图深度解密大厂秒杀系统库存设计,不是所有的库存都能支持高并发!
  • 口语训练材料
  • OpenCV特征检测(5)检测图像中的角点函数cornerMinEigenVal()的使用
  • Debezium日常分享系列之:将容器镜像移至 quay.io
  • LPDDR4芯片学习(一)——基础知识与引脚定义
  • ONNX那些事
  • 豆包 MarsCode 代码练习体验
  • vue-baidu-map的基本使用
  • 快速构建串口调试工具
  • PyTorch使用------自动微分模块
  • kotlin—— withTimeoutOrNull的介绍和使用场景
  • js中正则表达式中【exec】用法深度解读
  • Linux相关概念和重要知识点(7)(git、冯诺依曼体系结构)
  • python爬虫:从12306网站获取火车站信息
  • YOLOv9改进策略【注意力机制篇】| CVPR2024 CAA上下文锚点注意力机制
  • angular2 简述
  • co.js - 让异步代码同步化
  • crontab执行失败的多种原因
  • css选择器
  • ES学习笔记(12)--Symbol
  • Git同步原始仓库到Fork仓库中
  • Java 网络编程(2):UDP 的使用
  • JWT究竟是什么呢?
  • Laravel Telescope:优雅的应用调试工具
  • Mysql5.6主从复制
  • Python 使用 Tornado 框架实现 WebHook 自动部署 Git 项目
  • Spark VS Hadoop:两大大数据分析系统深度解读
  • Vue2.x学习三:事件处理生命周期钩子
  • Webpack 4 学习01(基础配置)
  • 工作中总结前端开发流程--vue项目
  • 聚簇索引和非聚簇索引
  • 设计模式走一遍---观察者模式
  • 深入 Nginx 之配置篇
  • 问:在指定的JSON数据中(最外层是数组)根据指定条件拿到匹配到的结果
  • 大数据全解:定义、价值及挑战
  • ​TypeScript都不会用,也敢说会前端?
  • ​人工智能书单(数学基础篇)
  • # Swust 12th acm 邀请赛# [ A ] A+B problem [题解]
  • #LLM入门|Prompt#1.7_文本拓展_Expanding
  • #QT 笔记一
  • #传输# #传输数据判断#
  • (2024)docker-compose实战 (9)部署多项目环境(LAMP+react+vue+redis+mysql+nginx)
  • (32位汇编 五)mov/add/sub/and/or/xor/not
  • (c语言+数据结构链表)项目:贪吃蛇
  • (M)unity2D敌人的创建、人物属性设置,遇敌掉血
  • (STM32笔记)九、RCC时钟树与时钟 第二部分
  • (含react-draggable库以及相关BUG如何解决)固定在左上方某盒子内(如按钮)添加可拖动功能,使用react hook语法实现
  • (区间dp) (经典例题) 石子合并
  • (五)大数据实战——使用模板虚拟机实现hadoop集群虚拟机克隆及网络相关配置
  • (一) springboot详细介绍
  • (转载)虚函数剖析
  • (轉)JSON.stringify 语法实例讲解
  • .net core Redis 使用有序集合实现延迟队列
  • .NET Core/Framework 创建委托以大幅度提高反射调用的性能
  • .net 提取注释生成API文档 帮助文档