Files
suixinkan_uikit/应用启动流程与接口数据存储梳理.md

17 KiB
Raw Blame History

应用启动流程与接口数据存储梳理

本文按当前 iOS 工程代码梳理应用从冷启动到各首屏展示期间发生的初始化、接口调用顺序,以及接口数据和本地状态的存储方式。范围仅覆盖当前 iOS 实现,不额外补充 Android 中存在但 iOS 尚未接入的能力。

1. 冷启动立即发生

冷启动入口在 AppDelegate.application(_:didFinishLaunchingWithOptions:)

  1. 调用 UmengBootstrap.configureIfNeeded()
    • 友盟初始化受 AppStore.shared.privacyAgreementAccepted 控制。
    • 未同意隐私协议时直接返回,不初始化 SDK。
  2. 调用 WeChatManager.shared.registerIfNeeded()
    • 校验 Universal Link。
    • 调用微信 SDK WXApi.registerApp 注册 AppID。
  3. AppStore.shared.privacyAgreementAccepted == trueAppStore.shared.token 非空,则调用 AMapBootstrap.configureIfNeeded()
    • 写入高德隐私合规状态。
    • 设置 AMapServices.shared().apiKey 和 HTTPS。
  4. 此阶段不直接调用任何业务后端接口。

随后系统进入 SceneDelegate.scene(_:willConnectTo:options:)

  1. 调用 AppNavigationBarAppearance.applyGlobalAppearance() 设置全局导航栏样式。
  2. 创建 UIWindow
  3. 设置 window.rootViewController = AppRouter.makeRootViewController()
  4. AppRouter 通过 AppStore.shared.isLoggedIn 判断根页面:
    • token 非空:进入 MainTabBarController
    • token 为空:进入 UINavigationController(rootViewController: LoginViewController())
  5. 注册全局通知:
    • sessionDidExpire:清除登录态并切回登录页。
    • userDidLogout:清除登录态并切回登录页。
    • userDidLogin:切到主 Tab。

2. 网络依赖初始化

NetworkServices.shared 是全局网络依赖容器,不在 AppDelegate 中主动创建,而是在页面或 Tab 首次访问业务 API 时懒加载。

初始化顺序如下:

  1. 创建 APIClient(environment: .current)
  2. 用同一个 APIClient 创建各业务 API
    • AuthAPI
    • ProfileAPI
    • StatisticsAPI
    • OrderAPI
    • HomeAPI
    • 以及其他业务模块 API。
  3. 创建 OSSUploadService(configService: uploadAPI)
  4. 调用 client.bindAuthTokenProvider,从 AppStore.shared.token 读取 token。

APIClient 的公共请求规则:

  • APIEnvironment.current
    • Debughttps://api-test.zhifly.cn
    • Releasehttps://api.zhifly.cn
  • 每个请求默认写入 Header
    • Content-Type: application/json
    • Accept: application/json
    • X-APP-VERSION
    • X-OS-TYPE: iOS
    • token 非空时写入 token
  • 响应通过 APIEnvelope 解包。
    • 成功业务码:100000
    • 非成功业务码抛出 APIError.serverCode
    • 认证失效时广播 sessionDidExpire,由 SceneDelegate 清会话并回登录页。

3. 未登录启动与登录流程

未登录时根页面为登录页。

3.1 登录页展示前

LoginViewController.setupUI 会调用 viewModel.applyStoredPreferences(),从 AppStore 读取:

  • lastLoginUsername
  • privacyAgreementAccepted

这一步只恢复本地表单状态,不调用后端接口。

3.2 登录页出现后

LoginViewController.viewDidAppear 调用:

GET /api/app/config

调用链:

LoginViewController.viewDidAppear
-> LoginViewModel.loadAppConfig(authAPI:)
-> AuthAPI.getAppConfig()

用途:

  • 读取 App 配置。
  • 当前只使用 enableRegister 控制验证码登录/注册入口显隐。
  • 配置加载失败会静默忽略,不影响密码登录。
  • 该结果只保存在 LoginViewModel.enableRegister 内存态,不写入 AppStore

3.3 点击登录后的接口顺序

用户点击登录按钮后,先做本地校验:

  • 手机号必须是 11 位且以 1 开头。
  • 密码不能为空。
  • 必须勾选隐私协议。

校验通过后发起登录:

POST /api/app/v9/login

调用链:

LoginViewController.performLogin()
-> LoginViewModel.login(authAPI:)
-> AuthAPI.login(username:password:)

返回数据包含临时 token 和可选账号列表。随后根据账号数量分流:

  • 只有一个账号:立即调用 set-user 换正式 token。
  • 多个账号:展示账号选择弹窗,用户确认后再调用 set-user

