這篇整理自家行動 App 呼叫自家後端 API 時,在 11.6 向後相容的變更紀律 之外多出來的契約機制,也收進商店審核、分階段發布與熱更新這幾項發布端的約束,因為它們決定 API 什麼時候上線、舊版要支援多久。範圍是經公開商店(App Store、Google Play)散布、透過自家 API 存取資料的原生或跨平台 App,使用者不論有沒有登入,服務方都沒有一份能逐一通知、要求更新的名單;從最早上架的版本該內建什麼開始,到多個版本同時在線、新版出問題時怎麼回退為止。以 WebView 包網頁的 App、PWA、經企業裝置管理(MDM)派送的 App,以及直接用 SDK 連 BaaS 而沒有自家 API 的 App,前提不同,不在本篇範圍內。公開 API 的版本方案與退場工具在 11.5 版本策略與 deprecation,App 的簽章、送審與 CI 流程在 App 簽章、商店審核與分批發布流程。

自家行動 App 在消費者形態裡的位置

自家行動 App 是一種混合形態的 API 消費者:呼叫 API 的程式碼由服務方自己寫,而執行中的版本由使用者決定要不要更新。11.6 向後相容的變更紀律 依消費者的更新能力,把它歸在「消費者無法被強制更新」這一形態:服務方沒辦法替使用者把新版裝上,使用者不更新,舊版就一直在線上呼叫 API。本篇〈最低支援版本與強制更新〉講的強制更新做的是另一件事——讓舊版停止使用、把使用者帶到更新畫面,按下更新仍由使用者完成。「無法被強制更新」與「強制更新」講的是同一個限制的兩面。

拿它和 API Consumer Shape 卡列出的消費者形態並排,差別落在兩個地方:

  • 和內部服務相比:內部服務可以和 API 在同一次提交裡一起改完(可原子更新)。App 的程式碼也在同一個團隊手上,但改完的程式要經過商店審核、再等使用者更新,才會出現在使用者的裝置上;已經安裝的版本不會跟著提交改變。所以 App 不屬於可原子更新的消費者。
  • 和公開 API 的整合團隊相比:整合團隊是另一家公司或另一個團隊,有帳號、聯絡得到人;App 的使用者不論有沒有登入,服務方都沒有一份能逐一要求更新的名單,這一點和身分不明的外部整合(例如經第三方轉售的終端客戶)相同;有登入的 App 可以用推播或 email 通知使用者,而通知不等於能讓人更新。反過來,服務方握有整合團隊身上沒有的兩個手段:客戶端是自己寫的,可以在出貨前內建版本回報與更新檢查;也可以在客戶端內建遠端設定,讓功能的開關不必等審核。遠端設定在本篇指 App 啟動時與執行期間向服務方讀取的一組設定值,例如 Firebase Remote Config;值由工程師或營運人員在設定服務的後台修改,App 不必更新就能讀到新值。

所以 App 最早上架的版本就要內建這幾樣,每一樣在後面各有一節:

  • 版本回報:每個請求帶上 App 版本與作業系統版本(〈版本回報:每個請求帶上 App 版本〉)。
  • 客戶端啟動檢查與對「需要更新」錯誤碼的處理:讓舊版能被帶到更新畫面(〈最低支援版本與強制更新〉)。
  • 對回應裡多出的欄位、未知的列舉值與空值的容錯:後端之後依版本調整回應、新增欄位時,舊版才不會壞(〈新版 App 或新功能出問題時的回退手段〉)。
  • 讀取遠端設定,而且讀取間隔要短到出問題時關得掉功能,或整合即時更新(〈功能提前內建與上線日遠端啟用〉)。
  • 帶版本標記的當機回報:回退手段的觸發都靠先發現問題;當機回報與依版本分組的監控見 App 簽章、商店審核與分批發布流程。
  • 要用熱更新的話,熱更新服務也要在送審的版本裡整合(〈熱更新:不經商店審核的程式更新〉)。

服務方另外多了一個公開 API 沒有的限制——自己的修正也要排隊等商店審核。

版本回報:每個請求帶上 App 版本

