解密Android7.0 8.0进程保活与拉活的实现方式 如何才能让APP常驻内存 躲避系统的追杀

前言

近期被一些朋友问到关于进程保活这块的知识点,想必是很多开发者研究的重点,虽然我不支持做成这类【流氓软件】,因为大家都这么干的话,Android系统的内存永远不够用,电量消耗的贼快,流畅度肯定就大大的降低了;但是程序猿可能也架不住产品的需求,哪一个产品经理不希望自己的APP在用户的手机上随叫随到

其实说实话一个APP很难做到真正的不死,特別是Android 5.0以后Google对应用进程管理的更加严格,杀的也很彻底,除非你的应用被手机厂商拉到了白名单中;那么作为一个不在白名单中的应用怎么才能做到尽量不死呢?就算死了也能立马活过来呢?这篇文章就总结下几种实现方法

APP进程被系统杀死

在实操前先阐述一些基本知识

第三方应用退出后台后,并不会一直在后台运行,在某些情况下其应用所在进程会被干掉以释放出系统资源,一般被干掉的原因有:

  • 手机厂商:比如华为、小米、OV等厂商出于对性能的考虑,会对第三方应用进行检测,看是否有开启后台服务且是长时间运行的,那么在某些情况下就会将其杀死释放资源;当然了,如果你的应用做的够大够强,比如微信支付宝,那设备厂商直接就将你的应用放到白名单了,也就不存在进程保活了
  • 原生系统回收机制:当手机系统可用内存过低时,系统会根据进程优先级,杀死优先级最低的进程以释放资源

Low Memory Killer

Android对于内存的回收主要依靠Low Memory Killer完成:系统出于用户体验和性能的考虑,app在退到后台时,系统并不会立即将其kill掉,而是将其缓存起来;但是当打开的应用越多,后台缓存的进程也就越多,也就意味着系统可用内存越来越少;那么当内存不足时,系统就根据oom_adj值触发相应力度的进程回收机制来判断要杀死哪些进程,以释放出内存来供当前的应用使用,这套杀死进程回收内存的机制就叫Low Memory Killer

进程优先级

其中一个重要的判断依据就是进程优先级,要知道Android系统会尽量长时间保持应用进程,但是为了新建进程或运行更重要的进程,需要清除一些旧进程来回收内存;为了确保保留或终止哪些进程,系统会对进程进行分类,确定进程优先级;需要时系统首先清除优先级最低的进程,再清除稍低的进程,以此类推来回收资源;其中进程优先级划分如下:

  • 前台进程 Active Process:一般是前台正在交互的activity、与前台activity绑定的service、调用startForeground()方法使之位于前台运行的Service、执行它的某个生命周期回调方法,比如onCreate()、 onStart()或onDestroy()的Service、正在执行onReceive事件处理的函数的BroadCast Receiver等所在的进程,这几种情况的进程就是可见进程,这些是android通过回收资源尽力保护的进程

  • 可见进程 Visible Process:比如一个activity处于可见但并不是处于前台或者不响应用户事件,处于暂停(OnPause)状态;还有一种就是被这种Activity绑定的Service,这种就是可见进程;这些情况一般发生在当一个activity被部分遮盖的时候(被一个非全屏或者透明的Activity)。可见进程只在极端的情况下,才会被杀死来保护前台进程的运行。

  • 服务进程 Service Process:包含已经启动的service,service以动态的方式持续运行但没有可见的界面。因为Service不直接和用户交互,它们拥有比Visible Process较低的优先级

  • 后台进程 Background Process:进程中的Activity不可见或进程中没有任何启动的service,这些进程都可以是后台进程;比如按home键,activity的前台进程就变成了后台进程;在系统中,拥有大量的后台进程,并且Android会按照后看见先杀掉的原则来杀掉后台进程以获取系统资源给前台进程

  • 空进程 Empty Process:为了改善整个系统的性能,android经常在内存中保留那些已经走完生命周期的应用程序。android维护这些缓存来改善应用程序重新启动的时间,为使用总体系统资源在进程缓存和底层内核缓存之间保存平衡,系统会随时终止这些进程

