You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 17:15:01