版本回報是指 App 在每個 API 請求裡帶上自己的版本與平台,讓後端知道是哪一個版本在呼叫。後端拿這個值做四件事:依版本調整回應(舊版收到它認得的格式)、依版本統計用量(知道舊版還有多少使用者)、上線與事故時依版本比較錯誤率並判斷影響了哪些版本,以及判斷要不要要求更新。

版本回報本身是契約的一部分,所以它的格式在 App 最早上架的版本就要定:

1GET /api/orders HTTP/1.1
2X-App-Platform: ios
3X-App-Version: 3.12.0
4X-App-Build: 3120045
5X-OS-Version: 17.5

四個 header 名稱是示意用的佔位,實際名稱各團隊自訂,定了之後就不能改,因為已經上架的版本會一直送出這個名稱。

  • 平台:iOS 與 Android 的版本各自獨立演進,同一個版本字串在兩個平台上可能對應不同的程式,判斷時一律帶著平台。
  • 作業系統版本:提高最低支援版本之前,要知道有多少使用者的裝置裝不了新版(見〈使用者拿不到新版的情況〉),這個數字只能從作業系統版本算出來。
  • 建置號在兩個平台的規則不同:商店顯示給使用者的是版本字串(3.12.0),每次上傳另有一個建置號。Android 的 versionCode 是每次發布都要變大的正整數;iOS 的建置號只要求在同一個版本字串底下不重複,可以帶句點,換一個版本字串時也可以重新計數(Apple 已歸檔的技術說明 TN2420)。
  • 比較新舊的方法:先把版本字串拆成整數逐段比較,版本字串相同時再比建置號,所以 iOS 建置號換版本後重新計數不影響結果;也可以由團隊自訂一個全域遞增的整數(像範例裡的 3120045),寫明那是團隊自己的約定。直接比字串會排錯,"1.10.0" 依字典序排在 "1.2.0" 前面。
  • 缺少版本的請求:加入版本回報之前上架的版本不會帶這個 header。後端先依 API key、client id 或平台 header 辨識出請求來自自家 App,再把其中缺少版本的請求視為最舊的版本(加入版本回報之前的舊版若連這些識別都沒帶,只能靠 User-Agent 這類診斷欄位推估,準確度另外標明);同一組 API 若也服務網頁前端、合作夥伴或內部服務,那些請求本來就不帶 App 版本,不能一起當成最舊的 App 版本處理。缺少版本的請求是加入版本回報之前所有版本的總和,彼此分不開,見〈使用者拿不到新版的情況〉。
  • 熱更新的版本:用熱更新推送過修正的 App,同一個建置號底下可能跑著不同的程式。有用熱更新的話,請求再帶上目前套用的更新識別碼(見〈熱更新:不經商店審核的程式更新〉)。

決定要讓哪些 App 版本退場時,要知道每個版本還有多少使用者,所以版本回報要準確、要常設統計。一般的 API 用量觀測容許版本欄位有誤差,自家 App 的版本不在此列;用量觀測怎麼分層見 11.12 API 消費者用量觀測 的〈消費者身分的識別符:歸屬層與診斷層〉。

版本 header 是客戶端自己填的值,任何人都能改寫它,所以它不是安全邊界。強制更新不受影響(改寫版本號的人擋到的是自己),但後端依版本選擇回應時,若舊版的處理路徑比較寬鬆(舊的驗證流程、舊的欄位權限),改寫成舊版本號就能選中它;安全修補要在伺服器端對所有版本生效,不依賴版本號。

最低支援版本與強制更新

最低支援版本是服務方保存的一個版本值,低於它的 App 不再被服務,使用者要更新才能繼續使用(概念見 Minimum Supported Version)。它是讓舊版 App 退場的手段:公開 API 的退場要等整合方自己改程式,服務方只能在退場日拒絕舊呼叫;舊版 App 的退場多了一個由服務方推動的手段,提高這個值就能把舊版的使用者帶到更新畫面。

讓舊版退場之前,先問新舊行為能不能並存。 退場是並存成本太高時的選項:能以新增端點或新增欄位的方式提供新行為,舊版就不必退場(紀律見 11.6 向後相容的變更紀律)。並存的成本太高時,順序是先提高最低支援版本讓舊版 App 退場,再照 11.5 版本策略與 deprecation 的分階段日期讓舊 API 退場。

