独立开发一款呼吸训练App:自适应帧率降级、hilog性能分析与课程状态机设计

借助Claude实现Playwright的自动化(MCP Server) MCP Server自己开发也是可以的,现在也有很多开箱即用的MCP Server,可以在https://github.com/modelcontextprotocol/servers中查找。 阅读详情

背景

去年工作压力大,试了一圈呼吸训练App,体验都不太行——要么就是个计时器套层动画,要么练完没任何记录,坚持不了两天。索性自己做了一个:「呼吸视界」,支持4-7-8、盒式呼吸等多种方案,带结构化课程进度追踪,本地持久化存储。目前版本1.8,评分5分。这篇主要聊聊开发过程中几个比较有意思的技术点。

呼吸动画的帧率问题:三个方案的血泪史

呼吸动画是整个App的核心交互——用户跟着一个圆形的扩张/收缩来控制吸气、屏气、呼气节奏。这个动画对帧率要求很高,一旦掉帧,用户的呼吸节奏就乱了,体验直接崩盘。

我前后试了三个方案:

方案一:纯GPU渲染,全程60fps目标。 用Core Animation做高帧率动画,视觉效果确实好。但在iPhone SE 2和部分老款iPad上实测fps经常掉到28-32fps,有用户反馈"看着动画头晕"。Pass。

方案二:Canvas 2D软渲染兜底。 想着GPU不行就用Canvas画,兼容性肯定没问题。确实不掉帧了,但在高端机上视觉质感明显差一截——渐变不够平滑,边缘有锯齿。而且Canvas方案CPU占用高,练习时间长了设备发热明显。也不理想。

方案三:GPU渲染 + 自适应降级。 这是最终方案。默认走GPU高质量渲染,实时监控帧率,发现撑不住就动态切到balanced模式(降低渐变精度和阴影复杂度),再不行就fallback到Canvas。关键是切换过程中不能让动画跳帧。

三个方案在低端机(iPhone SE 2)上的实测数据:

方案平均fps最低fpsCPU占用发热
纯GPU高质量3422中轻微
Canvas软渲染5852高明显
自适应降级5545中轻微

自适应降级的核心判定逻辑

降级策略的关键在于:采样窗口多大、阈值怎么定、切换时怎么避免视觉跳变。

我用了一个滑动窗口来采集fps,窗口大小60个采样点(大约1秒的数据量)。判定逻辑大概是这样:

// 滑动窗口fps采样 & 降级判定
let kWindowSize = 60
let kDegradeThreshold: Double = 40.0   // 连续低于40fps触发降级
let kRecoverThreshold: Double = 55.0   // 连续高于55fps尝试恢复
let kDegradeCount = 45                 // 窗口内低于阈值的帧数达到45就降级

func evaluatePerformance(currentFps: Double) {
    fpsWindow.append(currentFps)
        if fpsWindow.count > kWindowSize { fpsWindow.removeFirst() }
            guard fpsWindow.count == kWindowSize else { return }
                
                    let lowCount = fpsWindow.filter { $0 < kDegradeThreshold }.count
                        let highCount = fpsWindow.filter { $0 > kRecoverThreshold }.count
                            
                                if lowCount >= kDegradeCount && quality != .low {
                                        // 降级:high→balanced→low→canvas fallback
                                                degradeQuality()
                                                        fpsWindow.removeAll()  // 清空窗口,给新模式一个干净的评估周期
                                                            } else if highCount >= kDegradeCount && quality != .high {
                                                                    recoverQuality()
                                                                            fpsWindow.removeAll()
                                                                                }
                                                                                }
                                                                                ```
几个细节说一下:

- 降级后清空采样窗口,不然旧数据会干扰新模式的评估,导致反复切换抖动
- - 切换渲染模式时,我会在当前动画相位的"过零点"(比如吸气→屏气的切换瞬间)执行,这样视觉上几乎感知不到切换
- - 恢复阈值(55fps)故意设得比降级阈值(40fps)高不少,形成一个迟滞区间,避免在临界状态反复跳
## hilog性能分析工具

为了在真机上持续采集性能数据,我写了一套基于hilog的分析工具。动画每帧会输出一条结构化日志:

```python
# 从hilog中提取性能采样点
ADAPT_RE = re.compile(
    r"BF_PERF_ADAPT.*?"
        r"(?P<msg>degrade quality -> balanced"
            r"|degrade quality -> low"
                r"|switch renderer -> canvas fallback"
                    r"|recover renderer -> gpu balanced"
                        r"|recover quality -> high)"
                        )
