Android保活实现方案梳理

参考资料:
gitbub上的一个进程保活的库
安卓进程保活解决方案
Android进程保活招式大全
双进程守护APP保活方案
最近查看进程保活的各种资料,实现方式无非就那么几种,可以参考上面的资料链接,本篇博文对这些实现进行了分析和测试,并把从中体会到的知识点做个总结汇总,算是加深相关知识的理解。

本篇博文以github的一个库为例,来逐步讲解说明
该库总的来说就是整合了:一像素Activity,前台Servcie,Service设置START_STICKY,播放无声音乐Servcie,双进程守护Service,JobServcie的多种手段来确保保活的成功率。

1像素Activity

该方案适用场景: 本方案主要解决第三方应用及系统管理工具在检测到锁屏事件后一段时间内会杀死后台进程.适用于android所有版本。1像素Activity的特点: 需要设置该activity的style设置透明,在手机锁屏时start;在屏幕解锁时finish,主要作用就是在App退到后台之后且锁屏的时候启动一个看不见的Activity,造成一个该App没有回退到后台的假象,降低被杀的几率,伪代码如下:

  protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //设定一像素的activity
        Window window = getWindow();
        window.setGravity(Gravity.START | Gravity.TOP);
        WindowManager.LayoutParams params = window.getAttributes();
        params.x = 0;
        params.y = 0;
        params.height = 1;
        params.width = 1;
        window.setAttributes(params);
    }
    
 public void onReceive(final Context context, Intent intent) {
        Log.i("ScreenStateReceiver", "---屏幕锁屏监听---");
        if (action.equals(Intent.ACTION_SCREEN_OFF)) {
           //屏幕锁定,开启OnePixelActivity

        } else if (action.equals(Intent.ACTION_SCREEN_ON)) {
           //屏幕解锁,finish OnePixelActivity
        }
    }

说起来很简单,但是还是有几个知识点需要注意下。

1、需要注册一个监听屏幕锁屏和截屏的BroadcastReceiver。

关于这一点有一个细节需要处理,为了放置用户快速 进行锁屏和截屏的切换操作,而导致OnePixelActivity频繁开关。需要用一个Handler发送一个延迟消息处理最佳:

   private Handler mHandler;
    private boolean isScreenOn = true;
    private PendingIntent  pendingIntent;
    private List<ScreenStateListener> screenStateListeners = null;
    @Override
    public void onReceive(final Context context, Intent intent) {
        String action = intent.getAction();
      
        if (action.equals(Intent.ACTION_SCREEN_OFF)) {
           //标记屏幕为锁屏状态
            isScreenOn = false;
            //开启一像素的Activity
            startOnePixelActivity(context);

        } else if (action.equals(Intent.ACTION_SCREEN_ON)) {
            //标记屏幕为解屏状态
            isScreenOn = true;     
            if(pendingIntent!=null){
                pendingIntent.cancel();
            }
     
        }
    }

  //开启一像素的Activity
 private void startOnePixelActivity(final Context context){
        if(mHandler==null){
            mHandler = new Handler(Looper.myLooper());
        }
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
              //如果屏幕此时已经打开,则不执行
                if(isScreenOn){
                    return;
                }
                if(pendingIntent!=null){
                    pendingIntent.cancel();
                }
                
                Intent startOnePixelActivity = new Intent(context, OnePixelActivity.class);
                startOnePixelActivity.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);

                //启动一像素包活activity
                pendingIntent = PendingIntent.getActivity(context, 0, startOnePixelActivity, 0);
                try {
                    pendingIntent.send();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                notifyScreenOff();
            }
        },1000);
    }

2、需要将该activity的 android:excludeFromRecents设置为true

3、需要将该Activity设置为singleInstance启动模式

  <activity android:name=".OnePixelActivity"
            android:theme="@style/onePixelActivity"
            android:launchMode="singleInstance"
            android:excludeFromRecents="true"/>

为什么需要将此Activity的启动模式设置为singleInstance呢?原因是因为如果设置成其他模式,如果按照如下步骤操作的话会出现不友好的状况:
1、启动App(比如进入MainActivity),按home键让app返回后台
2、锁定屏幕,此时注册好的监听广播会启动OnePixelActivity
3、解锁屏幕,此时广播接受到此广播后会finish 掉OnePixelActivity.
因为OnePixelActivity是非singleInstance,所以此时本来已经进入后台的MainActivity的页面会自动打开。给用户造成干扰:我明明已经按下home键了,怎么此页面又自动打开了?而换成OnePixelActivity的启动模式改成singleInstance的话就可以很好的避免此问题。

另外需要注意的是,当屏幕解锁的时候,OnePixelActivity的onResume得到执行,所以在该Activity的onResume方法执行finish效果最好:

//OnePixelActivity的onResume
   protected void onResume() {
        super.onResume();
      
        if (DeviceUtils.isScreenOn(this)) {//如果屏幕已经打开
            finish();
        }
    }

一像素Activity的整体流程如下:
在这里插入图片描述


播放无声音乐

处理逻辑跟OneActivity一样,在App回到后台且锁屏的时候在Service里播放一个无声的mp3文件,当解锁的时候暂停改mp3的播放。

if(screenOn){//屏幕解锁
    puseMp3();
}else{//锁屏
   playMp3();
}

所以此是可以在程序启动的时候启用这个service,在在service里面注册锁屏广播,需要注意的一些地方(此Service姑且称之为KeepAliveService):

//播放无声音乐的Service
public class KeepAliveService extends Service {
    private boolean isScreenON = true;//控制暂停
    private MediaPlayer mediaPlayer;
    //锁屏广播监听
    private ScreenStateReceiver screenStateReceiver;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //注册锁屏广播
        registerScreenStateReceiver();
        //初始化播放器
        initMediaPlayer();
        return START_STICKY;
    }
 }

需要注意的是在播放器播放的时候,需要判断当前情况是否是解屏状态,而且在播放完成监听里面,继续调用start方法循环播放mp3,部分代码如下(本篇博文只贴部分关键代码,全部代码点此查看):

private void initMediaPlayer() {
        if (mediaPlayer == null) {
            mediaPlayer = MediaPlayer.create(this, R.raw.novioce);
            mediaPlayer.setVolume(0f, 0f);
            mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mediaPlayer) {
                   //如果此屏幕处于解锁状态则不在播放
                    if (isScreenON) {
                        return;
                    }
                    //播放完毕后循环播放
                    play();
                }
            });
        }
        //防止Service启动的时候屏幕处于解锁状态
        if(!DeviceUtils.isScreenOn(this)){
            play();
        }
    }

所以如果结合OnePixelActivity的这种方案一起实施的话,上图的流程就是如下所示:
在这里插入图片描述

另外关于在Service里面播放无声音乐,有个知识点需要注意下,如要将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后自动拉活:

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //将service设置为START_STICKY
        return START_STICKY;
    }

此种情况下,Service被异常关闭的情况下,系统会尝试创建改Service。从而提高保活的成功性!

根据博主查阅资料得到如下论点:Android 中 Service 的优先级为4,通过 setForeground 接口可以将后台 Service 设置为前台 Service,使进程的优先级由4提升为2,从而使进程的优先级仅仅低于用户当前正在交互的进程,与可见进程优先级一致,使进程被杀死的概率大大降低!所以我们可以将我们的播放无声音乐的Service调用setForeground 来提高优先级!但是从 Android2.3 开始调用 setForeground 将后台 Service 设置为前台 Service 时,必须在系统的通知栏发送一条通知,也就是前台 Service 与一条可见的通知时绑定在一起的 (注:本论点出处来源于此)

为此需要在KeepAliveService 的onStartCommand添加如下方法:

 public int onStartCommand(Intent intent, int flags, int startId) {
        //省略注册锁屏广播以及初始化播放器的代码
        startForeground(this);
        return START_STICKY;
    }

 public static  void startForeground(Service service){
        Intent intent = new Intent(service.getApplicationContext(), com.fanjun.keeplive.receiver.NotificationClickReceiver.class);
        intent.setAction(com.fanjun.keeplive.receiver.NotificationClickReceiver.CLICK_NOTIFICATION);
        Notification notification = NotificationUtils.createNotification(service, "1", "2", R.drawable.ic_launcher_background, intent);
        service.startForeground(13691, notification);
    }

设置前台进程之所以要传一个notification对象,是因为从 Android2.3 开始调用 setForeground 将后台 Service 设置为前台 Service 时,必须在系统的通知栏发送一条通知,也就是前台 Service 与一条可见的通知时绑定在一起的!(本段出处点处

但是这有一个弊端,就是在该Service启动的时候会在手机的顶部通知栏会展示一个该应用运行的相关通知,也就是说这种方式可以让用户感知,所以不是太友好!运行效果如下,回来通知栏展示自己app的icon:
在这里插入图片描述

那么怎么才能取消这个呢?startForeground方法有两个参数,第一个参数是notification的Id.所以解决思路就出来了(不得不佩服那些前辈高人们):

1、可以创建另一个Service,名为HideNotifactionService,该Service在KeepAliveService里面启动之

//KeepAliveService的onStartCommand方法
 public int onStartCommand(Intent intent, int flags, int startId) {
        //省略注册锁屏广播以及初始化播放器的代码

       //将自己设置为前台Service
        startForeground(this);
     //启动HideNotificationService
      startService(new Intent(this, HideNotificationService.class));
        return START_STICKY;
    }

2、将HideNotifactionService调用startForeground将其设置为前台Service!
3、但是该Service调用startForeground方法的时候,第一个参数传的ID跟KeepAliveService调用时传同样的ID。

这样的话就确保有HideNotifactionService和KeepAliveService两个发送具有同样ID的Notification!

4、调用HideNotifactionService的stopSelf方法关闭自己,随着这个Service的关闭,通知栏的那条相同id的通知也就没了;但是同样确保了KeepAliveService的进程优先级得到了提高

HideNotifactionService代码如下:

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
         //将自己设置为前台 Service
        KeepAliveService.startForeground(this);
        
       //  stopForeground(true);//不调用此方法也能达到效果
       //关闭HideNotifactionService自己
        stopSelf();
        return START_NOT_STICKY;
    }

上面对KeepAliveService的说白了从根本上来说就是做了两大功能:
1、播放一个无声音乐提高app被杀的概率
2、通过设置START_STICKY和设置startForeground提高Service的优先级,降低被杀的概率

通过上面的处理,我们的整体流程图就变成如下这个样子:
在这里插入图片描述

但是一像素Activity和播放无声音乐的Service仍然是有被杀掉的可能,所以现在问题是如果被杀掉了,该怎么才能激活这个KeepAliveServcie? 答案是双进程Service守护


双进程守护法

所谓双进程守护原理就是很简单:两个进程的Service,相互守护;当一个进程的Service挂的时候,另一个进程的Service负责重启挂掉的Service。在本篇博文中KeepAliveService是跟我们要包活的App是一个处于一个进程,RemoteService是另外一个进程的Service;将RemoteService设置成单独的进程很方便,在AndroidMainfest.xml里面配置 android:process即可。

<service android:name=".KeepAliveService"/>
//将RemoteService和KeepAliveServcie处于不同的进程
<service android:name=".RemoteService" android:process=":remote"/>

守护进程其实是一个双向守护的过程,比如KeepAliveService挂了,那么RemoteService负责将KeepAliveService重启;同理,如果RemoteService挂了的话,KeepAliveService负责将RemoteService重启!那么二者是怎么知道彼此是否挂了呢?因为是位于两个进程,所以我们可以通过AIDL来确保两个不同进程的Service可以通信。

定义一个AIDL接口

interface GuardAidl {
    void notifyAlive();
}

接着我们重写KeepAliveSerivice的onBind方法,该方法翻译一个IBinder给RemoteService使用:

     //将keepAliveBinder 交给RemoteService
    public IBinder onBind(Intent intent) {
        return keepAliveBinder;
    }
 
    private GuardAidl.Stub keepAliveBinder = new GuardAidl.Stub(){
        @Override
        public void notifyAlive() throws RemoteException {
            Log.i(null,"Hello RemoteService!");
        }
    };

通过KeepAliveService的onBind方法返回一个IBinder对象交给RemoteService使用,这样RemoteService就可以通过keepAliveBinder对象跟KeepAliveServcie进行通信!同样的RemoteService也是一样的代码逻辑:

public class RemoteService extends Service {
    public IBinder onBind(Intent intent) {
        return remoteBinder;
    }
    private GuardAidl.Stub remoteBinder = new GuardAidl.Stub(){
        @Override
        public void notifyAlive() throws RemoteException {
            Log.i(null,"Hello KeepAliveService!");
        }
    };
}

两个Servcie相互绑定

现在两个Service都重写onBind返回了自己的IBinder,但是怎么将自己创建的IBinder 交给对象使用呢?答案是双方调用bindSerice方法相互绑定对方,该方法是Service的一个方法,其签名如下:

bindService(Intent service, ServiceConnection conn, int flags) 

所以我们绑定的时候除了传一个intent之外,还要传一个ServiceConnection。看看KeepAliveService绑定RemoteService的代码:

 //KeepAliveService的onStartCommand方法
 public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e("KeepAliveService", "---KeepAliveService 启动---");
        //注册锁屏广播
        registerScreenStateReceiver();
        //初始化播放器
        initMediaPlayer();
        
        //开启前台Service
        startForeground(this);
        
        //start HideNotifactionService
        startHideNotificationService();
        
        //绑定守护进程
        bindRemoteService();
        
        return START_STICKY;
    }
    
   //绑定守护进程
 private void bindRemoteService(){
        Intent intent = new Intent(this,RemoteService.class);
        bindService(intent,connection,Context.BIND_ABOVE_CLIENT);
    }

   private ServiceConnection connection = new ServiceConnection() {
       //调用此方法说明RemoteServcie已经挂掉,需要重新启动
        public void onServiceDisconnected(ComponentName name) {
         Intent remoteService = new Intent(KeepAliveService.this,
                        RemoteService.class);
                KeepAliveService.this.startService(remoteService);
                
            Intent intent = new Intent(KeepAliveService.this, RemoteService.class);
            //将KeepAliveService和RemoteService进行绑定
            KeepAliveService.this.bindService(intent, connection,
                    Context.BIND_ABOVE_CLIENT);
        }
        
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            try {
                //与RemoteService绑定成功
                GuardAidl remoteBinder = GuardAidl.Stub.asInterface(service);
                remoteBinder.notifyAlive();
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    };

其中ServiceConnection有两个方法onServiceDisconnected和onServiceConnected,当RemoteService被异常销毁挂掉的时候,onServiceDisconnected会被调用。此时需要在该方法里面重新启动RemoteService;而onServiceConnected方法则是系统调用它来传送在RemoteService的onBind()中返回的IBinder!同理,RemoteService绑定KeepAlivce的代码跟上面雷同,再此就不在贴出来了,源码可以点击此处查阅

所以KeepAliceService和RemoteService的相互守护的关系可以用如下来表示:
在这里插入图片描述
到此为止我们的保活手段整合了一像素Activity,无声音乐播放Service,设置前台Servcie,双进程守护的措施,整体流程图就丰富成如下所示。
在这里插入图片描述
虽然有进程保活手段,但是仍然有可能KeepAliveServcie和RemoteService都被杀掉的情况,有没有什么手段在他们都被杀掉的时候重新唤起呢?有!JobServcie闪亮登场


JobServcie 保活(android version>5.0的时候使用)

网上关于JobServcie以及JobScheduler的资料很多,为了不偏离本文主题,在这里就说他们是怎么使用的了!引用网上的的一段话:
JobScheduler是Android5.0 开始引入了一个新系统服务。它将后台任务调度直接交给系统服务(JobSchedulerSevice)管理,并且可以设置许多约束条件,如周期调度,延迟调度,网络连接,电源插入,还有AndroidL引入的空闲模式。在条件符合的情况下,系统服务BindService的方式把应用内Manifest中配置的JobService启动起来,并通过进程间通信Binder方式调用JobService的onStartJob、onStopJob等方法来进行Job的管理。即便在执行任务之前应用程序进程被杀,也不会导致任务中断,Jobservice不会因应用退出而退出 当然JobScheduler不只是用于保活这个黑科技,可以利用这种机制做很多后台定时任务。

下面看具体操作步骤:

创建JobService

在项目中创建一个JobService,并在AndroidManifest.xml配置,主要是多了一个permission的配置:

<service android:name=".impl.KeepAliveJobService" android:permission="android.permission.BIND_JOB_SERVICE"/>

因为JobService也是一个Serivice,所以我们也可以像KeepAliveService一样将JobServcie设置为START_STICKY,设置为前台服务。

public class KeepAliveJobService extends JobService {
    private JobScheduler mJobScheduler;
    @Override
    public boolean onStartJob(JobParameters jobParameters) {
        startService(this);
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters jobParameters) {
        startService(this);
        return false;
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //启动Servcie 
        startService(this);
        //初始化定时任务
        scheduleJob(startId);
        
        return START_STICKY;
    }

可以看到在onStartCommand里面和onStartJob,onStopJob都调用了startService。同时在onStartCommand方法里设置了START_STICKY。看看startService做了啥:

  private void startService(Context context) {
        //设置为前台服务
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            Intent intent2 = new Intent(getApplicationContext(), com.fanjun.keeplive.receiver.NotificationClickReceiver.class);
            intent2.setAction(NotificationClickReceiver.CLICK_NOTIFICATION);
            Notification notification = NotificationUtils.createNotification(this, "1", "2", R.drawable.ic_launcher_background, intent2);
            startForeground(13691, notification);
        }
        //启动本地服务
        Intent keepAliveIntent= new Intent(context, KeepAliveService.class);
        //启动守护进程
        Intent guardIntent = new Intent(context, RemoteService.class);
        startService(keepAliveIntent);
        startService(guardIntent);
    }

startService做了两件事:
1、将JobService设置为前台Servcie
2、启动KeepAliveService及其守护进程RemoteServcie

然后看看scheduleJob其实就是初始化了JobScheduler,主要职责是让JobServcie调用startService启动KeepAliveService及其守护进程RemoteServcie:

 private void scheduleJob(int startId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
            JobInfo.Builder builder = new JobInfo.Builder(startId++,
                    new ComponentName(getPackageName(), KeepAliveJobService.class.getName()));
            if (Build.VERSION.SDK_INT >= 24) {
                builder.setMinimumLatency(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS); //执行的最小延迟时间
                builder.setOverrideDeadline(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS);  //执行的最长延时时间
                builder.setMinimumLatency(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS);
                builder.setBackoffCriteria(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS, JobInfo.BACKOFF_POLICY_LINEAR);//线性重试方案
            } else {
                builder.setPeriodic(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS);
            }
            builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
            builder.setRequiresCharging(true); // 当插入充电器,执行该任务
            mJobScheduler.schedule(builder.build());
        }
    }

所以我们怎么使用这个JobServcie呢?可以在Application里面启动,如果是5.0以上的版本就用JobService,否则仍然直接常规启动KeepAliveService和RemoteService:

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                //启动定时器,在定时器中启动本地服务和守护进程
                Intent intent = new Intent(application, JobHandlerService.class);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    application.startForegroundService(intent);
                } else {
                    application.startService(intent);
                }
            } else {
                //启动本地服务
                Intent localIntent = new Intent(application, LocalService.class);
                //启动守护进程
                Intent guardIntent = new Intent(application, RemoteService.class);
                application.startService(localIntent);
                application.startService(guardIntent);
            }

到此为止我们的保活手段整合了一像素Activity,无声音乐播放Service,设置前台Servcie,双进程守护,JobService的措施,整体流程图就丰富成如下所示。

在这里插入图片描述

本篇博文到此结束,因为博主技术能力有限,所以保活的具体原理就没描述,主要从代码实现层面结合github的一个库进行了梳理和分析。如有不当之处,欢迎批评指正,共同学习。

本篇博文设计到的demo代码点此,详细的实现方式还是以原作者的github为准为准!

发布了257 篇原创文章 · 获赞 484 · 访问量 144万+

猜你喜欢

转载自blog.csdn.net/chunqiuwei/article/details/95649955
今日推荐