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

电商商城网站建设/热搜关键词

电商商城网站建设,热搜关键词,制作网页的基本技术标准是什么,江苏网站建设代理商引子LiveData 是能感知生命周期的,可观察的,粘性的,数据持有者。LiveData 用于以“数据驱动”方式更新界面。换一种描述方式:LiveData 缓存了最新的数据并将其传递给正活跃的组件。关于数据驱动的详解可以点击我是怎么把业务代码越…

引子

LiveData 是能感知生命周期的,可观察的,粘性的,数据持有者。LiveData 用于以“数据驱动”方式更新界面。

换一种描述方式:LiveData 缓存了最新的数据并将其传递给正活跃的组件。

关于数据驱动的详解可以点击我是怎么把业务代码越写越复杂的 | MVP - MVVM - Clean Architecture。

这一篇就 LiveData 的面试题做一个归总、分析、解答。

1. LiveData 如何感知生命周期的变化?

先总结,再分析:

Jetpack 引入了 Lifecycle,让任何组件都能方便地感知界面生命周期的变化。只需实现 LifecycleEventObserver 接口并注册给生命周期对象即可。
LiveData 的数据观察者在内部被包装成另一个对象(实现了 LifecycleEventObserver 接口),它同时具备了数据观察能力和生命周期观察能力。

常规的观察者模式中,只要被观察者发生变化,就会无条件地通知所有观察者。比如java.util.Observable:

publicclassObservable {privatebooleanchanged=false;private Vector<Observer> obs;publicvoidnotifyObservers(Object arg) {Object[] arrLocal;synchronized (this) {if (!hasChanged())return;arrLocal = obs.toArray();clearChanged();}// 无条件地遍历所有观察者并通知for (inti= arrLocal.length-1; i>=0; i--)((Observer)arrLocal[i]).update(this, arg);}
}
// 观察者publicinterfaceObserver {voidupdate(Observable o, Object arg);
}
复制代码

LiveData 在常规的观察者模式上附加了条件,若生命周期未达标,即使数据发生变化也不通知观察者。这是如何实现的?

生命周期

生命周期是一个对象从构建到消亡过程中的各个状态的统称。

比如 Activity 的生命周期用如下函数依次表达:

onCreate()
onStart()
onResume()
onPause()
onStop()
onDestroy()
复制代码

要观察生命周期就不得不继承 Activity 重写这些方法,想把生命周期的变化分发给其他组件就很麻烦。

于是 Jetpack 引入了 Lifecycle,以让任何组件都可方便地感知生命周期的变化:

publicabstractclassLifecycle {AtomicReference<>();// 添加生命周期观察者publicabstractvoidaddObserver(LifecycleObserver observer);// 移除生命周期观察者publicabstractvoidremoveObserver(LifecycleObserver observer);// 获取当前生命周期状态publicabstract State getCurrentState();// 生命周期事件publicenumEvent {ON_CREATE,ON_START,ON_RESUME,ON_PAUSE,ON_STOP,ON_DESTROY,ON_ANY;}// 生命周期状态publicenumState {DESTROYED,INITIALIZED,CREATED,STARTED,RESUMED;}// 判断至少到达了某生命周期状态publicbooleanisAtLeast(State state) {return compareTo(state) >= 0;}
}
复制代码

Lifecycle 即是生命周期对应的类,提供了添加/移除生命周期观察者的方法,在其内部还定义了全部生命周期的状态及对应事件。

生命周期状态是有先后次序的,分别对应着由小到大的 int 值。

生命周期拥有者

描述生命周期的对象已经有了,如何获取这个对象需要个统一的接口(不然直接在 Activity 或者 Fragment 中新增一个方法吗?),这个接口叫LifecycleOwner:

publicinterfaceLifecycleOwner {Lifecycle getLifecycle();
}
复制代码

Activity 和 Fragment 都实现了这个接口。

只要拿到 LifecycleOwner,就能拿到 Lifecycle,然后就能注册生命周期观察者。

生命周期 & 数据观察者

生命周期观察者是一个接口:

// 生命周期观察者(空接口,用于表征一个类型)publicinterfaceLifecycleObserver {}
// 生命周期事件观察者publicinterfaceLifecycleEventObserverextendsLifecycleObserver {voidonStateChanged(LifecycleOwner source, Lifecycle.Event event);
}
复制代码

要观察生命周期只要实现LifecycleEventObserver接口,并注册给LifeCycle即可。

除了生命周期观察者外,LiveData 场景中还有一个数据观察者

// 数据观察者publicinterfaceObserver<T> {// 数据发生变化时回调voidonChanged(T t);
}
复制代码

数据观察者 会和 生命周期拥有者 进行绑定:

publicabstractclassLiveData<T> {// 数据观察者容器private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers =newSafeIterableMap<>();publicvoidobserve(LifecycleOwner owner, // 被绑定的生命周期拥有者Observer<? super T> observer // 数据观察者) {...// 将数据观察者包装成 LifecycleBoundObserverLifecycleBoundObserverwrapper=newLifecycleBoundObserver(owner, observer);// 存储观察者到 map 结构ObserverWrapperexisting= mObservers.putIfAbsent(observer, wrapper);...// 注册生命周期观察者。owner.getLifecycle().addObserver(wrapper);}
}
复制代码

在观察 LiveData 时,需传入两个参数,生命周期拥有者和数据观察者。这两个对象经过LifecycleBoundObserver的包装被绑定在了一起:

classLifecycleBoundObserverextendsObserverWrapperimplementsLifecycleEventObserver {// 持有生命周期拥有者final LifecycleOwner mOwner;LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) {super(observer);mOwner = owner;}// 生命周期变化回调@OverridepublicvoidonStateChanged(LifecycleOwner source, Lifecycle.Event event) { ...activeStateChanged(shouldBeActive())...}
}// 观察者包装类型privateabstractclassObserverWrapper {// 持有原始数据观察者final Observer<? super T> mObserver;// 注入数据观察者ObserverWrapper(Observer<? super T> observer) {mObserver = observer;}// 尝试将最新值分发给当前数据观察者voidactiveStateChanged(boolean newActive) {...}...
}
复制代码

