Appearance
App 啟動
最後更新:2026-04-11
功能說明
App 從冷啟動(完全關閉狀態重新打開)到使用者可操作主畫面的完整流程。啟動分兩大階段:
- 系統初始化 — 設定預設域名、探測最快可用域名、初始化第三方服務
- 登入判斷 — 根據登入狀態決定自動登入或顯示登入頁
使用者流程
- 使用者點擊 App icon → App 冷啟動
- 背景自動探測最快可用域名(使用者不感知)
- 如果之前登入過且登入狀態還有效 → 自動靜默登入,直接進主畫面
- 如果登入狀態過期或從未登入 → 顯示登入/注冊頁
- 登入成功 → 切換至主畫面(TabBar)
頁面跳轉
- App 冷啟動(登入狀態有效)→ 自動登入 → 主畫面
- App 冷啟動(登入狀態無效)→ 登入/注冊頁 → 登入成功 → 主畫面
- App 維護中 → 停在維護頁,不進入主畫面
技術視角(開發看這裡)
相關檔案
| 類型 | 檔案路徑 |
|---|---|
| AppDelegate | BBSport/AppDelegate/AppDelegate.swift |
| SceneDelegate | BBSport/AppDelegate/SceneDelegate.swift |
| 域名探測管理器 | BBSport/Tools/STDomainListManager/STDomainListManager.swift |
| 域名候選清單 JSON | BBSport/Tools/Domain/M/domain.json |
| 域名探測配置 JSON | BBSport/Tools/STDomainListManager/st_domain_config.json |
完整檔案清單
| 類型 | 檔案路徑 |
|---|---|
| AppDelegate 推送擴展 | BBSport/AppDelegate/AppDelegate+JPPush.swift |
| AppDelegate SDK 擴展 | BBSport/AppDelegate/AppDelegate+SDK.swift |
| 登入服務(OC) | BBSport/Tab/我的/Mine/Login_Register/IFLoginService.m |
| 登入服務 Header | BBSport/Tab/我的/Mine/Login_Register/IFLoginService.h |
| 維護管理 | BBSport/Tools/MaintenanceManager/GameMaintenanceManager.swift |
| 域名預設值定義 | BBSport/Tools/Domain/M/AppDomain.swift |
| 環境類型定義 | BBSport/Tools/Domain/M/EnvironmentType.swift |
| 域名回應 Model(OC) | BBSport/Tools/RouterComponent/Classes/Main/BBUrlListModel.h |
| 域名路由開關(OC) | BBSport/Tools/RouterComponent/Classes/Main/IFMainSwitch.m |
| Base URL 取得(OC) | BBSport/Tools/UtilityToolComponentOC/Classes/Utils/IFCommonUtil.m |
| 三方 SDK 初始化 | BBSport/Tools/STThirdPartyManager/STThirdPartyManager.swift |
| 極光推送 Header | BBSport/Tools/Push/JPPService.h |
| 路由註冊(OC) | BBSport/Tab/我的/Mine/Login_Register/ModuleApi.m |
| 通知定義 | BBSport/Tools/NotificationManager/STNotifyExtension.swift |
API
| 功能說明 | Namespace | Endpoint | Method | 主要參數 |
|---|---|---|---|---|
| 域名探測(找最快域名用) | — | api/forehead/system/domain/list/v2 | POST | device-id, os-type, timestamp, version, sign, pid=bb |
| 投注自訂參數 | STAPI | STAPI.BetListParameter | POST | — |
| 充值運營圖 | STAPI | api/forehead/system/config/images/query | POST | — |
| 分享螢幕圖 | STAPI | api/forehead/common/pic/load | POST | — |
| 好友域名 | STAPI | STAPI.FriendDomainRequest | POST | — |
| 邀請碼 | STAPI | STAPI.InviteCodeRequest | POST | — |
| 產品配置(維護狀態) | STAPI | api/forehead/gamebet/product/productConfig | POST | — |
啟動整體時序圖(先看全局)
左邊(AppDelegate)做域名探測,右邊(SceneDelegate)同時做登入判斷,兩件事是並行的。下面逐步展開每個環節。
域名選定:從打開 App 到確定 Base URL 的完整過程
這是啟動流程中最核心的部分。App 要能跟後端溝通,必須先知道「要打哪個 Host」。以下逐步說明。
先搞懂:兩份配置檔的差別
App 裡有兩份域名 JSON,用途完全不同:
| 檔案 | 位置 | 用途 | 使用場景 |
|---|---|---|---|
domain.json | Tools/Domain/M/ | 定義 AppDomain.shared.root 的預設值(分正式/UAT/測試三個環境),只有 gateway 一種域名 | 開發測試用:DEBUG 模式下直接用這份域名,透過切換 STEnvironment 指向不同環境,不走域名探測 |
st_domain_config.json | Tools/STDomainListManager/ | 定義域名探測用的候選清單(realese_domains)、探測 API 路徑、OSS 兜底地址 | 正式發布用:Release 模式下的域名探測完全依賴這份配置,線上用戶的域名來源就是這裡 |
排查線上問題看哪份?
看 st_domain_config.json。線上(Release)環境的域名探測流程全部走這份檔案,包括:
realese_domains裡的候選域名是否都能連通- OSS 兜底地址是否正常
- 探測 API 路徑
api/forehead/system/domain/list/v2回應是否正確
domain.json 只影響開發人員本地 DEBUG 模式,與線上用戶無關。
域名選定總覽
第 1 步:讀取本地預設域名(同步,毫秒級)
時機:AppDelegate.didFinishLaunchingWithOptions 一開始
做了什麼:AppDomain.shared.setDefaultUrls()
domain.json 的完整內容:
json
[{
"title": "Root域名,用来获取服务端配置的域名",
"type": 0,
"domains": [
{ "title": "正式", "url": "https://m.bbtyv16.com" },
{ "title": "UAT", "url": "https://m.bbuat2021.com" },
{ "title": "测试", "url": "http://m.bbtstxqm7.com" }
]
}]此時的狀態
AppDomain.shared.root 有值了(例如正式環境 = https://m.bbtyv16.com)。 但大部分 API 請求最終是透過 IFMainSwitch(ObjC BBUrlListModel)取域名,不是從 AppDomain 取。
注意:App 裡有兩套域名類型系統
App 有 Swift DomainType(9 種)和 ObjC UrlType(19 種)兩套獨立的域名編號系統,編號不同、用途也不同。線上實際的域名走 ObjC UrlType,Swift DomainType 主要用於開發環境切換和少數 Release 功能。
domain.json 只配了主域名(root)一種預設值,其他域名首次安裝時是空的,要等 Step 4 域名探測成功後才有值。
第 2 步:載入上次存的域名清單(同步)
做了什麼:IFMainSwitch.getURLListFromLocal()
- 從
UserDefaults讀取上次成功拿到的域名清單(BBUrlListModel) - 這份清單包含所有類型的域名(主域名、推送域名、聊天室域名 ... 共 20+ 種
UrlType) - 目的是讓 App 在還沒聯網之前就有一組可用域名
首次安裝
首次安裝時,UserDefaults 裡沒有任何域名清單。BBUrlListModel 所有欄位都是空陣列,直到 Step 4 域名探測成功後才會被填入。
第 3 步:配置域名探測參數(同步)
做了什麼:STDomainListManager.shared.registerDomainList(with: readDomainConfig())
從 st_domain_config.json 讀取探測所需的配置:
json
{
"api_domain_path": "/api/forehead/system/domain/list/v2",
"aliyun_oss_url": [
"https://ui26ftj02lvf.qodmazcn53.com/bb99/bbpub.json",
"https://cu02ftj02lvf3.pukyazpv57.com/bb99/bbpub.json",
"https://ou34ftj02lvf2.qkdoazbi55.com/bb99/bbpub.json"
],
"realese_domains": [
"https://m-nu42sdu3xfca6.kzpw72mndq19.com",
"https://m-ayhc45u7p3.iabf52qmhg7.com",
"https://m-4ytsuxn33i1i0ca3.vafrazwu44.com",
"https://m-djwnsdfs09a6z3m2.vshwazor54.com",
"https://m-5xov436trc5b0j.wejaazhp47.com",
"https://m-uthb76i2c2.zljzazmm59.com"
],
"debug_domains": [
"https://m.bbmobileproaz.com",
"http://m.bbtstxqm7.com",
"https://m.bbuat2021.com",
"http://m.bbgamedev.com",
"http://m.bbsama.com",
"http://www.bbdev.com",
"http://www.seeknewt.com"
]
}| 欄位 | 說明 |
|---|---|
api_domain_path | 拼在每個候選域名後面的路徑,用來測試該域名是否能通 |
realese_domains | Release 版的候選域名列表(6 個)— 首次安裝時,這是唯一的域名來源 |
debug_domains | Debug 版的候選域名列表(7 個),目前沒有被 startFindFastDomain 使用 |
aliyun_oss_url | 阿里雲 OSS 備援地址(3 個),域名全掛時用來取得 IP 清單兜底 |
第 4 步:併發探測最快域名(異步,核心邏輯)
做了什麼:STDomainListManager.shared.startFindFastDomain(completion:)
這是整個域名選定的核心。
4-1. 組合候選域名列表
候選清單的組合邏輯會因為「是否首次安裝」而不同。
BTHostList.plist 是域名探測成功後存到本地的主域名清單(Documents 目錄),下次啟動時直接讀取作為候選,不用從零開始探測。首次安裝時這個檔案不存在。
各場景下的候選清單來源:
| 場景 | 候選清單內容 | 域名數量 |
|---|---|---|
| 首次安裝(Release) | 只有 st_domain_config.json 的 realese_domains | 6 個 |
| 非首次安裝(Release) | 本地快取 + realese_domains(去重)+ 上次最快域名 | 約 6~10 個 |
| DEBUG 模式 | 只有 AppDomain.shared.root(來自 domain.json) | 1 個 |
4-2. 併發探測 + 處理結果
「最先回來的就贏」:多個候選域名同時打,第一個成功回應的就被選為 Base URL,後面回來的都忽略。
成功回應後做了什麼
回應的 JSON data 陣列包含各類型域名(type 0~36),處理方式:
IFMainSwitch.setUpURLList(data)— 解析回應,存到BBUrlListModel,所有 20+ 種域名一次更新,同時存入UserDefaults供下次啟動使用- 存到本地檔案
BTHostList.plist— 主域名(type 1)清單寫入 Documents 目錄,下次啟動時 Step 4-1 讀取 - 更新客服域名 — 如果回應包含 type=15(主線客服),更新
AppDomain.shared.serviceDomain並重新初始化客服 SDK STDomainListManager.fastMainDomain = 最快域名— 記錄在記憶體,後續所有 API 都用這個 Host
域名探測走 IP 兜底時的特殊標記
如果候選域名全掛,改用 OSS IP 探測成功:
- 會在
UserDefaults存一個IPHOST標記(= 成功的 IP 域名) - 不會存入
KEY_MAIN_DOMAIN(因為 IP 不穩定,下次不應優先用) IPHOST標記會影響活動 URL 拼接邏輯(改用IFMainSwitch.getActivityUrl而非正常拼法)
正常域名探測成功時:
- 存入
KEY_MAIN_DOMAIN - 清除
IPHOST標記
第 5 步:域名就緒,觸發業務請求(異步)
域名選定成功後(在 startFindFastDomain 的 completion 裡),並行發出:
| 請求 | 說明 |
|---|---|
STAPI.BetListParameter | 自訂投注參數 |
RechargeOperatePicRequest | 充值運營圖 |
OperatePicRequest | 分享螢幕圖 |
LiveComponent.registerLiveServices() | 直播服務註冊 |
SportManager.registerServices() | 體育服務路由註冊 |
LotteryConfigManager.registerServices() | 彩票服務路由註冊 |
DDConfigManager.registerServices() | 客服 SDK 初始化 |
關鍵:上面這些都依賴域名探測完成。如果域名探測失敗,這些請求不會發出。
首次安裝的代理綁定邏輯
域名探測成功的 completion 裡還有一段首次開啟專屬邏輯:
首次開啟的 getBaseUrl() 用的是哪個域名?
IFCommonUtil.getBaseUrl() 的取值優先順序:
STDomainListManager.fastMainDomain(記憶體中的最快域名)UserDefaults KEY_MAIN_DOMAIN(上次存的最快域名)normalDefaultDomains.first(st_domain_config.json的realese_domains第一個)
首次安裝時 1 和 2 都是空的,所以用的是 realese_domains 第一個域名。 但這段代碼在 startFindFastDomain 的 completion 裡,此時 fastMainDomain 已經有值了,所以實際上用的是剛探測成功的最快域名。
第 6 步:SceneDelegate 判斷登入狀態(與域名探測並行)
時機:SceneDelegate.scene(willConnectTo:) — 與 AppDelegate 的域名探測幾乎同時執行
首次安裝
首次安裝時 KEY_TOKEN 不存在,一定走「顯示登入頁」分支。
注意
域名探測和登入判斷是並行的。使用者可能已經進入主畫面了,域名探測才完成。但業務 API 請求(投注參數、充值運營位等)會等域名就緒才打。
各場景域名來源總結
| 場景 | 配置檔 | AppDomain.shared.root 來源 | 域名探測候選清單來源 | 最終 Base URL 來源 |
|---|---|---|---|---|
| 首次安裝(Release) | st_domain_config.json | domain.json(正式環境域名) | realese_domains(6 個) | 探測最快的那個 realese_domains |
| 非首次安裝(Release) | st_domain_config.json | domain.json(但會被之前存的覆蓋) | 本地快取 + realese_domains + 上次最快域名 | 探測最快的域名 |
| 域名全掛 + OSS 兜底成功 | st_domain_config.json | 同上 | OSS 下載的 IP 清單 | 成功的 IP |
| 域名全掛 + OSS 也掛 | st_domain_config.json | 同上 | 無 | 探測失敗,業務請求無法發出 |
| DEBUG 模式 🔧 | domain.json | 依 STEnvironment 切換 | 直接用 AppDomain.shared.root(不探測) | domain.json 裡的域名 |
Release 場景全部走
st_domain_config.json,線上問題排查看這份。domain.json僅供開發人員 DEBUG 模式切換環境用。
IFCommonUtil.getBaseUrl() — 全域 Base URL 取得邏輯
整個 App 取得 Base URL 都透過 IFCommonUtil.getBaseUrl(),它的優先順序:
在 DEBUG 模式下,getFastMainDomain() 會直接回傳 AppDomain.shared.root,跳過上面所有判斷。
注意
getDeviceId()(GuardianHelper)在 startFindFastDomain 之前就呼叫了,它也用 getBaseUrl()。首次安裝時 fastMainDomain 和 KEY_MAIN_DOMAIN 都是空的,所以 getDeviceId 用的是 realese_domains 第一個域名。
實作重點
- 域名探測是前置條件:
startFindFastDomain成功後才觸發核心業務請求(投注參數、充值運營位等),域名探測失敗則後續請求無法進行 - 三層兜底機制:本地快取域名 → 配置檔候選域名(
realese_domains)→ 阿里雲 OSS IP 地址 - 首次安裝只靠
realese_domains:沒有任何快取可用,6 個寫死在st_domain_config.json的域名是唯一來源 - DEBUG 模式跳過探測:直接使用
AppDomain.shared.root(domain.json裡設定的域名) - Token 自動登入:
SceneDelegate判斷KEY_TOKEN存在且TOKENTIME未過期,直接走LoginByToken免登入 - 前後台輪詢管理:進入背景
STPauseAllPolling()、回到前台STResumeAllPolling(),避免背景無效請求 - 登出統一收口:無論手動登出、被踢、平台失效、重連失敗,都走
GameStatusManager.logout()→resetProviders()→STNotify.logout.post()
setUpChangeWindows() 切換到主畫面邏輯
情境 A:從已有頁面 push 出的登入頁(isShowingLoginVC = true)
- dismiss 登入頁
- 已登入 → 延遲 0.1s →
STNotify.logged.post()
情境 B:冷啟動的登入頁(isShowingLoginVC = false)
- 檢查 APP 級別維護 → 維護中直接 return(停在維護頁)
STNotify.changeKeyWindow.post()→ SceneDelegate 收到 →window.rootViewController = MainViewController()- 已登入 → 延遲 0.2s →
STNotify.logged.post()
👉 所有寫死在本地的域名、IP、金鑰清單已移至獨立頁面:域名總覽
登入成功後的 API 呼叫
附錄:兩套域名類型系統
App 裡有兩套獨立的域名 enum,編號不同,容易搞混:
- ObjC
UrlType(BBUrlListModel,19 種定義)— 線上實際走這套,伺服器回應 → BBUrlListModel → IFMainSwitch getter → API 請求 - Swift
DomainType(AppDomain,9 種)— 主要給開發環境切換用,少數 Release 功能也會直接讀
編號對照表
| 域名 | Swift DomainType | ObjC UrlType |
|---|---|---|
| 推送 | .push = 3 | PushUrl = 2 |
| 聊天 | .chat = 2 | ChatSocketUrl = 6 |
| 賽果 | .result = 5 | MatchResultUrl = 8 |
| 代理 | .proxy = 7 | ProxyUrl = 12 |
AppDomain 管理的 9 種域名
| 屬性 | 用途 | DomainType | 對應的 ObjC UrlType |
|---|---|---|---|
root | 主 API 域名 | .gateway (0) | 無直接對應 |
pxdd | 雷速數據 | .leisu (1) | PXDDUrl (18) |
chat | 聊天室 | .chat (2) | ChatSocketUrl (6) |
push | 推送 | .push (3) | PushUrl (2) |
h5 | 廣場/資訊 | .h5 (4) | SportInformationUrl (11) |
result | 賽果 | .result (5) | MatchResultUrl (8) |
dataH5 | 詳情頁 H5 | .dataH5 (6) | H5Url (36) |
proxy | 代理 | .proxy (7) | ProxyUrl (12) |
serviceDomain | 客服 | .serviceDomain (8) | ServiceMainLine (15) |
AppDomain 在 Release 也有人用
AppDomain.shared.root 不是只給 DEBUG 用。以下 Release code 也直接讀它:
CustomerChatManager.swift:210, 819— 客服 URL 拼接ExponentView.swift:27— 體育指數
AppDomain.shared.serviceDomain 會被伺服器回應的 UrlType 15 動態更新。
死 code
getUrlByUrlType() 試圖橋接兩套系統,但全專案無任何呼叫,是死 code。
完整的 UrlType 盤點見 域名總覽。