Android ViewModel 生命周期感知与配置变更存活实战指南
1. 从一次屏幕旋转引发的崩溃说起很多人第一次接触 ViewModel 是在一个很具体的场景里辛辛苦苦写了个页面用户把手机横过来界面直接回到初始状态输入框清空、列表滚动位置丢失、网络请求重新发起。更让人头疼的是如果此时有个异步回调还在跑它持有的 Activity 引用已经失效一个空指针就崩了。这个问题的根源在于 Android 的组件生命周期天然不稳定——配置变更、页面跳转、系统回收任何一个环节都可能让界面层对象被销毁重建。ViewModel 就是为解决这类问题而生的。它的核心定位是以生命周期感知的方式持有和管理界面相关的数据让数据在配置变更时存活下来同时把数据逻辑从 Activity 或 Fragment 中剥离出去。关键词里提到的“生命周期感知”“配置变更存活”“数据与界面分离”本质上都是围绕这个定位展开的。这篇文章适合已经写过几个 Android 页面、被生命周期问题折磨过、想真正把 ViewModel 用对而不是用个皮毛的开发者。我会从它到底解决了什么问题讲起拆解底层原理再落到实际项目里怎么组织代码、怎么避坑尽量把那些文档里一笔带过但实际开发中一定会遇到的东西说透。需要先明确一点ViewModel 不是“万能数据容器”也不是“替代所有状态管理方案”的银弹。它有自己的适用边界用错了地方反而会引入内存泄漏或者数据错乱。下面我们一层层来看。2. ViewModel 到底在生命周期里扮演什么角色2.1 配置变更时数据为什么能活下来要理解 ViewModel 的存活机制得先搞清楚 Activity 重建时到底发生了什么。当屏幕旋转时系统会销毁当前 Activity 实例并创建一个新的这个过程走的是正常的 onDestroy 和 onCreate但不是进程被杀。ViewModelStore 就挂在这个重建链条之外——它由 Activity 的 NonConfigurationInstance 机制持有在配置变更时被系统临时保存并传递给新的 Activity 实例。用生活化的类比Activity 像是一个前台接待员换班时人换了但交接的文件夹还在。ViewModelStore 就是这个文件夹里面装着各个 ViewModel。新接待员上岗后从文件夹里取出原来的资料继续用而不是从头再问一遍客户信息。这里有个关键细节ViewModel 的 onCleared 回调只在真正销毁时触发比如用户按返回键退出页面、或者 Activity 被系统正常 finish。配置变更导致的重建不会触发 onCleared。所以如果你在 ViewModel 里注册了监听器、开了线程判断“什么时候该清理”就要以 onCleared 为准而不是 onDestroy。2.2 ViewModel 和 onSaveInstanceState 的分工很多人会问既然 onSaveInstanceState 也能保存数据为什么还要 ViewModel这两者的适用场景完全不同搞混了会写出很别扭的代码。对比维度ViewModelonSaveInstanceState数据存放位置内存中Bundle 中可序列化存活范围配置变更配置变更 进程被系统回收后恢复适合的数据列表、网络结果、复杂对象轻量级 UI 状态、ID、简单标记数据大小限制基本无限制受内存约束有严格限制过大易触发 TransactionTooLarge是否可持有引用可持有 Repository 等只能放可序列化基础类型实际项目里的常见做法是两者配合ViewModel 持有主要数据onSaveInstanceState 只存一个最小的“恢复锚点”比如当前选中的 tab、列表的滚动位置 ID。进程被回收后重建时用这个锚点重新拉取数据。我见过有人把整个列表塞进 Bundle结果页面一复杂就崩这个坑一定要避开。2.3 一个 ViewModel 被多个 Fragment 共享时的作用域Fragment 之间通信是另一个高频场景。传统做法是用接口回调或者 Activity 做中转代码耦合严重。ViewModel 的作用域可以绑定到 Activity这样同一个 Activity 下的多个 Fragment 拿到的是同一个 ViewModel 实例天然实现了数据共享。这里的关键是ViewModelProvider的构造参数。传入 Activity 作为 owner得到的就是 Activity 作用域的 ViewModel传入 Fragment 作为 owner得到的就是 Fragment 作用域的。共享场景下必须统一用 Activity 作为 owner否则每个 Fragment 会各自创建一份数据根本不同步。这个点看起来简单但实际开发中因为传错 owner 导致“数据不刷新”的问题非常常见排查起来还挺费时间。3. 把 ViewModel 接进项目从依赖到第一个可用页面3.1 依赖引入与版本选择的实际考量现在新建项目基本都会带上 ViewModel 相关的依赖但如果你是在老项目里手动加需要注意几个点。核心依赖是 lifecycle-viewmodel 和 lifecycle-livedata或者用 Flow 的话是 lifecycle-runtime-ktx。用 Kotlin 的话推荐直接用 ktx 版本扩展函数用起来更顺手。版本选择上有个经验lifecycle 相关库的版本要和你的 compileSdk、Kotlin 版本大致匹配。我遇到过升级 lifecycle 到较新版本后因为 Kotlin 协程版本不兼容导致编译报错的情况。稳妥的做法是查一下当前项目用的 Android Gradle Plugin 版本对照官方文档推荐的 lifecycle 版本区间不要盲目追最新。如果你用 Hilt 做依赖注入还需要加 hilt-lifecycle-viewmodel 相关依赖并且 ViewModel 要用 HiltViewModel 注解构造函数注入依赖。这块配置稍微复杂但一旦配好ViewModel 里拿 Repository 会非常干净。3.2 定义一个职责清晰的 ViewModel先看一个反面例子很多人刚开始会这么写class UserViewModel : ViewModel() { var userName var userAge 0 var isLoading false var errorMsg // ... 一堆散落的字段 }这种写法的问题在于状态分散、没有单一数据源、界面层要自己拼装这些字段一旦字段多了就乱。更合理的做法是用一个数据类描述界面状态data class UserUiState( val userName: String , val userAge: Int 0, val isLoading: Boolean false, val errorMsg: String? null ) class UserViewModel(private val repo: UserRepository) : ViewModel() { private val _uiState MutableStateFlow(UserUiState()) val uiState: StateFlowUserUiState _uiState.asStateFlow() fun loadUser(id: String) { viewModelScope.launch { _uiState.update { it.copy(isLoading true) } runCatching { repo.fetchUser(id) } .onSuccess { user - _uiState.update { it.copy(userName user.name, userAge user.age, isLoading false) } } .onFailure { e - _uiState.update { it.copy(errorMsg e.message, isLoading false) } } } } }这样界面层只需要订阅一个 uiState状态变化一目了然。单一数据源是 ViewModel 设计里最重要的原则之一它让“界面当前处于什么状态”这个问题只有一个答案。3.3 在 Activity 和 Fragment 中正确获取实例获取 ViewModel 实例的写法随着版本演进有过几次变化。现在推荐用by viewModels()委托Activity或by activityViewModels()/by viewModels()Fragment。这几个扩展函数背后做的事情是根据 owner 找到对应的 ViewModelStore没有就创建有就复用。一个容易忽略的点Fragment 里用by viewModels()拿到的是 Fragment 作用域的实例用by activityViewModels()才是 Activity 作用域的。共享数据时用错这两个就会出现“我在 A Fragment 改了数据B Fragment 没反应”的情况。我建议在团队里约定好命名比如共享的 ViewModel 变量名统一加 Shared 前缀减少误用。4. viewModelScope 与协程异步任务该怎么收尾4.1 viewModelScope 自动取消的边界在哪里viewModelScope 是 ViewModel 提供的一个协程作用域它绑定 ViewModel 的生命周期在 onCleared 时自动取消所有子协程。这个特性极大简化了异步任务管理——你不用担心页面销毁后网络回调还在跑。但要注意它的边界viewModelScope 取消的是协程不是协程里启动的、脱离了该作用域的任务。比如你在 viewModelScope 里启动了一个协程协程内部又用 GlobalScope 或者自己 new 了一个 CoroutineScope 去跑任务那这个任务不会被自动取消。我见过有人在 ViewModel 里手动创建线程池执行任务结果页面退出后任务还在跑回调时访问已销毁的界面直接崩溃。正确做法是让所有异步任务都挂在 viewModelScope 下需要并行就用 async需要串行就用挂起函数顺序调用。4.2 网络请求与数据库操作的典型编排实际业务里一个页面加载往往涉及多个数据源先查本地缓存没有再去网络请求请求回来再写回数据库。用 viewModelScope 编排这种流程非常自然fun loadData() { viewModelScope.launch { _uiState.update { it.copy(isLoading true) } val local repo.getFromLocal() if (local ! null) { _uiState.update { it.copy(data local, isLoading false) } returnlaunch } val remote repo.getFromRemote() repo.saveToLocal(remote) _uiState.update { it.copy(data remote, isLoading false) } } }这段代码的可读性比回调嵌套好太多。但有个细节如果用户在请求过程中退出了页面viewModelScope 被取消repo.getFromRemote()这个挂起函数会抛 CancellationException。如果你在 Repository 层用 runCatching 包了所有异常可能会把取消异常也吞掉导致协程无法正常取消。CancellationException 必须重新抛出这是协程取消机制的基础runCatching 里要单独判断一下。4.3 并发请求的合并与竞态处理搜索框联想、下拉刷新这类场景用户操作很快可能连续触发多次请求。如果不做处理先发的请求后返回就会覆盖掉新数据出现“输入了 abc结果显示了 ab 的结果”这种竞态问题。处理方式有几种一是用flatMapLatest把新的请求流切换掉旧的二是在 ViewModel 里维护一个 job 引用新请求来时先 cancel 旧的三是用 debounce 操作符做防抖。我一般推荐第一种用 Flow 的 flatMapLatest 最干净private val queryFlow MutableStateFlow() val searchResult queryFlow .debounce(300) .flatMapLatest { query - if (query.isBlank()) flowOf(emptyList()) else repo.search(query) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())这里的SharingStarted.WhileSubscribed(5000)是个很实用的配置它表示界面订阅停止后 5 秒内如果重新订阅不重启上游流。这样屏幕旋转时不会重新发起请求体验更顺滑。5. 那些文档不会告诉你但一定会踩的坑5.1 在 ViewModel 里持有 Context 的后果这是最经典也最致命的坑。ViewModel 的存活时间可能比 Activity 长如果你在 ViewModel 里直接持有 Activity 或 Context 引用配置变更后旧 Activity 无法被回收直接内存泄漏。正确的做法分几种情况需要 Application 级别的 Context用 AndroidViewModel 或者通过依赖注入传入 Application需要访问资源把资源 ID 或者已经取出的字符串传进来需要启动组件用 Application Context 或者把操作封装到 Repository 层。判断标准很简单ViewModel 里不应该出现任何 Activity、Fragment、View 类型的字段。5.2 事件只消费一次的正确姿势用 StateFlow 或 LiveData 传递“弹 Toast”“跳转页面”这类一次性事件时会遇到一个尴尬界面重建后订阅重新建立又收到了上一次的事件导致 Toast 弹两次、页面跳两次。这个问题的本质是状态和事件没有区分开。状态是可以重复读取的比如当前用户名事件是只能消费一次的比如“登录成功跳转首页”。解决方案有几种用 Channel 替代 StateFlow 传递事件用 SingleLiveEvent但官方不推荐或者在状态里加一个“已消费”标记。我目前更倾向于用 Channel 配合 receiveAsFlowprivate val _events ChannelUiEvent(Channel.BUFFERED) val events _events.receiveAsFlow() fun onLoginClick() { viewModelScope.launch { if (repo.login()) _events.send(UiEvent.NavigateToHome) } }Channel 的每个元素只会被一个消费者接收一次天然符合事件的语义。但要注意 Channel 在界面没有订阅者时send 会挂起BUFFERED 模式下有缓冲缓冲满了才挂起所以事件发送要放在协程里。5.3 ViewModel 里做数据转换的度ViewModel 该不该做数据转换比如把网络返回的 DTO 转成界面需要的 UI 模型。我的经验是轻量的、和界面展示强相关的转换放在 ViewModel重型的、可复用的转换放在 Repository 或专门的 Mapper。举个例子把时间戳格式化成“3 分钟前”这种展示文案放 ViewModel 合理因为它和界面语言、展示逻辑强相关。但把网络 JSON 解析成领域模型应该放在数据层ViewModel 只拿领域模型。如果 ViewModel 里出现大量 JSON 解析、字段映射代码说明分层没做好数据层职责被侵占了。5.4 测试 ViewModel 时最容易忽略的依赖ViewModel 的一大优势是可测试性但前提是你写对了。如果 ViewModel 里直接 new 了 Repository测试时就没法替换成 mock。所以依赖必须从构造函数注入这是可测试性的基础。测试时用Dispatchers.setMain(TestDispatcher())替换主线程调度器否则 viewModelScope 里的协程在测试环境下不会执行。这个坑我踩过好几次测试代码跑完发现状态根本没变排查半天才发现是调度器没替换。另外测试 StateFlow 时要用runTest配合advanceUntilIdle让挂起的协程执行完再断言。6. 从 ViewModel 到整体架构它该站在哪一层6.1 ViewModel 与 Repository 的边界划分一个常见的困惑是什么逻辑该放 ViewModel什么该放 Repository我的划分原则是Repository 负责“数据从哪来、怎么存”ViewModel 负责“界面当前需要什么数据、怎么组合”。具体来说Repository 处理数据源切换本地/网络、缓存策略、数据格式转换。ViewModel 处理界面状态编排、多个数据源的组合、用户交互的响应。比如“先读缓存再请求网络”这个策略如果它是全局通用的放 Repository如果只是某个页面特有的加载逻辑放 ViewModel。边界清晰的好处是Repository 可以被多个 ViewModel 复用ViewModel 可以专注于界面逻辑测试时各测各的互不干扰。6.2 多模块项目里 ViewModel 的归属在模块化项目里ViewModel 通常放在 feature 模块因为它和具体界面强绑定。但这里有个依赖方向的问题feature 模块依赖 core 模块ViewModel 依赖 Repository 接口Repository 的实现放在 data 模块。这样 feature 模块不直接依赖 data 模块编译隔离做得好。如果多个 feature 需要共享某个 ViewModel比如用户信息可以把它下沉到 core 或者一个 shared 模块。但要注意共享 ViewModel 意味着共享状态如果两个页面对同一份数据的期望不一致就会互相干扰。共享之前想清楚这份数据是真的全局唯一还是只是碰巧两个页面都要用。6.3 什么时候不该用 ViewModelViewModel 不是所有场景都适用。比如一个纯展示的静态页面没有任何状态需要跨配置变更保留硬套 ViewModel 反而增加复杂度。再比如一些工具类、纯函数式的数据处理也不需要 ViewModel。还有一种情况如果页面状态非常简单用 rememberSaveableCompose或者 onSaveInstanceState 就能搞定没必要引入 ViewModel。技术选型要看实际收益不要为了用而用。我见过一些项目每个页面都建一个 ViewModel里面就一两个字段这种过度设计反而让代码更难维护。7. 我在实际项目里沉淀下来的几条经验第一条ViewModel 的命名要体现它的职责范围。比如 UserProfileViewModel、SearchViewModel一看就知道管什么。如果出现 CommonViewModel、MainViewModel 这种大而全的命名基本可以判断这个 ViewModel 承担了太多职责后续会越来越臃肿。第二条状态更新尽量用不可变数据类的 copy而不是直接修改字段。这样每次状态变化都是一个新对象配合 StateFlow 的去重机制界面只在真正变化时刷新。直接改字段的话StateFlow 判断的是引用是否变化可能该刷新的没刷新不该刷新的刷了。第三条onCleared 里不要做耗时操作。onCleared 是在主线程调用的如果里面做数据库写入、文件操作会阻塞主线程。需要清理的资源尽量在业务逻辑结束时主动释放而不是攒到 onCleared 里统一处理。第四条调试 ViewModel 状态时给 StateFlow 加个日志操作符。在开发阶段打印每次状态变化能快速定位“界面为什么没更新”这类问题。上线前记得去掉或者用 BuildConfig 控制避免日志泄露。第五条团队里统一 ViewModel 的模板。比如统一用 StateFlow 还是 LiveData、统一事件用 Channel 还是 SharedFlow、统一依赖注入方式。统一之后任何人看别人的 ViewModel 都能快速理解减少沟通成本。这个收益在多人协作项目里非常明显。ViewMode 这个组件本身不复杂难的是把它放在合适的架构位置上和生命周期、协程、数据层配合好。我自己的体会是每次觉得“这里好像不太对”的时候往往就是职责边界模糊了——要么 ViewModel 管了数据层的事要么界面层管了状态的事。把边界理清楚很多问题自然就消失了。

相关新闻

JDK 25新特性解析与升级指南

JDK 25新特性解析与升级指南

1. JDK 25发布背景与技术定位Oracle在2024年9月如期发布了JDK 25的GA版本。作为Java SE平台的最新长期支持版本(LTS),这次更新在保持Java语言稳定性的同时,引入了多项影响深远的特性变更。从版本号就能看出,这已经是Ja…

2026/10/15 14:21:23 阅读更多 →
克林纱是什么?格林丝曲丝涤纶装饰纱选型安装指南

克林纱是什么?格林丝曲丝涤纶装饰纱选型安装指南

提到克林纱,很多第一次跑面料市场的人都会愣一下——同一个卷轴上明明写着“格林丝”,隔壁摊位又管它叫“曲丝”,翻翻标签还看到“克林纱”三个字。其实就是同一种东西:一种以涤纶为主要原料,织造时让纱线保持轻微卷曲…

2026/10/15 14:20:23 阅读更多 →
从 REA 到 libuvcpp:当 AI 能“读懂”软件,框架的未来形态是什么?

从 REA 到 libuvcpp:当 AI 能“读懂”软件,框架的未来形态是什么?

从 REA 到 libuvcpp:当 AI 能“读懂”软件,框架的未来形态是什么? 2026 年 10 月,一个叫 REA 的项目三天内拿下了 12 万颗 GitHub 星。它让 AI 能够逆向分析任何软件,理解功能实现,甚至尝试写出相似功能的代…

2026/10/15 14:20:23 阅读更多 →

最新新闻

Windows 10 1803安全基线:本地化终端合规落地实践

Windows 10 1803安全基线:本地化终端合规落地实践

简介:本资源是面向企业IT管理员、安全合规工程师及Windows系统运维人员的Windows 10 Version 1803(RS4)安全基线实施套件,用于快速落地等保2.0、ISO 27001等合规要求中的终端安全配置。压缩包含72个文件,总计1.06MB&am…

2026/10/15 14:56:39 阅读更多 →
C语言迷宫求解课程设计:DFS/BFS/A*算法实现与终端可视化

C语言迷宫求解课程设计:DFS/BFS/A*算法实现与终端可视化

简介:本资源是一份面向计算机专业本科生的C语言与数据结构综合课程设计实践项目,聚焦迷宫路径求解与交互式游戏开发,适用于算法实现、链表/栈/队列应用及图形化控制等核心知识点训练。压缩包共5个文件(3个txt迷宫配置文件、1个cpp…

2026/10/15 14:56:39 阅读更多 →
Ceph OSD启动失败的四层校验机制深度解析

Ceph OSD启动失败的四层校验机制深度解析

简介:本资源为Ceph分布式存储系统的中文技术文档合集,面向Linux系统运维工程师、云平台架构师及存储方向开发者,解决Ceph集群部署、配置调优与日常管理中的核心知识缺口。文档覆盖对象存储(RGW)、块设备(RB…

2026/10/15 14:56:39 阅读更多 →
视频加密与转码协同方案:AES-CBC+CENC分片加密实践

视频加密与转码协同方案:AES-CBC+CENC分片加密实践

简介:这是一套基于C#开发的视频文件加密与转码一体化解决方案,面向多媒体安全开发人员及音视频应用开发者,解决商业视频内容防盗播、本地化安全分发等核心需求。程序采用AES算法对视频文件进行全量加密,结合内置微型Web服务器实现…

2026/10/15 14:56:39 阅读更多 →
三页静态网页设计源码实战:HTML+CSS实现文字、图片与超链接跳转

三页静态网页设计源码实战:HTML+CSS实现文字、图片与超链接跳转

简介:这份网页设计资源面向刚接触前端、需要完成课程作业或入门练习的学习者,围绕“三个页面”这一典型场景,讲解如何用HTML搭建文字、图片与超链接内容,并借助CSS完成基本美化,解决页面结构组织与页面间跳转的入门问题…

2026/10/15 14:56:39 阅读更多 →
CD44重组蛋白是肿瘤干细胞标志物研究与靶向药物开发的重要工具

CD44重组蛋白是肿瘤干细胞标志物研究与靶向药物开发的重要工具

简述: CD44又称HCAM、PGP‑1、ECMR‑III,是位于人类11号染色体的跨膜糖蛋白,也是透明质酸、骨桥蛋白等多种配体的受体。CD44参与细胞黏附、迁移、淋巴细胞归巢、上皮‑间质转化、肿瘤干细胞维持及转移调控,其标准型与变异体在不同…

2026/10/15 14:55:37 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →