Android development learning HelloDaemon source code reading

background

Recently, the dual-service process keep alive is used in the unit project, the purpose is to ensure that the service is not killed.

Dual-process keep-alive actually means that two processes monitor each other and start each other in their respective destruction callback methods. There is a good dual-service process keep-alive open source framework on the Internet, called HelloDaemon , github address:

https://github.com/xingda920813/HelloDaemon
Now, record the reading process of its source code

Source code reading

AbsWorkService

Our work (service) process should inherit from AbsWorkService and call onBind() and onStartCommand() methods when starting

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return onStart(intent, flags, startId);
    }
 
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        onStart(intent, 0, 0);
        return onBind(intent, null);
    }


The onBind(intent, null) in onBind() is reserved for subclasses to implement the business logic when binding. So the key is the onStart() method

onStart()

    /**
     * 1.防止重复启动,可以任意调用 DaemonEnv.startServiceMayBind(Class serviceClass);
     * 2.利用漏洞启动前台服务而不显示通知;
     * 3.在子线程中运行定时任务,处理了运行前检查和销毁时保存的问题;
     * 4.启动守护服务;
     * 5.守护 Service 组件的启用状态, 使其不被 MAT 等工具禁用.
     */
    protected int onStart(Intent intent, int flags, int startId) {
 
        //启动守护服务,运行在:watch子进程中
        DaemonEnv.startServiceMayBind(WatchDogService.class);
 
        //业务逻辑: 实际使用时,根据需求,将这里更改为自定义的条件,判定服务应当启动还是停止 (任务是否需要运行)
        Boolean shouldStopService = shouldStopService(intent, flags, startId);
        if (shouldStopService != null) {
            if (shouldStopService) stopService(intent, flags, startId); else startService(intent, flags, startId);
        }
 
        if (mFirstStarted) {
            mFirstStarted = false;
            //启动前台服务而不显示通知的漏洞已在 API Level 25 修复,大快人心!
            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
                //利用漏洞在 API Level 17 及以下的 Android 系统中,启动前台服务而不显示通知
                startForeground(HASH_CODE, new Notification());
                //利用漏洞在 API Level 18 及以上的 Android 系统中,启动前台服务而不显示通知
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
                    DaemonEnv.startServiceSafely(new Intent(getApplication(), WorkNotificationService.class));
            }
            getPackageManager().setComponentEnabledSetting(new ComponentName(getPackageName(), WatchDogService.class.getName()),
                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
        }
 
        return START_STICKY;
    }

It can be divided into four steps:

1. Start the watchdog

2. Stop or start itself

3. When the first call is made, the foreground service is started and the notification is not displayed.

4. Set the application where the watchdog is located not to be killed

First, a WatchDogService is started, which is the watchdog service, which is used to start the worker process at regular intervals. As for why the DaemonEnv.startServiceMayBind() method can be called arbitrarily without fear of repeated service startup (in fact, it is not afraid of repeated service binding), you can click in to see the implementation of the source code

DaemonEnv#startServiceMayBind()

public static void startServiceMayBind(@NonNull final Class<? extends Service> serviceClass) {
        if (!sInitialized) return;
        final Intent i = new Intent(sApp, serviceClass);
        startServiceSafely(i);
        ServiceConnection bound = BIND_STATE_MAP.get(serviceClass);
        if (bound == null) sApp.bindService(i, new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                BIND_STATE_MAP.put(serviceClass, this);
            }
 
            @Override
            public void onServiceDisconnected(ComponentName componentName) {
                BIND_STATE_MAP.remove(serviceClass);
                startServiceSafely(i);
                if (!sInitialized) return;
                sApp.bindService(i, this, Context.BIND_AUTO_CREATE);
            }
        }, Context.BIND_AUTO_CREATE);
    }

sInitialized indicates that it is initialized before use (before starting the work service), and then instantiates an intent, calling startServiceSafely()

    static void startServiceSafely(Intent i) {
        if (!sInitialized) return;
        try { sApp.startService(i); } catch (Exception ignored) {}
    }

In fact, the service is started.

Return to DaemonEnv.startServiceMayBind() to see how it handles service binding. Obviously, DaemonEnv uses a mapping to save the service and the corresponding connection. Before binding, check whether the connection corresponding to the service is empty or not. It indicates that it is the first binding, otherwise it is not; for the first binding service, in the callback of the new connection when binding, the callback when connecting is to save the corresponding relationship between the service and the connection, and the callback when disconnecting It is to repeat the startServiceMayBind() process to ensure that the bound service will not be unbound. (Although the code in onServiceDisconnected() is written in four lines, it is obvious that the process is repeated again by comparing with the DaemonEnv.startServiceMayBind() method, but the original data is deleted before putting the mapping)