# 分析降级事件的触发频率和分布
for line in log_lines:
    match = ADAPT_RE.search(line)
        if match:
                events.append(match.group("msg"))
                ```
跑一次测试大概能采集几千个采样点,算出平均fps、最低fps、帧耗时分布。降级事件的触发频率也能看到——如果某台设备频繁在balanced和low之间跳,说明迟滞区间还不够大,需要调参。

## 课程状态机与本地存储设计

市面上大部分呼吸App的问题:打开→练一次→关掉→再也不打开。我想解决这个留存问题,所以做了结构化课程系统。

课程状态机有四个状态:`locked → unlocked → inProgress → completed`。每完成一节课,下一节自动从locked变成unlocked。状态转换规则:

- 用户点击一节未完成的课 → `unlocked → inProgress`
- - 练习完成(跟完整个呼吸序列) → `inProgress → completed`,同时下一节 `locked → unlocked`
- - 用户中途退出 → 状态回退到 `unlocked`,但这次的部分练习数据仍然保存
存储方案纠结了一阵。最终选了SQLite而不是UserDefaults,原因是训练会话数据量会随时间增长,UserDefaults不适合存列表型数据,查询也不方便。

数据表结构很简单,两张核心表:

- `session_records`:每次训练会话,记录时间戳、呼吸法类型、实际时长、完成状态
- - `program_progress`:课程进度,记录每个课程每一节的状态和最后练习时间
中途退出的处理:练习开始时就写一条`inProgress`状态的session记录,每完成一个呼吸循环更新一次进度。如果App被杀掉,下次启动时检测到有`inProgress`但未`completed`的session,就标记为`interrupted`并保留已完成的循环数。这样用户能看到"你上次完成了3/8个循环",而不是数据直接丢了。

## 设计Token体系:统一视觉语言

产品调性是"平静克制",为了保证整个App视觉一致,我把间距、圆角、阴影、动效时长全部抽成了Design Token。项目结构大概这样:

ui/theme/
Style.ets ← 所有token定义
ui/components/
SheetBackground.ets ← 弹窗底板,引用Style中的RADIUS和SHADOW
AppBackdrop.ets ← 全局背景,引用Style中的渐变色定义
ui/pages/
RootPage.ets ← 根页面,引用AppBackdrop作为统一背景
```
Style.ets里面定义了四组常量:SPACE(间距,4/8/12/16/24/32六档)、RADIUS(圆角,sm/md/lg/xl四档)、SHADOW(阴影,light/medium两级)、MOTION(动效时长,快200ms/标准350ms/慢500ms)。

任何页面和组件都从Style引用,不允许写魔法数字。改一处全局生效。这个约束看起来简单,但真正执行起来需要纪律——我写了个CI检查脚本,每次提交会扫描所有.ets文件,发现硬编码的圆角或间距数值直接报错。

SwiftUI → ArkUI 迁移:两个具体的坑

鸿蒙版本正在用ArkTS重写。ArkUI的声明式范式和SwiftUI思路确实像,但具体API差异踩了不少坑,说两个印象深的:

坑一:动画状态驱动的差异。 SwiftUI里我用withAnimation(.easeInOut(duration: 0.35))包裹状态变更就行,动画自动插值。ArkUI里对应的是animateTo,但它的回调时机和SwiftUI不一样——animateTo的onFinish在动画真正结束后才触发,而SwiftUI的withAnimation闭包里状态是立即变更的,动画只是视觉过渡。这个差异导致我的呼吸相位切换逻辑在鸿蒙上第一版跑起来时序全乱了,吸气动画还没播完就跳到屏气了。

解决方式是把相位切换逻辑从动画回调里拆出来,用独立的Timer驱动相位状态机,动画只负责视觉表现,不参与逻辑控制。

坑二:本地持久化API。 iOS上用SQLite.swift这个库很顺手,鸿蒙上对应的是@ohos.data.relationalStore。API设计差异挺大的,鸿蒙的relationalStore用ValuesBucket来做插入,查询结果要用ResultSet逐行读取,没有Swift里那种类型安全的ORM映射。我花了大概两天把数据层重写了一遍,抽了一个适配层出来,上层业务代码不用关心底层是SQLite还是relationalStore。