LifecycleBoundObserver 实现了LifecycleEventObserver接口,并且它被注册给了绑定的生命周期对象,遂具备了生命周期感知能力。同时它还持有了数据观察者,所以它还具备了数据观察能力。

2. LiveData 是如何避免内存泄漏的?

先总结,再分析:

LiveData 的数据观察者通常是匿名内部类,它持有界面的引用,可能造成内存泄漏。
LiveData 内部会将数据观察者进行封装,使其具备生命周期感知能力。当生命周期状态为 DESTROYED 时,自动移除观察者。

内存泄漏是因为长生命周期的对象持有了短生命周期对象,阻碍了其被回收。

观察 LiveData 数据的代码通常这样写:

classLiveDataActivity : AppCompatActivity() {privateval viewModel by lazy {ViewModelProviders.of(this@LiveDataActivity).get(MyViewModel::class.java)}overridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)viewModel.livedata.observe(this@LiveDataActivity) {// 观察 LiveData 数据更新(匿名内部类)}}
}
复制代码

Observer 作为界面的匿名内部类,它会持有界面的引用,同时 Observer 被 LiveData 持有,LivData 被 ViewModel 持有,而 ViewModel 的生命周期比 Activity 长。(为啥比它长,可以点击这里)。

最终的持有链如下:NonConfigurationInstances 持有 ViewModelStore 持有 ViewModel 持有 LiveData 持有 Observer 持有 Activity。

所以得在界面生命周期结束的时候移除 Observer,这件事情,LiveData 帮我们做了。

在 LiveData 内部 Observer 会被包装成LifecycleBoundObserver:

classLifecycleBoundObserverextendsObserverWrapperimplementsLifecycleEventObserver {final LifecycleOwner mOwner;LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) {super(observer);mOwner = owner;}@OverridepublicvoidonStateChanged(LifecycleOwner source, Lifecycle.Event event) {// 获取当前生命周期Lifecycle.StatecurrentState= mOwner.getLifecycle().getCurrentState();// 若生命周期为 DESTROYED 则移除数据观察者并返回if (currentState == DESTROYED) {removeObserver(mObserver);return}...}...
}
复制代码

3. LiveData 是粘性的吗?若是,它是怎么做到的?

先总结,再分析:

LiveData 的值被存储在内部的字段中,直到有更新的值覆盖,所以值是持久的。
两种场景下 LiveData 会将存储的值分发给观察者。一是值被更新,此时会遍历所有观察者并分发之。二是新增观察者或观察者生命周期发生变化(至少为 STARTED),此时只会给单个观察者分发值。
LiveData 的观察者会维护一个“值的版本号”,用于判断上次分发的值是否是最新值。该值的初始值是-1,每次更新 LiveData 值都会让版本号自增。
LiveData 并不会无条件地将值分发给观察者,在分发之前会经历三道坎:1. 数据观察者是否活跃。2. 数据观察者绑定的生命周期组件是否活跃。3. 数据观察者的版本号是否是最新的。
“新观察者”被“老值”通知的现象叫“粘性”。因为新观察者的版本号总是小于最新版号,且添加观察者时会触发一次老值的分发。

如果把 sticky 翻译成“持久的”,会更好理解一些。数据是持久的,意味着它不是转瞬即逝的,不会因为被消费了就不见了,它会一直在那。而且当新的观察者被注册时,持久的数据会将最新的值分发给它。

“持久的数据”是怎么做到的?

显然是被存起来了。以更新 LiveData 数据的方法为切入点找找线索:

publicabstractclassLiveData<T> {// 存储数据的字段privatevolatile Object mData;// 值版本号privateint mVersion;// 更新值protectedvoidsetValue(T value) {assertMainThread("setValue");// 版本号自增mVersion++;// 存储值mData = value;// 分发值dispatchingValue(null);}
}
复制代码

setValue() 是更新 LiveData 值时必然会调用的一个方法,即使是通过 postValue() 更新值,最终也会走这个方法。

LiveData 持有一个版本号字段,用于标识“值的版本”,就像软件版本号一样,这个数字用于判断“当前值是否是最新的”,若版本号小于最新版本号,则表示当前值需要更新。

LiveData 用一个 Object 字段mData存储了“值”。所以这个值会一直存在,直到被更新的值覆盖。

LiveData 分发值即是通知数据观察者:

publicabstractclassLiveData<T> {// 用键值对方式持有一组数据观察者private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers =newSafeIterableMap<>();voiddispatchingValue(ObserverWrapper initiator) {...// 指定分发给单个数据观察者if (initiator != null) {considerNotify(initiator);initiator = null;} // 遍历所有数据观察者分发值else {for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {considerNotify(iterator.next().getValue());}}...}// 真正地分发值privatevoidconsiderNotify(ObserverWrapper observer) {// 1. 若观察者不活跃则不分发给它if (!observer.mActive) {return;}// 2. 根据观察者绑定的生命周期再次判断它是否活跃,若不活跃则不分发给它if (!observer.shouldBeActive()) {observer.activeStateChanged(false);return;}// 3. 若值已经是最新版本,则不分发if (observer.mLastVersion >= mVersion) {return;}// 更新观察者的最新版本号observer.mLastVersion = mVersion;// 真正地通知观察者observer.mObserver.onChanged((T) mData);}}
复制代码

分发值有两种情况:“分发给单个观察者”和“分发给所有观察者”。当 LiveData 值更新时,需分发给所有观察者。

所有的观察者被存在一个 Map 结构中,分发的方式是通过遍历 Map 并逐个调用considerNotify()。在这个方法中需要跨过三道坎,才能真正地将值分发给数据观察者,分别是:

  1. 数据观察者是否活跃。

  1. 数据观察者绑定的生命周期组件是否活跃。

  1. 数据观察者的版本号是否是最新的。

跨过三道坎后,会将最新的版本号存储在观察者的 mLastVersion 字段中,即版本号除了保存在LiveData.mVersion,还会在每个观察者中保存一个副本mLastVersion,最后才将之前暂存的mData的值分发给数据观察者。

每个数据观察者都和一个组件的生命周期对象绑定(见第一节),当组件生命周期发生变化时,会尝试将最新值分发给该数据观察者。

每一个数据观察者都会被包装(见第一节),包装类型为ObserverWrapper:

// 原始数据观察者publicinterfaceObserver<T> {voidonChanged(T t);
}// 观察者包装类型privateabstractclassObserverWrapper {// 持有原始数据观察者final Observer<? super T> mObserver;// 当前观察者是否活跃boolean mActive;// 当前观察者最新值版本号,初始值为 -1intmLastVersion= START_VERSION;// 注入原始观察者ObserverWrapper(Observer<? super T> observer) {mObserver = observer;}// 当数据观察者绑定的组件生命周期变化时,尝试将最新值分发给当前观察者voidactiveStateChanged(boolean newActive) {// 若观察者活跃状态未变,则不分发值if (newActive == mActive) {return;}// 更新活跃状态mActive = newActive;// 若活跃,则将最新值分发给当前观察者if (mActive) {dispatchingValue(this);}}// 是否活跃,供子类重写abstractbooleanshouldBeActive();
}
复制代码

