17 KiB
应用启动流程与接口数据存储梳理
本文按当前 iOS 工程代码梳理应用从冷启动到各首屏展示期间发生的初始化、接口调用顺序,以及接口数据和本地状态的存储方式。范围仅覆盖当前 iOS 实现,不额外补充 Android 中存在但 iOS 尚未接入的能力。
1. 冷启动立即发生
冷启动入口在 AppDelegate.application(_:didFinishLaunchingWithOptions:)。
- 调用
UmengBootstrap.configureIfNeeded()。- 友盟初始化受
AppStore.shared.privacyAgreementAccepted控制。 - 未同意隐私协议时直接返回,不初始化 SDK。
- 友盟初始化受
- 调用
WeChatManager.shared.registerIfNeeded()。- 校验 Universal Link。
- 调用微信 SDK
WXApi.registerApp注册 AppID。
- 若
AppStore.shared.privacyAgreementAccepted == true且AppStore.shared.token非空,则调用AMapBootstrap.configureIfNeeded()。- 写入高德隐私合规状态。
- 设置
AMapServices.shared().apiKey和 HTTPS。
- 此阶段不直接调用任何业务后端接口。
随后系统进入 SceneDelegate.scene(_:willConnectTo:options:)。
- 调用
AppNavigationBarAppearance.applyGlobalAppearance()设置全局导航栏样式。 - 创建
UIWindow。 - 设置
window.rootViewController = AppRouter.makeRootViewController()。 AppRouter通过AppStore.shared.isLoggedIn判断根页面:- token 非空:进入
MainTabBarController。 - token 为空:进入
UINavigationController(rootViewController: LoginViewController())。
- token 非空:进入
- 注册全局通知:
sessionDidExpire:清除登录态并切回登录页。userDidLogout:清除登录态并切回登录页。userDidLogin:切到主 Tab。
2. 网络依赖初始化
NetworkServices.shared 是全局网络依赖容器,不在 AppDelegate 中主动创建,而是在页面或 Tab 首次访问业务 API 时懒加载。
初始化顺序如下:
- 创建
APIClient(environment: .current)。 - 用同一个
APIClient创建各业务 API:AuthAPIProfileAPIStatisticsAPIOrderAPIHomeAPI- 以及其他业务模块 API。
- 创建
OSSUploadService(configService: uploadAPI)。 - 调用
client.bindAuthTokenProvider,从AppStore.shared.token读取 token。
APIClient 的公共请求规则:
APIEnvironment.current- Debug:
https://api-test.zhifly.cn - Release:
https://api.zhifly.cn
- Debug:
- 每个请求默认写入 Header:
Content-Type: application/jsonAccept: application/jsonX-APP-VERSIONX-OS-TYPE: iOS- token 非空时写入
token
- 响应通过
APIEnvelope解包。- 成功业务码:
100000 - 非成功业务码抛出
APIError.serverCode - 认证失效时广播
sessionDidExpire,由SceneDelegate清会话并回登录页。
- 成功业务码:
3. 未登录启动与登录流程
未登录时根页面为登录页。
3.1 登录页展示前
LoginViewController.setupUI 会调用 viewModel.applyStoredPreferences(),从 AppStore 读取:
lastLoginUsernameprivacyAgreementAccepted
这一步只恢复本地表单状态,不调用后端接口。
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 的内容:
tokenlastLoginUsernameprivacyAgreementAccepteduserIduserNamerealNameavatarphoneaccountTypeaccountDisplayNameroleCoderoleNamecurrentScenicIdcurrentScenicNamecurrentStoreId
其中账号上下文来自 AccountSwitchAccount、V9ScenicUser 或 V9StoreUser。
如果用户已同意隐私协议,还会再次触发:
AMapBootstrap.configureIfNeeded()UmengBootstrap.configureIfNeeded()
最后发送:
NotificationName.userDidLogin
SceneDelegate.handleUserDidLogin() 收到通知后调用:
AppRouter.setRoot(.mainTab, on: window)
根页面切换到主 Tab。
4. 已登录启动与默认首页首次展示
已登录时,AppRouter.makeRootViewController() 创建 MainTabBarController。
4.1 主 Tab 创建
MainTabBarController.viewDidLoad 顺序:
- 设置
delegate。 - 配置 TabBar 外观。
- 调用
configureTabs()创建五个 Tab slot:- 首页:
HomeViewController - 订单:
OrdersViewController - 中间扫码占位页
- 数据:
StatisticsViewController - 我的:
ProfileViewController
- 首页:
- 绑定订单角标 ViewModel。
- 绑定扫码处理器。
- 调用
badgeViewModel.refreshPendingWriteOffCount()。
注意:MainTabBadgeViewModel.refreshPendingWriteOffCount() 当前仅执行:
pendingWriteOffCount = nil
代码注释标明“接口未接入前保持为空”,因此这里不记录为真实后端接口调用。
4.2 默认首页首次出现
默认选中首页。HomeViewController.viewDidAppear 首次出现时调用:
initializeHome()
调用顺序:
- 展示全局 loading。
HomeViewModel.initialize(api:)- 隐藏 loading。
- 刷新首页 UI。
- 延迟评估弹窗。
HomeViewModel.initialize(api:) 内部顺序:
locationStateStore.restoreStateIfNeeded()- 从
AppStore恢复在线状态、上次位置上报时间、提醒分钟数。 - 如果上次在线状态已过期,会清除在线状态和上次上报时间。
- 不调用后端接口。
- 从
refreshLocalDisplayState()- 从
AppStore读取当前景区、当前角色。 - 无有效景区时清空
currentScenicId/currentScenicName。
- 从
- 强制调用
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:)
弹窗优先级:
- 权限弹窗。
- 景区弹窗。
- 位置超时弹窗。
如果不需要权限弹窗、不需要景区弹窗、不是简化顶部角色,并且本地设置了位置提醒分钟数,则可能调用位置详情接口:
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:)
内部顺序:
- 加载汇总数据。
- 设置日明细日期范围为今日并刷新列表。
摄影师汇总:
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(_:)回写展示字段:userNamerealNameavatarphoneroleName,仅在本地为空时由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_tokenkey_last_login_usernamekey_privacy_agreement_acceptedkey_in_user_idkey_in_user_namekey_in_real_namekey_in_avatarkey_in_phonekey_in_account_typekey_in_account_display_namekey_in_role_codekey_in_role_namekey_in_current_scenic_idkey_in_current_scenic_namekey_in_current_store_id
账号作用域由 accountCachePrefix 生成:
userId_accountType
如果没有 userId,则使用 guest。账号作用域 key 用于存储:
- 旧版角色 ID:
key_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 的字段:
onlineStatuslastLocationReportTimelocationReminderMinutes
内存态字段:
isOnlineelapsedSecondsnextReportCountdownSecondsreminderMinutes- 倒计时
Task
启动恢复逻辑:
- 如果本地
onlineStatus == false,停止倒计时。 - 如果没有
lastLocationReportTime,清除在线状态。 - 如果距离上次上报已超过 2 小时,清除在线状态和上次上报时间。
- 否则恢复倒计时。
6.4 ViewModel 内存态
多数页面接口结果只保存在 ViewModel 内存属性中,不持久化。
典型示例:
- 登录配置:
LoginViewModel.enableRegister - 首页门店卡片:
HomeViewModel.storeItem - 首页弹窗状态:
HomeViewModel.showPermissionDialog、showScenicDialog、showLocationTimeoutDialog - 订单列表:
OrderListViewModel.orderList、DepositOrderListViewModel.orderList - 数据统计:
StatisticsViewModel.statistics、statisticsList - 我的页面实名/银行卡状态:
ProfileViewModel.realNameInfo、bankCardInfo
例外是明确调用 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