国际化一致性检查

App做了中英文双语。漏翻译这种低级错误很容易出现——加个新功能写了中文文案,忘了补英文,上线了才发现某个按钮显示的是key名。

我写了个脚本每次CI跑一遍,对比中英文i18n文件的key集合,有差集直接报错。简单但救过我好几次,特别是版本迭代快的时候。

当前状态和一个问题

App上架一段时间了,有用户评价说"可以跟随练习呼吸,保持稳定的心情",看到这条挺开心的。下载量目前还很少,推广这块还在摸索。

最后抛个问题:你们在做呼吸/冥想类App的动画时,帧率降级策略是怎么处理的? 我现在用的滑动窗口+迟滞区间方案还算稳定,但在某些设备上偶尔会出现降级后恢复不回来的情况(明明性能够了但一直卡在low模式)。怀疑是采样窗口清空后冷启动阶段的数据有偏差,有类似经验的朋友聊聊?

从理论到实践:AI设备维护系统的架构实现 第一部分:理论基石——从设备维护的演进讲起,理解AI如何重塑维护模式;第二部分:架构设计——详解六层高可用系统架构,明确各层职责与技术选型;第三部分:核心技术——拆解数据处理、特征工程、模型训练、部署等关键环节的实现细节;第四部分:实践落地——8步流程带你从需求到上线,附代码示例与工具链;第五部分:案例研究——风电齿轮箱预测性维护系统的真实案例,从问题到方案再到效果;第六部分:挑战与展望——探讨当前技术瓶颈与未来趋势(数字孪生、联邦学习等)。准备好了吗? 阅读详情

相关推荐

告别有线:用PVE直通无线网卡,打造一台真正的‘无线’Win10软路由/测试机

本文详细介绍了如何通过PVE直通无线网卡技术,构建全无线Win10软路由或测试机。从硬件选型、PVE环境配置到PCIe和USB双模直通技术,逐步指导实现Wi-Fi AP和蓝牙连接功能,为家庭实验室和小型办公环境提供灵活高效的网络解决方案。

weixin_28683689的博客 586

GitHub Actions 自动化:5 个真实运维提效案例

回顾五个案例的共同思路:把高频、规则明确、易出错的运维动作交给流水线,把人的精力留给判断与优化。给出从单点试点到体系化推广的推进建议。

2205_76017107的博客 143

23.网工入门篇--------介绍一下园区网典型组网架构及案例实践

园区网典型组网架构主要分为小型、中型、大型三种类型,以下是详细介绍及相关案例实践:

qq_31450765的博客 964

Spring AI + GPT-5.5 + Ollama 配置教程:Base URL、超时与流式调用

由于不同 Spring AI、Spring Boot 小版本之间仍可能存在差异,创建项目时应检查 Spring Initializr、Spring AI BOM 和官方兼容矩阵,不要直接复制旧教程中的 Starter 名称和版本号。多人项目最好隔离不同客户端,并在脱敏前提下记录最终 URL、状态码、请求耗时、请求 ID 和模型 ID。Spring AI 接入模型 API,至少要同时确认 Starter、Base URL、请求路径、API Key、模型 ID、连接超时、读取超时和流式客户端这 8 项。

2601_95893075的博客 218

基于PLC的茶叶揉捻机自动控制系统设计【附代码】

茶叶揉捻是初茶加工过程中的一道关键工序,通过揉捻可以使鲜茶叶成条、碎茶及叶组织细胞破损等。影响茶叶成条率、碎茶率和叶组织细胞破损的主要因素包括揉捻时间、压力和揉捻桶转速。为了提高茶叶品质的稳定性和减轻工人的劳动强度,本文提出了一种基于PLC自动控制技术的茶叶揉捻机控制系统设计。📊 PLC自动化设计 | 毕业设计指导 | 工业自动化解决方案。✅ 具体问题可以私信或查看文章底部二维码。✅ 感恩科研路上每一位志同道合的伙伴!

lagougongzuoshi的博客 1341

保姆级教程:在Windows 11上用VSCode和Conda搞定Depth-Anything-3(含常见报错修复)

本文提供在Windows 11系统上使用VSCode和Conda配置Depth-Anything-3的完整教程,涵盖环境搭建、依赖安装、常见报错修复及深度图生成实战。特别针对CUDA与PyTorch版本匹配、Hugging Face镜像加速等关键问题提供解决方案,帮助开发者高效实现计算机视觉中的深度估计应用。