正式账号选择接口:

POST /api/app/v9/set-user

调用链:

单账号:
LoginViewModel.resolveLoginResponse(...)
-> AuthAPI.setUser(..., tokenOverride: 临时 token)

多账号:
LoginViewController.selectAccount(_:)
-> LoginViewModel.selectAccount(_:authAPI:)
-> AuthAPI.setUser(..., tokenOverride: 临时 token)

3.4 登录完成后的存储与跳转

登录完成调用:

AuthSessionHelper.completeLogin(...)

写入 AppStore 的内容:

  • token
  • lastLoginUsername
  • privacyAgreementAccepted
  • userId
  • userName
  • realName
  • avatar
  • phone
  • accountType
  • accountDisplayName
  • roleCode
  • roleName
  • currentScenicId
  • currentScenicName
  • currentStoreId

其中账号上下文来自 AccountSwitchAccountV9ScenicUserV9StoreUser

如果用户已同意隐私协议,还会再次触发:

  • AMapBootstrap.configureIfNeeded()
  • UmengBootstrap.configureIfNeeded()

最后发送:

NotificationName.userDidLogin

SceneDelegate.handleUserDidLogin() 收到通知后调用:

AppRouter.setRoot(.mainTab, on: window)

根页面切换到主 Tab。

4. 已登录启动与默认首页首次展示

已登录时,AppRouter.makeRootViewController() 创建 MainTabBarController

4.1 主 Tab 创建

MainTabBarController.viewDidLoad 顺序:

  1. 设置 delegate
  2. 配置 TabBar 外观。
  3. 调用 configureTabs() 创建五个 Tab slot
    • 首页:HomeViewController
    • 订单:OrdersViewController
    • 中间扫码占位页
    • 数据:StatisticsViewController
    • 我的:ProfileViewController
  4. 绑定订单角标 ViewModel。
  5. 绑定扫码处理器。
  6. 调用 badgeViewModel.refreshPendingWriteOffCount()

注意:MainTabBadgeViewModel.refreshPendingWriteOffCount() 当前仅执行:

pendingWriteOffCount = nil

代码注释标明“接口未接入前保持为空”,因此这里不记录为真实后端接口调用。

4.2 默认首页首次出现

默认选中首页。HomeViewController.viewDidAppear 首次出现时调用:

initializeHome()

调用顺序:

  1. 展示全局 loading。
  2. HomeViewModel.initialize(api:)
  3. 隐藏 loading。
  4. 刷新首页 UI。
  5. 延迟评估弹窗。

HomeViewModel.initialize(api:) 内部顺序:

  1. locationStateStore.restoreStateIfNeeded()
    • AppStore 恢复在线状态、上次位置上报时间、提醒分钟数。
    • 如果上次在线状态已过期,会清除在线状态和上次上报时间。
    • 不调用后端接口。
  2. refreshLocalDisplayState()
    • AppStore 读取当前景区、当前角色。
    • 无有效景区时清空 currentScenicId/currentScenicName
  3. 强制调用 loadPermissions(api:force: true)

权限接口:

GET /api/yf-handset-app/role-permission

调用链:

HomeViewModel.initialize(api:)
-> HomeViewModel.loadPermissions(api:force: true)
-> HomeAPI.rolePermissions()

权限返回后的存储:

  • 完整角色权限列表:AppStore.saveRolePermissionList(_:)
  • 当前匹配角色的景区列表:AppStore.saveRoleScenicList(_:)
  • 当前匹配角色的扁平权限:AppStore.savePermissionItems(_:)
  • 匹配角色名:必要时写入 AppStore.roleName

这些权限数据都按账号作用域 key 写入 UserDefaults,值为 JSON Data。

随后首页重建常用应用:

  • AppStore.permissionItems() 读取权限。
  • HomeMenuCatalog.visibleMenus(from:) 生成可见菜单。
  • HomeCommonMenuStore 读取或写入常用应用 URI。
  • 常用应用 key 由 accountScope + roleCode 组成。

4.3 首页门店接口

权限加载完成后会调用:

HomeViewModel.loadStoreListIfNeeded(api:)

只有同时满足以下条件才会请求门店:

  • 当前角色为 storeAdmin
  • AppStore.currentScenicId > 0

接口:

GET /api/app/store/all

调用链:

HomeViewModel.loadStoreListIfNeeded(api:)
-> HomeAPI.storeList()

返回后的处理:

  • 根据 currentScenicId 和已保存的 currentStoreId 匹配当前门店。
  • 匹配结果保存在 HomeViewModel.storeItem
  • 若匹配到门店,写入 AppStore.currentStoreId
  • 门店列表本身不持久化。

4.4 首页弹窗评估与位置详情接口

