Android后台保活机制,应用进程长存的可行性分析

转自:https://blog.csdn.net/hlq19901005/article/details/53818186

一.什么才叫后台常驻

应用位于后台时不被干掉,或者被干掉后依然能顽强地重新启动起来。

被干掉分为两种情况:

第一种:当系统资源不足的时候or基于某种系统自身的后台运行规则选择干掉你的后台应用来获得更多的资源(定制系统);

第二种:用户手动调用某些安全软件的清理功能干掉你的后台应用。

Android杀应用实质上是杀进程,Android应用启动后都会对应一个主进程和若干子进程。

二.后台进程常驻的策略与选择
1.怎样让应用位于后台时不被干掉(a.轻量化进程   b.进程提权)

Android进程的生命周期,Android将一个进程分为五种不同的状态:
(坑:Android碎片化严重,定制的ROM都会修改一部分系统逻辑来做所谓的优化,有可能无效甚至出现诡异的现象)
    前台进程 Foreground process

处于该状态下的进程表示其当前正在与用户交互,是必须存在的,无论如何系统都不会去干掉一个前台进程除非系统出现错误或者说用户手动杀掉。那么系统是通过怎样的一个规则去判断某个进程是否前台进程呢?下面是一些具体的情景:

    某个进程持有一个正在与用户交互的Activity并且该Activity正处于resume的状态。
    某个进程持有一个Service,并且该Service与用户正在交互的Activity绑定。
    某个进程持有一个Service,并且该Service调用startForeground()方法使之位于前台运行。
    某个进程持有一个Service,并且该Service正在执行它的某个生命周期回调方法,比如onCreate()、 onStart()或onDestroy()。
    某个进程持有一个BroadcastReceiver,并且该BroadcastReceiver正在执行其onReceive()方法。

    可见进程 Visible process

可见进程与前台进程相比要简单得多,首先可见进程不包含任何前台组件,也就是说不会出现上述前台进程的任何情境,其次,可见进程依然会影响用户在屏幕上所能看到的内容,一般来说常见的可见进程情景可以分为两种:

    某个进程持有一个Activity且该Activty并非位于前台但仍能被用户所看到,从代码的逻辑上来讲就是调用了onPause()后还没调用onStop()的状态,从视觉效果来讲常见的情况就是当一个Activity弹出一个非全屏的Dialog时。
    某个进程持有一个Service并且这个Service和一个可见(或前台)的Activity绑定。

服务进程 Service process

服务进程要好理解很多,如果某个进程中运行着一个Service且该Service是通过startService()启动也就是说没有与任何Activity绑定且并不属于上述的两种进程状态,那么该进程就是一个服务进程。
后台进程 Background process

差不多就是按HOME键的那种状态,也就是说当Activity隐藏到后台但未退出时,后台进程会被系统存储在一个LRU表中以确保最近使用的进程最后被销毁。
空进程 Empty process

空进程很好理解,当某个进程不包含任何活跃的组件时该进程就会被置为空进程,空进程很容易会被系统盯上而被干掉,但是如果系统资源充足,空进程也可以存活很久。

反映在代码中,我们可以看看进程的Importance等级以及adj值,这两个玩意是具体决定了系统在资源吃紧的情况下该杀掉哪些进程。

(Importance等级:ActivityManager.RunningAppProcessInfo  ;adj值:Android SDK里边去找 D:\android\sdk\sources\android-23\com\android\server\am)
Android平台App进程优先级:http://www.jianshu.com/p/6201dc3a447a   (cat /proc/1728/oom_adj)
进程提权

我们上面曾说到adj值越小的进程越不容易被杀死,相对普通进程来说能让adj去到0显然是最完美的,可是我们如何才能让一个完全没有可见元素的后台进程拥有前台进程的状态呢?Android给了Service这样一个功能:startForeground,它的作用就像其名字一样,将我们的Service置为前台,不过你需要发送一个Notification:

/**
     * 1.防止重复启动,可以任意调用startService(Intent i);
     * 2.利用漏洞启动前台服务而不显示通知;
     * 3.在子线程中运行定时任务,处理了运行前检查和销毁时保存的问题;
     * 4.启动守护服务.
     * 5.简单守护开机广播.
     */
    private int onStart(Intent intent, int flags, int startId) {
        //启动前台服务而不显示通知的漏洞已在 API Level 25(7.1系统) 修复。
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
            //利用漏洞在 API Level 17 及以下的 Android 系统中,启动前台服务而不显示通知
            startForeground(sHashCode, new Notification());
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                //利用漏洞在 API Level 18 (4.3)及以上的 Android 系统中,启动前台服务而不显示通知
                startService(new Intent(this, WorkNotificationService.class));

//                //前台看得见的通知
//                Notification.Builder builder = new Notification.Builder(this);
//                builder.setSmallIcon(R.mipmap.ic_launcher);
//                startForeground(sHashCode, builder.build());
            }
        }