強制更新機制由兩層組成:客戶端啟動檢查只在內建了檢查程式的版本上運作;伺服器端拒絕由後端執行,對每一個呼叫 API 的版本都有效,包括沒有客戶端啟動檢查的版本。

客戶端啟動檢查。 App 啟動時、以及從背景回到前景時,從遠端設定讀取最低支援版本,和自己的版本比較,低於就顯示更新畫面。Firebase 的 最低版本指南 用三個參數描述這個做法:minimum_version(最早仍支援的版本)、latest_version(目前最新的版本,選用)、update_url(商店頁面,選用)。兩個版本值分出兩種強度:

  • 硬阻擋:版本低於 minimum_version,畫面擋住,更新之前不能使用。用在舊版與後端已經不相容、或有安全問題的時候。
  • 軟提示:版本低於 latest_version 而高於 minimum_version,顯示可以關閉的提示。用在新功能與一般修正。

Android 另有 Google Play 提供的 In-app Updates,讓從 Google Play 安裝的 App 在 App 內直接下載與安裝新版(需要 Android 5.0 以上):flexible 流程在背景下載、使用者可以繼續使用,對應軟提示;immediate 流程是全螢幕、更新並重新啟動後才能繼續,對應硬阻擋。iOS 沒有讓 App 在內部下載安裝新版的系統介面,更新畫面開啟 App Store 的商店頁面(可以在 App 內呈現,也可以跳到 App Store)。

客戶端啟動檢查有兩種常見的錯誤。第一種是內建的預設值設得太高:App 讀不到遠端設定時(離線、第一次啟動)會用內建的預設值,預設值若等於當下的最低支援版本,日後提高門檻之前就上架的版本離線時會把自己擋住。所以內建的預設值設成不會擋下任何已上架版本的值,讀不到設定時就不擋。這個預設方向和〈功能提前內建與上線日遠端啟用〉裡 Feature Flag 的預設方向相反,是刻意的:讀不到設定時,最低支援版本選擇放行、功能選擇關閉,兩者各自把失敗導向傷害較小的一側(取捨見 Fail-Safe Default)。第二種是用字串比較版本,1.10.0 會排在 1.2.0 前面而誤擋新版;比較方法照〈版本回報〉拆成整數。

伺服器端拒絕。 後端依版本回報拒絕低於最低支援版本的請求,回一個專用的「需要更新」錯誤碼;認得這個錯誤碼的版本收到它就顯示更新畫面。錯誤碼搭配的 HTTP status 要選舊版的通用錯誤處理不會誤解的那一個:選 401,舊版的通用處理可能把使用者登出或進入 token 重新整理的迴圈;選 5xx,舊版會重試,流量跟著放大;426 Upgrade Required 的語意是協定升級,不是 App 更新。所以先列出舊版的通用錯誤處理對各個 4xx 會怎麼反應,選一個不重試、不登出、不清除 token 的 status,在 body 帶專用錯誤碼與商店網址,並套用在所有端點,包括登入與 token 重新整理。

不認得錯誤碼的版本照樣送出請求,請求被拒,畫面上只會出現那個版本當時寫好的錯誤處理。很多 App 的通用錯誤畫面會顯示回應 body 裡的訊息欄位,所以先查那些舊版的錯誤處理會顯示什麼:會顯示訊息的,把「請到商店更新」寫進那個欄位,使用者至少讀得到該做什麼;連訊息都不顯示的,只剩推播、email 這些 App 以外的管道。

兩層並存的理由是它們生效的時機不同:客戶端啟動檢查在任何 API 呼叫之前就生效,也能顯示可關閉的軟提示;伺服器端拒絕不受遠端設定讀取間隔的影響,門檻一提高,下一次呼叫就生效,遠端設定讀取失敗時也擋得住。兩者若是在某個版本才加入,加入之前上架的版本只能被拒絕,使用者不一定知道該更新——這就是強制更新的客戶端部分要從 App 最早上架的版本就內建的理由。

