Fragment中配合Service实现的倒计时器关闭应用后失效报错求助
解决方案
核心问题定位
你当前实现存在4个核心缺陷,是导致Fragment适配后失效、杀应用后计时中断的原因:
- 当前使用的是普通后台Service,Android 8.0及以上系统会在应用退后台1~2分钟后强制销毁后台Service,计时直接中断
- Service的
onStartCommand默认返回值不会在进程被杀后自动重启服务,计时状态完全丢失 - 动态注册的广播接收器在Fragment销毁/进程被杀后直接失效,还会因为未及时解绑抛出内存泄漏错误
- 用变量存储剩余时间、Timer计数的方式没有持久化,进程被杀后无法恢复计时状态,且计数容易因系统调度出现偏差
具体修复步骤
第一步:调整AndroidManifest配置
添加必需权限,注册前台服务:
<!-- 前台服务权限 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Android 13+ 通知权限,用于显示前台服务常驻通知 --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <application ...> <!-- 注册Service --> <service android:name=".MyService" android:enabled="true" android:exported="false" /> </application>
第二步:重写Service为前台服务+持久化计时
放弃Timer逐秒计数的方式,改为存储结束时间戳,就算进程中断重启也能通过系统时间计算准确的剩余时间:
public class MyService extends Service { private static final int NOTIFICATION_ID = 1001; private static final String CHANNEL_ID = "CountdownChannel"; private SharedPreferences sp; private Timer timer; @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); sp = getSharedPreferences("CountdownSp", MODE_PRIVATE); // 创建前台服务通知通道 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "倒计时服务", NotificationManager.IMPORTANCE_LOW); NotificationManager manager = getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { // 启动前台服务,显示常驻通知 Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("倒计时正在运行") .setSmallIcon(R.drawable.ic_launcher_foreground) // 替换为你的应用图标 .setPriority(NotificationCompat.PRIORITY_LOW) .build(); startForeground(NOTIFICATION_ID, notification); // 读取传入的总时长,计算结束时间戳存入SP if (intent != null && intent.hasExtra("TimeValue")) { int totalTime = intent.getIntExtra("TimeValue", 0); long endTime = System.currentTimeMillis() + totalTime * 1000L; sp.edit().putLong("end_time", endTime).apply(); } // 启动逐秒更新任务 timer = new Timer(); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { long endTime = sp.getLong("end_time", 0); long remaining = (endTime - System.currentTimeMillis()) / 1000; if (remaining <= 0) { timer.cancel(); sp.edit().remove("end_time").apply(); stopForeground(STOP_FOREGROUND_REMOVE); stopSelf(); } // 发送广播更新UI Intent updateIntent = new Intent("Counter"); updateIntent.putExtra("TimeRemaining", (int) remaining); sendBroadcast(updateIntent); // 更新通知显示剩余时间 Notification updateNotification = new NotificationCompat.Builder(MyService.this, CHANNEL_ID) .setContentTitle("倒计时剩余:" + remaining + "秒") .setSmallIcon(R.drawable.ic_launcher_foreground) .setPriority(NotificationCompat.PRIORITY_LOW) .build(); NotificationManager manager = getSystemService(NotificationManager.class); manager.notify(NOTIFICATION_ID, updateNotification); } }, 0, 1000); // 返回START_REDELIVER_INTENT,进程被杀后系统会自动重启服务并传入原始Intent return START_REDELIVER_INTENT; } @Override public void onDestroy() { super.onDestroy(); if (timer != null) { timer.cancel(); } } }
第三步:调整Fragment内的逻辑
注意广播的注册解绑时机,避免空指针和内存泄漏:
public class CountdownFragment extends Fragment { private TextView textView; private EditText editText; private BroadcastReceiver broadcastReceiver; private SharedPreferences sp; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); sp = requireContext().getSharedPreferences("CountdownSp", Context.MODE_PRIVATE); // 申请Android 13+通知权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100); } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_countdown, container, false); textView = view.findViewById(R.id.textView); editText = view.findViewById(R.id.editText); view.findViewById(R.id.start_btn).setOnClickListener(v -> startCountdown()); return view; } @Override public void onStart() { super.onStart(); // 注册广播接收器 IntentFilter intentFilter = new IntentFilter("Counter"); broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int remaining = intent.getIntExtra("TimeRemaining", 0); textView.setText(String.valueOf(remaining)); } }; requireContext().registerReceiver(broadcastReceiver, intentFilter); // 页面打开时直接读取SP更新剩余时间,避免服务正在运行时UI不更新 long endTime = sp.getLong("end_time", 0); long remaining = (endTime - System.currentTimeMillis()) / 1000; if (remaining > 0) { textView.setText(String.valueOf(remaining)); } } @Override public void onStop() { super.onStop(); // 解绑广播接收器,避免内存泄漏 if (broadcastReceiver != null) { requireContext().unregisterReceiver(broadcastReceiver); broadcastReceiver = null; } } private void startCountdown() { String input = editText.getText().toString(); if (TextUtils.isEmpty(input)) return; int timeSet = Integer.parseInt(input); Intent serviceIntent = new Intent(requireContext(), MyService.class); serviceIntent.putExtra("TimeValue", timeSet); // Android 8.0+ 用startForegroundService启动前台服务 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { requireContext().startForegroundService(serviceIntent); } else { requireContext().startService(serviceIntent); } } }
极端场景适配
如果需要在用户主动滑掉应用杀掉进程后仍能保持计时准确,不需要额外保活(国内ROM大多禁止第三方应用后台保活),直接通过存储的结束时间戳计算即可:下次打开应用时读取SP里的结束时间,直接计算剩余时间更新UI,就算中间进程完全被杀,计时结果也不会出错。
内容的提问来源于stack exchange,提问作者Marcus
相关产品推荐
相关产品推荐