首页 UI 刷新后调用:

evaluateDialogsWithDelay()
-> HomeViewModel.evaluateDialogs(api:)

弹窗优先级:

  1. 权限弹窗。
  2. 景区弹窗。
  3. 位置超时弹窗。

如果不需要权限弹窗、不需要景区弹窗、不是简化顶部角色,并且本地设置了位置提醒分钟数,则可能调用位置详情接口:

GET /api/yf-handset-app/photog/loacation/detail?staff_id=...

调用链:

HomeViewModel.evaluateDialogs(api:)
-> HomeViewModel.checkLocationTimeout(api:)
-> HomeAPI.locationDetail(staffId:)

返回后的处理:

  • 只用于判断是否展示位置超时提醒。
  • 结果不写入 AppStore
  • 弹窗状态保存在 HomeViewModel.showLocationTimeoutDialog 等内存属性中。

5. 其他 Tab 首次展示时的接口

其他 Tab 的根控制器会在 MainTabBarController.configureTabs() 时创建,但各自的数据接口通常在首次展示时才调用。

5.1 订单 Tab

OrdersViewController.viewDidAppear 首次出现时调用:

initializeList()

按当前角色选择接口。

摄影师:

GET /api/yf-handset-app/photog/order/listv2

景区管理员:

GET /api/app/scenic-admin/order/list

门店管理员:

GET /api/app/store/order/list

调用链:

OrdersViewController.initializeList()
-> OrderListViewModel.refreshOrderList(api:)
-> OrderAPI.photographerOrderList(...) 或 OrderAPI.scenicAdminOrderList(...)

或

OrdersViewController.initializeList()
-> DepositOrderListViewModel.refreshOrderList(api:)
-> OrderAPI.storeOrderList(...)

数据存储:

  • 摄影师/景区管理员订单列表保存在 OrderListViewModel.orderList
  • 门店管理员订单列表保存在 DepositOrderListViewModel.orderList
  • 分页状态、筛选状态、搜索状态都保存在对应 ViewModel 内存属性中。
  • 首屏订单列表不写入 AppStore

5.2 数据 Tab

StatisticsViewController.viewDidAppear 首次出现时调用:

initializeStatistics()
-> StatisticsViewModel.initStatistics(api:)

内部顺序:

  1. 加载汇总数据。
  2. 设置日明细日期范围为今日并刷新列表。

摄影师汇总:

GET /api/yf-handset-app/photog/analyse/user

摄影师日明细:

GET /api/yf-handset-app/photog/analyse/user/daily

景区管理员汇总:

GET /api/app/scenic-admin/analyse

景区管理员日明细:

GET /api/app/scenic-admin/analyse/daily

门店管理员汇总:

GET /api/app/store/analyse

门店管理员日明细:

GET /api/app/store/analyse/daily

数据存储:

  • 汇总数据保存在 StatisticsViewModel.statistics
  • 日明细列表保存在 StatisticsViewModel.statisticsList
  • 当前周期、日期范围、分页状态保存在 StatisticsViewModel 内存属性中。
  • 统计数据不写入 AppStore

5.3 我的 Tab

ProfileViewController.viewWillAppear 每次出现都会调用:

reloadProfile(showGlobalLoading:)
-> ProfileViewModel.reload(api:)

第一步固定调用用户资料接口:

GET /api/yf-handset-app/userinfo

调用链:

ProfileViewModel.reload(api:)
-> ProfileAPI.userInfo()

返回后的处理:

  • 保存在 ProfileViewModel.userInfo
  • 调用 AppStore.applyUserInfo(_:) 回写展示字段:
    • userName
    • realName
    • avatar
    • phone
    • roleName,仅在本地为空时由 applyUserInfo 写入;随后 ProfileViewModel.reload 也会在接口 roleName 非空时写入。

如果当前为摄影师角色,则并发调用:

GET /api/yf-handset-app/photog/real-name/info
GET /api/yf-handset-app/photog/wallet/bank-card/info

调用链:

async let realName = api.realNameInfo()
async let bankCard = api.bankCardInfo()

返回后的处理:

  • 实名信息保存在 ProfileViewModel.realNameInfo
  • 银行卡信息保存在 ProfileViewModel.bankCardInfo
  • 这两个结果不写入 AppStore

6. 数据存储总览

6.1 AppStore / UserDefaults

AppStore 是本地会话和业务快照的统一入口,底层使用 UserDefaults

非账号作用域的主要 key

  • key_in_token
  • key_last_login_username
  • key_privacy_agreement_accepted
  • key_in_user_id
  • key_in_user_name
  • key_in_real_name
  • key_in_avatar
  • key_in_phone
  • key_in_account_type
  • key_in_account_display_name
  • key_in_role_code
  • key_in_role_name
  • key_in_current_scenic_id
  • key_in_current_scenic_name
  • key_in_current_store_id