最低支援版本本身也要定幾件事:

  • 每個平台一個門檻,伺服器端另有封鎖清單:iOS 與 Android 的版本各自演進,同一個門檻不能套在兩邊。門檻是低於它就要求更新的版本值;封鎖清單列出個別要拒絕的版本(見〈新版 App 或新功能出問題時的回退手段〉),只由伺服器端讀取,客戶端啟動檢查只比對門檻。
  • 門檻由兩層讀同一份設定:客戶端啟動檢查與伺服器端拒絕若各存一份門檻,兩份會長期分歧而無從判斷哪一份是現行值;分歧的方向若是客戶端的門檻較高,使用者會被擋在更新畫面,而 API 還能用。讀取間隔內短暫的不一致(客戶端還沒讀到新門檻、伺服器端已經拒絕)是兩層設計內的正常狀態。做法是由自家的設定服務同時供應兩層,或讓後端透過遠端設定服務的伺服器端讀取介面讀同一個值。
  • 修改要有權限控管與檢查:這份設定改錯一次(例如多打一位數),所有使用者都會被硬阻擋。修改時檢查新門檻不得高於該平台已經全量上架的最新版本,以及要保留的舊系統使用者能裝的最後一版(見〈使用者拿不到新版的情況〉)。

使用者拿不到新版的情況

提高最低支援版本,等於要求低於它的使用者更新。這個要求只對「有一條路拿到新版」的使用者有意義;對拿不到新版的人,硬阻擋的結果是一個走不出去的更新畫面。所以提高之前,依下面幾種情況逐一分出拿不到新版的那幾群人,各自處置:

  • 新版還沒在商店全量上架:Google Play 的分階段推出還沒到 100% 時,比例外的使用者拿不到新版,In-app Updates 也查不到可用的更新。提高門檻的前提是新版在該平台已經全量可以取得,兩個平台分開判斷。
  • 裝置的作業系統裝不了新版:新版若提高了作業系統下限(iOS 的 minimum deployment target、Android 的 minSdkVersion),作業系統低於下限的裝置拿不到新版。這群人不能被硬阻擋;從版本回報的作業系統版本算出人數。要保留他們,門檻的上限就是他們能裝的最後一版,他們呼叫的舊 API 也要留到這群人自然退場為止;讓門檻越過這一版,等於決定放棄這群使用者。另一個做法是讓門檻依作業系統版本分段,舊系統裝置停在它們那一段,代價是客戶端要能解析分段的設定格式,而這個格式和版本 header 一樣,要從最早上架的版本就定,之後才加分段的話,舊客戶端只讀得懂單一門檻。
  • 舊版沒有任何機制:加入版本回報之前上架的版本,請求不帶版本,彼此分不開;拒絕「缺少版本的請求」會一次擋下所有這些版本,不能只挑其中一段。處置是先發一版帶版本回報、客戶端啟動檢查與錯誤碼處理的 App,等它擴散,再用「缺少版本的請求」佔自家 App 請求的比例估算這一群的流量佔比,換算成人數(見下一段)後決定要不要整群退場。這一群被拒時看到的是什麼,照〈最低支援版本與強制更新〉的做法先查舊版會顯示哪些欄位。

確認這幾群之後,提高最低支援版本的代價就剩下願意更新、也拿得到新版的那群使用者:人數從依版本統計的活躍使用者數估計;只有呼叫量時,要依登入的使用者 ID 或安裝識別碼換算成不重複的使用者或裝置,呼叫量本身不是人數;兩者都沒有時,以這一群呼叫量佔自家 App 呼叫量的比例,乘上分析工具統計的總活躍使用者數估計,並標明誤差。硬阻擋等於讓他們今天先更新才能使用。

商店審核下的上線順序

網頁部署之後所有使用者立刻拿到新版;App 的新版要先通過商店審核才能上架,審核時間不固定,被退件時要修改後重送。所以 App 功能的上線日先定,後端部署與送審從上線日往前排:後端在送審之前上線,功能在上線日之前的某個版本就送審並內建、保持關閉,到上線日再開啟。

