Files
suixinkan_ios_new/docs/startup-analysis.md
汉秋 d9f897038d 移除 Splash 固定 1.5 秒等待,并补充启动流程分析文档。
未登录冷启动可立即进入登录页;已登录仍等待 bootstrap 接口完成,但不再叠加人为延迟。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 15:27:18 +08:00

15 KiB
Raw Blame History

App 启动流程分析与优化方案

本文档梳理 iOS App 冷启动期间的接口调用、数据依赖,并评估去掉启动 Splash 等待层、直接进入登录页或首页的可行性。

相关模块文档:App 模块Splash 模块


1. 概述

App 启动链路为:

suixinkanAppRootViewSplashView 覆盖层 + 并行 bootstrap → 按 AppSession.phase 展示 LoginViewMainTabsView

用户感知的「启动 Loading」并非 Lottie 全局 Loading而是

  1. 系统 Launch Screen(白底 + LogoiOS 原生)
  2. 应用内 SplashView品牌页bootstrap 完成即结束,无固定最短展示时长)
  3. 有 token 时的 .restoring 阶段Splash 下方空白背景,等待 4 个 API 完成)

冷启动期间 不使用 GlobalLoadingCenter / Lottie 动画。


2. 启动时序

sequenceDiagram
    participant Launch as SystemLaunchScreen
    participant App as suixinkanApp
    participant Root as RootView
    participant Splash as SplashView
    participant Coord as SplashCoordinator
    participant Boot as SessionBootstrapper
    participant Loader as AccountContextLoader
    participant UI as LoginView_or_MainTabsView

    Launch->>App: 系统 Launch Screen
    App->>Root: 挂载 RootView
    Root->>Splash: ZStack 覆盖层
    Root->>Coord: start(bootstrap)
    par BootstrapOnly
        Coord->>Boot: restore()
        alt 无 token
            Boot->>Boot: 读 UserDefaultslogout
        else 有 token
            Boot->>Boot: 恢复 AccountSnapshotStore
            Boot->>Loader: refresh 4 个接口
            Loader->>Loader: userInfo + rolePermissions 并行
            Loader->>Loader: scenicListAll + storeAll 串行
        end
    end
    Coord->>Splash: isFinished = true淡出
    Root->>UI: 按 phase 展示登录页或主 Tab
    opt loggedIn
        Root->>Root: 推送注册、景点列表、排队等异步任务
    end

核心代码路径

环节 文件 说明
入口 suixinkan/App/suixinkanApp.swift @main,挂载 RootView
编排 suixinkan/App/RootView.swift .task 绑定 token、启动 Splash、bootstrap
时序 suixinkan/App/SplashCoordinator.swift bootstrap 完成后结束 Splash默认无最短展示时长
恢复 suixinkan/App/State/SessionBootstrapper.swift 读 token、恢复快照、校验登录态
接口批量 suixinkan/App/State/AccountContextLoader.swift 用户资料 + 权限 + 景区 + 门店
路由 RootView.rootContent switch appSession.phase 切换页面

3. 启动期间接口清单

3.1 阶段 A — Splash 阻塞期

未登录(无本地 token

操作 类型 说明
SessionTokenStore 本地 UserDefaults 读 token
AppPreferencesStore 本地 登录页偏好Splash 期间不读LoginView 出现时读)

零网络请求。 Splash 仅覆盖本地 token 读取bootstrap 完成即进入登录页。