用一幅图展示下

在这里插入图片描述
那系统是怎么知道这些进程的优先级的呢?

这就要说到oom_adj这个东西了,进程的优先级通过进程的adj值来反映,它是Linux内核分配给每个系统进程的一个值,adj值越小,进程优先级越高,进程回收机制根据这个值来决定是否进行回收;adj越大,占用内存越多,越会被系统最先干掉,所以进程保活的实现就成了如何降低oom_adj的值,以及如何使应用所占内存降低

有一张adj表可以看到其与进程优先级的更详细的关系

在这里插入图片描述
红色部分代表比较容易被杀死的进程(oom_adj>4),绿色部分代表不容易被杀死的进程,白色部分表示非Android进程

可以通过 cat /proc/进程id/oom_adj命令查看进程的adj值,不过需要root权限,不同厂商的手机获取到的值会有所不同

比如:当activity处于前台的时候,adj值是0
在这里插入图片描述
当按下home键回到桌面,adj值会变大
在这里插入图片描述
当按下返回键,adj值会变得更大,也就意味着进程优先级变得越来越低


进程保活

从上面那张图也可以看出,我们首先要做的就是将我们的进程的adj值尽量往绿色部分移,也就是降低adj值,白色部分的不用管;其实现方式有如下几种:

1px Activity

在一些设备上,有些第三方应用或者系统管理工具在检测到锁屏事件后一段时间内还存在的一些后台进程,并将其回收已达到省电和释放内存的目的;那么我们就在锁屏后将应用置位前台进程,因为系统基本不会杀死前台进程,做法就是当手机屏幕关闭时打开一个像素的Activity,降低adj值,提高应用进程的优先级

具体实现:注册一个广播,监听系统发出的关屏和开屏的广播,当关屏时开启一个像素的Activity,开屏时销毁这个Activity

首先定义一个像素的Activity

public class KeepActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.act_keep);
        Window window = getWindow();
        //设置Activity处于左上角
        window.setGravity(Gravity.START | Gravity.TOP);
        WindowManager.LayoutParams attr = window.getAttributes();
        //设置宽高都是1px
        attr.width = 1;
        attr.height = 1;
        //设置坐标
        attr.x = 0;
        attr.y = 0;
        window.setAttributes(attr);
        KeepManager.getManager().setKeepActivity(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.e("KeepActivity","onResume");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.e("KeepActivity","onDestroy");
    }
}

在Manifest中注册

        <activity android:name=".onepx.activity.KeepActivity"
                  android:excludeFromRecents="true"
            	  android:exported="false"
            	  android:finishOnTaskLaunch="false"
            	  android:launchMode="singleInstance"
                  android:theme="@style/KeepTheme">
        </activity>

主题设置

    <style name="KeepTheme">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowAnimationStyle">@null</item>
        <item name="android:windowNoTitle">true</item>
    </style>

接下来就要定义广播

public class KeyReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

        String action = intent.getAction();
        Log.e("KeyReceiver","action="+action);
        if (TextUtils.equals(action,Intent.ACTION_SCREEN_OFF)) {
            //当屏幕关闭时
            KeepManager.getManager().startKeepActivity(context);
        } else if (TextUtils.equals(action,Intent.ACTION_SCREEN_ON)) {
            //当屏幕开启
            KeepManager.getManager().finishActivity();
        }
    }
}

动态注册或者静态注册都行

<receiver android:name=".onepx.receiver.KeyReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.SCREEN_OFF"></action>
        <action android:name="android.intent.action.SCREEN_ON"></action>
    </intent-filter>
</receiver>

这里就是要当关屏时开启一个像素的Activity,让应用处于前台进程;开屏时销毁这个Activity;这些事情通过一个工具类管理

public class KeepManager {

    private static class Instance {
        private static final KeepManager INSTANCE = new KeepManager();
    }
    private KeepManager(){}
    public static KeepManager getManager(){
        return Instance.INSTANCE;
    }

    private KeyReceiver mKeyReceiver;
    private WeakReference<Activity> mKeepAct;

    /**
     * 注册开关屏广播
     * @param context
     */
    public void registerReceiver(Context context){
        if (mKeyReceiver != null) return;
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_SCREEN_ON);
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        mKeyReceiver = new KeyReceiver();
        context.registerReceiver(mKeyReceiver,filter);
    }

    /**
     * 注销广播
     * @param context
     */
    public void unRegisterReceiver(Context context){
        if (mKeyReceiver != null) {
            context.unregisterReceiver(mKeyReceiver);
        }
    }

    public void setKeepActivity(KeepActivity activity){
        mKeepAct = new WeakReference<Activity>(activity);
    }

    /**
     * 关屏时打开1px的Activity
     * @param context
     */
    public void startKeepActivity(Context context){
        Intent intent = new Intent(context,KeepActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }

    /**
     * 开屏时把Activity销毁
     */
    public void finishActivity(){
        if (mKeepAct != null) {
            Activity activity = mKeepAct.get();
            if (activity != null) {
                activity.finish();
            }
            mKeepAct = null;
        }
    }
}

代码比较简单,就不做过多叙述了,最终的效果就是你退出了应用,变成了后台进程;然后按下电源键关屏屏幕,此时应用内的广播接收器接收到关屏广播,然后打开一个像素的Activity,让应用成为前台进程,大大降低被系统杀死的几率

这种方案的局限性就是必须要用户关屏后你的方案才能发挥作用,要是用户一直在使用的过程中你的进程在后台被回收了,那这个方法也就无效了

同时要注意在Android8.0开始,静态注册是收不到这些系统广播了,需要改为动态注册;但是动态注册退出APP后又要注销广播,那这方案就失效了,除非用户按home键退出,然后锁屏

前台Service

第一种情况:

在API18以下,直接调用startForeground(ID, new Notification()),发送空的Notification ,将后台服务推到前台成为前台Service,这样即使退出应用,或者在Recent Task中删除该应用,前台Service依然存在,应用的优先级依然很高

这样能实现的原理其实是Android的一个漏洞,发送一个空的Notification,通知栏并不会显示我们发送的Notification,这样用户就不知道你的应用还是在运行了,真正的做到了用户无感知下的进程保活

public class ForegroundService extends Service {


    private class ProcessBinder extends Binder{}

    @Override
    public IBinder onBind(Intent intent) {
        return new ProcessBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }
        @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        startForeground(NOTIFICATION_ID, new Notification());
        return super.onStartCommand(intent, flags, startId);
    }
}

如图:
在这里插入图片描述

第二种情况:

从api 18(4.3)开始,Google已经意识到了这个漏洞,因为非常多的流氓应用都这样处理,即使你发送一个空的Notification,在通知栏也会有一个通知框提示你的应用正常运行,如图:

在这里插入图片描述
这样用户大概率会主动将你的应用杀掉,那我们怎么继续做到用户无感知呢?看下面

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //API 18[4.3]以下,直接发送Notification并将其置为前台
        if (Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN_MR2) {
            startForeground(NOTIFICATION_ID, new Notification());
        } else {
            //API 18开始,发送Notification并将其置为前台后,启动InnerService
            startForeground(NOTIFICATION_ID, new Notification());
            startService(new Intent(this, InnerService.class));
        } 
        return super.onStartCommand(intent, flags, startId);
    }

    public  static class  InnerService extends Service{
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }

        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            //发送与FrontService中ID相同的Notification,覆盖上面的,然后将其停止并结束自己
            //因为两个通知ID相同,所以结束掉一个相当于两个通知都结束了
            startForeground(NOTIFICATION_ID, new Notification());
            stopForeground(true);
            stopSelf();
            return super.onStartCommand(intent, flags, startId);
        }
    }

我们知道唯一确定一个通知是根据id,如果我们先发送通知,然后再开启一个服务,在里面发送另一个id一样的通知,最后将第二个前台服务停止并销毁自己,这样的结果就是第一次发送的通知也会被取消掉,也就是在下拉栏看不到通知框了,用户自然也看不到任何你的应用还在运行的信息了;但是这在某些设备上会有一些问题,发送前台通知会唤醒设备并点亮屏幕,这样会很耗电

第三种情况:

到了Android 26,Notification 需要设置channel,也就是NotificationChannel 对象,可以理解为渠道,就是做一下通知的兼容处理

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //API 18[4.3]以下,直接发送空的Notification并将其置为前台,可以不显示通知栏消息
        if (Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN_MR2) {
            startForeground(NOTIFICATION_ID, new Notification());
        } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            //API 18-26,发送Notification并将其置为前台后,启动InnerService
            startForeground(NOTIFICATION_ID, new Notification());
            startService(new Intent(this, InnerService.class));
        } else {
            NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            if (manager != null) {
                //进行渠道设置
                //第三个参数值越小,该通知的重要性越低,因为我们只需要开启前台服务,不需要让用户知道
                NotificationChannel channel = new NotificationChannel("channel","keep",NotificationManager.IMPORTANCE_MIN);
                manager.createNotificationChannel(channel);
                Notification notification = new NotificationCompat.Builder(this,"channel").build();
                startForeground(NOTIFICATION_ID,notification);
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }

这样操作以后,应用在退出后,进程adj值基本上还是在1-3之间,这种方案可以说是使用的比较广的一种了,使用简单,通过前台服务将自己的进程优先级大大的提高

8.0兼容处理:如果是在后台启动ForegroundService ,那么一定要注意,从Android api26开始(即8.0),Google对后台启动Service做了限制,即不允许后台应用启动Service,什么叫后台应用呢?先看看什么是前台应用:

  • 具有可见 Activity(不管该 Activity 已启动还是已暂停)
  • 具有前台服务
  • 另一个前台应用已关联到该应用(不管是通过绑定到其中一个服务,还是通过使用其中一个内容提供程序)

如果以上条件均不满足,应用将被视为处于后台,这时创建Service将会报错

解决办法:Android 8.0 引入了一种全新的方法,即 Context.startForegroundService()

if (Build.VERSION.SDK_INT >= 26) {
    Context.startForegroundService(service);
} else {
    Context.startService(service);
}

启动完前台service, 一定记得在5s以内要执行如下类似代码, 否则程序会报ANR问题

if (Build.VERSION.SDK_INT >= 26) {
      startForeground(1, new Notification());
  }

9.0兼容处理:需要在AndroidManifest中添加权限:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

进程拉活

上面说的两种方式是【保活】,重点是保,也就是说应用其实没死,我们只是通过这些方法提高它的生存几率【降低adj值】,如果应用被用户在设置界面强行停止或者被360等软件强行杀死,那上面两种方案也行不通了

这样就要讲到另一种说法了,也就是【拉活】,意思就是进程死了再将其拉起来,死而复生

粘性Service

这种是系统提供的合法方案,根据onStartCommand方法返回值操作;系统内存是有限的,当系统内存资源不足,Service是会被销毁,如果你在Service里做了什么重要事情,那被销毁显然是你不愿意看到的,所以要有一种方法让系统帮我们重启该服务,那要不要重启就由这个返回值决定了,看下方:

  • START_STICKY:如果service进程被kill掉,系统会尝试重新创建Service,如果在此期间没有任何启动命令被传递到Service,那么参数intent将为null。
  • START_NOT_STICKY:使用这个返回值时,如果在执行完onStartCommand()后,服务被异常kill掉,系统不会自动重启该服务。
  • START_REDELIVER_INTENT:使用这个返回值时,如果在执行完onStartCommand()后,服务被异常kill掉,系统会自动重启该服务,并将intent的值传入。
  • START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保证服务被kill后一定能重启。

作为一个后台常驻Service,不建议在使用时通过Intent传递数据,所以我们返回START_STICKY和START_STICKY_COMPATIBILITY即可;不过这只是在原生系统上是这样操作,至于国内各个厂商深度定制后就不一定能保证重启了;同时Service第一次被异常杀死后会在5S内重启,第二次被杀后10S内重启,第三次就是20S,如果短时间内被杀5次,系统就不会再将其拉起,所以单单靠这个还是不保险的

JobScheduler

相对于隐式广播和Service,Google推荐使用JobScheduler,它是在Android 5.0推出来的API,允许开发者创建在后台执行的Job,当到达未来某个时间点或者未来满足某种条件(比如插入电源或者连接WiFi)的情况下,这些Job将会在后台被执行。和AlarmManager类似,但有点不一样,执行这些操作的时间并不是严格准确的。 JobScheduler会把一系列的job收集起来一起执行,这样既允许我们的job被执行,又能兼顾到手机电量的使用情况,达到节电的目的。

JobScheduler是一个系统提供的框架,旨于在应用进程、而非系统进程内执行各种作业调度,其原理是启动通过bindservice的方式启动应用进程的service,并在Service中进行作业。在执行一个Job时,将会使得系统持有一个WakeLock锁,以防止系统休眠进入Suspend。
在创建一个作业时,会设置多个约束条件,比如可以指定特定的网络、是否只在充电时执行作业等,JobScheduler框架会根据这些约束条件,智能地执行作业,并尽可能对作业进行批操作和推迟,以防止频繁唤醒系统而影响功耗,还可以指定该Job的执行的截至期限。如果不指定一个作业的截至期限,那么该作业可能会在任意一个时刻运行,这取决于JobScheduler的内部队列。

要想使用JobScheduler,必须使项目最小API达到21,目前还没有support library提供低版本的兼容;JobScheduler根据其字面意思理解为工作日程表,也就是将所有应用添加的工作进行统一调度执行;它的使用比较简单,JobScheduler框架为应用提供了如下四个组件,通过这四个类的API可以让用户在应用中创建一个作业,并让系统对他进行调度

  • JobScheduler:JobScheduler类负责将应用需要执行的作业发送给框架,以准备对该应用Job的调度。JobScheduler是一个系统服务,可通过如下方式获取:

    JobScheduler jobScheduler = (JobScheduler) Context.getSystemService(Context.JOB_SCHEDULER_SERVICE). 
    
  • JobInfo:JobInfo是传递给JobScheduler类的数据容器,它封装了针对调用应用程序调度作业所需的各种约束,也可以认为一个JobInfo对象对应一个作业,JobInfo对象通过JobInfo.Builder创建。它将作为参数传递给JobScheduler

  • JobInfo.Builder:JobInfo.Builder是JobInfo的一个内部类,顾名思义,它就是用来创建JobInfo的Builder类

  • JobService:JobService是一个继承于Service的抽象类,他作为系统回调执行作业内容的终端,JobScheduler框架将通过bindService()方式来启动该服务.因此,用户必须在应用程序中创建一个JobService的子类,并实现其onStartJob()等回调方法,以及在清单文件中对它授予如下权限:

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

使用方法如下:

第一步:创建JobService的子类

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class JobSchedulerService extends JobService {

    
    @Override
    public boolean onStartJob(JobParameters params) {
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        return false;
    }
}

我们需要重写这两个回调方法,它们都是系统回调的

  • onStartJob:当系统要触发执行我们的Job的时候,会调用onStartJob方法。这个方法会返回一个布尔型的值:

    • 返回false:表明该方法内的任务已经完成,同时系统会认为当前已经没有任务在运行, 就不会调用对应的onStopJob方法了
    • 返回true:也就是告诉系统,我这里在做一个耗时任务(要注意,这个方法在主线程回调,所以耗时任务要放到子线程执行),即使该方法已经返回,我们的工作还在异步执行,这时候系统就会把任务的结束调用交给用户去做,所以当任务结束时,我们需要手动调用jobFinished方法;如果不调用jobFinished,系统会一直认为我们在执行当前Job,那么系统就不会再入队本应用其它的Job去执行,也就是说JobScheduler的执行队列就会被阻塞了
  • onStopJob:当系统收到取消请求且onStartJob返回true的时候才会调用onStopJob,否则不调用

实际操作中这个类就类似于下面这样了

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class JobSchedulerService extends JobService {

    private final String TAG = JobSchedulerService.class.getSimpleName();

    @Override
    public boolean onStartJob(JobParameters params) {
        Log.e(TAG,"onStartJob");
        //将耗时任务放到子线程执行
        new Thread(new Task(params)).start();
        //返回true,意味着我们需要做耗时操作,需要手动调用jobFinished方法
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        Log.e(TAG,"onStopJob");
        return false;
    }

    class Task implements Runnable {
        JobParameters params;

        public Task(JobParameters params) {
            this.params = params;
        }

        @Override
        public void run() {
            try {
                //做一些耗时操作
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                jobFinished(params,false);
            }

        }
    }

}

注意jobFinished方法有两个参数,第一个是onStartJob方法的参数,第二参数表示是否在条件满足时重复执行该任务

第二步:创建JobScheduler,开启任务

mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName componentName = new ComponentName(this,JobSchedulerService.class);
JobInfo.Builder builder = new JobInfo.Builder(++mJobId,componentName);
builder.setPeriodic(10*1000);
int result = mJobScheduler.schedule(builder.build());
Log.e(TAG,"result="+result);

setPeriodic方法是设置任务执行周期,这里是每10秒钟执行一次,但是实际过程中并不一定会严格按照这个周期执行
schedule方法有一个返回值,当schedule失败的时候会返回一个负数,成功的时候会返回我们在创建JobInfo.Builder时传入的JobId

第三步:取消任务

如果需要在某个时间段取消任务,进行如下操作:

@Override
protected void onDestroy() {
    super.onDestroy();
    //根据JobId取消某个任务
    mJobScheduler.cancel(mJobId);
    //也可以取消全部任务
    mJobScheduler.cancelAll();
}

所以要想使用JobScheduler实现进程保活或者拉活有两种方法:

第一种:每隔一段时间执行周期性作业

 mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
 ComponentName componentName = new ComponentName(this,JobSchedulerService.class);
 JobInfo.Builder builder = new JobInfo.Builder(++mJobId,componentName);
 mJobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
 if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
     mJobBuilder.setMinimumLatency(10 * 1000);
 } else {
     mJobBuilder.setPeriodic( 10 * 1000);
 }
 mJobScheduler.schedule(builder.build());

这就是每隔10秒钟执行一次JobSchedulerService,它里面可以什么逻辑都不写,但是onStartJob要返回true,这种能实现保活是因为JobSchedulerService实际上就是一个Service,当启动该Service时,如果其所在进程不存在,系统会创建相应的进程

但是要做一下版本兼容处理,在Android 7.0以下,setPeriodic的值可以随意设置,但是从7.0开始Google对于新设备功耗要求越来越严格,对于APP的限制也越来约多,导致setPeriodic的值,也就说定期作业的间隔时间>=15分钟时才能运行。

第二种:每隔一段时间检查保活服务是否存在,不存在就将其启动;然后再创建一个新的JobScheduler任务,并结束当前JobScheduler任务。

public class JobSchedulerService extends JobService {

    private final String TAG = JobSchedulerService.class.getSimpleName();
    private int mJobId;

    @Override
    public boolean onStartJob(JobParameters params) {
        Log.e(TAG,"onStartJob");
        /**
         * 做版本兼容
         */
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                String servicename = params.getExtras().getString("servicename");
                Class service = getClassLoader().loadClass(servicename);
                if (service != null) {
                    //判断保活的service是否被杀死
                    if (!isServiceRunning(service)) {
                        //重启service
                        startService(new Intent(getApplicationContext(), service));
                    }
                }
                //创建一个新的JobScheduler任务
                scheduleRefresh(servicename);
                jobFinished(params, false);
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        Log.e(TAG,"onStopJob");
        return false;
    }