審核人員操作時呼叫的是正式環境的 API。 送審的二進位檔就是之後上架的那一個,它呼叫的就是正式環境;Apple 的 App Review Guidelines 2.1 也要求送審時後端服務已經開啟,並會退回當機或有明顯技術問題的 App。所以新版 App 會呼叫的 API 要在送審前部署到正式環境。部署時舊版 App 仍在呼叫既有的 API,新 API 以新增的方式上線,既有 API 的行為不變(紀律見 11.6 向後相容的變更紀律)。只對特定國家或地區開放的 API,要為審核人員與商店的自動測試放行:審核人員不一定在那個地區,呼叫失敗時 App 呈現的就是 2.1 所說的明顯技術問題;審核與自動測試流量也會打到正式環境,產生的訂單與帳號要能辨識出來。

功能提前內建與上線日遠端啟用

綁定日期的功能(行銷活動、檔期)的上線日不能依賴審核進度。團隊把這類功能完整的程式與素材放進一個提前送審的版本,用遠端設定裡的 Feature Flag 保持關閉,到上線日在後台開啟。這種開關在 6.17 Feature Flag Governance 的角色分類裡是 release flag;上線日因此只依賴遠端設定,出問題時關掉它就退回,不必再送審。

提前內建有幾項代價:

  • 後端也要擋提前呼叫:遠端設定的值不是祕密,使用者或攻擊者修改過的 App 也能自己把開關打開;而後端已經在送審前上線。所以後端要依上線日或伺服器端的設定拒絕提前的呼叫,客戶端的 Feature Flag 只控制畫面、不是權限邊界。
  • 內容提前外流:安裝檔可以被拆開,還沒公告的活動名稱、日期與素材會被看到。
  • 安裝檔變大、留下死碼:素材越大越明顯;活動取消時,關閉的程式會跨好幾個版本留在 App 裡。
  • 素材放在哪裡決定了素材出錯時能怎麼辦:打包在安裝檔裡的素材,出錯時只能送出修正版(有些熱更新服務也不能更新素材檔);執行期間從伺服器下載的素材,伺服器端換掉檔案就能修正。〈熱更新:不經商店審核的程式更新〉引的兩家條款限制的對象是程式碼,從伺服器下載的內容若會改變 App 的功能,仍以條款原文判斷;送審說明一併交代哪些內容從伺服器下載。

提前內建的功能要讓審核人員看得到。App Review Guidelines 2.3.1(a) 規定 App 不得含有隱藏、休眠或未說明的功能;所有新功能都要在送審備註(Notes for Review)裡具體描述,籠統的描述會被退件,而且審核時要能操作。所以送審備註要寫明哪些功能由遠端設定控制、預定的上線日期,以及審核人員怎麼開啟它們;App 有登入時,2.1 要求附上測試帳號,可以提供一個已開啟這些功能的帳號。Google Play 的規範另查它的開發者政策。

遠端設定本身也是多版本共存的契約:一個 Feature Flag 的值會被不同版本的 App 讀到。Feature Flag 的內建預設值是關閉,讀不到遠端設定時功能維持關閉;開啟時依 App 版本設條件,只對實作已經完整、沒有已知問題的版本開啟——先前的版本可能帶著不完整的實作,flag 的名稱日後也可能被重用。這也決定了 release flag 什麼時候能移除:功能全面開放之後,遠端設定裡的定義若被刪掉,還在線上、仍讀這個 flag 的版本會改用內建預設值「關閉」,功能跟著消失。所以收斂為開啟的 release flag,移除分兩步:先發一版不再讀這個 flag、行為寫死為開啟的 App,再等最後一個讀它的版本低於最低支援版本,才刪掉遠端定義;在那之前只把值固定為開啟。收斂為關閉(功能撤掉)的 flag,內建預設值本來就是關閉,刪掉遠端定義不改變舊版的行為,不必等。

App Store 與 Google Play 的分階段發布:新版的擴散與新功能的曝光

分階段發布是把新版先給一部分使用者、再逐步放大比例的發布方式(發布策略的總覽見 Rollout Strategy)。兩個商店的做法如下:

項目App Store 的分階段發布Google Play 的分階段推出
比例怎麼放大預設分七天:1%、2%、5%、10%、20%、50%、100%;可以隨時改成全量發布由開發者設定比例,逐步調高
涵蓋哪些使用者開啟自動更新的使用者裡隨機抽出的一部分;任何人都能到商店手動下載新版隨機選出的使用者,比例外的使用者拿不到新版
發現問題時可以暫停,暫停時間總計最多 30 天,恢復後從暫停的那一天接著走可以停止推出,已更新的使用者停留在那個版本

「涵蓋哪些使用者」那一列決定了新功能的曝光要怎麼控制。App Store 的分階段發布擋不住主動更新的使用者,所以新功能接觸到多少人,要由 Feature Flag 的開啟比例控制,分階段發布只降低一個出問題的版本被自動推給所有人的風險。Google Play 的分階段推出本身就限制了拿到新版的人,再加上 Feature Flag,可以分別控制「誰拿到新版」與「誰看到新功能」;同一個限制也讓比例外的使用者拿不到新版,提高最低支援版本時要等推出到 100%(見〈使用者拿不到新版的情況〉)。資料來源是 Apple 的 分階段發布說明 與 Google Play 的 分階段推出說明;分階段發布時該看哪些指標、何時暫停,見 App 簽章、商店審核與分批發布流程 的〈流程定位〉。

熱更新:不經商店審核的程式更新

熱更新(over-the-air update)在本篇指把程式的修正直接推送到已安裝的行動 App,不經過商店審核;它和裝置韌體透過正規更新管道整包更新的 OTA 是兩回事。一般說的 hotfix 版本是重新建置、送審的修正版,在本篇的回退手段裡屬於「送出修正版」,和熱更新也是兩回事。熱更新能更新的範圍由框架決定,兩個常見的服務各自的範圍:

服務能更新不能更新
Expo EAS Update(React Native)JavaScript、樣式、圖片等非原生的部分原生程式碼與原生相依套件、App 權限、SDK 版本、任何需要新安裝檔的變更
Shorebird(Flutter)Dart 程式碼、產生的程式碼、不含原生程式碼的套件素材檔(圖片、字型)、原生程式碼、Flutter 引擎

不能更新的部分仍要送審。Expo 用 runtime version 標記一個更新相容於哪些原生版本,原生程式改了就要換一個 runtime version;這也是版本回報要多帶更新識別碼的理由,同一個建置號底下可能跑著不同的程式。

熱更新的使用範圍受兩家商店的條款界定:

  • Apple:App Review Guidelines 2.5.2 規定 App 不得下載、安裝或執行會新增或改變 App 功能的程式碼。另一方面,Apple Developer Program License Agreement 3.3.1(B) 允許下載直譯式程式碼,條件包括不改變 App 經 App Store 散布時的主要用途、不用來繞過審核或以不符合條款與審核指南的方式更新 App。以直譯方式執行的熱更新,依據的就是這條許可。
  • Google Play:裝置與網路濫用政策 規定 App 不得用 Google Play 更新機制以外的方式修改、取代或更新自己,也不得從 Google Play 以外下載可執行程式碼(dex、JAR、.so);在虛擬機或直譯器裡執行、只間接存取 Android API 的程式碼不受這條限制。某個熱更新服務落不落在這個例外,看那個熱更新服務的實作與它自己的文件。

Expo 與 Shorebird 的文件也都寫明,更新的內容要符合商店規範由開發者自己負責。Apple 的界線是不新增、不改變功能與主要用途;Google Play 的界線是程式碼的執行方式(直譯器裡的程式碼例外)。取兩家的交集,保守的用法是用熱更新修正已經通過審核的功能,新功能走送審、再用 Feature Flag 在上線日開啟。

熱更新推送出去的修正也可能出錯,推送時同樣要分批,並保留退回上一個更新的方式;各服務的做法查它自己的文件。

新版 App 或新功能出問題時的回退手段

已經安裝在使用者裝置上的 App 無法被撤回,回退要靠事先準備好的手段。下表列出每個手段對「已經裝上出問題版本的使用者」多久生效,以及事先要準備什麼。表的順序不代表快慢:同一個手段的生效時間取決於事前準備,沒有整合即時更新的遠端設定,可能比部署一次後端還慢。