Stopping and starting services

Back to the onStart() method of the worker process, after DaemonEnv.startServiceMayBind() starts the watchdog, the service is stopped and started according to whether the service is stopped or not.

        Boolean shouldStopService = shouldStopService(intent, flags, startId);
        if (shouldStopService != null) {
            if (shouldStopService) stopService(intent, flags, startId); else startService(intent, flags, startId);
        }

shouldStopService() is implemented by subclasses, and then we look at the stopService() and startService() methods. The source code of the stopService() method is as follows:

    void stopService(Intent intent, int flags, int startId) {
        //取消对任务的订阅
        stopWork(intent, flags, startId);
        //取消 Job / Alarm / Subscription
        cancelJobAlarmSub();
    }

stopWork() is left to the subclass to implement, and the cancelJobAlarmSub() method ends the subscription to jobScheduler, AlaramManager and disposable by sending a broadcast, which cancels the watchdog's regular start of the service process (opening does not really end the service , but cancels the guard)

    public static void cancelJobAlarmSub() {
        if (!DaemonEnv.sInitialized) return;
        DaemonEnv.sApp.sendBroadcast(new Intent(WakeUpReceiver.ACTION_CANCEL_JOB_ALARM_SUB));
    }
public class WakeUpReceiver extends BroadcastReceiver {
    /**
     * 向 WakeUpReceiver 发送带有此 Action 的广播, 即可在不需要服务运行的时候取消 Job / Alarm / Subscription.
     *
     * 监听 8 种系统广播 :
     * CONNECTIVITY\_CHANGE, USER\_PRESENT, ACTION\_POWER\_CONNECTED, ACTION\_POWER\_DISCONNECTED,
     * BOOT\_COMPLETED, MEDIA\_MOUNTED, PACKAGE\_ADDED, PACKAGE\_REMOVED.
     * 在网络连接改变, 用户屏幕解锁, 电源连接 / 断开, 系统启动完成, 挂载 SD 卡, 安装 / 卸载软件包时拉起 Service.
     * Service 内部做了判断,若 Service 已在运行,不会重复启动.
     * 运行在:watch子进程中.
     */
    protected static final String ACTION_CANCEL_JOB_ALARM_SUB =  "com.xdandroid.hellodaemon.CANCEL_JOB_ALARM_SUB";
 
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent != null && ACTION_CANCEL_JOB_ALARM_SUB.equals(intent.getAction())) {
            WatchDogService.cancelJobAlarmSub();
            return;
        }
        if (!DaemonEnv.sInitialized) return;
        DaemonEnv.startServiceMayBind(DaemonEnv.sServiceClass);
    }
 
    .. // WakeUpAutoStartReceiver,暂时没有用到
}

After sending the broadcast of the specified action, if the service is to be canceled, the receiver calls the cancelJobAlarmSub() method in the watchdog to end the subscription to jobScheduler, AlaramManager, and disposable.

    public static void cancelJobAlarmSub() {
        if (!DaemonEnv.sInitialized) return;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            JobScheduler scheduler = (JobScheduler) DaemonEnv.sApp.getSystemService(JOB_SCHEDULER_SERVICE);
            scheduler.cancel(HASH_CODE);
        } else {
            AlarmManager am = (AlarmManager) DaemonEnv.sApp.getSystemService(ALARM_SERVICE);
            if (sPendingIntent != null) am.cancel(sPendingIntent);
        }
        if (sDisposable != null) sDisposable.dispose();
    }

Back to the onStart() of the worker process, the next step is to start the foreground service and not display the notification

Start foreground service without showing notification

If it is the first time to start the work service, set the service as the foreground service, but do not display the notification. The specific method varies according to the sdk version: for 25 and above, don't care; 18-24, you need to call startForeground() twice in a row; 17 and below, you can call it once. However, when calling startForeground(), make sure that the id is not equal to 0, otherwise it will not be a foreground service

For sdk is 18-24, it starts a WorkNotificationService, in fact, this service is very simple

    public static class WorkNotificationService extends Service {
 
        
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            startForeground(AbsWorkService.HASH_CODE, new Notification());
            stopSelf();
            return START_STICKY;
        }
 
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    }

After the second call to startForeground(), it stops itself.

Note that the onStartCommand() method of all services in HelloDaemon returns START_STICK, indicating that the service can be activated or stopped at any time, and the incoming intent can be null

Go back to the onStart() of the worker process, at the end of the first start, he sets not to kill the application where the watchdog service is located

watchdog service