值得注意的是在Android 4.3以前我们可以通过构造一个空的Notification,这时通知栏并不会显示我们发送的Notification,但是自从4.3以后谷歌似乎意识到了这个问题,太多流氓应用通过此方法强制让自身悄无声息置为前台,于是从4.3开始谷歌不再允许构造空的Notification,如果你想将应用置为前台那么请发送一个可见的Notification以告知用户你的应用进程依然在后台运行,这么就比较恶心了,本来我的进程是想后台龌龊地运行,这下非要让老子暴露出来,因此我们得想办法将这个Notification给干掉。上面的代码中我们在发送Notification的时候给了其一个唯一ID,那么问题来了,假设我启动另一个Service同时也让其发送一个Notification使自己置为前台,并且这个Notification的标志值也跟上面的一样,然后再把它取消掉再停止掉这个Service的前台显示会怎样呢:
 

public static class WorkNotificationService extends Service {

        /**
         * 利用漏洞在 API Level 18 及以上的 Android 系统中,启动前台服务而不显示通知
         */
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            startForeground(WorkService.sHashCode, new Notification());
            stopSelf();
            return START_STICKY;
        }

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

如上代码所示,自导自演装了一次逼。虽然我们通过WorkNotificationService干掉了前台显示需要的Notification,但是,请大家查看一下当前进程的adj值,我们进程竟然还是可见进程!(但是大家需要注意的是,这个漏洞在7.1的系统以上又被google修复了,所以想设置前台通知又不想让用户知道已经game over)

130|root@HWATH:/ # cat /proc/28587/oom_adj
2
root@HWATH:/ #

2.怎么满血复活
上面已经说过了一些怎么尽量不让系统干掉进程的策略,但是还是不能保证进程不被kill,除非你跟系统有关系有契约,说白了就是ROM是定制的且可以给你开特殊权限。下面就来说说怎么在进程被kill掉后自动重启。
Service重启

Android的Service是一个非常特殊的组件,按照官方的说法是用于处理应用一些不可见的后台操作,对于Service我们经常使用,也知道通过在onStartCommand方法中返回不同的值可以告知系统让系统在Service因为资源吃紧被干掉后可以在资源不紧张时重启:

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_REDELIVER_INTENT;
    }

 

关于onStartCommand方法的返回值,系统一共提供了四个:
START_STICKY

如果Service进程因为系统资源吃紧而被杀掉,则保留Service的状态为起始状态,但不保留传递过来的Intent对象,随后当系统资源不紧张时系统会尝试重新创建Service,由于服务状态为开始状态,所以创建服务后一定会调用onStartCommand方法,如果在此期间没有任何启动命令被传递到Service,那么参数Intent将为null。
START_STICKY_COMPATIBILITY

START_STICKY的兼容版本,不同的是其不保证服务被杀后一定能重启。
START_NOT_STICKY

与START_STICKY恰恰相反,如果返回该值,则在执行完onStartCommand方法后如果Service被杀掉系统将不会重启该服务。
START_REDELIVER_INTENT

同样地该值与START_STICKY不同的是START_STICKY重启后不会再传递之前的Intent,但如果返回该值的话系统会将上次的Intent重新传入。

同时需要注意的是,默认情况下Service的返回值就是START_STICKY或START_STICKY_COMPATIBILITY:

public int onStartCommand(Intent intent, int flags, int startId) {
    onStart(intent, startId);
    return mStartCompatibility ? START_STICKY_COMPATIBILITY : START_STICKY;
}

虽然Service默认情况下是可以被系统重启的,但是在某些情况or某些定制ROM上会因为各种原因而失效,因此我们不能单靠这个返回值来达到进程重启的目的。

进程守护

关于进程守护,其逻辑也很简单,AB两个进程,A进程被杀死,B将其拉起,同样B进程被杀死,A进程将其拉起,而我们的后台逻辑则随便放在某个进程里执行即可,一个简单的例子是使用两个Service。除掉安全软件等情况,一般来说进程的被杀总是有个先后顺序,不存在一下子多个进程同时被干掉的情况。这里又有2种方案:

第一种:A进程里面轮询检查B进程是否存活,没存活的话将其拉起,同样B进程里面轮询检查A进程是否存活,没存活的话也将其拉起。

补充:检测服务是否在运行的方法,就是获取所有正在运行的服务,一一与相应的服务名称做比较:

public static boolean isServiceRunning(String serviceClassName){
    final ActivityManager activityManager = (ActivityManager)Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
    final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE); //这个value取任意大于1的值,但返回的列表大小可能比这个值小。

    for (RunningServiceInfo runningServiceInfo : services) {
        if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
            return true;
        }
    }
    return false;
}

上述的方式可以在很大程度上在进程被杀后拉起进程,理论上来说轮询间隔时间越小越容易在双方都被杀死前唤醒对方,但是业务逻辑本身就复杂的话,建议还是不要将该值设置太小,否则对系统来说是一种负担,同时也会使你的进程更容易更频繁地让系统杀死。

第二种:建立AB进程间通道,谁死了就通知另外一个进程,将其拉活。具体方法,实现AIDL接口:

private class MyBilder extends IMyAidlInterface.Stub implements IBinder {

    @Override
    public void doSomething() throws RemoteException {
        Log.e("TAG", "绑定成功!");
        Intent localService = new Intent(LocalService.this, RemoteService.class);
        LocalService.this.startService(localService);
        LocalService.this.bindService(new Intent(LocalService.this, RemoteService.class),
                connection, Context.BIND_ABOVE_CLIENT);
    }
}


Receiver触发和AlarmManager 定时触发
为什么要把广播和AlarmManager一起讲,这里面是有一个同样的坑。后面我们再来深究。先来看看这两个复活方式。

Receiver触发:使用Receiver来检测目标进程是否存活不失为一个好方法,静态注册一系列广播,什么开机启动、网络状态变化、时区地区变化、充电状态变化等等等等,这听起来好像很6,而且在大部分手机中都是可行的方案,但是对于部分深度定制的ROM,我们避开就不谈了。

<receiver
    android:name=".receivers.WakeUpReceiver"
    android:process=":run">
    <intent-filter android:priority="90000">
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        <action android:name="android.intent.action.USER_PRESENT" />
        <action android:name="android.intent.action.DATE_CHANGED" />
        <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />

        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</receiver>

AlarmManager 定时触发:使用AlarmManage间隔一定的时间来检测并唤醒进程不失为一个好方法,虽然说从Android 4.4和小米的某些版本开始AlarmManage已经变得不再准确但是对我们拉活进程来说并不需要太精确的时间,对于4.4以前的版本,我们只需通过AlarmManage的setRepeating方法即可达到目的:

public static void startPollingService(Context context, int seconds, Class<?> cls, String action) {
    AlarmManager manager = (AlarmManager) context
            .getSystemService(Context.ALARM_SERVICE);
    Intent intent = new Intent(context, cls);
    intent.setAction(action);
    PendingIntent pendingIntent = PendingIntent.getService(context, 0,
            intent, PendingIntent.FLAG_UPDATE_CURRENT);
    long triggerAtTime = SystemClock.elapsedRealtime();
    manager.cancel(pendingIntent);
    manager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime,
            seconds * 1000, pendingIntent);
}

看到这里很多朋友会问是不是OK了啊?NO,下面就来说说这2个方法的坑:
        自从Android 3.1开始系统对我们的应用增加了一种叫做STOPPED的状态,什么叫STOPPED?就是所有处于STOPPED状态的应用都不可以接收到系统广播。

什么时候会造成STOPPED状态:

重未启动过的应用,原生的系统中,当应用初次启动后就会被标识为非STOPPED状态,而且再也没有机会被打回原形除非重新安装应用;但是一些定制系统,在清理应用时加入了将应用重置为STOPPED的逻辑。

与系统Service捆绑

Android系统提供给我们一系列的Service,注意这里我们所指的系统Service并非“SystemService”提供的那些玩意,而是类似于系统广播的便于我们使用的Service,常见常用的就是IntentService,当然还有其它更多更不常用的系统Service,那么为什么爱哥要在这里提到这玩意呢?因为某些系统Service一旦绑定就像拥有开了挂一样的权限,这在大部分机型包括某些深度定制系统上简直就像BUG般存在,以最BUG的NotificationListenerService为例,大家可能很少会用到这玩意,这玩意是用来读取通知的,也就是说只要是通知不管你谁发的,NotificationListenerService都可以检测到,使用它也很简单,和IntentService一样定义一个类继承一下即可:

    package com.aigestudio.daemon.core;
     
    import android.service.notification.NotificationListenerService;
    import android.service.notification.StatusBarNotification;
     
    /**
     * @author AigeStudio
     * @since 2016-05-05
     */
    public class DService extends NotificationListenerService {
        @Override
        public void onNotificationPosted(StatusBarNotification sbn) {
        }
     
        @Override
        public void onNotificationRemoved(StatusBarNotification sbn) {
        }
    }

 