观察者的包装类型通过组合的方式持有了一个原始观察者,并在此基础上为其扩展了活跃状态和版本号的概念。

观察者包装类型是抽象的,是否活跃由子类定义:

classLifecycleBoundObserverextendsObserverWrapperimplementsLifecycleEventObserver {final LifecycleOwner mOwner;LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) {super(observer);mOwner = owner;}// 当与观察者绑定的生命周期组件至少为STARTED时,表示观察者活跃@OverridebooleanshouldBeActive() {return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);}@OverridepublicvoidonStateChanged( LifecycleOwner source, Lifecycle.Event event) {Lifecycle.StatecurrentState= mOwner.getLifecycle().getCurrentState();// 当生命周期状态发生变化,则尝试将最新值分发给数据观察者while (prevState != currentState) {prevState = currentState;// 调用父类方法,进行分发activeStateChanged(shouldBeActive());currentState = mOwner.getLifecycle().getCurrentState();}}
}
复制代码

总结一下,LiveData 有两次机会通知观察者,与之对应的有两种分发值的方式:

  1. 当值更新时,遍历所有观察者将最新值分发给它们。

  1. 当与观察者绑定组件的生命周期发生变化时,将最新的值分发给指定观察者。

假设这样一种场景:LiveData 的值被更新了一次,随后它被添加了一个新的数据观察者,与之绑定组件的生命周期也正好发生了变化(变化到RESUMED),即数据更新在添加观察者之前,此时更新值会被分发到新的观察者吗?

会!首先,更新值会被存储在 mData 字段中。

其次,在添加观察者时会触发一次生命周期变化:

// androidx.lifecycle.LifecycleRegistrypublicvoidaddObserver(@NonNull LifecycleObserver observer) {StateinitialState= mState == DESTROYED ? DESTROYED : INITIALIZED;ObserverWithStatestatefulObserver=newObserverWithState(observer, initialState);...// 将生命周期事件分发给新进的观察者statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));...
}// LifecycleBoundObserver 又被包了一层staticclassObserverWithState {State mState;GenericLifecycleObserver mLifecycleObserver;ObserverWithState(LifecycleObserver observer, State initialState) {mLifecycleObserver = Lifecycling.getCallback(observer);mState = initialState;}voiddispatchEvent(LifecycleOwner owner, Event event) {StatenewState= getStateAfter(event);mState = min(mState, newState);// 分发生命周期事件给 LifecycleBoundObservermLifecycleObserver.onStateChanged(owner, event);mState = newState;}
}
复制代码

最后,这次尝试必然能跨过三道坎,因为新建观察者版本号总是小于 LiveData 的版本号(-1 < 0,LiveData.mVersion 经过一次值更新后自增为0)。

这种“新观察者”会被“老值”通知的现象称为粘性

4. 粘性的 LiveData 会造成什么问题?怎么解决?

购物车-结算场景:假设有一个购物车界面,点击结算后跳转到结算界面,结算界面可以回退到购物车界面。这两个界面都是 Fragment。

结算界面和购物车界面通过共享ViewModel的方式共享商品列表:

classMyViewModel:ViewModel() {// 商品列表val selectsListLiveData = MutableLiveData<List<String>>()// 更新商品列表funsetSelectsList(goods:List<String>){selectsListLiveData.value = goods}
}
复制代码

下面是俩 Fragment 界面依托的 Activity

classStickyLiveDataActivity : AppCompatActivity() {// 用 DSL 构建视图privateval contentView by lazy {ConstraintLayout {layout_id = "container"layout_width = match_parentlayout_height = match_parent}}overridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(contentView)// 加载购物车界面supportFragmentManager.beginTransaction().add("container".toLayoutId(), TrolleyFragment()).commit()}
}
复制代码

其中使用了 DSL 方式声明性地构建了布局,详细介绍可以点击Android性能优化 | 把构建布局用时缩短 20 倍(下)

购物车页面如下:

classTrolleyFragment : Fragment() {// 获取与宿主 Activity 绑定的 ViewModelprivateval myViewModel by lazy { ViewModelProvider(requireActivity()).get(MyViewModel::class.java) }overridefunonCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {return ConstraintLayout {layout_width = match_parentlayout_height = match_parent// 向购物车添加两件商品onClick = {myViewModel.setSelectsList(listOf("meet","water"))}TextView {layout_id = "balance"layout_width = wrap_contentlayout_height = wrap_contenttext = "balance"gravity = gravity_center// 跳转结算页面onClick = {parentFragmentManager.beginTransaction().replace("container".toLayoutId(), BalanceFragment()).addToBackStack("trolley").commit()}}}}overridefunonViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)// 观察商品列表变化myViewModel.selectsListLiveData.observe(viewLifecycleOwner) { goods ->// 若商品列表超过2件商品,则 toast 提示已满goods.takeIf { it.size >= 2 }?.let {Toast.makeText(context,"购物车已满",Toast.LENGTH_LONG).show()}}}
}
复制代码

