FCM 註冊管理最佳做法

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

過時和過期的註冊

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

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

如果是 Android,當註冊項目閒置 270 天後,FCM 會將其視為過期並進行垃圾收集。註冊過期後,FCM 會將其標示為無效,並拒絕傳送至該註冊的訊息。請注意,Firebase 安裝 ID (FID) 本身是由 Firebase Installations 服務 (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 並更新伺服器上的時間戳記。

範例:在 Cloud Firestore 中儲存商店 FID 和時間戳記

舉例來說,您可以使用 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 導致訊息傳送失敗的比例偏高?

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