账号作用域由 accountCachePrefix 生成:

userId_accountType

如果没有 userId则使用 guest。账号作用域 key 用于存储:

  • 旧版角色 IDkey_in_role_id
  • 完整 role-permission 列表:key_role_permission_list
  • 当前角色扁平权限:key_in_permission
  • 当前角色可选景区列表:key_current_role_scenic_list
  • 在线状态:key_online_status
  • 上次位置上报时间:key_last_location_report_time
  • 位置提醒分钟数:key_location_reminder_minutes
  • 收款到账语音播报开关:key_is_open_receive_voice
  • 排队叫号相关本地设置。

权限和景区列表以 JSON Data 写入 UserDefaults

  • saveRolePermissionList(_:)
  • savePermissionItems(_:)
  • saveRoleScenicList(_:)

6.2 HomeCommonMenuStore

首页常用应用由 HomeCommonMenuStore 单独管理,底层也是 UserDefaults

存储 key

{accountScope}_role_{roleCode}_common_uris

值为 URI 字符串数组。

首次有权限但没有常用应用记录时,会从权限列表生成最多 4 个默认常用入口并写入本地。

6.3 HomeLocationStateStore

HomeLocationStateStore 负责首页在线状态和 2 小时位置上报倒计时。

持久化到 AppStore 的字段:

  • onlineStatus
  • lastLocationReportTime
  • locationReminderMinutes

内存态字段:

  • isOnline
  • elapsedSeconds
  • nextReportCountdownSeconds
  • reminderMinutes
  • 倒计时 Task

启动恢复逻辑:

  • 如果本地 onlineStatus == false,停止倒计时。
  • 如果没有 lastLocationReportTime,清除在线状态。
  • 如果距离上次上报已超过 2 小时,清除在线状态和上次上报时间。
  • 否则恢复倒计时。

6.4 ViewModel 内存态

多数页面接口结果只保存在 ViewModel 内存属性中,不持久化。

典型示例:

  • 登录配置:LoginViewModel.enableRegister
  • 首页门店卡片:HomeViewModel.storeItem
  • 首页弹窗状态:HomeViewModel.showPermissionDialogshowScenicDialogshowLocationTimeoutDialog
  • 订单列表:OrderListViewModel.orderListDepositOrderListViewModel.orderList
  • 数据统计:StatisticsViewModel.statisticsstatisticsList
  • 我的页面实名/银行卡状态:ProfileViewModel.realNameInfobankCardInfo

例外是明确调用 AppStore 的数据:

  • 登录 token 与账号上下文。
  • 用户资料展示字段。
  • 权限、景区、角色相关快照。
  • 门店当前 ID。
  • 位置在线状态与倒计时锚点。

7. 启动接口顺序速查

7.1 未登录冷启动

AppDelegate.didFinishLaunching
-> 友盟/微信/高德 SDK 条件初始化
-> SceneDelegate.willConnect
-> AppRouter.makeRootViewController()
-> LoginViewController.setupUI
-> LoginViewController.viewDidAppear
-> GET /api/app/config

点击登录后:

POST /api/app/v9/login
-> POST /api/app/v9/set-user
-> AppStore 写入登录态和账号上下文
-> userDidLogin
-> AppRouter.setRoot(.mainTab)

7.2 已登录冷启动默认首页

AppDelegate.didFinishLaunching
-> 友盟/微信/高德 SDK 条件初始化
-> SceneDelegate.willConnect
-> AppRouter.makeRootViewController()
-> MainTabBarController.viewDidLoad
-> HomeViewController.viewDidAppear
-> HomeLocationStateStore.restoreStateIfNeeded()
-> GET /api/yf-handset-app/role-permission
-> 条件满足时 GET /api/app/store/all
-> 条件满足时 GET /api/yf-handset-app/photog/loacation/detail?staff_id=...

7.3 用户首次切到订单 Tab

OrdersViewController.viewDidAppear
-> 摄影师 GET /api/yf-handset-app/photog/order/listv2
   或 景区管理员 GET /api/app/scenic-admin/order/list
   或 门店管理员 GET /api/app/store/order/list

7.4 用户首次切到数据 Tab

StatisticsViewController.viewDidAppear
-> 按角色 GET 汇总接口
-> 按角色 GET 日明细接口

7.5 用户切到我的 Tab

ProfileViewController.viewWillAppear
-> GET /api/yf-handset-app/userinfo
-> 摄影师角色并发:
   GET /api/yf-handset-app/photog/real-name/info
   GET /api/yf-handset-app/photog/wallet/bank-card/info