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 傳送至伺服器。
  • 自動初始化功能已停用或不受支援:在應用程式啟動時呼叫 register() (例如在 Android 的主要活動 onCreate() 中),強制執行註冊序列,並觸發將 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 導致訊息傳送失敗的比例偏高?

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