MMP 如何自動對應 SKAdNetwork 架構?流動應用衡量夥伴 (MMP) 或歸因後端透過將應用程式內事件與收益層級轉換為集中式控制台上的動態、版本化 JSON 設定架構,來自動化 SKAdNetwork 架構對應。流動應用 SDK 會在啟動時擷取此設定,並在執行階段於本地評估轉換規則,允許支援的轉換規則變更生效,而無需重新發布應用程式二進位檔。
SKAdNetwork 轉換數值架構是一個供應商或應用程式層級的規則集,將應用程式內的使用者行為(例如收益交易、新手引導里程碑或功能互動)對應到 Apple 的 6 位元精細數值(0 到 63)以及 3 個層級的粗略數值(
low、medium、high)。動態對應架構將版本化的設定檔從雲端後端分發到用戶端 SDK,從而消除了在編譯好的 iOS 應用程式二進位檔中寫死轉換邏輯的需求。
| 名詞 | 定義 |
|---|---|
| SKAdNetwork | Apple 用於保護隱私的廣告活動歸因平台級框架。 |
| 轉換數值架構 | 由供應商或應用程式定義的設定,將應用程式內事件里程碑對應到精細與粗略數值。 |
| 動態架構對應 | 透過 SDK 自動分發與執行階段評估轉換規則。 |
| 視窗鎖定 (Window Locking) | 提早完成有效轉換視窗的 API 參數 (lockWindow: true)。 |
自動化 SKAdNetwork 轉換數值對應的架構
劃分 Apple 平台層與供應商架構層
為了建構強健的轉換數值引擎,工程團隊必須將 Apple 的原生框架規則與供應商層級的架構抽象化區分開來:
- Apple 平台層:管理核心作業系統原語,包含三個連續的轉換視窗(首次啟動後的第 0–2 天、第 3–7 天、第 8–35 天)、6 位元精細數值 (0–63)、粗略數值(
low、medium、high)、回傳資料層級以及SKAdNetwork.updatePostbackConversionValueAPI。 - 供應商架構層:包含應用程式定義的商業規則,例如收益區間分級、新手引導漏斗進程、位元旗標配置、遠端 JSON 同步以及用戶端規則評估。
┌────────────────────────────────────────┐
│ Vendor Schema Layer │
│ [MMP / Analytics Console] ──► [Publishes Versioned JSON Configuration] │
│ │ │
│ [Client Mobile SDK] ──► [Evaluates In-App Events Locally in Memory] │
└──────────────────────────────────────┬─┘
│ (Calculates Fine, Coarse, & Lock)
▼
┌────────────────────────────────────────┐
│ Apple Platform Layer │
│ [StoreKit Framework] ──► [SKAdNetwork.updatePostbackConversionValue] │
│ [Operating System] ──► [Manages Conversion Windows & Timers] │
│ [System] ──► [Prepares and Sends Signed Postback] │
└────────────────────────────────────────┘

寫死轉換邏輯的陷阱
直接在 iOS 應用程式目標內寫死轉換邏輯會造成顯著的營運限制:
- App Store 審查依賴:對收益門檻、事件權重或視窗鎖定觸發條件的任何修改,都需要完整的二進位檔發布週期。
- 版本碎片化:生產環境中多個歷史應用程式版本會傳輸互相衝突的轉換語意,從而破壞下游報表模型。
- 最佳化缺乏彈性:成長團隊無法為了回應即時行銷表現,而在專注於互動與專注於變現的活動之間調整轉換策略。
動態設定交付管線
自動化對應架構透過多階段管線將轉換邏輯與編譯好的二進位檔解耦:
- 控制台設定:行銷人員與分析師可在集中式儀表板上設定事件權重、貨幣層級與視窗鎖定規則。
- 架構版本控制與鎖定:後端會發布版本化的 JSON 設定負載。為了防止在使用者的 35 天轉換生命週期中發生語意漂移,穩健的供應商實作會鎖定在初始轉換視窗期間建立的有效架構設定,確保即使在應用程式重新啟動時,精確的對應規則仍然可用。
- 用戶端擷取與快取:流動應用 SDK 會在應用程式初始化時下載有效架構,並將設定負載與版本後設資料快取在本地永久儲存空間中。
- 本地規則評估:當應用程式內事件發生時,SDK 會針對快取的規則集在本地進行評估,而不會在事件執行路徑中新增同步的遠端設定請求。

