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

谈一谈在两个商业项目中使用MVI架构后的感悟

作者:leobertlan

前言

当时项目采用MVP分层设计,组员的代码风格差异也较大,代码中类职责赋予与封装风格各成一套,随着业务急速膨胀,代码越发混乱。试图用 MVI架构 + 单向流 形成 掣肘 带来一致风格。 但这种做法不够以人为本,最终采用 “在MVP的基础上进行了适当改造+设计约定的方式” 解决了问题,并未将MVI投入到商业项目中,于是 放弃了纸上谈兵

在半年前终于有机会在商业项目中进行实践,同诸位谈一谈使用后的 个人感悟 ,并藉此讲透MVI等架构。

所有内容将按照以下要点展开:

  • 从架构的理念出发 – 简单列明各种 MVX 的理念MVX:指代 MVC、MVP、MVVM、MVI
  • 拥抱复杂的同时实现简化 – 通过对比理解单向数据流动所解决的痛点、设计Intent的原因等问题
  • 单一可信数据源,不可僵化信奉
  • 要想优雅,需要工具 – 借助声明式、响应式编程工具,构建屏蔽命令式编程中的细节,同样是聚焦和简化
  • 状态和事件分家,绝不是吃饱了撑的 – 为什么要裂变出状态和事件,如何界定

内容会很长,我会酌情再写一些 ,结合实例和代码演示内容。

两个项目的基本情况

相比于之前的巨型项目,这两个项目的业务量均不大,一个是基于蓝牙和局域网的操控类APP,下午简称APP-A,一个是内部使用的工具,分析公司各个产品的日志,简称APP-B。

虽然他们的业务深度要比一般的APP要深,但在 本质上一致 ,毕竟同类型业务量再多也仅仅是重复运用一套模式 ,并不影响本质。

和诸多项目的本质一致,均符合如下图所示的逻辑分层,并在人机交互过程中执行业务逻辑:

  • APP-A 是Android项目,图方便纯kotlin
  • APP-B 是 Compose-Desktop项目,不得不kotlin

过于絮叨了,我们进入正文。

从架构的理念出发

谨记,实际情况中,MVI、MVVM这些架构均先由Web应用领域提出,用于解决浏览器Web应用研发中的问题。

在后续的应用领域发展过程中,存在共性问题,便引入了这些设计,并结合自身特点进行了拓展。

接下来我们聊一聊理念,不比武功。

图片出自电影一代宗师

MVI的理念

MVI 脱胎于 Model View Intent

  • Intent:驱动model发生改变的意图,以UI中的事件最为常见;
  • Model:业务模型,包含数据和逻辑,是对应 客观实体程序建模
  • View:表现层的视图,以UI方式呈现Model的状态(以及事件),接受用户输入,转换为UI事件

官方的这幅图很好的呈现了三者之间的驱动关系:

这张图非常简单,它摒弃了驱动方式的细节,只体现了角色与驱动关系。

注意,只要设计中满足 角色和驱动关系 符合上图,就是MVI架构设计,并不限制 驱动方式的实现细节

经典的MVI驱动细节要比上图复杂很多,下文再聊。

从软件设计的原则出发:职责分离并封装 的目的是 解耦可独立变化复用

显然,区别于 MVVMMVPMVC,角色上的差别在于 ViewModel、Presenter、Controller、Intent四者,而它们又是View和Model之间的纽带。除此之外,V和M亦稍有不同。

MVC、MVP

MVC、MVP 中,C和P的职责体现为 控制、调度

MVP中 VM 完全解耦可独立变化,MVC中 M 直接操作 V 耦合高,在web应用中,C 需要直接操作DOM。

MVVM

MVVM中,提倡 数据驱动数据源 被剥离到 VM 中,在 双向绑定框架 的加持下,View层的输入反映为数据的变化,数据的变化驱动视图内容。

显然,VM的职责限于维护数据状态,如有必要,驱动View层消费数据状态, 不必再关注如何操作视图。

一般来说,双向绑定框架已经引入观察者模式实现,可响应式驱动,VM一般没有必要关心 响应式驱动和下游观察者生命周期问题

简单思考之后会发现MVVM的问题,它的侧重点在于 利用双向绑定让开发者专注于数据状态的维护,从操作视图更新中得以解放,它难以解决 无天然状态 问题,例如:按钮点击这类事件。

MVI

在MVI中,结合业务背景将UI事件等内容转换为 Intent ,驱动Model层业务,Model层的业务结果反映为 视图状态 + 事件

因此View层和Model层之间已经解耦,并可以吸收MVVM中的优点采用如下设计:

  • 将双向绑定退化为单向绑定,View层消费UI状态流和事件流,这也意味着UI状态的职责精简,它不再承载View层的用户输入等事件
  • 将UI状态独立,Model层仅产生 UI状态的局部变化事件

下图为经典的MVI原理示意图:

在上文中,我们已经讨论了各个角色的职责,下面逐步展开讨论角色具备的特性和细节知识。

在此之前,还请谨记:合适的才是最好的

没有绝对的最好的设计,只有最合适的设计。

再好的架构,都需要遵循其理念并结合项目因地制宜地进行调整,以获得最佳使用效果。所以请读者诸君务必在阅读时,结合自身项目的情况仔细思考以下问题:

  • 引入新框架所解决的痛点、衍生的问题、是否需要进行框架调整?
  • 框架中的角色功能,为什么出现,又有怎样的局限?

单向数据流动

MVI拥抱了结构复杂,但能够灵活应对业务编码时的各种情况,按部就班即可。

从MVI原理图中,可以清晰的看到 “数据” 的流动方向。 起始于 Intent,经过分类和选择性消费后产生 Result,对应的reducer函数计算后,得到最新的 State (以及裂变出必要的 Event,图中未体现) ,驱动视图。

注意:

  • 单向 是指 单一方向
  • 此处的 数据 是广义的、宽泛的。
  • 仅描述数据流的 变化方向 ,与数据流的数量无关,但一般 形成有效工作 均需要两条数据流(上行数据流和下行数据流)

即驱动数据流变化的方向是唯一的,在英文中的术语为:Unidirectional Data Flow 简称 UDF

MVC、MVP中的痛点

前文我们提到,在MVC和MVP中,着眼于 控制、调度 ,并不强调 数据流 的概念。

View和Model间之间的交互,一般有两种编码风格:双向的API调用、单向的API调用+回调:

注意:以下两图并未体现Controller和Presenter细节,仅表意,从View层出发的API调用和回到View层的UI更新

双向API调用如上图。

单向API调用+回调更新UI如上图。

显而易见,这两种方式无法继续抽象,需根据实际业务进行命令式编码。当UI复杂时,难以写出清晰、易读的代码,维护难度激增。

MVVM解决UI更新代码混乱问题

前文我们已经提到:MVVM中通过绑定框架,将UI事件转化为数据变化,驱动业务;业务结果表现为数据变化,驱动UI更新。

显而易见,维护朴素的数据要比直接维护复杂的UI要简单

但问题也同时产生,data1的变化有两个可能的原因:

  • Model层业务结果使其变化,并期望它驱动UI更新
  • View层发生事件,反馈数据变化,并期望它驱动Model层逻辑

因此,框架需要考虑标识数据变化来源、或者其他手段消除方向性所带来的问题。

并且MVVM难以灵活决定的 “何时调用Model层逻辑”,即大多数业务中,都需要结合多个属性的变化形成组合条件来驱动Model层逻辑。

本篇并不重点讨论MVVM,故不再展开MVVM解决循环更新的方案,以及衍生的问题。

尽管如此,MVVM中的数据绑定依旧解决了View层更新繁杂的问题。

用Intent灵活决定何时调用Model

既然数据驱动UI有极大的益处,且View层事件驱动ViewModel的数据变化有很多弊端 (需要建立很高的复杂度) ,那自然需要 趋利避害

仅保留数据驱动UI的部分,并增加Intent用以驱动Model层业务

在于 MVC/MVP 以及 MVVM 对比后不难得出结论:

  • MVC/MVP中,View层通过调用C/P层API的方式最终调用到Model层业务,方式质朴、无难度。但业务量规模增大后接口方法数也会增多,导致C/P层尾大不掉,难以重用。
  • MVVM中,VM层总是需要利用 技巧 进行模型概念转换,以满足业务响应满足实际需求,需要很深厚的设计经验才能写出非常优秀的代码,这并不友好。

作者按:我个人认为一个友好的设计,不应当剑走偏锋,而应当大巧不工,能够以力破法,达成 “使用者只需要吃透理论就可以解决各类问题” 的目标。

而MVI在架构角色中设计了Intent的角色:

  • 它包含了业务调用的意图和数据
  • 从设计上可满足 调用实现 的分离
  • 架构模型中以Intent流的形式出现,下游对其的 筛选转换消费 等行为可遵循 FP范式 (即函数式编程范式、Functional Programming Patterns) ,逻辑的复用粒度为方法级,复用度更高更灵活
  • 解决了MVVM中的方向性问题、MVC/MVP 中的灵活度问题等

单一可信数据源

我猜测读者诸君都曾听过这个词,将 单一可信数据源 拆解一下:

  • 单一
  • 可信
  • 数据源

在MVI背景下,数据源 指的是视图对应的数据实体,它代表视图的内容状态。

可信指从数据源中获取的数据是 最新的完整的可靠的,否则是不可信的,我们没有理由在编码中使用不可信的数据源

单一是指这样的数据源仅一个。

在经典设计中,其内涵如下图:

  • 按照视图的 所有的 内容状态,定义一个不可变的 ViewState
  • 按照业务初始化 ViewState 实例
  • Model业务生成驱动 ViewState变化的Result
  • 计算出新状态,Reduce(Pre-ViewState,Result) -> New-ViewState
  • 更新数据源
  • View层消费ViewState

借助于数据绑定框架,可以很方便地解决视图更新的问题。

想象一下,此时页面UI非常复杂……

如果僵化的信奉这样的 单一 ,情况会如何呢?

  • 复杂(大量属性)的ViewState
  • 复杂的UI更新计算,e.g. 100个属性变了2个,依然需要计算98个属性未变或者全量强制更新

在 APP-A和APP-B中,我分别使用了 DataBinding和Compose,但均无法避免该问题。

何为单一

从机器执行程序的原理上看,我们无法实现 多个内容一致的数据源任意时刻 满足 最新的可靠的

将视图视为一个整体,规定它只拥有 一个 可信的数据源。在此基础上看局部的视图,它们也顺其自然地仅拥有一个可信的数据源。

反过来看,当任意的局部视图仅具有一个可信数据源时,整体视图也仅有一个逻辑上的可信数据源。

据此,我们可以对 经典MVI实现 进行一定程度的改造,将ViewState进行局部分解,使得UI绑定部分的业务逻辑更 清晰、干净

请注意,复杂度不会凭空消失,我们为了让 “UI绑定的业务逻辑更清晰、干净”、“更新UI的计算量更少”,将复杂度转移到了ViewState的拆分。拆分后,将具有 多个视图部件的单一可信数据源,注意,为了不引起额外的麻烦、并且便于维护扩展,建议遵守以下条件:

  • 基于业务需求,组合数据源形成新数据源
  • 不在数据源的逻辑范围之外进行数据源组合操作

举个虚拟的例子:用户需要实名认证 且 关注博主 ,才在界面上显示某功能按钮。下面使用代码分别演示。

考虑到RxJava的广泛度依旧高于Kotlin-Coroutine+flow,数据流的实现采用RxJava

注意,考虑到读者可能会编写demo做UDF局部的验证,下文中的代码以示例目的为主,兼顾编写场景冒烟的方便性,流的类型不一定是构建完整UDF的最佳选择。

经典实现

在经典MVI实现中,需要先定义ViewState

data class ViewState(/*unique id of current login user*/val userId: Int,/*true if the current login user has complete real-name verified*/val realNameVerified: Boolean,/*true if the current login user has followed the author*/val hasFollowAuthor: Boolean
) {
}

并定义ViewModel,创建ViewState流,忽略掉其初始化和其他部分

class VM {val viewState = BehaviorSubject.create<ViewState>()//ignore
}

并定义View层,忽略掉其他部分,简单起见暂时不使用数据绑定框架

