FCM 註冊管理最佳做法

如果您使用 FCM API 以程式輔助方式建構傳送要求,可能會發現隨著時間推移,您會將訊息傳送至註冊資料過時的不活躍裝置,因而浪費資源。這種情況可能會影響 Firebase 控制台中回報的訊息傳送資料,或匯出至 BigQuery 的資料,導致傳送率大幅下降 (但實際上並非如此)。本指南將說明一些措施,協助您確保訊息指定目標有效,且傳送報表正確。

過時和過期的註冊

如果裝置超過一個月未連上 FCM,就會產生過時的註冊記錄。隨著時間過去,裝置再次連線至 FCM 的可能性會越來越低。這些過時註冊的訊息傳送和主題展開不太可能傳送。

註冊資訊過時的原因有很多,舉例來說,與註冊程序相關聯的裝置可能遺失、損壞或存放起來,導致您忘記。

在 Android 裝置上,如果註冊資訊閒置 270 天,FCM 會將其視為過期並進行垃圾收集。註冊過期後,FCM 會將其標示為無效,並拒絕傳送至該註冊的訊息。請注意,Firebase 安裝 ID (FID) 本身是由 Firebase 安裝服務 (FIS) 管理,而非 FCM。在極少數情況下,如果裝置再次連線,且應用程式在註冊資料經過垃圾收集後開啟,用戶端應用程式會使用從 FIS 擷取的 FID,再次向 FCM 註冊。請注意,FID 可能會變更;如要瞭解 FID 何時會重新發放,請參閱「管理 Firebase 安裝作業」。

在 iOS 等其他平台上,FCM 則依賴基礎推播服務 (例如 APNs),這類服務不會在閒置 270 天後失效。建議您主動維護註冊權杖的新鮮度,並移除過時的註冊權杖。

基本最佳做法

在任何使用 FCM API 以程式輔助方式建構傳送要求的應用程式中,您都應遵循一些基本做法。主要最佳做法如下:

  • 從 FCM 擷取 Firebase 安裝 ID (FID) ,並儲存在應用程式伺服器上。伺服器的重要角色是追蹤每個用戶端註冊的 FID,並保留有效的 FID 最新清單。強烈建議您在資料庫中實作註冊時間戳記,並在每次上傳註冊資料時更新。
  • 維持註冊資訊的時效性,並移除過時的註冊資訊。除了移除 FCM 不再視為有效的註冊項目,您可能還想監控其他註冊項目是否過時,並主動移除這些項目。本指南將說明幾種做法。

擷取及儲存 Firebase 安裝 ID

應用程式初次啟動時,FCM SDK 會向 FCM 註冊應用程式執行個體,並傳回 Firebase 安裝 ID (FID)。您必須在 API 的指定傳送要求中加入這個 ID,或用於主題訂閱。

強烈建議您在每次上傳 FID 時,將 FID 和時間戳記一併儲存至應用程式伺服器。更新每個上傳要求的時間戳記後,伺服器就能瞭解應用程式執行個體上次開啟的時間,以及與FCM後端成功同步的時間。

視自動初始化功能是否啟用 (包括不支援),您應按照下列方式處理註冊和更新:

  • (建議) 啟用自動初始化功能:SDK 會自動保持註冊狀態最新,並監控變更。在應用程式啟動期間,系統會定期叫用 onRegistered() 回呼,並在 FID 變更時叫用。只要實作這個回呼,即可將 FID 上傳至伺服器,並儲存目前的時間戳記。
  • 停用自動初始化:系統不會在啟動時自動叫用 onRegistered() 回呼。如要追蹤註冊並保持最新狀態,請在應用程式啟動時呼叫 register(),例如在 Android 的主要活動 onCreate() 中。呼叫成功後,系統會使用 FID 觸發 FCM 註冊程序,並將 FID 傳送至 onRegistered() 回呼,讓應用程式上傳 FID 並更新伺服器上的時間戳記。

範例:將 FID 和時間戳記儲存在 Cloud Firestore

舉例來說,您可以使用 Cloud Firestore 將 FID 儲存在名為 fcmRegistrations 的集合中。集合中的每個文件 ID 都對應一個使用者 ID,而文件會儲存目前的 FID 和上次更新的時間戳記。使用 set 函式,如以下 Kotlin 範例所示:

private fun sendRegistrationToServer(installationId: String?) {
    // If you're running your own server, call API to send registration details and today's date for the user

    // Example shown uses Firestore
    // Add FID and timestamp to Firestore for this user
    val deviceFid = hashMapOf(
        "installationId" to installationId,
        "timestamp" to FieldValue.serverTimestamp(),
    )
    // Get user ID from Firebase Auth or your own server
    Firebase.firestore.collection("fcmRegistrations").document("myuserid")
        .set(deviceFid)
}

每當 Firebase 安裝 ID 成功註冊或更新時,系統就會叫用 onRegistered() 回呼。您應實作這項回呼,上傳 FID 並更新時間戳記:

override fun onRegistered(installationId: String) {
    Log.d(TAG, "Registered installation ID: $installationId")

    // Send the Firebase Installation ID (FID) to your app server. Your app
    // server should save the FID and update the timestamp upon receipt.
    sendRegistrationToServer(installationId)
}

如果停用自動初始化功能,請在應用程式啟動時 (例如在 onCreate() 中) 呼叫 register(),透過 onRegistered() 觸發註冊流程和 FID 傳送:

// Trigger manual registration if auto-initialization is turned off.
FirebaseMessaging.getInstance().register()
    .addOnCompleteListener(this) { task ->
        if (task.isSuccessful) {
            // The registration callback onRegistered() will be invoked with the current FID.
        } else {
            Log.w(TAG, "Failed to register with Firebase Cloud Messaging", task.exception)
        }
    }

維持註冊資訊的時效性,並移除過時的註冊資訊

判斷註冊資訊是否為最新狀態並不總是那麼簡單。為涵蓋所有情況,您應採用門檻,判斷註冊資訊何時過時。根據預設,如果應用程式執行個體一個月未連線,FCM 就會將註冊視為過時。如果註冊時間超過一個月,裝置可能處於閒置狀態;否則,裝置應該會重新註冊。

視用途而定,一個月可能太短或太長,因此請自行決定適合的條件。

偵測 FCM 後端傳回的無效回應

請務必偵測 FCM 的無效回應,並從系統中刪除已知無效或已過期的任何註冊資料。使用 HTTP v1 API 時,如果傳送要求指定無效或過期的註冊,可能會出現下列錯誤訊息:

  • UNREGISTERED (HTTP 404)
  • INVALID_ARGUMENT (HTTP 400)

如果您確定訊息酬載有效,且收到上述任一回應,則可以安全刪除這筆註冊記錄,因為該記錄不會再有效。舉例來說,如要從 Cloud Firestore 刪除無效註冊,可以部署並執行下列函式:

        // Firebase Installation ID comes from the client FCM SDKs
        const firebaseInstallationId = 'YOUR_FIREBASE_INSTALLATION_ID';

        const message = {
            data: {
                // Information you want to send inside of notification
            },
            fid: firebaseInstallationId
        };

        // Send message to device with provided Firebase Installation ID
        getMessaging().send(message)
        .then((response) => {
            // Response is a message ID string.
        })
        .catch((error) => {
            // Delete registration for user if error code is UNREGISTERED or INVALID_ARGUMENT.
            if (error.errorCode == "messaging/registration-token-not-registered") {
                // If you're running your own server, call API to delete the registration for the user
                // Example shown uses Firestore
                // Get user ID from Firebase Auth or your own server
                Firebase.firestore.collection("fcmRegistrations").document(user.uid).delete()
            }
        });

如果 Android 裝置的註冊在閒置 270 天後過期,或用戶端明確取消註冊,FCM 會傳回無效的回應。如需根據自己的定義更準確地追蹤過時狀態,可以主動移除過時註冊。

定期更新註冊資訊

無論註冊是根據 FID 還是舊版註冊權杖,伺服器都應在每次上傳要求時,更新資料庫中的註冊時間戳記。這個時間戳記會做為應用程式安裝的信號,讓用戶端成功開啟應用程式,並與 FCM 後端同步。請根據您使用的 API 實作適當的策略:

如果用戶端應用程式使用 FID API,您不需要在用戶端應用程式中排定定期背景工作,來擷取或重新整理註冊。SDK 會在自動初始化時自動處理重新整理作業,並在應用程式啟動時定期將正確的目前 FID 傳送至 onRegistered() 回呼,以便進行例行同步。

如要讓伺服器保持最新狀態,請按照「擷取及儲存 Firebase 安裝 ID」一文所述,實作啟動上傳策略:

  • 已啟用自動初始化:SDK 會在應用程式啟動期間的例行同步作業中,自動確保將最新的 FID 傳送至伺服器。
  • 自動初始化已停用或不支援:在應用程式啟動時 (例如在 Android 中,於主要活動的 onCreate() 內) 呼叫 register(),強制執行註冊序列,並觸發 FID 傳送至 onRegistered() 回呼。