參見:SKAdNetwork ──> 流動應用歸因架構
在 SKAN 4.0 視窗中設計動態轉換數值架構
多視窗架構分割
SKAdNetwork 4.0 將轉換衡量架構分割為以應用程式首次啟動為錨點的三個連續視窗:
- 視窗 1 (第 0–2 天):首次啟動後的最初 48 小時。
- 視窗 2 (第 3–7 天):首次啟動後的第 48 小時至第 168 小時。
- 視窗 3 (第 8–35 天):首次啟動後的第 168 小時至第 840 小時。
動態架構引擎將這些視窗的規則進行分割,並根據自初始應用程式啟動以來經過的時間來執行適當的數值計算。
視窗 1 (第 0–2 天):建構精細與粗略數值
視窗 1 是唯一有資格揭露精細轉換數值的轉換視窗。視窗 1 的設定定義了兩個並行的對應:
- 精細對應 (0–63):擷取初始變現層級、新手引導里程碑或綜合互動分數的高解析度規則。
- 粗略對應(
low、medium、high):當指定的回傳資料層級不允許進行精細報告時所揭露的較低粒度後備狀態。
視窗 2 (第 3–7 天) 與視窗 3 (第 8–35 天):粗略粒度生命週期追蹤
第二個與第三個回傳資料不會暴露精細的轉換數值;對於符合資格的資料層級,它們僅會揭露粗略數值。
視窗 2 與視窗 3 的架構著重於長期留存與變現里程碑:
- 視窗 2 粗略對應:評估中段漏斗留存(例如:
low= 第 3–7 天活躍;medium= 完成 3 個工作階段;high= 重複購買或試用轉換)。 - 視窗 3 粗略對應:評估長尾留存與訂閱續約(例如:
low= 第 8–35 天留存;medium= 達到級別里程碑;high= 活躍付費訂閱者)。
設定轉換架構的開發人員可以查閱 SKAN 轉換對應說明文件以取得多視窗規則結構的技術指南。