在 onViewCreated() 中观察购物车的变化,如果购物车超过 2 件商品,则 toast 提示。

下面是结算页面:

classBalanceFragment:Fragment() {privateval myViewModel by lazy { ViewModelProvider(requireActivity()).get(MyViewModel::class.java) }overridefunonCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {return ConstraintLayout {layout_width = match_parentlayout_height = match_parent}}overridefunonViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)// 结算界面获取购物列表的方式也是观察商品 LiveDatamyViewModel.selectsListLiveData.observe(viewLifecycleOwner) {...}}
}
复制代码

跑一下 demo,当跳转到结算界面后,点击返回购物车,toast 会再次提示购物车已满。

因为在跳转结算页面之前,购物车列表 LiveData 已经被更新过。当购物车页面重新展示时,onViewCreated()会再次执行,这样一个新观察者被添加,因为 LiveData 是粘性的,所以上一次购物车列表会分发给新观察者,这样 toast 逻辑再一次被执行。

解决方案一:带消费记录的值

// 一次性值openclassOneShotValue<out T>(privateval value: T) {// 值是否被消费privatevar handled = false// 获取值,如果值未被处理则返回,否则返回空fungetValue(): T? {returnif (handled) {null} else {handled = truevalue}}// 获取上次被处理的值funpeekValue(): T = value
}
复制代码

在值的外面套一层,新增一个标记位标识是否被处理过。

用这个方法重构下 ViewModel:

classMyViewModel:ViewModel() {// 已选物品列表val selectsListLiveData = MutableLiveData<OneShotValue<List<String>>>()// 更新已选物品funsetSelectsList(goods:List<String>){selectsListLiveData.value = OneShotValue(goods)}
}
复制代码

观察购物车的逻辑也要做修改:

classTrolleyFragment : Fragment() {overridefunonViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)myViewModel.selectsListLiveData.observe(viewLifecycleOwner) { goods ->goods.getValue()?.takeIf { it.size >= 2 }?.let {Toast.makeText(context,"购物车满了",Toast.LENGTH_LONG).show()}}}
}
复制代码

重复弹 toast 的问题是解决了,但引出了一个新的问题:当购物车满弹出 toast 时,购物车列表已经被消费掉了,导致结算界面就无法再消费了。

这时候只能用peekValue()来获取已经被消费的值:

classBalanceFragment:Fragment() {overridefunonViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)myViewModel.selectsListLiveData.observe(viewLifecycleOwner) {val list = it.peekValue()// 使用 peekValue() 获取购物车列表}}
}
复制代码

bug 全解完了。但不觉得这样处理有一些拧巴吗?

用“一次性值”封装 LiveData 的值,以去除其粘性。使用该方案得甄别出哪些观察者需要粘性值,哪些观察者需要非粘性事件。当观察者很多的时候,就很难招架了。若把需要粘性处理和非粘性处理的逻辑写在一个观察者中,就 GG,还得新建观察者将它们分开。

解决方案二:带有最新版本号的观察者

通知观察者前需要跨过三道坎(详见第三节),其中有一道坎是版本号的比对。若新建的观察者版本号小于最新版本号,则表示观察者落后了,需要将最新值分发给它。

LiveData 源码中,新建观察者的版本号总是 -1。

// 观察者包装类型privateabstractclassObserverWrapper {// 当前观察者最新值版本号,初始值为 -1intmLastVersion= START_VERSION;...
}
复制代码

若能够让新建观察者的版本号被最新版本号赋值,那版本号对比的那道坎就过不了,新值就无法分发到新建观察者。

所以得通过反射修改 mLastVersion 字段。

该方案除了倾入性强之外,把 LiveData 粘性彻底破坏了。但有的时候,我们还是想利用粘性的。。。

解决方案三:SingleLiveEvent

这是谷歌给出的一个解决方案,源码可以点击这里

publicclassSingleLiveEvent<T> extendsMutableLiveData<T> {// 标志位,用于表达值是否被消费privatefinalAtomicBooleanmPending=newAtomicBoolean(false);publicvoidobserve(LifecycleOwner owner, final Observer<T> observer) {// 中间观察者super.observe(owner, newObserver<T>() {@OverridepublicvoidonChanged(@Nullable T t) {// 只有当值未被消费过时,才通知下游观察者if (mPending.compareAndSet(true, false)) {observer.onChanged(t);}}});}publicvoidsetValue(@Nullable T t) {// 当值更新时,置标志位为 truemPending.set(true);super.setValue(t);}publicvoidcall() {setValue(null);}
}
复制代码

专门设立一个 LiveData,它不具备粘性。它通过新增的“中间观察者”,拦截上游数据变化,然后在转发给下游。拦截之后通常可以做一点手脚,比如增加一个标记位mPending是否消费过的判断,若消费过则不转发给下游。

在数据驱动的 App 界面下,存在两种值:1. 非暂态数据 2. 暂态数据

demo 中用于提示“购物车已满”的数据就是“暂态数据”,这种数据是一次性的,转瞬即逝的,可以消费一次就扔掉。

demo 中购物车中的商品列表就是“非暂态数据”,它的生命周期要比暂态数据长一点,在购物车界面和结算界面存活的期间都应该能被重复消费。

SingleLiveEvent 的设计正是基于对数据的这种分类方法,即暂态数据使用 SingleLiveEvent,非暂态数据使用常规的 LiveData。

这样尘归尘土归土的解决方案是符合现实情况的。将 demo 改造一下:

classMyViewModel : ViewModel() {// 非暂态购物车列表 LiveDataval selectsListLiveData = MutableLiveData<List<String>>()// 暂态购物车列表 LiveDataval singleListLiveData = SingleLiveEvent<List<String>>()// 更新购物车列表,同时更新暂态和非暂态funsetSelectsList(goods: List<String>) {selectsListLiveData.value = goodssingleListLiveData.value = goods}
}
复制代码

在购物车界面做相应的改动:

classTrolleyFragment : Fragment() {overridefunonViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)// 只观察非暂态购物车列表myViewModel.singleListLiveData.observe(viewLifecycleOwner) { goods ->goods.takeIf { it.size >= 2 }?.let {Toast.makeText(context,"full",Toast.LENGTH_LONG).show()}}}
}
复制代码

但该方案有局限性,若为 SingleLiveEvent 添加多个观察者,则当第一个观察者消费了数据后,其他观察者就没机会消费了。因为mPending是所有观察者共享的。

解决方案也很简单,为每个中间观察者都持有是否消费过数据的标记位:

openclassLiveEvent<T> : MediatorLiveData<T>() {// 持有多个中间观察者privateval observers = ArraySet<ObserverWrapper<in T>>()@MainThreadoverridefunobserve(owner: LifecycleOwner, observer: Observer<inT>) {observers.find { it.observer === observer }?.let { _ ->return}// 构建中间观察者val wrapper = ObserverWrapper(observer)observers.add(wrapper)super.observe(owner, wrapper)}@MainThreadoverridefunobserveForever(observer: Observer<inT>) {observers.find { it.observer === observer }?.let { _ ->return}val wrapper = ObserverWrapper(observer)observers.add(wrapper)super.observeForever(wrapper)}@MainThreadoverridefunremoveObserver(observer: Observer<inT>) {if (observer is ObserverWrapper && observers.remove(observer)) {super.removeObserver(observer)return}val iterator = observers.iterator()while (iterator.hasNext()) {val wrapper = iterator.next()if (wrapper.observer == observer) {iterator.remove()super.removeObserver(wrapper)break}}}@MainThreadoverridefunsetValue(t: T?) {// 通知所有中间观察者,有新数据observers.forEach { it.newValue() }super.setValue(t)}// 中间观察者privateclassObserverWrapper<T>(val observer: Observer<T>) : Observer<T> {// 标记当前观察者是否消费了数据privatevar pending = falseoverridefunonChanged(t: T?) {// 保证只向下游观察者分发一次数据if (pending) {pending = falseobserver.onChanged(t)}}funnewValue() {pending = true}}
}
复制代码

解决方案四:Kotlin Flow

限于篇幅原因及主题的原因(主题是 LiveData),直接给出代码(当前做法有问题),关于 LiveData vs Flow 的详细分析可以点击如何把业务代码越写越复杂?(二)| Flow 替换 LiveData 重构数据链路,更加 MVI

classMyViewModel : ViewModel() {// 商品列表流val selectsListFlow = MutableSharedFlow<List<String>>()// 更新商品列表funsetSelectsList(goods: List<String>) {viewModelScope.launch {selectsListFlow.emit(goods)}}
}
复制代码

购物车代码如下:

classTrolleyFragment : Fragment() {privateval myViewModel by lazy { ViewModelProvider(requireActivity()).get(MyViewModel::class.java) }overridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 1.先产生数据myViewModel.setSelectsList(listOf("food_meet", "food_water", "book_1"))}overridefunonViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)// 2.再订阅商品列表流lifecycleScope.launch {myViewModel.selectsListFlow.collect { goods ->goods.takeIf { it.size >= 2 }?.let {Log.v("ttaylor", "购物车满")}}}}
}
复制代码

数据生产在订阅之前,订阅后并不会打印 log。

如果这样修改 SharedFlow 的构建参数,则可以让其变得粘性:

classMyViewModel : ViewModel() {val selectsListFlow = MutableSharedFlow<List<String>>(replay = 1)
}
复制代码

replay = 1 表示会将最新的那个数据通知给新进的订阅者。

这只是解决了粘性/非粘性之间方便切换的问题,并未解决仍需多个流的问题。带下一篇继续深入分析。

5. 什么情况下 LiveData 会丢失数据?

先总结,再分析:

在高频数据更新的场景下使用 LiveData.postValue() 时,会造成数据丢失。因为“设值”和“分发值”是分开执行的,之间存在延迟。值先被缓存在变量中,再向主线程抛一个分发值的任务。若在这延迟之间再一次调用 postValue(),则变量中缓存的值被更新,之前的值在没有被分发之前就被擦除了。

下面是 LiveData.postValue() 的源码:

publicabstractclassLiveData<T> {// 暂存值字段volatileObjectmPendingData= NOT_SET;privatefinalRunnablemPostValueRunnable=newRunnable() {@Overridepublicvoidrun() {Object newValue;synchronized (mDataLock) {// 同步地获取暂存值newValue = mPendingData;mPendingData = NOT_SET;}// 分发值setValue((T) newValue);}};protectedvoidpostValue(T value) {boolean postTask;synchronized (mDataLock) {postTask = mPendingData == NOT_SET;// 暂存值mPendingData = value;}...// 向主线程抛 runnableArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);}
}
复制代码

6. 在 Fragment 中使用 LiveData 需注意些什么?

先总结,再分析:

在 Fragment 中观察 LiveData 时使用viewLifecycleOwner而不是this。因为 Fragment 和 其中的 View 生命周期不完全一致。LiveData 内部判定生命周期为 DESTROYED 时,才会移除数据观察者。存在一种情况,当 Fragment 之间切换时,被替换的 Fragment 不执行 onDestroy(),当它再次展示时会再次订阅 LiveData,于是乎就多出一个订阅者。

还是购物-结算的场景:购物车和结算页都是两个 Fragment,将商品列表存在共享 ViewMode 的 LiveData 中,购物车及结算页都观察它,结算页除了用它列出购物清单之外,还可以通过更改商品数量来修改 LiveData。当从结算页返回购物车页面时,购物车界面得刷新商品数量。

上述场景,若购物车页面观察 LiveData 时使用this会发生什么?