weixin_30338481的博客 346

Docker Compose:搭建本地多服务开发环境

从一个 Java API、PostgreSQL 和 Redis 开始,配置服务名、端口、环境变量、健康检查、数据卷和依赖关系。理解 Compose 文件如何被解析、项目如何隔离,以及哪些数据不能依赖“重建容器”保留,能够把个人开发环境整理成团队可复用的启动入口。

m0_62221377的博客 349

AutoCAD2014与ObjectARX开发环境搭建实战指南

本文提供了一份详细的AutoCAD2014与ObjectARX开发环境搭建实战指南。内容涵盖从AutoCAD2014、Visual Studio 2010及ObjectARX 2014 SDK的版本匹配与环境准备,到Wizard安装、编码问题解决、Visual Studio配置,并最终通过一个完整的绘图命令开发示例,手把手引导开发者创建并加载第一个可运行的ObjectARX插件,为后续深入开发奠定坚实基础。

weixin_28475533的博客 286

【Linux网络】TCP 服务器并发处理多客户端:从 fork 分发到双重 fork 彻底消灭僵尸进程

拆解 TCP 服务器并发演进链:fork 分发连接、SIGCHLD 循环回收僵尸、双重 fork 交孙进程给 init 收养,并给出模型选型与线上排错对照。

2301_78967866的博客 411

Appstore 3.2(f)违规自救指南:从触发到申诉的完整应对策略

本文详细解析了Appstore 3.2(f)违规的应对策略,从触发原因到申诉流程,帮助开发者有效自救。文章涵盖隐瞒行为、账户关联和信息造假三大违规场景的判定逻辑,并提供申诉信写作的黄金法则和长期合规防御措施,确保开发者能够顺利通过苹果审核。

weixin_29304985的博客 658

国产化办公新选择:在银河麒麟V10上部署ToDesk远程桌面

本文详细介绍了在国产操作系统银河麒麟V10上部署ToDesk远程桌面的完整指南。作为国产远程控制工具,ToDesk在连接速度、系统适配性和用户体验方面表现优异,特别适合国产化办公场景。文章包含X86和ARM架构的安装步骤、常见问题排查及性能优化技巧,帮助用户高效实现远程办公。

weixin_33742618的博客 231

企业版虚拟机ESXi网络配置

1. 把卷组里剩下的所有空间(100%FREE)全加给根目录。# 2. 刷新文件系统,让系统立刻认出新容量。

墨痕诉清风的博客 224

C语言--第三大的数

第三大的数 给定一个非空数组,返回此数组中第三大的数。如果不存在,则返回数组中最大的数. 示例1: 输入: [3, 2, 1] 输出: 1 解释: 第三大的数是 1. 示例2: 输入: [1, 2] 输出: 2 解释: 第三大的数不存在, 所以返回最大的数 2 . ...

loreal8的博客 2130

自动化运维选型:四条技术路线的能力边界,以及它们为什么不能互相替代

自动化运维怎么选型?拆解配置管理、基础设施即代码、作业调度与场景化编排四条技术路线边界,分析账本归属与时间语义差异,给出四种架构组合与判断点,帮企业看清为何不能互相替代。

trainsignalcn的专栏 306

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

AI Agent 的安全风险,正在从 Prompt 扩展到运行环境

从 Cursor 恶意扩展事件看 AI Agent 运行环境的安全风险与权限边界。

2501_92842654的博客 470

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

闲置Windows主机搭建私有网盘:轻量局域网文件服务方案实测

本文介绍基于闲置Windows主机搭建轻量级私有文件服务的实用方案,针对个人开发者、小团队及实验室用户痛点,提出以Web形态文件系统替代公有网盘或复杂NAS的低成本解决方案。该方案无需额外硬件,部署简单,支持多设备浏览器访问、权限隔离、临时分享链接与原文件预览,适合局域网内素材归档、协作共享与安全分发。但需注意其局限性:依赖主机常开、无磁盘冗余、外网访问需谨慎配置,且不适用于高并发或核心业务存储。建议结合备份机制与安全策略使用,合理权衡成本与可靠性。

zxcvb153的博客 230

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

越华环保集团碳惠小屋|面向碳普惠场景的 IoT 终端与碳数据链路设计方案