供應商定義的編碼模型:收益區間分級、漏斗與位元運算邏輯
這些編碼模型代表供應商與應用程式層級的設計模式,而非 Apple 規定的架構類型。
基於收益的架構
收益架構將可用的精細數值分配到累積購買金額中:
- 線性區間分級:將收益範圍劃分為相等的區間(例如:高達 $96.00,每 $1.50 增量分為 64 個區間)。非常適合交易金額可預測的應用程式。
- 對數區間分級:將細粒度區間分配給低成本購買,同時為高價值交易擴展區間範圍(例如:數值 1–20 涵蓋 $0.99–$19.99;數值 21–50 涵蓋 $20.00–$100.00;數值 51–63 涵蓋 $100.00–$1000.00+)。
- 基於百分位數的區間分級:根據經驗變現曲線,將歷史使用者購買分佈對應到同級群組區段。
漏斗進程架構與數值方向性
在 SKAdNetwork 3 及更早版本中,Apple 要求轉換數值必須單調遞增。在 SKAdNetwork 4.0 中,Apple 移除了此限制,允許視窗 1 中的轉換數值在後續的 API 呼叫中增加或減少。
然而,許多歸因架構刻意將單調遞進強制執行為供應商層級的設計慣例,以確保較高的數值代表漸進式更強的商業成果:
- 數值
0:已安裝並開啟應用程式。 - 數值
10:已完成註冊。 - 數值
20:已完成新手引導教學。 - 數值
30:已新增付款方式。 - 數值
45:已將商品加入購物車。 - 數值
63:已完成初始結帳。
位元分類架構
位元架構將 6 位元整數 (
| 位元位置 | 二進位權重 | 對應的應用程式內行為 |
|---|---|---|
| 位元 0 ( |
1 (0b000001) |
使用者已完成註冊 |
| 位元 1 ( |
2 (0b000010) |
使用者已啟用推播通知 |
| 位元 2 ( |
4 (0b000100) |
使用者已將商品加入願望清單 |
| 位元 3 ( |
8 (0b001000) |
使用者已分享推薦連結 |
| 位元 4 ( |
16 (0b010000) |
使用者已完成應用程式內購買 |
| 位元 5 ( |
32 (0b100000) |
使用者已訂閱付費試用 |
以下版本化的 JSON 負載說明了多視窗動態設定文件:
{
"schema_version": "4.0.1",
"app_id": "1234567890",
"currency": "USD",
"windows": {
"window_1": {
"mode": "hybrid_revenue_and_funnel",
"fine_mapping": [
{ "event": "app_open", "min_revenue_cents": 0, "fine_value": 0, "lock": false },
{ "event": "registration_complete", "min_revenue_cents": 0, "fine_value": 10, "lock": false },
{ "event": "tutorial_complete", "min_revenue_cents": 0, "fine_value": 20, "lock": false },
{ "event": "purchase", "min_revenue_cents": 99, "fine_value": 30, "lock": false },
{ "event": "purchase", "min_revenue_cents": 999, "fine_value": 45, "lock": false },
{ "event": "purchase", "min_revenue_cents": 4999, "fine_value": 63, "lock": true }
],
"coarse_mapping": {
"low": { "events": ["app_open", "registration_complete"] },
"medium": { "events": ["tutorial_complete"] },
"high": { "events": ["purchase"] }
}
},
"window_2": {
"mode": "coarse_retention_and_monetization",
"coarse_mapping": {
"low": { "events": ["app_open"], "lock": false },
"medium": { "events": ["session_milestone"], "lock": false },
"high": { "events": ["repeat_purchase"], "lock": true }
}
},
"window_3": {
"mode": "coarse_long_tail_ltv",
"coarse_mapping": {
"low": { "events": ["app_open"], "lock": false },
"medium": { "events": ["level_milestone"], "lock": false },
"high": { "events": ["subscription_active"], "lock": true }
}
}
}
}
動態 SDK 設定:在執行階段擷取並評估遠端設定
用戶端規則評估機制
歸因 SDK 會在應用程式執行階段於本地評估轉換規則:
- 事件路徑上無同步遠端設定擷取:應用程式內動作會針對有效規則集觸發本地記憶體內評估,並立即叫用 StoreKit API,而不會阻塞應用程式執行。
- 資料最小化:對於此處顯示的 SKAdNetwork 轉換更新路徑,原始事件輸入可以在本地進行評估,且僅需將產生的轉換數值傳遞給 StoreKit。這本身並不描述或限制 SDK 所實作的其他分析資料流。
處理離線狀態與本地持久化
當應用程式在離線或網路狀況不佳的情況下啟動時:
- SDK 會在本地持久化中獨立初始化首次啟動時間戳記錨點。
- SDK 會從持久化本地儲存空間載入已鎖定的設定架構,並驗證快取的負載是否與鎖定的架構版本相符。
- 如果在離線時發生應用程式內事件,SDK 會針對快取的規則集進行評估,並立即呼叫 StoreKit 更新 API。
- 回傳準備與交付仍然由系統管理且以非同步方式進行;應用程式不需要自行分派回傳。
以下 Swift 實作示範了一個多視窗架構評估引擎,它能計算精細與粗略數值、管理視窗特定的鎖定狀態、持久化鎖定的架構設定,並且僅在成功執行 StoreKit 後才提交狀態更新:
import Foundation
import StoreKit
// MARK: - Schema Configuration Models
struct SKANSchemaConfig: Codable {
let schemaVersion: String
let appId: String
let currency: String
let windows: SchemaWindows
enum CodingKeys: String, CodingKey {
case schemaVersion = "schema_version"
case appId = "app_id"
case currency, windows
}
}
struct SchemaWindows: Codable {
let window1: Window1Config
let window2: WindowCoarseConfig
let window3: WindowCoarseConfig
enum CodingKeys: String, CodingKey {
case window1 = "window_1"
case window2 = "window_2"
case window3 = "window_3"
}
}
struct Window1Config: Codable {
let mode: String
let fineMapping: [FineRule]
let coarseMapping: CoarseRuleGroup
enum CodingKeys: String, CodingKey {
case mode
case fineMapping = "fine_mapping"
case coarseMapping = "coarse_mapping"
}
}
struct FineRule: Codable {
let event: String
let minRevenueCents: Int
let fineValue: Int
let lock: Bool
enum CodingKeys: String, CodingKey {
case event
case minRevenueCents = "min_revenue_cents"
case fineValue = "fine_value"
case lock
}
}
struct WindowCoarseConfig: Codable {
let mode: String
let coarseMapping: [String: CoarseRule]
enum CodingKeys: String, CodingKey {
case mode
case coarseMapping = "coarse_mapping"
}
}
struct CoarseRuleGroup: Codable {
let low: CoarseRule
let medium: CoarseRule
let high: CoarseRule
}
struct CoarseRule: Codable {
let events: [String]?
let lock: Bool?
}
// MARK: - Multi-Window SKAN 4.0 Schema Engine
final class SKANSchemaEngine {
static let shared = SKANSchemaEngine()
private init() {}
private var activeSchema: SKANSchemaConfig?
private var firstLaunchDate: Date?
private var lockedWindows = Set<Int>()
private var lastRecordedFineValue: Int = 0
private var pinnedSchemaVersion: String?
/// Initializes the first-launch timestamp anchor independently of remote configuration fetches
func initializeLifecycleAnchor() {
let defaults = UserDefaults.standard
if let storedLaunch = defaults.object(forKey: "skan_first_launch_date") as? Date {
self.firstLaunchDate = storedLaunch
} else {
let now = Date()
self.firstLaunchDate = now
defaults.set(now, forKey: "skan_first_launch_date")
}
let lockedArray = defaults.array(forKey: "skan_locked_windows") as? [Int] ?? []
self.lockedWindows = Set(lockedArray)
self.lastRecordedFineValue = defaults.integer(forKey: "skan_last_fine_value")
self.pinnedSchemaVersion = defaults.string(forKey: "skan_pinned_schema_version")
// Restore previously cached schema payload if it matches the pinned version
if let pinnedVersion = self.pinnedSchemaVersion,
let cachedData = defaults.data(forKey: "skan_cached_schema_payload"),
let cachedSchema = try? JSONDecoder().decode(SKANSchemaConfig.self, from: cachedData),
cachedSchema.schemaVersion == pinnedVersion {
self.activeSchema = cachedSchema
}
}
/// Loads active schema, persisting the pinned payload to maintain consistency across the 35-day lifecycle
func configure(schema: SKANSchemaConfig) {
let defaults = UserDefaults.standard
if let pinned = pinnedSchemaVersion {
// If already pinned, accept only schemas matching the pinned version
if pinned == schema.schemaVersion {
self.activeSchema = schema
if let data = try? JSONEncoder().encode(schema) {
defaults.set(data, forKey: "skan_cached_schema_payload")
}
}
} else {
// Pin the initial schema version for this lifecycle
self.activeSchema = schema
self.pinnedSchemaVersion = schema.schemaVersion
defaults.set(schema.schemaVersion, forKey: "skan_pinned_schema_version")
if let data = try? JSONEncoder().encode(schema) {
defaults.set(data, forKey: "skan_cached_schema_payload")
}
}
}
/// Determines the active conversion window based on elapsed time from first launch
private var currentWindowIndex: Int {
guard let firstLaunch = firstLaunchDate else { return 0 }
let elapsedHours = Date().timeIntervalSince(firstLaunch) / 3600.0
switch elapsedHours {
case 0.0..<48.0:
return 1
case 48.0..<168.0:
return 2
case 168.0...840.0:
return 3
default:
return 0 // Window closed (>35 days)
}
}
/// Evaluates an in-app event against the active schema for the current window
func trackEvent(name: String, revenueCents: Int = 0) {
guard #available(iOS 16.1, *),
let schema = activeSchema else { return }
let window = currentWindowIndex
guard window >= 1 && window <= 3, !lockedWindows.contains(window) else { return }
var targetFineValue: Int?
var targetCoarseValue: SKAdNetwork.CoarseConversionValue?
var shouldLock = false
var matchedRule = false
if window == 1 {
// Window 1: Evaluate fine-grained rules with highest-threshold precedence
let matchingFineRules = schema.windows.window1.fineMapping
.filter { $0.event == name && revenueCents >= $0.minRevenueCents }
.sorted { $0.minRevenueCents < $1.minRevenueCents }
if let highestRule = matchingFineRules.last {
targetFineValue = highestRule.fineValue
if highestRule.lock { shouldLock = true }
matchedRule = true
}
// Window 1: Evaluate coarse-grained rules explicitly
if schema.windows.window1.coarseMapping.high.events?.contains(name) == true {
targetCoarseValue = .high
matchedRule = true
} else if schema.windows.window1.coarseMapping.medium.events?.contains(name) == true {
targetCoarseValue = .medium
matchedRule = true
} else if schema.windows.window1.coarseMapping.low.events?.contains(name) == true {
targetCoarseValue = .low
matchedRule = true
}
} else {
// Windows 2 & 3: Evaluate coarse rules only
let coarseConfig = (window == 2) ? schema.windows.window2 : schema.windows.window3
if let highRule = coarseConfig.coarseMapping["high"], highRule.events?.contains(name) == true {
targetCoarseValue = .high
if highRule.lock == true { shouldLock = true }
matchedRule = true
} else if let medRule = coarseConfig.coarseMapping["medium"], medRule.events?.contains(name) == true {
targetCoarseValue = .medium
if medRule.lock == true { shouldLock = true }
matchedRule = true
} else if let lowRule = coarseConfig.coarseMapping["low"], lowRule.events?.contains(name) == true {
targetCoarseValue = .low
if lowRule.lock == true { shouldLock = true }
matchedRule = true
}
}
// If no explicit rule matched for this event, do not trigger a StoreKit update
guard matchedRule else { return }
let fineToSubmit = targetFineValue ?? (window == 1 ? lastRecordedFineValue : 0)
let clampedFine = max(0, min(63, fineToSubmit))
let coarseToSubmit = targetCoarseValue ?? .low
// Dispatch StoreKit conversion update
// Note: StoreKit ignores the fineValue parameter after Window 1
SKAdNetwork.updatePostbackConversionValue(
clampedFine,
coarseValue: coarseToSubmit,
lockWindow: shouldLock
) { [weak self] error in
guard let self = self else { return }
if let error = error {
print("StoreKit conversion update failed: \(error.localizedDescription)")
} else {
// Commit local state only after StoreKit successfully accepts the update
DispatchQueue.main.async {
if window == 1 {
self.lastRecordedFineValue = clampedFine
UserDefaults.standard.set(clampedFine, forKey: "skan_last_fine_value")
}
if shouldLock {
self.lockedWindows.insert(window)
UserDefaults.standard.set(Array(self.lockedWindows), forKey: "skan_locked_windows")
}
print("SKAN 4.0 update succeeded: Window=\(window), Fine=\(clampedFine), Coarse=\(coarseToSubmit.rawValue), Locked=\(shouldLock)")
}
}
}
}
}

自動化 lockWindow 執行以加速回傳準備
lockWindow 參數的營運機制
當應用程式呼叫帶有 lockWindow: true 的 updatePostbackConversionValue(_:coarseValue:lockWindow:) 時,該更新會成為有效視窗的最後一個轉換數值更新。作業系統會立即準備回傳,並忽略該視窗剩餘時間內的額外轉換數值更新。
Default Window 1 (No Lock):
[First Launch] ─────────────── 48 Hours Open ───────────────► [Closes] ──► Delay (24-48h) ──► Postback 1
Locked Window 1 (Purchase at Hour 6):
[First Launch] ── 6h (Lock: true) ──► [Conversion Locked / Postback Prepared] ──► Delay (24-48h) ──► Postback 1 Sent Sooner
自動化視窗鎖定的策略取捨
- 加速回傳分派:提早完成轉換可讓 Apple 隨機化的回傳延遲立即開始,進而更快將轉換資料交付給廣告網路。
- 視窗獨立性:鎖定目前視窗並不會將下一個視窗的開始時間往前移;無論視窗 1 是何時被鎖定的,視窗 2 仍然會在第 3 天開始。
- 觀察截斷:一旦視窗被鎖定,系統就會忽略該轉換視窗剩餘時間內的後續轉換數值更新呼叫。應用程式內事件可能會繼續發生,但它們無法再變更該視窗的 SKAdNetwork 轉換狀態。
協調 SKAN 架構與 AdAttributionKit
Apple 不斷演進的歸因堆疊
Apple 現在建議針對 App Store 和替代應用程式市場上的應用程式廣告活動使用 AdAttributionKit。SKAdNetwork 對於現有的整合與互通性仍然具有關聯性,因此動態對應引擎應將其商業規則層與框架特定的轉換 API 分開:
- 共用數值維度:兩個框架都評估 6 位元精細數值(0 到 63)和 3 個層級的粗略數值(
low、medium、high)。 - 不同的 API 層級:SKAdNetwork 使用
SKAdNetwork.updatePostbackConversionValue,而 AdAttributionKit 使用Postback.updateConversionValue。 - 橋接行為:如果整合支援這兩個框架,Apple 建議呼叫這兩個框架的轉換更新 API,同時考量有文件記錄的 SKAdNetwork 至 AdAttributionKit 橋接行為。
比較決策矩陣:寫死用戶端邏輯與動態設定
| 評估維度 | 寫死用戶端邏輯 | 動態架構設定 |
|---|---|---|
| 架構修改速度 | 需要透過 App Store 審查(數天到數週) | 針對支援的規則變更進行遠端更新,無需發布新的二進位檔 |
| 測試與迭代靈活性 | 高阻力 / 高工程負擔 | 透過版本與同級群組隔離規則進行受控的架構實驗 |
| 多視窗協調 | Swift 中的複雜手動狀態機器 | 自動化生命週期感知引擎 |
| 自動化視窗鎖定 | 固定、缺乏彈性的規則觸發條件 | 動態、事件觸發的鎖定規則 |
| 跨框架一致性 | 跨框架的碎片化程式碼 | 統一的雲端設定矩陣 |
常見問題 (FAQ)
如果使用者觸發了對應到不同轉換數值的多個事件,會發生什麼事?
如果應用程式處於離線狀態,自動化架構是否可以更新轉換數值?
自動化架構如何處理全球使用者的貨幣換算?
總結與決策架構
自動化 SKAdNetwork 轉換數值對應將成長實驗與流動應用二進位檔發布週期解耦。透過從集中式歸因儀表板分發動態架構並在 SDK 內本地評估它們,工程團隊可以微調收益區間、最佳化漏斗里程碑並設定自動化視窗鎖定,讓支援的轉換規則變更得以生效,而無需將應用程式二進位檔重新提交至 App Store Connect。
應用程式層級的深層連結路由可以作為獨立的衡量與引導層,與 Apple 保護隱私的歸因框架並行運作。類似 OpoInstall 的平台提供第一方情境路由與延遲深層連結的基礎設施,讓團隊能夠在網頁轉應用程式的轉換漏斗中保留使用者意圖。
若要進一步了解如何設定相容隱私的歸因與深層連結管線,請查閱 OpoInstall 說明文件。
相關資料
-
概念:轉換數值架構、動態架構對應、收益區間分級、視窗鎖定、單調性
-
技術:Apple SKAdNetwork、Apple AdAttributionKit、StoreKit 框架、OpoInstall 流動應用 SDK
-
標準:IETF RFC 8259 JSON 規範
-
API:StoreKit
updatePostbackConversionValueAPI、AdAttributionKitPostback.updateConversionValueAPI
官方說明文件
Share this article