I just mentioned that the watchdog service will be started when the worker process starts. Let's take a look at the onBind() and onStartCommand() methods of the watchdog WatchDogService.

    @Override
    public final int onStartCommand(Intent intent, int flags, int startId) {
        return onStart(intent, flags, startId);
    }
 
    @Override
    public final IBinder onBind(Intent intent) {
        onStart(intent, 0, 0);
        return null;
    }

Like the worker process, it calls its own onStart() method

onStart()

    protected final int onStart(Intent intent, int flags, int startId) {
 
        if (!DaemonEnv.sInitialized) return START_STICKY;
 
        if (sDisposable != null && !sDisposable.isDisposed()) return START_STICKY;
 
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
            startForeground(HASH_CODE, new Notification());
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
                DaemonEnv.startServiceSafely(new Intent(DaemonEnv.sApp, WatchDogNotificationService.class));
        }
 
        //定时检查 AbsWorkService 是否在运行,如果不在运行就把它拉起来
        //Android 5.0+ 使用 JobScheduler,效果比 AlarmManager 好
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            JobInfo.Builder builder = new JobInfo.Builder(HASH_CODE, new ComponentName(DaemonEnv.sApp, JobSchedulerService.class));
            builder.setPeriodic(DaemonEnv.getWakeUpInterval());
            //Android 7.0+ 增加了一项针对 JobScheduler 的新限制,最小间隔只能是下面设定的数字
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) builder.setPeriodic(JobInfo.getMinPeriodMillis(), JobInfo.getMinFlexMillis());
            builder.setPersisted(true);
            JobScheduler scheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
            scheduler.schedule(builder.build());
        } else {
            //Android 4.4- 使用 AlarmManager
            AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
            Intent i = new Intent(DaemonEnv.sApp, DaemonEnv.sServiceClass);
            sPendingIntent = PendingIntent.getService(DaemonEnv.sApp, HASH_CODE, i, PendingIntent.FLAG_UPDATE_CURRENT);
            am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + DaemonEnv.getWakeUpInterval(), DaemonEnv.getWakeUpInterval(), sPendingIntent);
        }
 
        //使用定时 Observable,避免 Android 定制系统 JobScheduler / AlarmManager 唤醒间隔不稳定的情况
        sDisposable = Flowable
                .interval(DaemonEnv.getWakeUpInterval(), TimeUnit.MILLISECONDS)
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(Long aLong) throws Exception {
                        DaemonEnv.startServiceMayBind(DaemonEnv.sServiceClass);
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        throwable.printStackTrace();
                    }
                });
 
        //守护 Service 组件的启用状态, 使其不被 MAT 等工具禁用
        getPackageManager().setComponentEnabledSetting(new ComponentName(getPackageName(), DaemonEnv.sServiceClass.getName()),
                PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
 
        return START_STICKY;
    }

It can be divided into five steps:

1. Determine whether to initialize and whether to cancel the subscription to disposable, if so, return directly

2, open the foreground process, do not display the notification

3. According to the different versions of sdk, start the timing of jobScheduler or alarm clock, and start the service process regularly

4. Use disposable to start the service process periodically, which can indicate the service status of the watchdog

5. Set the application where the worker process is located not to be killed

With the reading record of the service process, the onStart() of the watchdog is very simple, but I don't understand why it is necessary to use the jobScheduler and the alarm clock since there is a disposable timing.

Manual termination or withdrawal of services

When we manually end the work service or watchdog (exit in the settings or open the process manager to exit), the respective onDestroy() or onTaskRemoved() methods will be called back, and the implementation of these two methods in the two classes is the same of

   /**
     * 设置-正在运行中停止服务时回调
     */
    @Override
    public void onDestroy() {
        onEnd(null);
    }
 
   /**
     * 最近任务列表中划掉卡片时回调
     */
    @Override
    public void onTaskRemoved(Intent rootIntent) {
        onEnd(rootIntent);
    }
 
    protected void onEnd(Intent rootIntent) {
        if (!DaemonEnv.sInitialized) return;
        DaemonEnv.startServiceMayBind(DaemonEnv.sServiceClass);
        DaemonEnv.startServiceMayBind(WatchDogService.class);
    }


In fact, it's just restarting them both.

Epilogue

By reading the source code of HelloDaemon, we can find:

1. The worker process and the watchdog monitor and guard each other, the worker process starts the watchdog, and the watchdog starts the worker process regularly

2. DaemonEnv ensures that the binding of each service will only be bound once, and will not be unbound after binding

3. The so-called termination of the service actually only cancels the scheduled start of the work process, and does not directly stop the process

4. If you want to close the process manually, it can only cause the restart of the service.

It can be said that it provides a dual-process keep-alive idea.
———————————————

Reprinted in: https://blog.csdn.net/qq_37475168/article/details/87921791

Guess you like

Origin blog.csdn.net/weixin_42602900/article/details/123086565