// 购物车界面classTrolleyFragment : Fragment() {privateval myViewModel by lazy { ViewModelProvider(requireActivity()).get(MyViewModel::class.java) }overridefunonCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {return ConstraintLayout {layout_width = match_parentlayout_height = match_parentonClick = {parentFragmentManager.beginTransaction().replace("container".toLayoutId(), BalanceFragment()).addToBackStack("trolley")// 将购物车页面添加到 back stack.commit()}}}// 不得不增加这个注释,因为 this 会飘红@SuppressLint("FragmentLiveDataObserve")overridefunonViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)// 将 this 作为生命周期拥有者传给 LiveDatamyViewModel.selectsListLiveData.observe(this, object : Observer<List<String>> {overridefunonChanged(t: List<String>?) {Log.v("ttaylor", "商品数量发生变化")}})}
}
复制代码

这样写this会飘红,AndroidStudio 不推荐使用它作为生命周期拥有者,不得不加 @SuppressLint("FragmentLiveDataObserve")

结算界面修改商品数量的代码如下:

// 结算界面classBalanceFragment:Fragment() {privateval myViewModel by lazy { ViewModelProvider(requireActivity()).get(MyViewModel::class.java) }overridefunonViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)// 模拟结算界面修改商品数量myViewModel.selectsListLiveData.value = listOf("数量+1")}
}
复制代码

当从结算页返回购物车时,“商品数量发生变化” 会打印两次,如果再进一次结算页并返回购物车,就会打印三次。

若换成viewLifecycleOwner就不会有这个烦恼。因为使用 replace 更换 Fragment 时,Fragment.onDestroyView()会执行,即 Fragment 对应 View 的生命周期状态会变为 DESTROYED。

LiveData 内部会将生命周期为 DESTROYED 的数据观察者移除(详见第二节)。当再次返回购物车时,onViewCreated() 重新执行,LiveData 会添加一个新的观察者。一删一增,整个过程 LiveData 始终只有一个观察者。又因为 LiveData 是粘性的,即使修改商品数量发生在观察之前,最新的商品数量还是会被分发到新观察者。(详见第三节)

但当使用 replace 更换 Fragment 并将其压入 back stack 时,Fragment.onDestroy() 不会调用(因为被压栈了,并未被销毁)。这导致 Fragment 的生命周期状态不会变为 DESTROYED,所以 LiveData 的观察者不会被自动移除。当重新返回购物车时,又添加了新的观察者。如果不停地在购物车和结算页间横跳,则观察者数据会不停地增加。

在写 demo 的时候遇到一个坑:

// 购物车界面classTrolleyFragment : Fragment() {overridefunonViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)// 故意使用 object 语法myViewModel.selectsListLiveData.observe(this, object : Observer<List<String>> {overridefunonChanged(t: List<String>?) {Log.v("ttaylor", "商品数量发生变化")}})}
}
复制代码

在构建 Observer 实例的时候,我特意使用了 Kotlin 的 object 语法,其实明明可以使用 lambda 将其写得更简洁:

classTrolleyFragment : Fragment() {overridefunonViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)myViewModel.selectsListLiveData.observe(this) {Log.v("ttaylor", "商品数量发生变化")}}
}
复制代码

如果这样写,那 bug 就无法复现了。。。。

因为 java 编译器会擅作主张地将同样的 lambda 优化成静态的,可以提升性能,不用每次都重新构建内部类。但不巧的是 LiveData 在添加观察者时会校验是否已存在,若存在则直接返回:

// `androidx.lifecycle.LiveDatapublicvoidobserve( LifecycleOwner owner,  Observer<? super T> observer) {...LifecycleBoundObserverwrapper=newLifecycleBoundObserver(owner, observer);// 调用 map 结构的写操作,若 key 已存在,则返回对应 valueObserverWrapperexisting= mObservers.putIfAbsent(observer, wrapper);...// 已存在则直接返回if (existing != null) {return;}owner.getLifecycle().addObserver(wrapper);
}
复制代码

这样的话,Fragment 界面之间反复横跳也不会新增观察者。

7. 如何变换 LiveData 数据及注意事项?

先总结,再分析:

androidx.lifecycle.Transformations类提供了三个变换 LiveData 数据的方法,最常用的是 Transformations.map(),它使用MediatorLiveData作为数据的中间消费者,并将变换后的数据传递给最终消费者。需要注意的是,数据变化操作都发生在主线程,主线程有可能被耗时操作阻塞。解决方案是将 LiveData 数据变换操作异步化,比如通过CoroutineLiveData。

还是购物-结算的场景:购物车和结算页都是两个 Fragment,将商品列表存在 LiveData 中,购物车及结算页都观察它。结算界面对打折商品有一个特殊的 UI 展示。

此时就可以将商品列表 LiveData 进行一次变换(过滤)得到一个新的打折商品列表:

classMyViewModel : ViewModel() {// 商品列表val selectsListLiveData = MutableLiveData<List<String>>()// 打折商品列表val foodListLiveData = Transformations.map(selectsListLiveData) { list ->list.filter { it.startsWith("discount") }}
}
复制代码

每当商品列表发生变化,打折商品列表都会收到通知,并过滤出新的打折商品。打折商品列表是一个新的 LiveData,可以单独被观察。

其中的过滤列表操作发生在主线程,如果业务略复杂,数据变换操作耗时的话,可能阻塞主线程。

如何将 LiveData 变换数据异步化?

LiveData 的 Kotlin 扩展包里提供了一个将 LiveData 和协程结合的产物:

classMyViewModel : ViewModel() {// 商品列表val selectsListLiveData = MutableLiveData<List<String>>()// 用异步方式获取打折商品列表val asyncLiveData = selectsListLiveData.switchMap { list ->// 将源 LiveData 中的值转换成一个 CoroutineLiveDataliveData(Dispatchers.Default) {emit( list.filter { it.startsWith("discount") } )}}
}
复制代码

其中的switchMap()是 LiveData 的扩展方法,它是对Transformations.switchMap()的封装,用于方便链式调用:

publicinlinefun<X, Y> LiveData<X>.switchMap(crossinline transform: (X) -> LiveData<Y>
): LiveData<Y> = Transformations.switchMap(this) { transform(it) }
复制代码

switchMap() 内部将源 LiveData 的每个值都转换成一个新的 LiveData 并订阅。

liveData是一个顶层方法,用于构建CoroutineLiveData:

publicfun<T>liveData(context: CoroutineContext = EmptyCoroutineContext,timeoutInMs: Long = DEFAULT_TIMEOUT,block: suspendLiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)
复制代码

CoroutineLiveData 将更新 LiveData 值的操作封装到一个挂起方法中,可以通过协程上下文指定执行的线程。

使用 CoroutineLiveData 需要添加如下依赖:

implementation  "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

相关文章:

LiveData 面试题库、解答、源码分析

引子LiveData 是能感知生命周期的&#xff0c;可观察的&#xff0c;粘性的&#xff0c;数据持有者。LiveData 用于以“数据驱动”方式更新界面。换一种描述方式&#xff1a;LiveData 缓存了最新的数据并将其传递给正活跃的组件。关于数据驱动的详解可以点击我是怎么把业务代码越…...

kotlin用object实现单例模式,companion object与java静态

kotlin用object实现单例模式&#xff0c;companion object与java静态 kotlin中很容易使用object实现java中的单例模式。由于kotlin中没有static修饰词&#xff0c;可以用companion object实现Java中的static效果。 //object相当于java的单例 object Singleton {var count: In…...

智慧楼宇中的“黑科技”

据不完全统计&#xff0c;无论是居家、办公、学习还是社交&#xff0c;人们有80%的时间都是在室内空间度过的。而随着社会生产力水平与人们消费理念的提升&#xff0c;用户对于楼宇建筑的使用要求也在不断提高&#xff0c;从最基本的舒适为先逐步朝着数字化、智慧化升级。 如果…...

炫云渲染质量功能测试

炫云已经支持优化渲染质量&#xff0c;分别是保守优化、中度优化和深度优化&#xff0c;使用后效果图的渲染时间会有所缩短&#xff0c;尤其对低版本V-Ray和参数设置不当的场景非常有效&#xff0c;能大幅提升渲染速度及节省渲染费用&#xff0c;当然最终效果图有可能有稍许差异…...

SpringBoot入门

文章目录前言一、约定大于配置二、使用步骤1.使用IDEA创建SpringBoot项目2.引入依赖3.测试三、application.properties和application.yml配置文件四、application.yml配置多环境五、测试&#xff1a;总结前言 SpringBoot并不是一门新的技术栈&#xff0c;它的主要目的是为了去…...

D. Constant Palindrome Sum(差分数组维护)

Problem - D - Codeforces 题意&#xff1a;给定长度为n的数组&#xff0c;每次操作可以选择一个数令a[i]变成[1,k]范围内的一个数&#xff0c;问最少需要多少次操作可以让a[i]a[n-i1]x (1< i < n/2)满足。 思路&#xff1a;利用差分数组d[i]表示x取i需要的总操作数。 …...

【C++】30h速成C++从入门到精通(IO流)

C语言的输入与输出C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键盘)读取数据&#xff0c;并将值存放在变量中。printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。注意宽度输出和精度输出控制。C语言借助了相应的缓冲区来…...

文件变成chk如何恢复正常

许多人不知道chk文件是什么&#xff1f;其实它是用户在使用“磁盘碎片整理程序”整理硬盘后所产生的“丢失簇的恢复文件”&#xff0c;而在u盘、内存卡等移动设备读取数据过程中&#xff0c;由于断电或强制拔出也容易产生大量的chk文件。那么文件变成chk如何恢复正常呢&#xf…...

Meta最新模型LLaMA细节与代码详解

Meta最新模型LLaMA细节与代码详解0. 简介1. 项目环境依赖2. 模型细节2.1 RMS Pre-Norm2.2 SwiGLU激活函数2.3 RoPE旋转位置编码3. 代码解读3.1 tokenizer3.2 model3.2.1 模型细节详解3.2.2 transformer构建3.3 generate4. 推理0. 简介 今天介绍的内容是Facebook Meta AI最新提…...

3/6考试总结

时间安排 7:30–7:50 看题&#xff0c;T1,T2 感觉是同类型的题&#xff0c;直接搜索状态然后 dp 一下&#xff0c;T3 估计是个独角晒。 7:50–8:20 T3&#xff0c;有 n^2 的式子&#xff0c;然后可以优化到 n ,写暴力验证一下发现不对。很迷&#xff0c;反复推了几遍都拍不上暴…...

产品经理必读书单

产品经理必读书单&#xff0c;世界变化那么快&#xff0c;不如静下来读读书。在这个浮躁的时代&#xff0c;能够安静下来读书的人太少了。古人云&#xff0c;“读万卷书&#xff0c;不如行万里路&#xff0c;行万里路不如阅人无数”。很多人别说阅人无数了&#xff0c;上学的时…...

UEFI移植LVGL

自己组装过游戏主机的应该都有看到过&#xff0c;进入BIOS设置&#xff0c;酷炫的界面便呈现在眼前&#xff0c;而很多BIOS&#xff0c;使用的还是标准的界面。现在有个趋势&#xff0c;phoenix和insyde也在慢慢朝这种GUI界面发展&#xff0c;而AMI的使用C编写的界面已经非常完…...

RK356x U-Boot研究所(命令篇)3.8 test命令的用法

平台U-Boot 版本Linux SDK 版本RK356x2017.09v1.2.3文章目录 一、test命令的介绍二、test命令的定义三、test命令的用法一、test命令的介绍 test 命令定义在cmd/test.c,需要使能以下配置: obj-$(CONFIG_HUSH_PARSER) += test.o以下介绍摘自cmd/Kconfig: config HUSH_PARS…...

LCD液晶段码驱动IC/LCD液晶驱动芯片VK2C22高抗干扰/抗噪,适用于汽车仪表/单相智能电表

产品型号&#xff1a;VK2C22A/B产品品牌&#xff1a;永嘉微电/VINKA封装形式&#xff1a;LQFP52/48、DICE(COB邦定片)、COG(邦定玻璃用)产品年份&#xff1a;新年份原厂&#xff0c;工程服务&#xff0c;技术支持&#xff01;VK2C22A/B概述&#xff1a;VK2C22是一个点阵式存储映…...

OpenMMLab 目标检测