這些策略可確保伺服器一律採用最新的有效 FID,並自動從上傳失敗的作業中復原,讓應用程式具備高度復原能力。

已淘汰的註冊權杖 API

如果您使用的是舊版註冊權杖,用戶端 SDK 不會自動管理例行同步作業的重新整理作業。因此,建議您定期擷取及更新伺服器上的所有註冊權杖。請按照下列步驟操作:

  • 在用戶端應用程式中加入應用程式邏輯,使用適當的 API 呼叫 (例如 Apple 平台的 token(completion): 或 Android 的 getToken()) 擷取目前的權杖,然後將目前的權杖傳送至應用程式伺服器進行儲存 (附上時間戳記)。這項工作可以設定為每月執行,涵蓋所有用戶端或權杖。
  • 新增伺服器邏輯,無論權杖是否變更,都定期更新權杖的時間戳記。

如需使用 WorkManager 更新舊版權杖的 Android 邏輯範例,請參閱 Firebase 網誌上的「管理雲端通訊權杖」。

無論採用何種時間模式,請務必定期更新權杖。每月更新一次的頻率,可兼顧電池用量和偵測閒置的註冊權杖。這樣一來,任何閒置裝置在重新啟用時,也會重新整理註冊資訊。如果重新整理頻率超過每週一次,不會有任何好處。

移除過時的註冊資訊

傳送訊息至裝置前,請確認裝置註冊的時間戳記在過時時間範圍內。舉例來說,您可以實作 Cloud Functions for Firebase,每天執行檢查作業,確保時間戳記位於定義的過時時間範圍 (例如 const EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 30;) 內,然後移除過時的註冊:

exports.pruneRegistrations = functions.pubsub.schedule('every 24 hours').onRun(async (context) => {
  // Get all documents where the timestamp exceeds is not within the past month
  const staleRegistrationsResult = await admin.firestore().collection('fcmRegistrations')
      .where("timestamp", "<", Date.now() - EXPIRATION_TIME)
      .get();
  // Delete devices with stale registrations
  staleRegistrationsResult.forEach(function(doc) { doc.ref.delete(); });
});
exports.pruneTokens = functions.pubsub.schedule('every 24 hours').onRun(async (context) => { // Get all documents where the timestamp exceeds is not within the past month const staleTokensResult = await admin.firestore().collection('fcmTokens') .where("timestamp", "<", Date.now() - EXPIRATION_TIME) .get(); // Delete devices with stale tokens staleTokensResult.forEach(function(doc) { doc.ref.delete(); }); });

取消訂閱主題中過時的註冊項目

如果您使用主題,可能也想從訂閱的主題中取消註冊過時的項目。這包含兩個步驟:

  1. 每當 Firebase 安裝 ID (FID) 變更時,應用程式應重新訂閱主題。這樣一來,當應用程式再次啟用時,訂閱項目就會自動重新顯示。
  2. 如果應用程式執行個體閒置一個月 (或您自訂的過時時間範圍),請使用 Firebase Admin SDK 取消訂閱主題,從 FCM 後端刪除 Firebase 安裝 ID 與主題的對應關係。

這兩個步驟的好處是,由於要展開的過時註冊較少,展開作業會更快完成,而且過時的應用程式執行個體在重新啟用後,會自動重新訂閱。

評估傳送成功率

如要取得最準確的訊息傳送情形,建議您只將訊息傳送至使用中的應用程式執行個體。如果您經常傳送訊息給大量訂閱者,這點就特別重要;如果其中一部分訂閱者實際上處於閒置狀態,長期下來,對傳送統計資料的影響可能相當顯著。

將訊息指定至應用程式例項前,請考量下列事項:

  • Google Analytics、BigQuery 中擷取的資料或其他追蹤信號,是否會指出註冊是否有效?
  • 先前嘗試傳送郵件時,是否持續在一段時間內失敗?
  • 過去一個月內,伺服器上的 Firebase 安裝 ID 是否有更新?
  • 如果是 Android 裝置,FCM Data API 是否會回報因 droppedDeviceInactive 導致訊息傳送失敗的比例偏高?

如要進一步瞭解訊息傳送方式,請參閱「瞭解訊息傳送方式」。