    private void scheduleRefresh(String serviceName) {

        if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return;

        JobScheduler mJobScheduler = (JobScheduler)getApplicationContext().getSystemService(JOB_SCHEDULER_SERVICE);

        JobInfo.Builder mJobBuilder =
                new JobInfo.Builder(++mJobId,new ComponentName(getPackageName(),JobSchedulerService.class.getName()));
        mJobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
        if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            mJobBuilder.setMinimumLatency(6 * 1000);
        } else {
            mJobBuilder.setPeriodic( 6 * 1000);
        }

        PersistableBundle persiBundle = new PersistableBundle();
        persiBundle.putString("servicename", serviceName);
        mJobBuilder.setExtras(persiBundle);

        if (mJobScheduler != null && mJobScheduler.schedule(mJobBuilder.build()) <= JobScheduler.RESULT_FAILURE) {
            Log.e(TAG, "schedule the service FAILURE!");
        }else{
            Log.e(TAG, "7.0 schedule the service SUCCESS!");
        }
    }

    /**
     * 判断保活Service是否启动
     * @param serviceClass
     * @return
     */
    public boolean isServiceRunning(Class<?> serviceClass) {
        ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningServiceInfo runningService : manager.getRunningServices(Integer.MAX_VALUE)) {
            if (serviceClass.getName().equals(runningService.service.getClassName())) {
                return true;
            }
        }
        return false;
    }

}