class View {private val vm = VM()lateinit var imgRealNameVerified: ImageViewlateinit var cbHasFollowAuthor: CheckBoxlateinit var someButton: Buttonfun onCreate() {//ignore view initializevm.viewState.subscribe {render(it)}}private fun render(state: ViewState) {imgRealNameVerified.isVisible = state.realNameVerifiedcbHasFollowAuthor.isChecked = state.hasFollowAuthorsomeButton.isVisible = state.realNameVerified && state.hasFollowAuthor//ignore other}
}

在JS中,JSON并不能附加逻辑,基本等价于Java中的POJO,故在数据源外部处理简单逻辑的情况较为常见。而在Java、Kotlin中可以进行适当的优化,适当封装,使得代码更加干净便于维护:

data class ViewState(//ignore
) {fun isSomeFuncEnabled():Boolean = realNameVerified && hasFollowAuthor
}class View {//ignoreprivate fun render(state: ViewState) {//...someButton.isVisible = state.isSomeFuncEnabled()}
}

拆分实现

依旧先定义逻辑上完整的ViewState:

class ComposedViewState(/*unique id of current login user*/val userId: Int,
) {/*** real-name-verified observable subject,feed true if the current login user has complete real-name verified* */val realNameVerified = BehaviorSubject.create<Boolean>()/*** follow-author observable subject, feed true if the current login user has followed the author* */val hasFollowAuthor = BehaviorSubject.create<Boolean>()val someFuncEnabled = BehaviorSubject.combineLatest(realNameVerified, hasFollowAuthor) { a, b -> a && b }
}

定义ViewModel,子模块数据流均已定义,故而无需再定义全ViewState的流

class VM(val userId: Int) {val viewState = ComposedViewState(userId)//ignore
}

编写View层的UI绑定,同样简单起见,不使用数据绑定框架

class View {private val vm = VM(1)lateinit var imgRealNameVerified: ImageViewlateinit var cbHasFollowAuthor: CheckBoxlateinit var someButton: Buttonfun onCreate() {//ignore view initializebindViewStateWithUI()}private fun bindViewStateWithUI() {vm.viewState.realNameVerified.subscribe {renderSection1(it)}vm.viewState.hasFollowAuthor.subscribe {renderSection2(it)}vm.viewState.someFuncEnabled.subscribe {renderSection3(it)}//...}private fun renderSection1(foo:Boolean) {imgRealNameVerified.isVisible = foo}private fun renderSection2(foo:Boolean) {cbHasFollowAuthor.isChecked = foo}private fun renderSection3(foo:Boolean) {someButton.isVisible = foo}
}

例子较为简单,在实际项目中,如果遇到复杂页面,则可以分块进行处理。

注意:实际情况中,并没有必要将每一个子数据源拆分到一个View级别的控件,那样过于啰嗦,例子因非常简单而无法丰满起来。 e.g. 针对每一块视图区,例如作者区域,定义子ViewState类,创建其数据流即可。

作者按:务必评估,在一次Model业务产生的Result中,会引起数据流下游的更新次数。 为避免产生不可预期的问题,可通过类似以下方式,使下游响应次数表现和经典实现的情况一致。

额外定义PartialChange流或者功能等价的流,它用于标识 reduce 计算的开始和结束,可以将此期间的数据流的变化延迟到最后发送终态

更加推荐定义功能上等价的流

class ComposedViewState(/*unique id of current login user*/val userId: Int,
) {internal val changes = BehaviorSubject.create<PartialChange>()//ignoreval someFuncEnabled =BehaviorSubject.combineLatest(realNameVerified, hasFollowAuthor) { a, b -> a && b }.sync(PartialChange.Tag, changes)
}inline fun <reified T, S> Observable<T>.sync(tag: S, sync: BehaviorSubject<S>): Observable<T> {return BehaviorSubject.combineLatest(this, sync) { source, syncItem ->if (syncItem == tag) {syncItem} else {source}}.filter { it is T }.cast(T::class.java)
}

修改PartialChange,为reduce函数添加边界:

PartialChange是Model产生的Result的表现物,封装了ViewState的reduce函数逻辑,即如何从 Pre-ViewState 生成 新 ViewState

sealed class PartialChange {open fun reduce(state: ComposedViewState) {}/*** 同步标记,从头开始到真实PartialChange之间,流的状态生效* */object Tag : PartialChange()object None : PartialChange()class Foo(val a: Boolean, val b: Boolean) : PartialChange() {override fun reduce(state: ComposedViewState) {state.changes.onNext(Tag)state.realNameVerified.onNext(a)state.hasFollowAuthor.onNext(b)state.changes.onNext(this)}}
}

要想优雅,需要工具

采用响应式流,避免命令式编码

想来这一点已不需要多做解释。

在Android中,存在 LiveData 组件,它通过简单的方式封装了可观测的数据,但实现方式简单也限制了它的功能 不够强大 。因此,建议使用 RxJava 或者 Kotlin-Coroutine & flow 构建数据流。

本节便不再展开。

采用数据绑定框架

采用 jetpack-compose 或者 DataBinding 均可以移除枯燥的UI命令式逻辑,在APP-A中我使用了DataBinding,在APP-B中我使用了Compose。

在 ViewState的代码很棒时,均可以获得优秀的编程体验,从啰嗦的UI中解放出来。

作者的个人观点:

关于Compose。Compose依旧属于较新的事物,在商业项目中使用存在学习门槛和造轮工作。在目标用户具有较高容忍度的情况下,已然可以进行尝试。

关于DataBinding。一个近乎毁誉参半的工具,关于它的批判,大多集中于:xml中实现的逻辑难以阅读、维护,这实际上是对DataBinding设计的误解而带来的错误使用。

DataBinding本身具有生成VM层的功能,但这一功能并不足够强大,且没有完善的使用指导,而在官方Demo中过度宣传了它,导致大家认为DataBinding就该这样使用。

仅使用基础的数据绑定功能、和Resource或者Context有关的功能(例如字符串模板)、组件生命周期绑定等,适度自定义绑定。

何为状态、何为事件。最后的一公里

首先区别于上文提到的UI事件,这里的状态和事件均产生于数据流的末段,而UI事件处于数据流的首段。

UI事件属于:A possible action that the user can perform that is monitored by an application or the operating system (event listener). When an event occurs an event handler is called which performs a specific task

在展开之前,先用一张图回顾总结上文中对于 单向数据流 & 单一可信数据源 的知识

单向数据流动 章节中,提到了MVI的UDF设计:

  • 系统捕获的UI事件、其他侦听事件(例如熄屏、应用生命周期事件),生成Intent,压入Intent流中
  • ViewModel层中筛选、转换、处理Intent,实际是使用Model层业务,产生业务结果,即PartialChange
  • PartialChange经过Reducer计算处理得到最新的ViewState,压入ViewState流
  • View层(广义的表现层)响应并呈现最新的ViewState

单一可信数据源 章节中,提到View层应当采用 单一可信数据源

在这张图中,我们仅体现了 状态 即 ViewState。

关于GUI程序的认知

在展开前,先聊点理念上的内容。请读者诸君思考下自己对于GUI程序的认知。

作者的理解:

程序狭义上是计算机能识别和执行的一组指令集,编程工作是在程序世界对 客观实体业务逻辑 进行 建模和逻辑表达。

而GUI程序拥有 用户图形界面 , 除了结合硬件接收用户交互输入外,可以将 程序世界中的模型用户图形界面 等方式表现给用户。

表现出来的内容代表着客观实体

其本质目的在于:通过 描述特征属性描述变化过程 等方式让用户感知并理解 客观实体

而除了通过 程序语言描述程序世界模拟展现 外,同样可以通过 自然语言描述 达到目的,这也是产品经理的工作。

当然,产品经理往往需要借助一些工具来提升自己的自然语言表达能力,但无奈的是能用数学公式和逻辑推演表达需求的产品经理太少见了。

写这段只是为了引入 他山之石

First-Order logic

在数学、哲学、语言学、计算机科学中,有一个概念 First-Order logic,无论是产品需求还是计算机程序,都可以建立FOL表达

当然,本篇不讨论FOL,那是一个很庞大且偏离主题的事情。我仅仅是想借用其中的概念。

FOL表达 Event或者State时:

  • Event 体现的是特定的变化
  • State 体现的是客观实体在任意时刻都适用的一组情况,即一段时间内无变化的条件或者特征

不难理解,变化是瞬时的,连续的变化是可分的。

但在人机交互中,瞬时意义很小,我们的目的在于让用户感知。

例如:“好友向你发送了一条消息的场景中”,消息抵达就是Event,它背后潜藏着 “消息数的变化”、“最新消息内容的变化” 等。 在常见的设计中:

  • 应用需要弹出一个气泡通知用户这一事件
  • 应用需要更新消息数,消息列表内容等,以呈现出最新的State

而为了让用户感知到,气泡呈现时长并不是瞬时的,但在产品交互设计中依旧将其定义为事件。

分离状态和事件,不是吃饱撑得

看山是山、看水是水

此时此刻,答案已经很明显。

在通用的产品设计中,状态和事件有不同的意义,如果程序中不分离出两者,则必然是自找麻烦,这是公然挑衅 面向对象编程 的行为。如果不明确定义不同的Class,则势必导致代码混乱不堪,毕竟这是违背编程原则的事情。

在大多MVVM设计中,状态和事件未分家,导致bug丛生,这一点便不再展开。

如何区分Event和State

State是一段时间内无变化的条件或者特征,它天然的 契合 了位于表现层的主体内容所对应的 数据模型特征

Event是特定的变化,它在表现层体现,但与State的生命周期不一致,且并无一一对应的关系。

基于经验主义,我们可以机械地、笼统地认为:页面主体静态内容所需要的数据属于State范畴,气泡提醒等短暂的物体所需要的数据属于Event范畴。

从逻辑推演的角度出发,进行 等价逻辑推断条件限定下的逻辑推断 ,一定序列的Event可以模型转换为State。

事件粘性导致重复?只是框架设计的bug

看山不是山,看水不是水

前面提到,State是一段时间内无变化的条件或者特征,所以在程序设计中State具有粘性的特征。

如果Event也设计出这样的粘性特征并造成重复消费,明显是违背需求的,无疑是框架设计的Bug。此问题在各大论坛中很常见。

注意,我们无法脱离实际需求去二元化的讨论事件本身该不该有粘性特征,只能结合实际讨论框架功能是否存在bug

如果要实现以力破法,在框架设计层面上 Event体系的设计要比State体系要复杂 。因为从交互设计上:

  • State 只需要考虑呈现的准确性和及时性,除去美观、可理解性等等
  • Event 需要考虑准确性、优先级、及时性、按条件丢弃等等,除去美观、可理解性等等

举个例子:网络连接问题导致的Web-API调用失败需要使用Toast提示网络连接失败

不难想象:

  • 可能一瞬间的断开网络连接,会导致多个连接均返回失败
  • 可能连接问题未修复,10秒前请求失败,当前请求又失败了

难道连续弹出吗?难道和上一次Event一致就不消费吗?…

或许您会使用一些 剑走偏锋的技巧 来解决问题,但技巧总是建立在特定条件下生效的,一旦条件发生变化,就会带来烦恼,您很难控制上游的PM和交互设计师。

所以在框架层面需要针对产品、交互设计的泛化理念,设计准确的、灵活的Event体系。

准确的、灵活的Event体系

看山还是山,看水还是水

回到FOL中,为了更加准确的表达Event和State的含义,还需要一些额外的参数,例如:参与者地点时间 等。

想通这一点会发现,产品中定义的Event事件、及其消费逻辑均含有隐藏属性,例如:

  • 发生时间
  • 客观有效期
  • 判断有效的条件(如呈现的条件)
  • 判断失效的条件 ,用于实现提前失效

产品经理和交互设计师一般会使用 “响应时间”、“优先级” 等词描述它们,但一般不严谨、不成体系,带来期望不一致的问题

反观State流,它代表了界面主体内容在时间轴上的完整变化,任意一个时间点均可以得出界面内容所对应的条件和特征。一旦State流中出现一个新的状态,它均被及时的、准确的在表现层予以体现。

不难理解,一个State的生命周期为 从init或者reducer计算生成开始reducer计算出新State、宿主生命期结束为止,在State流中已然暗含:

  • State之间无生命周期重叠
  • 所有State的生命周期相加可填满时间轴

前文提到Event是瞬时的,所以Event本身并没有实质意义上的生命周期,为了方便表述,我们将 “Event从生成到在表现层不可观测的阶段” 定义为Event生命周期

而Event流 不同于 State流 ,因为Event的生命周期情况更加复杂:

  • Event可能存在生命周期重叠
  • 所有Event的生命周期相加可能无法覆盖完整的时间轴

需要额外设计实现 。实现这一点后,从Event流中分流(以及裂变+组合)出的 子流 将和State流 性质一致

此刻,您会发现,根据不同类型的事件交互控件所对应的交互特征,又将Event流结合条件流衍生出各个State流。完整的数据流细节如下:

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

相关文章:

谈一谈在两个商业项目中使用MVI架构后的感悟

作者&#xff1a;leobertlan 前言 当时项目采用MVP分层设计&#xff0c;组员的代码风格差异也较大&#xff0c;代码中类职责赋予与封装风格各成一套&#xff0c;随着业务急速膨胀&#xff0c;代码越发混乱。试图用 MVI架构 单向流 形成 掣肘 带来一致风格。 但这种做法不够以…...

ApacheCon - 云原生大数据上的 Apache 项目实践

Apache 软件基金会的官方全球系列大会 CommunityOverCode Asia&#xff08;原 ApacheCon Asia&#xff09;首次中国线下峰会将于 2023 年 8 月 18-20 日在北京丽亭华苑酒店举办&#xff0c;大会含 17 个论坛方向、上百个前沿议题。 字节跳动云原生计算团队在此次 CommunityOve…...

Git 代码分支规范

目的 俗话说&#xff1a;没有规矩&#xff0c;不成方圆。遵循一个好的规章制度能让你的工作事半功倍。同时也可以展现出你做事的认真的态度以及你的专业性&#xff0c;不会显得杂乱无章&#xff0c;管理困难。Git分支规范也是一样。当遵循了某种约定的Git分支&#xff0c;在代…...

ATFX汇评:美7月通胀率数据基本符合预期,美指仍无法站稳103关口

ATFX汇评&#xff1a;据美劳工部&#xff0c;美国7月未季调CPI年率&#xff0c;最新值3.2&#xff0c;高于前值3%&#xff0c;低于预期值3.3%&#xff0c;这标志着连续12个月的下降已经停止&#xff1b;7月未季调核心CPI年率&#xff0c;最新值4.7%&#xff0c;低于前值4.8%&am…...

系统架构设计专业技能 · 软件工程(一)【系统架构设计师】

系列文章目录 系统架构设计高级技能 软件架构概念、架构风格、ABSD、架构复用、DSSA&#xff08;一&#xff09;【系统架构设计师】 系统架构设计高级技能 系统质量属性与架构评估&#xff08;二&#xff09;【系统架构设计师】 系统架构设计高级技能 软件可靠性分析与设计…...

C语言 指针的运算

目录 一、介绍 二、指针 整数 、指针 - 整数 二、指针 - 指针 四、指针的关系运算 一、介绍 在C语言中&#xff0c;指针的运算分为三类 1、指针 整数 、指针 - 整数2、指针 - 指针3、指针的关系运算 二、指针 整数 、指针 - 整数 因为数组在…...

【JAVA基础】- 同步非阻塞模式NIO详解

【JAVA基础】- 同步非阻塞模式NIO详解 文章目录 【JAVA基础】- 同步非阻塞模式NIO详解一、概述二、常用概念三、NIO的实现原理四、NIO代码实现客户端实现服务端实现 五、同步非阻塞NIO总结 一、概述 NIO&#xff08;Non-Blocking IO&#xff09;是同步非阻塞方式来处理IO数据。…...

dingding机器人

“自定义机器人”只支持消息发送&#xff0c;自动回复需要“企业内部机器人” 消息发送 import requests import jsonres requests.post(https://oapi.dingtalk.com/robot/send?access_token036a339axxx,data json.dumps({"text": {"content":"h…...

6.6 实现卷积神经网络LeNet训练并预测手写体数字

模型架构 代码实现 import torch from torch import nn from d2l import torch as d2lnet nn.Sequential(nn.Conv2d(1,6,kernel_size5,padding2),nn.Sigmoid(),#padding2补偿5x5卷积核导致的特征减少。nn.AvgPool2d(kernel_size2,stride2),nn.Conv2d(6,16,kernel_size5),nn.S…...

Django路由Router

文章目录 一、路由router路由匹配命名空间反向解析 二、实践创建用户模型Model添加子路由 - 创建用户首页页面跳转 - 使用反向解析和命名空间1. 不使用命名空间的效果2. 使用命名空间的效果 用户详情页面跳转 - 路由传参路由传递多个参数re_path 以前写法,了解即可重定向Redire…...

蜜蜂路线 P2437

蜜蜂路线 题目背景 无 题目描述 一只蜜蜂在下图所示的数字蜂房上爬动,已知它只能从标号小的蜂房爬到标号大的相邻蜂房,现在问你&#xff1a;蜜蜂从蜂房 m 开始爬到蜂房 n&#xff0c;m<n&#xff0c;有多少种爬行路线&#xff1f;&#xff08;备注&#xff1a;题面有误&…...

无脑——010 复现yolov8 使用yolov8和rt detr 对比,并训练自己的数据集

1.配置环境 1. 首先去官网下载yolov8的zip https://github.com/ultralytics/ultralytics 存放在我的目录下G:\bsh\yolov8 然后使用conda创建新的环境 conda create -n yolov8 python3.8 #然后激活环境 conda activate yolov8然后安装pytorch&#xff0c;注意 &#xff0c;py…...

如何给Google Chrome增加proxy

1. 先打开https://github.com/KaranGauswami/socks-to-http-proxy/releases 我的电脑是Liunx系统所以下载第一个 2. 下载完之后把这个文件变成可执行文件&#xff0c;可以是用这个命令 chmod x 文件名 3. 然后执行这个命令&#xff1a; ./sthp-linux -p 8080 -s 127.0.0.1:…...

设计模式——原型模式

原型模式就是有时我们需要多个类的实例&#xff0c;但是一个个创建&#xff0c;然后初始化&#xff0c;这样太麻烦了&#xff0c;此时可以使用克隆&#xff0c;来创建出克隆对象&#xff0c;就能大大的提高效率。具体就是要让此类实现Cloneable接口&#xff0c;然后重写Object类…...

Spring框架中的Bean生命周期

目录 Bean的实例化 BeanFactoryPostProcessor 属性赋值 循环依赖 初始化 处理各种Aware接口 执行BeanPostProcessor前置处理 执行InitializingBean初始化方法或执行init-method自定义初始化方法 执行BeanPostProcessor后置处理 销毁 Spring Bean 的生命周期总体分为…...

async和await修饰符

async和await是JavaScript中用来处理异步操作的关键字 。 async和await也是解决回调地域的终极方案&#xff0c;简单&#xff0c;而Promise链混杂难以看懂。 async关键字用于定义一个函数&#xff0c;使其返回一个Promise对象。这意味着该函数可以通过await关键字来暂停执行&…...

vivado tcl创建工程和Git管理

一、Tcl工程创建 二、Git版本管理 对于创建完成的工程需要Git备份时&#xff0c;不需要上传完整几百或上G的工程&#xff0c;使用tcl指令创建脚本&#xff0c;并只将Tcl脚本上传&#xff0c;克隆时&#xff0c;只需要克隆tcl脚本&#xff0c;使用vivado导入新建工程即可。 优…...

田间农业数字管理系统-高标准农田建设

政策背景 2019年11月&#xff0c;国务院办公厅印发的《国务院办公厅关于切实加强高标准农田建设提升粮食安全保障能力的意见》明确提出&#xff0c;到2022年&#xff0c;全国要建成10亿亩高标准农田。 2021年9月16日&#xff0c;由农业农村部印发的《全国高标准农田建设规划&a…...

【网络安全】等保测评系列预热

【网络安全】等保测评系列预热 前言1. 什么是等级保护&#xff1f;2. 为什么要做等保&#xff1f;3. 路人甲疑问&#xff1f; 一、等保测试1. 渗透测试流程1.1 明确目标1.2 信息搜集1.3 漏洞探索1.4 漏洞验证1.5 信息分析1.6 获取所需1.7 信息整理1.8 形成报告 2. 等保概述2.1 …...

解决: git拉取报错 git 未能顺利结束 (退出码 1)

拉取代码失败信息 解决方法: 执行一下"git push -f origin master"命令即可 步骤: 1.项目文件夹右击选择"Git Bash Here",打开命令窗口 2. 输入"git push -f origin master"后,回画 执行结束 3.再拉取代码,成功...

从零开始开发纯血鸿蒙应用之网络检测

从零开始开发纯血鸿蒙应用 〇、前言一、认识 connection 模块1、获取默认网络2、获取网络能力信息3、解析网络能力信息3.1、NetCap3.2、NetBearType 二、实现网络检测功能1、申请权限2、获取默认网路的 NetCap 数组 三、总结 〇、前言 在之前的博文里&#xff0c;介绍了如何实…...

Go 语言实现高性能 EventBus 事件总线系统(含网络通信、微服务、并发异步实战)

前言 在现代微服务与事件驱动架构&#xff08;EDA&#xff09;中&#xff0c;事件总线&#xff08;EventBus&#xff09; 是实现模块解耦与系统异步处理的关键机制。 本文将以 Go 语言为基础&#xff0c;从零构建一个高性能、可扩展的事件总线系统&#xff0c;深入讲解&#…...

使用 Windows 完成 iOS 应用上架:Appuploader对比其他证书与上传方案

iOS 应用上架流程对很多开发者来说都是一道复杂关卡&#xff0c;特别是当你并不使用 Mac 电脑时。虽然 Apple 一直强调使用其原生工具链&#xff08;Xcode 和 Transporter&#xff09;&#xff0c;但现实是大量开发者正在寻找更灵活的替代方案。 今天我将从证书申请和 IPA 上传…...

11 - ArcGIS For JavaScript -- 高程分析

这里写自定义目录标题 描述代码实现结果 描述 高程分析是地理信息系统(GIS)中的核心功能之一&#xff0c;主要涉及对地表高度数据(数字高程模型, DEM)的处理和分析。 ArcGIS For JavaScript4.32版本的发布&#xff0c;提供了Web端的针对高程分析的功能。 代码实现 <!doct…...

leetcode 2434. 使用机器人打印字典序最小的字符串 中等

给你一个字符串 s 和一个机器人&#xff0c;机器人当前有一个空字符串 t 。执行以下操作之一&#xff0c;直到 s 和 t 都变成空字符串&#xff1a; 删除字符串 s 的 第一个 字符&#xff0c;并将该字符给机器人。机器人把这个字符添加到 t 的尾部。删除字符串 t 的 最后一个 字…...

数仓面试提问:在资源(计算、存储、人力)受限的情况下,如何优先处理需求并保证核心交付?

在资源受限的情况下高效处理需求并保证核心交付,是每个团队管理者都会面临的挑战。这种既要“少花钱多办事”又要确保关键任务不延误的压力,面对这种情况,我们需要一套系统化的方法来实现需求评估、优先级排序和有效沟通。以下是经过实践验证的策略和方法: 🛠️ 一、 保证…...

论文解读:Locating and Editing Factual Associations in GPT(ROME)

论文发表于人工智能顶会NeurIPS(原文链接)&#xff0c;研究了GPT(Generative Pre-trained Transformer)中事实关联的存储和回忆&#xff0c;发现这些关联与局部化、可直接编辑的计算相对应。因此&#xff1a; 1、开发了一种因果干预方法&#xff0c;用于识别对模型的事实预测起…...

IDEA 中 Undo Commit,Revert Commit,Drop Commit区别

一、Undo Commit 适用情况&#xff1a;代码修改完了&#xff0c;已经Commit了&#xff0c;但是还未push&#xff0c;然后发现还有地方需要修改&#xff0c;但是又不想增加一个新的Commit记录。这时可以进行Undo Commit&#xff0c;修改后再重新Commit。如果已经进行了Push&…...

Qt(part1)Qpushbutton,信号与槽,对象树,自定义信号与槽,lamda表达式。

1、创建Qt程序 2、命名规范及快捷键 3、Qpushbutton按钮创建 4、对象树概念 5、信号与槽 6、自定义信号与槽 7、当自定义信号和槽发生重载时 8、信号可以连接信号&#xff0c;信号也可以断开。 9、lamda表达式...

基于智能代理人工智能(Agentic AI)对冲基金模拟系统:模范巴菲特、凯西·伍德的投资策略

股票市场涉及众多统计数据和模式。股票交易基于研究和数据驱动的决策。人工智能的使用可以实现流程自动化&#xff0c;让投资者在研究上花费更少的时间&#xff0c;同时提高准确性。这使他们能够更加专注于监督实际交易和服务客户。 顶尖对冲基金经理发挥着至关重要的作用&…...