手段對已裝上出問題版本的使用者多久生效事先要準備的
遠端設定關閉 Feature FlagApp 下一次讀到並套用新設定時;Firebase Remote Config 預設 12 小時才重新抓一次,整合即時更新後可到分鐘級那個功能有 Feature Flag;App 的設定讀取間隔夠短或已整合即時更新;問題發生在套用設定之後
伺服器端依版本調整部署一次後端的時間請求帶版本回報;App 對回應裡多出的欄位、未知的列舉值與空值有容錯
回滾後端部署一次後端的時間新版 App 對新 API 不存在或回錯誤有降級處理(例如呼叫點由 Feature Flag 守著),或回滾只退回出問題的部分、保留新版用到的端點;回滾也不能拿掉舊版在用的部分
熱更新推送修正App 下載到更新之後的下一次啟動(Expo 也可以在重新載入時套用)已整合熱更新服務;問題落在可熱更新的範圍內
只拒絕出問題的那一個版本下一次呼叫 API 時伺服器端有封鎖版本清單;出問題的版本認得「需要更新」錯誤碼;修正版已經全量上架;問題發生在第一次呼叫 API 之後
送出修正版商店審核加上使用者更新的時間,只涵蓋會更新的使用者無;修正嚴重錯誤時可以向 Apple 申請加急審核
提高最低支援版本修正版全量上架之後:客戶端啟動檢查在下一次讀到新設定時生效,伺服器端拒絕在下一次呼叫 API 時生效出問題的版本有客戶端啟動檢查,或認得「需要更新」錯誤碼

遠端設定的即時更新是另外整合的監聽機制,沒有整合就停在預設的讀取間隔;Firebase 的即時更新收到的是「有新值」的通知,App 仍要套用(activate)新值才生效。做法見它的 Android 入門文件,其他遠端設定服務的讀取間隔查它自己的文件。關閉 Feature Flag 對一種情況無效:當機發生在 App 讀到並套用設定之前(例如啟動路徑上的功能),使用者會一直卡在當機裡,這類問題要靠伺服器端調整、熱更新或修正版。

伺服器端依版本調整能處理的是由回應觸發的問題:對出問題的版本改回它能正確處理的回應形狀(包括新的列舉值或超長字串讓舊版解析時當機的情況)、在回應裡關掉某個功能入口、或把它導向相容的舊行為。不是由回應觸發的客戶端錯誤(畫面邏輯、本機資料處理)它處理不了,要靠 Feature Flag、熱更新或修正版。依版本切換回應的做法,在 API 層叫做轉換層,它的測試矩陣與出錯時的影響範圍見 版本策略流派之爭:URI 版本、header 版本、日期 pin 與不做版本的前提與成本。

「只拒絕出問題的那一個版本」與「提高最低支援版本」的差別在影響範圍:新版 3.12.0 壞掉、修正版是 3.12.1 時,把最低支援版本提高到 3.12.1,會連帶擋下原本正常的 3.11.x,以及裝置裝不了 3.12.1 的使用者;只把 3.12.0 加進伺服器端的封鎖清單,影響的只有裝了壞版本的人。兩者都要等修正版全量上架。

Google Play 不能把已發布的版本退回舊版;要回到舊的程式,是用更高的 versionCode 重新發布舊程式碼,等於一次不改程式的修正版。資料外洩這類事故還要撤銷使用者的登入狀態,那屬於事故處理,不在這張表裡。

停止分階段發布不在表裡,因為它對已經裝上出問題版本的使用者不生效:它的作用是讓還沒拿到新版的人不再拿到,App Store 的分階段發布還擋不住主動到商店更新的使用者。發現問題時通常先停止分階段發布、再用表裡的手段處理已經裝上的人。

一個功能在這張表上能用的手段越少,出問題時的處理就越慢;只剩送出修正版可用的功能,出問題時要等一輪審核,這一類功能的上線前測試要相應加強。

條款編號、分階段發布的比例與天數、各服務能更新的範圍,依 2026-10 查核的版本;平台會改版,以文中連結的頁面為準。

延伸閱讀:相容紀律、退場工具、依版本的用量觀測、送審流程與開關治理