Doze模式:在Android 6.0之后Google推出了一个Doze模式,即休眠、打盹之意。是为了延长电池使用寿命的一种节能方式,它的核心思想是在手机处于屏幕熄灭、不插电或静止不动一段时间后,手机会自动进入Doze模式。处于Doze模式的手机将停止所有非系统应用的WalkLocks、网络访问、闹钟、GPS/WIFI扫描、JobSheduler活动。当进入Doze模式的手机屏幕被点亮、移动或充电时,会立即从Doze模式恢复到正常,系统继续执行被Doze模式"冷冻"的各项活动。

换而言之,Doze模式不会杀死进程,只是停止了进程相关的耗电活动,使其进入"休眠"状态。至Android N(即Android 7.0)后,谷歌进一步对Doze休眠机制进行了优化,休眠机制的应用场景和使用规则进行了扩展。Doze在Android 6.0中需要将手机平行放置一段时间才能开启,在7.0中则可随时开启。

因此:

  • 对于Android 5.0:JobSheduler的唤醒是非常有效的;
  • 对于Android 6.0:虽然谷歌引入了Doze模式,但通常很难真正进入Doze模式,所以JobSheduler的唤醒依然有效;
  • 对于Android 7.0:JobSheduler的唤醒会有一定的影响,我们可以在电池管理中给APP开绿色通道,防止手机Doze模式后阻止APP使用JobSheduler功能。

如果遇到深度定制机型,这就要看运气了

双进程守护

所谓双进程(是Java层的双进程),就是一个主进程和一个子进程,一个前台Service放在主进程,一个前台Service放在子进程,只要有一个进程挂了,另外一个进程就将其拉起来

这个实现也是很简单的,主要就是创建两个Service

public class LocalService extends ForegroundService {

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //启动远程服务
        bindService(new Intent(LocalService.this,RemoteService.class),connection,Service.BIND_IMPORTANT);
        return super.onStartCommand(intent, flags, startId);
    }

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            //当与远程服务断开连接时,即守护进程挂了,那从新将远程服务启动,也就将守护进程拉起来了
            startService(new Intent(LocalService.this,RemoteService.class));
            bindService(new Intent(LocalService.this,RemoteService.class),connection,Service.BIND_IMPORTANT);
        }
    };
}

public class RemoteService extends ForegroundService {

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //启动本地服务
        bindService(new Intent(RemoteService.this,LocalService.class),connection,Service.BIND_IMPORTANT);
        return super.onStartCommand(intent, flags, startId);
    }

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            //当与本地服务断开连接时,即主进程挂了,那从新将本地服务启动,也就将主进程拉起来了
            startService(new Intent(RemoteService.this,LocalService.class));
            bindService(new Intent(RemoteService.this,LocalService.class),connection,Service.BIND_IMPORTANT);
        }
    };
}

配置文件

        <service android:name=".doubleprocess.LocalService"></service>
        <service android:name=".doubleprocess.RemoteService"
            android:process=":remote"></service>
  • 两个Service都继承前面讲的前台服务
  • 重点是将RemoteService置于一个子进程中
  • 当onServiceDisconnected方法被调用时,说明对方断开了连接,基本上是对方进程挂了,然后就将其启动起来就行了

Android5.0以后:

注意:在Android5.0以前,AMS在回收应用内存时的做法是