OpenMMLab 目标检测1. 目标检测简介1.1 滑窗2. 基础知识2.1 边界框&#xff08;Bounding Box&#xff09;3. 两阶段目标检测算法3.1 多尺度检测技术4. 单阶段目标检测算法4.1 YOLO: You Only Look Once (2015)4.2 SSD: Single Shot MultiBox Detetor (2016)5. 无锚框目标检测算…...

Jenkins部署angular11自动打包

可能年纪大了&#xff0c;对于新东西的学习和接收有点慢&#xff0c;花了差不多一周的时间&#xff0c;终于把jenkins配置好了&#xff0c;可以自动打包&#xff0c;与手动打出来的一样&#xff0c;以后就解放双手了。#!/bin/bashnpm cache clean -fnpm -vnode -vnpm install n…...

【状态管理】zustand 中文文档,它来了!!!

如果有兴趣了解更多用法及 api &#xff0c;点击此处解锁中文文档 前言 是不是觉得 Redux 很难用&#xff1f;想用 Context 代替&#xff0c;但是你知道吗&#xff0c;Context 也有个很大的缺点&#xff1a; context value发生变化时&#xff0c;所有用到这个context的组件都…...

【时序】特征工程-时间序列特征构造

数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。由此可见,特征工程在机器学习中占有相当重要的地位。在实际应用当中,可以说特征工程是机器学习成功的关键。 特征工程是什么? 特征工程是利用数据领域的相关知识来创建能够使机器学习算法达到最佳性能的…...

【独家】华为OD机试 - 环中最长子串(C 语言解题)

最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧文章目录 最近更新的博客使用说明本期…...

JavaScript新手学习手册-基础代码(一)

什么是JavaScript&#xff1f; 百度百科 什么是控制台&#xff1f; 网页➡快捷键F12 进入Console就是控制台&#xff0c;它的作用与开发软件相同&#xff0c;可以进行代码的编写在紫色位置进行编写&#xff0c;另外console.log()方法所打印的内容都是在此进行输出。 一&#…...

Firewall App Blocker v1.7 防火墙管理设置工具多语言版

Firewall App Blocker 是一款由 BlueLife 与 Velociraptor 开发的免费且功能强大的防火墙设置软件。在 Windows 操作系统中,您可以使用 Windows 防火墙来阻止或解除阻止某些应用程序的联网,然而微软并没有为 Windows 防火墙提供一个易于使用的界面,来让用户使用其强大的功能…...

windows常用

方式1 ctrlaltdelete 可以进入管理内存 服务 查询在运行的端口 可以图形化结束端口进程 方式2 netstat -ano|findstr "端口号" taskkill -PID 进程端口号&#xff08;最后一列&#xff09; -F netstat -ano|findstr taskkill -PID -F 1.calc&#xff1a;启…...

从源码的角度告诉你 spark是怎样完成对文件切片

目录 1.说明 2.怎样设置默认切片数 2.1 RDD默认切片设置 2.2 SparkSQL默认切片设置 3. makeRDD 切片原理 4. textFile 切片原理 4.1 切片规则 4.2 怎样设置切片大小 4.3 测试代码 5.hadoopFile 切片原理 5.1 说明 5.2 切片规则 5.3 怎样设置切片大小 5.4 代码测试…...

剑指 Offer II 019. 最多删除一个字符得到回文

题目链接 剑指 Offer II 019. 最多删除一个字符得到回文 easy 题目描述 给定一个非空字符串 s&#xff0c;请判断如果 最多 从字符串中删除一个字符能否得到一个回文字符串。 示例 1: 输入: s “aba” 输出: true 示例 2: 输入: s “abca” 输出: true 解释: 可以删除 “c”…...

RK3568驱动OV13850摄像头模组调试过程

摄像头介绍品牌&#xff1a;Omnivision型号&#xff1a;CMK-OV13850接口&#xff1a;MIPI像素&#xff1a;1320WOV13850彩色图像传感器是一款低电压、高性能1/3.06英寸1320万像素CMOS图像传感器&#xff0c;使用OmniBSI?技术提供了单-1320万像素&#xff08;42243136)摄像头的…...

Go项目的目录结构基本布局

前言 随着项目的代码量在不断地增长&#xff0c;不同的开发人员按自己意愿随意布局和创建目录结构&#xff0c;项目维护性就很差&#xff0c;代码也非常凌乱。良好的目录与文件结构十分重要&#xff0c;尤其是团队合作的时候&#xff0c;良好的目录与文件结构可以减少很多不必要…...

CHAPTER 1 Linux Filesystem Management

Linux Filesystem Management1 文件系统是什么2 文件系统的组成3 inode详解1. inode到底是什么2. inode的内容3. inode的大小4. inode的号码5. 硬链接6. 软链接4 存储区域5 常见文件系统的类型1. 根文件系统2. 虚拟文件系统3. 真文件系统4. 伪文件系统5. 网络文件系统1 文件系统…...

RocketMQ架构篇 - 读写队列与生产者如何选择队列

读、写队列 创建主题时&#xff0c;可以指定 writeQueueNums&#xff08;写队列的个数&#xff09;、readQueueNums&#xff08;读队列的个数&#xff09;。生产者发送消息时&#xff0c;使用写队列的个数返回路由信息&#xff1b;消费者消费消息时&#xff0c;使用读队列的个…...

华为OD机试真题Python实现【通信误码】真题+解题思路+代码(20222023)

通信误码 题目 信号传播过程中会出现一些误码,不同的数字表示不同的误码 ID,取值范围为 1~65535,用一个数组记录误码出现的情况,每个误码出现的次数代表误码频度,请找出记录中包含频度最高误码的最小子数组长度。 🔥🔥🔥🔥🔥👉👉👉👉👉👉 华为OD…...

【单目3D目标检测】MonoDDE论文精读与代码解析

文章目录PrefacePros and ConsAbstractContributionsPreliminaryDirect depth estimationDepth from heightPespective-n-point&#xff08;PnP&#xff09;PipelineDiverse Depth EstimationsRobust Depth CombinationOutput distributionSelecting and combining reliable de…...