Android 7/10设备FirebaseInstanceIdReceiver触发ANR问题排查及FCM Token上传100%成功率实现咨询
首先来拆解你遇到的两个核心问题:无堆栈ANR和Token上传成功率低,我会一步步给你分析原因并给出落地的优化方案。
一、先解决ANR问题:根源是依赖冲突+冗余逻辑
你的ANR日志指向FirebaseInstanceIdReceiver这个广播接收器,结合你的依赖和代码来看,问题出在这两点:
1. 废弃库导致的依赖冲突
你同时引入了firebase-iid和firebase-messaging,但从Firebase Messaging 21.0.0版本开始,firebase-iid就已经被完全废弃,它的所有功能都整合到了firebase-messaging中。同时引入这两个库会导致类加载冲突,干扰广播接收器的正常执行,最终触发无堆栈的ANR。
修复:移除废弃依赖
删掉gradle里的firebase-iid,只保留firebase-messaging:
// 删掉这行:implementation 'com.google.firebase:firebase-iid:21.1.0' implementation 'com.google.firebase:firebase-messaging:23.0.6'
2. onNewToken里的冗余逻辑
onNewToken方法本身已经直接提供了新生成的Token,但你又调用了FirebaseMessaging.getInstance().getToken(),这会额外发起异步请求,和广播接收器的生命周期产生冲突——广播接收器的onReceive(包括关联的回调)必须在10秒内完成,否则系统就会判定为ANR。
修复:直接使用onNewToken传入的Token
@Override public void onNewToken(@NonNull String token) { super.onNewToken(token); // 直接用传入的token,不要再调用getToken() new TokenUploader().uploadToken(token); }
二、提升Token上传成功率:重构上传逻辑
你的上传代码存在几个明显的缺陷,导致大量设备上传失败:
- 账号为空时直接返回,没有重试机制
- 无间隔连续重试,容易被服务器限流
- 异常捕获后无日志,无法排查问题
- 用裸线程处理后台任务,无法应对设备重启、网络中断等场景
优化方案:用WorkManager实现可靠的后台上传
WorkManager是Android官方推荐的后台任务处理库,它能自动处理网络状态、设备重启后的任务恢复,非常适合这种需要保证送达的场景。
1. 添加WorkManager依赖
implementation "androidx.work:work-runtime:2.8.1"
2. 编写Token上传Worker类
这个类负责实际的上传逻辑,包含指数退避重试、缓存机制和日志记录:
public class TokenUploadWorker extends Worker { private static final String TAG = "TokenUploadWorker"; public static final String KEY_TOKEN = "token"; public TokenUploadWorker(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); } @NonNull @Override public Result doWork() { String token = getInputData().getString(KEY_TOKEN); if (TextUtils.isEmpty(token)) { Log.e(TAG, "Token为空,无法上传"); return Result.failure(); } Account account = getCurrentAccount(); // 替换成你获取账号的逻辑 if (account == null) { // 账号未登录,缓存Token,等登录后再重试 saveTokenToSharedPrefs(token); Log.w(TAG, "账号未就绪,缓存Token待后续上传"); return Result.success(); } boolean uploadSuccess = false; int retryCount = 0; long backoffDelay = 1000; // 初始重试延迟1秒 while (retryCount < 4 && !uploadSuccess) { try { JSONObject params = new JSONObject(); params.put("email", account.getEmail()); params.put("password", getPassword()); // 注意:这里传明文密码不安全,建议改用认证Token params.put("key", getApiKey()); params.put("fcm_token", token); JSONObject response = sendRequestToApi("POST", params, "/fcm_route"); if (response != null && response.has("fcmId")) { uploadSuccess = true; clearCachedToken(); // 上传成功,清除缓存 Log.d(TAG, "Token上传成功"); } else { Log.w(TAG, "第" + (retryCount+1) + "次上传失败:响应无效"); } } catch (Exception e) { Log.e(TAG, "第" + (retryCount+1) + "次上传失败", e); } if (!uploadSuccess) { retryCount++; try { Thread.sleep(backoffDelay); backoffDelay *= 2; // 指数退避,每次延迟翻倍 } catch (InterruptedException e) { Thread.currentThread().interrupt(); return Result.failure(); } } } if (uploadSuccess) { return Result.success(); } else { // 4次重试都失败,缓存Token,后续通过WorkManager自动重试或登录后触发 saveTokenToSharedPrefs(token); Log.e(TAG, "所有上传尝试失败,缓存Token"); return Result.retry(); } } // 保存Token到SharedPreferences private void saveTokenToSharedPrefs(String token) { SharedPreferences prefs = getApplicationContext().getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE); prefs.edit().putString("cached_token", token).apply(); } // 清除缓存的Token private void clearCachedToken() { SharedPreferences prefs = getApplicationContext().getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE); prefs.edit().remove("cached_token").apply(); } // 替换成你的账号获取逻辑 private Account getCurrentAccount() { // TODO: 实现获取当前登录账号的逻辑 return null; } // 替换成你的密码获取逻辑(建议改用认证Token,避免明文传输) private String getPassword() { // TODO: 实现密码获取逻辑 return null; } // 替换成你的API Key获取逻辑 private String getApiKey() { // TODO: 实现API Key获取逻辑 return null; } // 替换成你的网络请求逻辑(建议用OkHttp等成熟库,处理网络异常) private JSONObject sendRequestToApi(String method, JSONObject params, String route) throws Exception { // TODO: 实现网络请求 return null; } }
3. 修改MyFirebaseMessagingService,提交WorkManager任务
@Override public void onNewToken(@NonNull String token) { super.onNewToken(token); // 配置任务约束:仅在有网络时执行 Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build(); OneTimeWorkRequest uploadRequest = new OneTimeWorkRequest.Builder(TokenUploadWorker.class) .setConstraints(constraints) .setInputData(new Data.Builder() .putString(TokenUploadWorker.KEY_TOKEN, token) .build()) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS) .build(); WorkManager.getInstance(this).enqueue(uploadRequest); }
4. 登录成功后触发缓存Token的上传
在用户登录成功的回调里,检查是否有缓存的未上传Token,然后触发上传:
// 登录成功的回调方法中 SharedPreferences prefs = getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE); String cachedToken = prefs.getString("cached_token", null); if (!TextUtils.isEmpty(cachedToken)) { Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build(); OneTimeWorkRequest uploadRequest = new OneTimeWorkRequest.Builder(TokenUploadWorker.class) .setConstraints(constraints) .setInputData(new Data.Builder() .putString(TokenUploadWorker.KEY_TOKEN, cachedToken) .build()) .build(); WorkManager.getInstance(this).enqueue(uploadRequest); }
三、关键优化点总结
- 移除废弃依赖:避免库冲突引发的ANR
- 简化
onNewToken逻辑:直接使用传入的Token,避免冗余异步请求 - 用WorkManager管理后台任务:自动处理网络状态、设备重启,保证任务不丢失
- 指数退避重试:避免服务器限流,提升重试成功率
- Token缓存机制:账号未登录时缓存Token,登录后自动上传
- 完善日志:方便排查上传失败的原因
内容的提问来源于stack exchange,提问作者Sujith S Manjavana