在这里插入图片描述
即回收内存时将主进程杀死,但是归属这个应用的其它子进程并没有杀死,这就给很多应用提供了双进程守护的机会

然而Google也发现了这个漏洞,于是在Android5.0以后就变成了这样

在这里插入图片描述
不仅把主进程给杀死,还把主进程所属的进程组一并杀死,所以说如果是系统将其进程组里的进程都杀死,那双进程守护也失效了(如果是用户在设置界面强行停止你的应用,双进程守护同样失效),当然了如果系统杀死进程所花的时间比你拉活逻辑的时间要久,那你的拉活还是能成功的

全家桶唤醒

这里指的是不同应用的进程之间相互唤醒,这种方式也是最无耻的了,比如你的手机装了百度全家桶,阿里全家桶,腾讯全家桶等归属一家公司的应用,那么每个公司的一堆应用就开始拉帮结派了,只要一个应用被打开,结果可能是全家桶中的所有应用都被唤醒了,不同APP之间相互唤醒主要通过广播

不光APP之间能唤醒,有些SDK也能唤醒APP,比如你的应用接入微信SDK,那么你的应用打开后,也会唤醒微信

像微信这样的应用在普通手机里可能有二三十条唤醒路径,其他的APP也都不是神马善类,同样几十条唤醒路径。所以你就可以知道为什么Android机子会慢慢卡成一坨翔

如果你的手机里安装了支付宝,天猫,淘宝,闲鱼等阿里系应用,可以试下打开一个应用,其它应用是不是也被拉起来了

系统广播唤醒

我们知道系统的很多事件产生后会发出一条广播,比如开机后会发一条开机广播,网络切换、拍照、拍视频等都会发出广播,于是很多应用就注册了非常多的广播,当接收到这些广播后就提高应用进程优先级,这可以说是相当恶心的做法了

Google可能也意识到这个问题了,在Android 7.0中做了后台优化,如图

在这里插入图片描述
大概意思是删除了三项隐式广播CONNECTIVITY_CHANGE,ACTION_NEW_PICTURE ,ACTION_NEW_VIDEO,以帮助优化内存使用和电量消耗

  • 在Android N平台下即使在Manifest.xml清单文件中注册了 CONNECTIVITY_ACTION广播,在网络发生变化时也不会接收到任何的信息。但是正在前台运行的应用程序如果在主线程中通过Context.registerReceiver()动态注册了CONNECTIVITY_ACTION广播,该应用程序仍然可以接收到该广播。(注:这样开发者就可以根据不同的网络状态加载相应的页面信息了,从而提高用户体验)。

  • 应用程序无法发送或接收 ACTION_NEW_PICTURE(拍照) 或 ACTION_NEW_VIDEO(录像) 广播。此项优化会影响所有应用,而不仅仅是面向 Android N 的应用。

这无疑是给了很多应用开发商一个沉重的打击

重点:而在Android 8.0中,Google做的更狠,除了官方文档列出的,其余所有的隐式广播都被移除了;Google 认为应用程序在其 manifest 中注册了太多没必要的 BraodcastReceiver,导致了不必要的耗电。比如,很多的应用和第三方 SDK 都会监听 CONNECTIVITY_ACTION 广播。当你离开家,断开了家里的 wifi。Android 发送 CONNECTIVITY_ACTION 广播,结果几乎所有的应用都会被唤醒并对此作出反应。于是Google移除了大量的隐式广播

Android 8.0行为变更
国内镜像

所以使用静态注册广播的朋友们要注意版本变化了

总结

站在一个Android开发者的立场来说,极力告诫大家不要这么干,为了维护好Android应用环境,给用户一个良好的用户体验,当用户退出应用后,就应该让这个应用彻底销毁掉,释放出系统资源;有的人可能说了我的应用需要实时上报数据,但是用户都退出了,你还上报个鸡毛啊,明显是站着茅坑不拉屎的干活;这种需求完全可以放在当手机处于充电且应用存活的状态下去实现,所以说你的应用是不是真的需要常驻后台,需要认真考虑下其实现方式;当然了,如果你碰到了一个做过一阵子开发就以为了解全世界的上司,那只能放弃抵抗了

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值