已登录(有本地 token

# 方法 HTTP 路径 调用方 获取数据 失败策略
1 userInfo() GET /api/yf-handset-app/userinfo AccountContextLoader 头像、昵称、真实姓名、手机号 鉴权失败 → 登出;其他错误 → 用快照进入主界面
2 rolePermissions() GET /api/yf-handset-app/role-permission AccountContextLoader 角色列表、权限 URI 树、角色关联景区 同上(与 userInfo 并行)
3 scenicListAll() GET /api/yf-handset-app/photog/scenic/list-all AccountContextLoader 全部可访问景区 失败时从 rolePermissions 中 dedupe 兜底
4 storeAll() GET /api/app/store/all AccountContextLoader 门店列表 try? 失败不阻断,返回空列表

并行策略: userInfo + rolePermissions 并行 → 完成后串行 scenicListAllstoreAll

阻塞原因: SplashCoordinator.start 必须等 bootstrap 完成才设置 isFinished = true;有 token 时 AppSession.phase 停留在 .restoring,主界面不可见。

3.2 阶段 B — Splash 结束后(不阻塞 Splash

方法 HTTP 路径 触发时机 获取数据 阻塞 UI
fetchAppConfig() GET /api/app/config LoginView.task(未登录) enable_register 等远程配置 否,失败静默
scenicSpotListAll(scenicId:) GET /api/yf-handset-app/photog/scenic-spot/list-all?scenic_id= RootView.task(id: scenicSpotTaskID) 当前景区打卡点列表
registerJPushId(_:) POST /api/app/user/register-jpush-id?jpush_reg_id= 登录后 Push 授权完成 注册推送 token
writeOffList(...) GET /api/yf-handset-app/photog/order/order-verification-list MainTabsView.task 订单 Tab 待核销角标
scenicQueueStats / scenicQueueHome GET /api/app/scenic-queue/* ScenicQueueRuntime(有排队配置时) 排队统计与列表

Profile 页: 切换到「我的」Tab 时 ProfileView.task 会再次调用 userInfo(),不属于冷启动首屏链路。

Home 页: HomeView.task 仅调用 rebuildMenusFromCurrentContext()不发起网络

3.3 启动链路中不存在的接口

类型 说明
Token 刷新 代码库中无 refresh/renew 接口
版本强更 无独立版本检查接口App 版本通过 APIClient 请求头 X-APP-VERSION 携带
AppDelegate 网络 AppDelegate 仅处理 APNs 回调,启动时不调 API
WebSocket GET /api/app/socket-token 仅在排队管理页面使用,冷启动不连接

4. 本地缓存说明

4.1 SessionTokenStore

  • 存储: UserDefaults
  • 键: suixinkan.session.token.{bundleId}.session.token
  • 内容: 正式登录 token 字符串
  • 用途: 判断是否有登录态;作为 APIClient 默认 Authorization

4.2 AccountSnapshotStore

  • 存储: UserDefaults
  • 键: suixinkan.account.snapshot.v1
  • 内容(AccountSnapshot
字段 说明
profile 用户资料(昵称、手机号、头像 URL 等)
accountType 账号类型
businessUserId 业务用户 ID
currentRoleCode 当前角色 code
currentRoleId 旧版 legacy 角色 ID兼容迁移
scenicScopes 景区作用域列表
storeScopes 门店作用域列表
currentScenicId 当前选中景区 ID
currentStoreId 当前选中门店 ID

注意:AccountSnapshot 不保存 rolePermissions(完整权限树)。 冷启动时 SessionBootstrapper.restoreCachedSnapshot 只恢复 AccountContextprofile + scopes不会填充 PermissionContext.rolePermissions。权限数据必须等 AccountContextLoader.refresh 成功后通过 replaceRolePermissions 写入。

4.3 AppPreferencesStore

  • 内容: 上次登录手机号、隐私协议勾选状态
  • 用途: 登录页表单预填,与 Splash bootstrap 无关

5. 各页面首屏数据依赖

5.1 LoginView未登录

数据 来源 是否阻塞首屏
上次手机号、协议勾选 AppPreferencesStore 本地
是否显示注册入口 GET /api/app/config Splash 后异步,失败默认隐藏)

结论:未登录路径可在读 token 后立即展示登录页,无网络前置依赖。

5.2 HomeView已登录首页

数据 来源 快照是否覆盖
当前景区名称 AccountContext.currentScenic
门店卡片(门店管理员) AccountContext.currentStore
常用应用菜单 PermissionContext.rolePermissions + 权限 URI
工作状态卡片 本地 State无 API

首页菜单构建逻辑(HomeView.rebuildMenusFromCurrentContext)依赖 permissionContext.rolePermissions。若仅恢复快照、不等 API菜单区域会为空,直到后台 refresh 完成。

5.3 MainTabsView

数据 来源 是否阻塞 Tab 展示
Tab 结构 本地静态配置
订单角标 writeOffList API 否(.task 异步刷新)

6. 去掉 Splash 的可行性分析

6.1 未登录路径 — 可行,收益明确

现状(方案 1 已实施):

  • bootstrap 仅读 UserDefaults耗时极短
  • 已去掉固定 1.5 秒品牌展示Splash 以 bootstrap 完成为准

建议:

  • 无 token → 直接 loggedOut → 展示 LoginView
  • 可保留系统 Launch Screen白底 Logo去掉应用内 SplashView 覆盖层
  • /api/app/config 维持现状LoginView 出现后异步加载

6.2 已登录路径 — 可行,但需处理权限缓存缺口

现状:

token → restoring空白屏→ 等 4 个 API → loggedIn → 去 Splash → MainTabsView

乐观恢复方案:

  1. 读 token + 恢复 AccountSnapshotStoreAccountContext
  2. 立即 markLoggedIn → 展示 MainTabsView
  3. 后台执行 AccountContextLoader.refresh
  4. 鉴权失败 → logout + Toast「登录状态已失效请重新登录」
  5. 网络失败 → 保持登录 + Toast「网络异常已使用本地登录状态」

风险与对策:

风险 影响 对策
Token 已失效 用户短暂看到首页后跳登录 可接受;或首页顶部加「同步中」轻提示
快照无 rolePermissions 首页菜单为空 扩展快照缓存权限树;或首页 skeleton或至少等 rolePermissions 再进首页
有 token 无快照(极端) 景区名、菜单均不可用 降级为短 loading 或强制等 refresh
UI Test 依赖 splash.root 自动化等待逻辑失效 改为等待 login.title 或 TabBar

6.3 最短展示时长(方案 1 已移除)

  • 原 1.5 秒为纯品牌需求,与业务数据无关
  • 已实施: SplashCoordinator 默认 minimumDisplayDuration = 0Splash 以 bootstrap 完成为准
  • 系统 Launch Screen白底 Logo仍保留作为唯一品牌曝光

7. 延后加载建议

按数据重要性与现有实现,分为三档:

7.1 必须同步(进入主界面前)

数据 原因 当前状态
Token 有效性 避免进入主界面后立刻登出 通过 rolePermissions / userInfo 间接校验
rolePermissions 首页菜单、权限控制依赖 阻塞 Splash;快照未缓存

若采用乐观恢复,建议至少保留 rolePermissions 的后台刷新用于鉴权;若要菜单立即可用,需扩展 AccountSnapshot 缓存权限树。

7.2 可后台刷新(不阻塞首屏)

数据 原因 当前状态
userInfo 头像昵称可先用快照 阻塞 Splash可延后
scenicListAll 已有 rolePermissions 兜底 阻塞 Splash可延后
storeAll 门店管理员卡片才需要 try? 不阻断,可延后

7.3 已延后Splash 后异步)

数据 触发点
App 远程配置 LoginView.task
打卡点列表 RootView.task(id: scenicSpotTaskID)
推送注册 登录后 PushNotificationManager
订单角标 MainTabsView.task
排队数据 ScenicQueueRuntime(有配置时)
个人资料刷新 Profile Tab 进入时

8. 推荐优化方案

方案 1 — 低风险(已实施

  • 去掉或缩短 SplashCoordinator 1.5 秒最短展示 → 默认改为 0
  • 未登录Splash 仅覆盖本地 token 读取(几乎瞬时)→ 登录页
  • 已登录:仍等 4 个 API但去掉人为 1.5 秒延迟

改动小,启动速度提升明显(未登录场景)。

方案 2 — 中等改动(乐观恢复)

  • 已登录:快照恢复 → 立即 loggedIn → 展示主 Tab
  • SessionBootstrapper.restore 拆为:
    • restoreFromCache() — 同步,填充 AccountContext
    • refreshInBackground() — 异步,填充 PermissionContext 并校验 token
  • 扩展 AccountSnapshot 缓存 rolePermissions(可选,解决菜单空白问题)
flowchart TD
    start[AppLaunch] --> readToken{本地有token?}
    readToken -->|否| loginPage[直接LoginView]
    readToken -->|是| loadCache[恢复AccountSnapshotStore]
    loadCache --> homePage[直接MainTabsView]
    homePage --> bgRefresh[后台AccountContextLoader.refresh]
    bgRefresh -->|鉴权失败| logout[logout + Toast]
    bgRefresh -->|成功| updateUI[静默更新PermissionContext和AccountContext]
    bgRefresh -->|网络失败| keepLocal[保持本地状态 + Toast]

方案 3 — 进一步延后

  • scenicListAll / storeAll 移出 bootstrap改为进入首页或切换景区/角色时加载
  • 推送、角标、打卡点、排队保持现状(已是 Splash 后异步)

建议实施顺序:方案 1 → 方案 2 → 方案 3。


9. 改造影响面

若后续实施代码改造,需关注以下文件:

文件 改动内容
suixinkan/App/SplashCoordinator.swift 去掉/缩短最短展示,或废弃
suixinkan/App/RootView.swift 调整 .task:乐观恢复 vs 去掉 Splash 覆盖
suixinkan/App/State/SessionBootstrapper.swift 拆分 cache restore 与 background refresh
suixinkan/Core/Storage/AccountSnapshotStore.swift (方案 2 可选)扩展快照字段
suixinkanTests/SplashCoordinatorTests.swift 更新时序测试
suixinkanUITests/Support/SuixinkanUIApplication.swift 调整 waitForSplashToDisappearIfNeeded
suixinkan/Features/Splash/Splash.md 同步 Splash 业务文档
suixinkan/App/App.md 同步启动流程描述

10. 结论

问题 结论
启动调用了哪些接口? 有 token 时 4 个 GETuserInfo、rolePermissions、scenicListAll、storeAll无 token 时零网络
获取了哪些数据? 用户资料、角色权限、景区/门店作用域Splash 后另有 config、打卡点、推送、角标等
能否去掉 Splash 可以。 未登录可立即进登录页;已登录可乐观恢复 + 后台刷新
哪些可延后? userInfo、scenicListAll、storeAll 可延后rolePermissions 建议至少后台刷新以校验 tokenconfig/角标/推送等已延后

当前最大可优化点:

  1. 去掉 1.5 秒人为延迟(方案 1 已完成
  2. 已登录乐观恢复(方案 2需处理 PermissionContext 未缓存问题)
  3. 景区/门店列表移出 bootstrap(方案 3代码已有部分兜底逻辑