里面什么逻辑都不用实现,是的你没听错,什么逻辑都不需要,然后在AndroidManifest中声明权限:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.aigestudio.daemon">
     
        <application>
            <service
                android:name=".core.DService"
                android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
                android:process=":service">
                <intent-filter>
                    <action android:name="android.service.notification.NotificationListenerService" />
                </intent-filter>
            </service>
        </application>
    </manifest>

 

这里为了区别主进程,我将该Service置于一个单独的进程中,然后启动应用,注意,这里我们的应用什么逻辑都没有,剔除掉上面所做的所有有关进程保护的逻辑,运行之后你发现看不到你NotificationListenerService所在的进程:

    AigeStudio:Android AigeStudio$ adb shell
    root@vbox86p:/ # ps|grep aigestudio
    u0_a61    9513  339   1002012 30452 ffffffff f74aa3b5 S com.aigestudio.daemon

 

先别急,NotificationListenerService是个特殊的系统Service,需要非常特别的权限,需要你手动在“设置-提示音和通知-通知使用权限”中打开,注意这个“通知使用权限”选项,如果你设备里没有需要使用通知使用权限换句话说就是没有含有NotificationListenerService的应用的话,这个设置选项是不可见的:
这里写图片描述
这时我们勾选我们的应用,会弹出一个提示框:
这里写图片描述
所以,你想好如何骗你的用户勾选这个勾勾了么,一旦勾上,一发不可收拾,这时你就会看到我们的进程启动起来了:

    root@vbox86p:/ # ps|grep aigestudio                                            
    u0_a61    9513  339   1003044 30532 ffffffff f74aa3b5 S com.aigestudio.daemon
    u0_a61    12869 339   993080 23792 ffffffff f74aa3b5 S com.aigestudio.daemon:service

 

好了,这时候,见证奇迹的时候来了,不管是某米、某族还是某某,请尝试下它们的一键清理,你会发现不管怎么杀,我们的进程都还在,除了一小部分名不经传的手机因为修改系统逻辑将其杀死外,绝大部分手机都不会杀掉该进程,为什么呢?好事的朋友一定会去check该进程的adj值:

    root@vbox86p:/ # ps|grep aigestudio                                            
    u0_a61    12869 339   993080 23792 ffffffff f74aa3b5 S com.aigestudio.daemon:service
    root@vbox86p:/ # cat /proc/12869/oom_adj
    0

 

你会发现我们的进程被置为前台进程了,而且不仅仅是这样哦,即便你重启设备开机,它也会首先被启动,因为其内部逻辑会使其在系统启动时绑定并开始监听通知,当然我们这里并没有任何关于通知的逻辑,那么你可能会问爱哥这又有什么用呢?我们又不能在NotificationListenerService里处理与通知不相关的逻辑,没错,是这样,但是我们也没必要啊,我们只需新建一个Service并使其与NotificationListenerService在同一进程下,那么我们的这个Sefvice不就一样不死了吗:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.aigestudio.daemon">
     
        <application>
            <service
                android:name=".services.DaemonService"
                android:process=":service" />
            <service
                android:name=".core.DService"
                android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
                android:process=":service">
                <intent-filter>
                    <action android:name="android.service.notification.NotificationListenerService" />
                </intent-filter>
            </service>
        </application>
    </manifest>

这种方式唯一的一个缺点就是你需要欺骗用户手动去开启通知权限,需要给用户一个合理的理由,所以对于跟通知权限根本不沾边的应用想想还是算了吧,用了这种方式安全软件都拿你没办法了。
KFC外带全家桶

以全家桶的方式去相互唤醒相互拉活是目前来说最稳定最安全的方式,各大牛逼点的应用都有类似行为,当然对于很多小应用来说,没有BAT那样的实力,不过你依然可以使用一些第三方的网络服务,比如XX推送,一旦设备上的某应用通过XX的推送获得消息时,是可以间接唤醒其它应用并推送与其相关的消息的。好了,就先扯这么多。
---------------------
作者:迷糊的胡胡
来源:CSDN
原文:https://blog.csdn.net/hlq19901005/article/details/53818186
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/blueangle17/article/details/86678533