越华环保集团碳惠小屋这套 IoT 碳数据方案,把物联网感知技术和碳普惠业务规则深度绑定,补齐了碳普惠线下可信数据源短板。相比通用回收设备,它的核心价值在于标准化碳数据链路与可对接上层碳普惠平台的开放能力。随着各地碳普惠数字化建设提速,对合规、可溯源线下采集终端需求持续增长。这套架构具备良好扩展性,后续可以继续接入骑行、节水节电等更多低碳感知终端,作为统一的公众低碳数据采集底座,支撑区域碳普惠数字化体系持续迭代升级。

MuMuMu1223的博客 152

ubuntu安装hadoop系统

本文详细介绍了基于VMware克隆虚拟机搭建三节点Hadoop分布式集群的全过程:使用同一Ubuntu镜像克隆出master、worker1、worker2三台虚拟机,依次配置主机名、固定IP(如192.168.82.10/11/12)、SSH服务及免密登录,确保节点间网络互通。随后安装Java,下载配置Hadoop并分发至各节点,最终初始化启动集群,验证其存储与计算功能,实现高效的大数据环境部署。

qq_43592674的博客 315

docker部署IPTV

【代码】docker部署IPTV。

wuxingge的博客 43

FineBI、Power BI、Tableau 建模能力实测:2026 年三款主流 BI 的性能与运维成本对比

其二为权限与安全的工程化程度。原因并非其某一单点最强,而是其将建模、性能、运维这三项最易被忽视又最决定成败的因素,做成了一个相对均衡、且面向国内企业场景默认到位的整体,这是其与另两款产品的真正差异所在。FineBI 在企业级落地与长期运维的省心程度上领先,Power BI 在建模与计算上限上领先,Tableau 在分析体验上领先,三者分别适合不同的团队与场景。FineBI、Power BI、Tableau 的建模能力并非简单的强弱之分,三款产品对数据模型的理解存在根本差异,这一差异决定了后续各维度的表现。

Leo的博客 92

IP 地址与子网划分

CIDR(无类别域间路由)是一种简写方式,用/数字/24 = 前 24 位是 1。

weixin_67448099的博客 135

Kubernetes 集群部署

实验环境:VMware Workstation 26 + Rocky Linux 9.8 + Kubernetes v1.35.10 + Docker 29.9.3 + cri-dockerd 0.4.10。

2302_80967029的博客 153

ssh 直接免密输入 sudo 密码

推荐方案:macOS Keychain。

weixin_40548182的博客 94

输电线路全景监测的多传感融合与应用效果评估方法

输电线路监测从“有数据”迈向“用数据”,核心挑战在于效果评估。需构建覆盖几何、姿态、环境、解算四层的全景监测体系,通过多源融合与统一解算提升告警准确性。评估应聚焦预警及时性、误报率、运维减负与数据可用性,并按风险分级部署、分场景统计、剥离干扰变量,确保效果可核对、可验证。

QXWZ_的博客 349

CentOS 7系统信息与硬件

本文整理 CentOS7 系统信息、硬件信息、内核日志查看命令,讲解 uname、hostnamectl、uptime、date、cal 查看系统基础信息;lscpu、free、lsmem、dmidecode、lspci、lsusb 查看 CPU、内存、PCI、USB 硬件;dmesg 查看内核日志,附带参数解析和实操案例,是服务器巡检、硬件识别、启动故障排查的运维笔记。

2301_81401822的博客 272

邮件提交协议(MSA):与标准 SMTP 有何不同?

上周有个客户的运维把发信程序从 25 端口改到 587,账号密码都填了,日志里还是 530 5.7.0。他把日志甩过来,问是不是我们把认证关了。认证没关。他那套代码从头到尾没发过 AUTH——EHLO、MAIL FROM、RCPT TO、DATA,四步走完就等 250。这套流程在 25 端口上跑了七八年,一次没出过事。同一个程序换个端口就死,这件事本身比故障有意思:25 和 587 之间隔着的,是两种角色,端口号只是外面那层皮。

2503_92398835的博客 310
上一篇: 我做了一款精力管理App「元気手账」,聊聊从 Swift 移植到鸿蒙 ArkTS 的真实经历
下一篇: 我做了一款「用脚步占领城市」的地图游戏:网格征服判定与热力衰减实现
SameX-4869
博客等级 码龄17年 3354粉丝 657原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值