Android service进程保护

Posted 天才少年_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android service进程保护相关的知识,希望对你有一定的参考价值。

应用进程保活基本就是围绕两个方面来展开:

1 尽量保证进程不被杀死。

2 进程被杀死后复活。细分如下:

1)Service重启

2)进程守护

3)Receiver触发

4) AlarmManager or JobScheduler循环触发

5)与系统Service捆绑-----可以不考虑,了解即可

下面将围绕这几点展开讨论。


一,基本概念

1.什么才叫应用进程保活

应用进程保活可以理解为应用位于后台永远不能被杀死。这里的可以简略地分为两种情况,第一种是当系统资源紧俏的时候或者基于某种系统自身的后台运行规则选择杀死你的后台应用来获得更多的资源,第二种是用户手动调用某些安全软件的清理功能干掉你的后台应用。对于android 5.0以前的系统我们可以考虑以上两种情况下的后台常驻,而对于Android 5.0以及以后的版本我们只能基于第一种情况考虑后台常驻,因为从Android 5.0开始对进程的管理更为严格,杀得也更为暴力。

2.Android进程的生命周期

Android系统会尽力保持应用的进程,但是有时为了给新的进程和更重要的进程回收一些内存空间,它会移除一些旧的进程。
为了决定哪些进程留下,哪些进程被杀死,系统根据在进程中在运行的组件及组件的状态,为每一个进程分配了一个优先级等级。
优先级最低的进程首先被杀死。
这个进程重要性的层次结构有五个等级,下面就列出这五种进程,按照重要性来排列,最重要的放在最前。

1)前台进程 Foreground process

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

某个进程持有一个正在与用户交互的Activity并且该Activity正处于resume的状态。
某个进程持有一个Service,并且该Service与用户正在交互的Activity绑定。
某个进程持有一个Service,并且该Service调用startForeground()方法使之位于前台运行。
某个进程持有一个Service,并且该Service正在执行它的某个生命周期回调方法,比如onCreate()、 onStart()或onDestroy()。
某个进程持有一个BroadcastReceiver,并且该BroadcastReceiver正在执行其onReceive()方法。
可以看到使进程位于前台的方法还是蛮多的,但是你要知道的事Android是一个碎片化非常严重的系统,很多定制的ROM都会修改一部分系统逻辑来做所谓的优化,所以说上述的情景以及下述我们将要讲到的其它进程状态其实都只能说可以在原生系统上完美生效而如果在一些定制ROM中则有可能无效甚至出现诡异的现象。

2)可见进程 Visible process

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

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

3)服务进程 Service process

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

4)后台进程 Background process

这里需要注意的是,我们这儿所说的后台进程只是指的进程的一种状态,与我们前后文提到的“后台进程”是两个概念,切记。当某个进程处于后台进程时,其一般会持有一个不可见的Activity,也就是说当Activity隐藏到后台但未退出时,从代码的逻辑上来讲就是该Activity的onStop被调用但onDestory未被执行的状态,后台进程会被系统存储在一个LRU表中以确保最近使用的进程最后被销毁。

5)空进程 Empty process

如果一个进程不包含任何活跃的应用组件,则认为是空进程。保存这种进程的唯一理由是为了缓存的需要,为了加快下次要启动这个进程中的组件时的启动时间。系统为了平衡进程缓存和底层内核缓存的资源,经常会杀死空进程。


这五种状态的进程相对于系统来说的重要性从上至下排列,空进程容易被杀死,其次是后台进程,然后是服务进程甚至是可见进程,而前台进程一般则不会被轻易干掉。系统杀进程会遵循一套规则,而这套规则则是建立在系统可用资源的基础上,打个比方,如果我的设备有高达3GB的运行内存且可用的内存还有2GB,那么即便是空进程系统也不会去干掉它,相反如果的设备只有256M的运行内存且可用内存不足16M,这时即便是可见进程也会被系统考虑干掉。这套依据系统资源来杀掉进程的规则Android称之为Low Memory Killer,而且Android在上述五种进程状态的基础上衍生出了更多的进程相关定义,比较重要的两个是进程的Importance等级以及adj值,关于这两个定义大家可以不必深究,但是要有一定的理解,这两个玩意是具体决定了系统在资源吃紧的情况下该杀掉哪些进程。其中Importance等级在ActivityManager.RunningAppProcessInfo中声明:

public static class RunningAppProcessInfo implements Parcelable {
   /**
     * Constant for {@link #importance}: This process is running the
     * foreground UI; that is, it is the thing currently at the top of the screen
     * that the user is interacting with.
     */
    public static final int IMPORTANCE_FOREGROUND = 100;

    /**
     * Constant for {@link #importance}: This process is running a foreground
     * service, for example to perform music playback even while the user is
     * not immediately in the app.  This generally indicates that the process
     * is doing something the user actively cares about.
     */
    public static final int IMPORTANCE_FOREGROUND_SERVICE = 125;

    /**
     * Constant for {@link #importance}: This process is running the foreground
     * UI, but the device is asleep so it is not visible to the user.  This means
     * the user is not really aware of the process, because they can not see or
     * interact with it, but it is quite important because it what they expect to
     * return to once unlocking the device.
     */
    public static final int IMPORTANCE_TOP_SLEEPING = 150;

    /**
     * Constant for {@link #importance}: This process is running something
     * that is actively visible to the user, though not in the immediate
     * foreground.  This may be running a window that is behind the current
     * foreground (so paused and with its state saved, not interacting with
     * the user, but visible to them to some degree); it may also be running
     * other services under the system's control that it inconsiders important.
     */
    public static final int IMPORTANCE_VISIBLE = 200;

    /**
     * Constant for {@link #importance}: This process is not something the user
     * is directly aware of, but is otherwise perceptable to them to some degree.
     */
    public static final int IMPORTANCE_PERCEPTIBLE = 130;

    /**
     * Constant for {@link #importance}: This process is running an
     * application that can not save its state, and thus can't be killed
     * while in the background.
     * @hide
     */
    public static final int IMPORTANCE_CANT_SAVE_STATE = 170;

    /**
     * Constant for {@link #importance}: This process is contains services
     * that should remain running.  These are background services apps have
     * started, not something the user is aware of, so they may be killed by
     * the system relatively freely (though it is generally desired that they
     * stay running as long as they want to).
     */
    public static final int IMPORTANCE_SERVICE = 300;

    /**
     * Constant for {@link #importance}: This process process contains
     * background code that is expendable.
     */
    public static final int IMPORTANCE_BACKGROUND = 400;

    /**
     * Constant for {@link #importance}: This process is empty of any
     * actively running code.
     */
    public static final int IMPORTANCE_EMPTY = 500;

    /**
     * Constant for {@link #importance}: This process does not exist.
     */
    public static final int IMPORTANCE_GONE = 1000;
}

而adj值则在ProcessList中定义:

final class ProcessList {
// OOM adjustments for processes in various states:

// Adjustment used in certain places where we don't know it yet.
// (Generally this is something that is going to be cached, but we
// don't know the exact value in the cached range to assign yet.)
static final int UNKNOWN_ADJ = 16;

// This is a process only hosting activities that are not visible,
// so it can be killed without any disruption.
static final int CACHED_APP_MAX_ADJ = 15;
static final int CACHED_APP_MIN_ADJ = 9;

// The B list of SERVICE_ADJ -- these are the old and decrepit
// services that aren't as shiny and interesting as the ones in the A list.
static final int SERVICE_B_ADJ = 8;

// This is the process of the previous application that the user was in.
// This process is kept above other things, because it is very common to
// switch back to the previous app.  This is important both for recent
// task switch (toggling between the two top recent apps) as well as normal
// UI flow such as clicking on a URI in the e-mail app to view in the browser,
// and then pressing back to return to e-mail.
static final int PREVIOUS_APP_ADJ = 7;

// This is a process holding the home application -- we want to try
// avoiding killing it, even if it would normally be in the background,
// because the user interacts with it so much.
static final int HOME_APP_ADJ = 6;

// This is a process holding an application service -- killing it will not
// have much of an impact as far as the user is concerned.
static final int SERVICE_ADJ = 5;

// This is a process with a heavy-weight application.  It is in the
// background, but we want to try to avoid killing it.  Value set in
// system/rootdir/init.rc on startup.
static final int HEAVY_WEIGHT_APP_ADJ = 4;

// This is a process currently hosting a backup operation.  Killing it
// is not entirely fatal but is generally a bad idea.
static final int BACKUP_APP_ADJ = 3;

// This is a process only hosting components that are perceptible to the
// user, and we really want to avoid killing them, but they are not
// immediately visible. An example is background music playback.
static final int PERCEPTIBLE_APP_ADJ = 2;

// This is a process only hosting activities that are visible to the
// user, so we'd prefer they don't disappear.
static final int VISIBLE_APP_ADJ = 1;

// This is the process running the current foreground app.  We'd really
// rather not kill it!
static final int FOREGROUND_APP_ADJ = 0;

// This is a process that the system or a persistent process has bound to,
// and indicated it is important.
static final int PERSISTENT_SERVICE_ADJ = -11;

// This is a system persistent process, such as telephony.  Definitely
// don't want to kill it, but doing so is not completely fatal.
static final int PERSISTENT_PROC_ADJ = -12;

// The system process runs at the default adjustment.
static final int SYSTEM_ADJ = -16;

// Special code for native processes that are not being managed by the system (so
// don't have an oom adj assigned by the system).
static final int NATIVE_ADJ = -17;
}

Importance等级与adj值在ActivityManagerService中被关联起来,相较于Importance等级而言adj值可以赋予我们更多的参考价值,从上述adj值的定义中我们可以看到,值越小优先级越高,比如native进程的adj值为-17,对于这个adj值的进程来说,系统根本不会动它一分一毫,实质上当进程的adj值去到2时系统就很少会因为其它原因而去杀死它。在平时的开发中,我们可以通过查看节点目录proc下的相关进程来获取其相应的adj值:(进程相关查看,请查看前面一篇文章)

adb shell
cat /proc/32366/oom_adj

注意“32366”为进程ID,你可以通过上面我们提到过的ps命令获取相关进程的ID。
cat查看进程的adj值后我们会得到其返回结果“0”,说明当前进程正位于前台,此刻我们再按返回键退出应用后再次查看adj值发现其会变为“8”,也就是说进程优先级变得很低了。这里需要注意的是上述操作均在原生的Android系统上执行,如果是其它的定制ROM则输出及结果可能会有出入,adj值仅仅能作为一个参考而非绝对的常量。

二,后台进程常驻的策略与选择

上面说了这么多,其实我们也差不多能总结出一套规律,要想让我们的后台进程长存,我们首先要应付的就是系统的“自杀”机制,而后台进程被杀的首要原因就是我们的进程优先级太低同时系统可用资源太少,其次如果真的被系统干掉,那么我们得重新拉起进程让它重复上次的故事,因此我们的进程后台常驻策略最终可以归结为两点:

1.轻量化进程

所谓轻量化进程,其实就是迫使我们的进程占用尽量少的资源,但是我们知道的是一个运行中的App就算功能再少也会占用相当一部分资源,因此在这里我们是不应该去想着让我们的应用主进程在后台常驻,让一个没有看不见的界面在后台跑既没意义也没必要,因此大多数情况下我们都会使用一个新的进程去常驻在后台,而这个进程一般会持有一个Service,后台所有的龌龊事都会交由它去处理,毕竟在Android中干这种龌龊事的也只有Service了:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.test.daemon">

    <application>
        <service
            android:name=".services.DaemonService"
            android:process=":service" />
    </application>
</manifest>

如上所示我们声明一个services并通过startService的方式启动它,在这个Service中我们通过一个死循环来不断Toast一段信息:

package com.test.daemon.services;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.SystemClock;
import android.widget.Toast;

/**
 *
 * @author caoyinfei
 * @version [版本号, 2016/5/11]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
public class DaemonService extends Service {
    private static boolean sPower = true;
    private Handler handler = new Handler();

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (sPower) {
                    if (System.currentTimeMillis() >= 123456789000000L) {
                        sPower = false;
                    }
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(DaemonService.this, "====" +
                                    System.currentTimeMillis(), Toast.LENGTH_SHORT).show();
                        }
                    });
                    SystemClock.sleep(3000);
                }
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
}

如果你想在进程中的Service里处理更复杂的逻辑,务必尽量多地使用弱引用或软引用,或者说尽量多地去置空一些不必要的引用并在需要的时候再赋值,其次Service本身也提供了onTrimMemory方法来告诉我们系统何时需要释放掉不必要的资源,灵活使用这类方法可以最大程度的让我们的后台Service长盛不衰。还是那句话,尽量让我们的后台进程做更少的事情,及时释放资源,才是硬道理。

2.被杀后重启

可以这么说,没有任何一个应用进程可以做到永远不被杀死,除非系统给你开了后门,进程被杀并不可怕,可怕的是杀掉后就永远GG思密达了,所以如何使我们的进程可以在被杀后重启呢?这就需要使用到一个叫做守护进程的东西,原理很简单,多开一个进程,让这个进程轮询检查目标进程是否存活,死了的话将其拉起,同时目标进程也需要做一个轮询检查守护进程是否存活,死了的话也将其拉起,相互唤醒一起龌龊。不过即便如此有时候意外也是难免的,在Android中我们还可以通过AlarmManager和系统广播来在一定条件下唤醒逝去的进程。后面会详细说明。

三,后台进程常驻的实现

1 尽量保证进程不被杀死

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

public class DaemonService extends Service {
    @Override
    public void onCreate() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            startForeground(250, builder.build());
        } else {
            startForeground(250, new Notification());
        }
    }
}

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

public class DaemonService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            startForeground(250, builder.build());
            startService(new Intent(this, CancelService.class));
        } else {
            startForeground(250, new Notification());
        }
    }

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }
}
public class CancelService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
       return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            startForeground(250, builder.build());
            new Thread(new Runnable() {
                @Override
                public void run() {
                    SystemClock.sleep(1000);
                    stopForeground(true);
                    NotificationManager manager =
                            (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                    manager.cancel(250);
                    stopSelf();
                }
            }).start();
        }
        return super.onStartCommand(intent, flags, startId);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="teach.focus.notification">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".DaemonService"
            android:enabled="true"
            android:exported="true"
            android:process=":service" />
        <service
            android:name=".CancelService"
            android:enabled="true"
            android:process=":service"
            android:exported="true"></service>
    </application>

</manifest>

tips:
android:exported="true"这个属性要注意一下:
该属性用来标示,其它应用的组件是否可以唤醒service或者和这个service进行交互:true可以,false不可以。如果为false,只有同一个应用的组件或者有着同样user ID的应用可以启动这个service或者绑定这个service。

默认值根据当前service是否有intent filter来定。如果没有任何filter意味着当前service只有在被详细的描述class name后才会被唤醒。这意味这当前service只能在应用内部使用(因为其它应用不知道这个class name).所以在这种情况下它的默认值为 false.从另一方面讲,如果至少有一个filter的话那么就意味着这个service可以被外部应用使用,这种情况下默认值为true。

其实,不只有这个属性可以指定service是否暴露给其它应用。你也可以使用permission来限制外部实体唤醒当前service


如上代码所示,我们先在DaemonService中发送一个Notification并将其置为前台,而后如果是4.3及其以上的版本的话我们就start另外一个CancelService,这个CancelService的逻辑很简单,发送与DaemonService中ID相同的Notification然后将其取消并取消自己的前台显示,然后停止,大家看到这里可能觉得很奇葩,其实我们就是自导自演装了一次逼。其实就是个小技巧而已,虽然我们通过CancelService干掉了前台显示需要的Notification,但是,请大家查看一下当前进程的adj值,你就会发现,我们DaemonService所在的进程竟然还是可见进程!

这里写图片描述

adj值变成了1,默认情况应该是8,是不是很6呢,前段时间就曾有人扒出支付宝曾经以这样的方式让自己的后台进程常驻,但是这个方法有个小小的bug,在一些手机上,发送前台通知会唤醒设备并点亮屏幕,这样会很耗电而且在电量管理界面系统还会统计到你的进程点亮屏幕的次数,不是很好。

除了使Service置为前台显示来提权外,还有很多不是很实用的方式,比如提升优先级和使用persistent权限等:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.aigestudio.daemon">

    <application
        android:persistent="true">
    </application>
</manifest>

不过这些方法意义都不会很大。

2 进程被杀死后复活
任何一个普通的应用进程都会有被干掉的那么一天,除非你跟系统有关系有契约,说白了就是ROM是定制的且可以给你开特殊权限,不然的话,系统总会在某个时刻因为某些原因把你杀掉,被杀掉不可怕,可怕的是被杀掉后就再也活不过来了……因此,我们得制定各种策略,好让进程能在被杀后可以自启。

1)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,个人建议是尽量不要传递Intent进来,避免有时候逻辑不好处理。同时需要注意的是,默认情况下Service的返回值就是START_STICKY或START_STICKY_COMPATIBILITY:

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

2)进程守护
关于进程守护其实也不是什么高深的技术,其逻辑也很简单,AB两个进程,A进程里面轮询检查B进程是否存活,没存活的话将其拉起,同样B进程里面轮询检查A进程是否存活,没存活的话也将其拉起,而我们的后台逻辑则随便放在某个进程里执行即可,一个简单的例子是使用两个Service:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="teach.focus.notification">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".DaemonService"
            android:enabled="true"
            android:exported="false"
            android:process=":service" />
        <service
            android:name=".ProtectService"
            android:enabled="true"
            android:exported="false"
            android:process=":remote"/>
    </application>

</manifest>

使用两个进程分别装载两个Service,在两个Service中开轮询,互相唤醒:

public class DaemonService extends Service {
    private static boolean isRunning;
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
         if (!isRunning) {
            isRunning = true;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    SystemClock.sleep(3000);
                    isRunning = false;
                    startService(new Intent(DaemonService.this, ProtectService.class));
                    System.out.println("DaemonService");
                }
            }).start();
        }
        return super.onStartCommand(intent, flags, startId);
    }
}
public class ProtectService extends Service {
     private static boolean isRunning;

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

    @Override
    public void onCreate() {
        super.onCreate();
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (!isRunning) {
            isRunning = true;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    SystemClock.sleep(1500);
                    System.out.println("ProtectService");
                    isRunning = false;
                    startService(new Intent(ProtectService.this, DaemonService.class));
                }
            }).start();
        }
        return START_STICKY;
    }
}

在原生系统及相当一部分的ROM下上述方法就已经很有用了,即便应用主进程被用户在Recent Task中被清理也无妨上述进程的进行,该方法直至Android 6.0也相当有效,但是对于一些深度定制的ROM就显得很鸡肋,比如魅族、小米。


有些时候,我们会使用一个更为纯净的进程来作为守护进程而非借助Service,你可以使用C层fork,也可以直接Java创建一个新的进程,在5.0以前的版本中,这两种方式创建的进程有一定的区别,不过5.0以后已经变得不再那么重要了,不依赖Android环境的进程唯一的好处是可以做到更轻量,除此之外并无卵用,这里以Java为例,使用Java的方式创建进程有两种途径,一是通过Runtime;二是通过ProcessBuilder,后者提供了更多的选择,使用ProcessBuilder创建进程的过程也很简单,三部曲:构建环境变量、指定用户目录、执行命令:(这边不详细介绍了)

3)Receiver触发

使用Receiver来检测目标进程是否存活不失为一个好方法,静态注册一系列广播,什么开机启动、网络状态变化、时区地区变化、充电状态变化等等,这听起来好像很6,而且在大部分手机中都是可行的方案,但是对于深度定制的ROM,是的,又是深度定制,你没有看错,而且代表性人物还是魅族、小米,这两个业界出了名的喜欢“深度定制”系统。

大部分手机可行的方法如下:

一. 在众多的Intent的action动作中,Intent.ACTION_TIME_TICK是比较特殊的一个,根据SDK描述:

Broadcast Action: The current time has changed. Sent every minute. You can not receive this through components declared in manifests, only by exlicitly registering for it withContext.registerReceiver()

意思是说这个广播动作是以每分钟一次的形式发送。但你不能通过在manifest.xml里注册的方式接收到这个广播,只能在代码里通过registerReceiver()方法注册。

1.服务里面注册广播

 /**
 * 调用startForeground,提高进程adj值
 * 该进程为需要保护的进程
 */
public class DaemonService extends Service {
    private static boolean isRunning;

    AlarmManager mAlarmManager = null;
    PendingIntent mPendingIntent = null;

    @Override
    public void onCreate() {
        super.onCreate();
        //服务启动广播接收器,使得广播接收器可以在程序退出后在后天继续执行,接收系统时间变更广播事件
        TimeChangeReceiver receiver=new TimeChangeReceiver();
        registerReceiver(receiver,new IntentFilter(Intent.ACTION_TIME_TICK));
    }

2.广播里面判断服务是否存在,不存在则开启服务。

public class TimeChangeReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        //启动service
        boolean isServiceRunning = false;
        System.out.println("TimeChangeReceiver");
        if (intent.getAction().equals(Intent.ACTION_TIME_TICK)) {
            //检查Service状态
            ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
            for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
                if ("so.xxxx.xxxxService".equals(service.service.getClassName())) {
                    isServiceRunning = true;
                }
            }
            if (!isServiceRunning) {
                Intent i = new Intent(context, DaemonService.class);
                context.startService(i);
            }
        }
    }
}

自从Android 3.1开始系统对我们的应用增加了一种叫做STOPPED的状态,什么叫STOPPED?就是安装了之后从未启动过的,大家可能经常在网上看到对开机广播的解释,说要想应用正确接收到开机广播那么就得先启动一下应用,这个说法的技术支持就来源于此,因为自Android 3.1后所有的系统广播都会在Intent添加一个叫做FLAG_EXCLUDE_STOPPED_PACKAGES的标识,说白了就是所有处于STOPPED状态的应用都不可以接收到系统广播,是不是感到很蛋疼菊紧?没事、更蛋疼的还在后面。在原生的系统中,当应用初次启动后就会被标识为非STOPPED状态,而且再也没有机会被打回原形除非重新安装应用,但是,但是,但是,一些深(fang)度(ni)定(gou)制(pi)的ROM按耐不住了,这样的话,如果每个应用都这么搞岂不是后台一大堆进程在跑?所以这些深度定制的ROM会在它们的清理逻辑中,比如小米的长按Home,魅族的Recent Task加入了将应用重置为STOPPED的逻辑,也就是直接或间接地调用ActivityManagerService中的forceStopPackageLocked:

private void forceStopPackageLocked(final String packageName, int uid, String reason) {
    // 省略一行代码……

    Intent intent = new Intent(Intent.ACTION_PACKAGE_RESTARTED,
            Uri.fromParts("package", packageName, null));

    // 省略多行代码……

    broadcastIntentLocked(null, null, intent,
            null, null, 0, null, null, null, AppOpsManager.OP_NONE,
            null, false, false, MY_PID, Process.SYSTEM_UID, UserHandle.getUserId(uid));
}

可以看到上面的代码里发送了一个ACTION_PACKAGE_RESTARTED广播,这个广播会调用broadcastIntentLocked等方法来将相应的应用重置为STOPPED状态,因此一旦我们的应用被重置为STOPPED则再也无法接受到相应的系统广播除非再次启动一下应用清除掉STOPPED标识。

  1. AlarmManager or JobScheduler循环触发

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

 Intent sendIntent = new Intent(getApplicationContext(), DaemonService.class);
        mAlarmManager = (AlarmManager)getSystemService(ALARM_SERVICE);
        mPendingIntent = PendingIntent.getService(this, 0, sendIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        long now = System.currentTimeMillis();
        mAlarmManager.setInexactRepeating(AlarmManager.RTC, now, 60000, mPendingIntent);

而对于4.4及其以上的版本来说如果我们想精确的方式重复启动的话,就得使用一点小手段,在4.4及其以上的版本中Android提供给我们一个新的API:setExact,顾名思义就是精确启动,但是与之前版本不同的是,4.4开始并不能精确地重复启动,也就是不能像setRepeating那样,setExact只能被唤醒一次,那么该如何做到重复精确呢?其实很简单,我们每次通过AlarmManager唤醒时都发送一个广播,在这个广播里我们处理一些必要的逻辑,尔后又设置一次AlarmManager,如此往复循环,实质就是对广播做一个递归以达到目的:

public class DReceiver extends BroadcastReceiver {
    private PendingIntent mPendingIntent;
    private AlarmManager am;

    @Override
    public void onReceive(Context context, Intent intent) {
        if (null == intent) return;
        if (null == mPendingIntent) {
            Intent i = new Intent(context, DReceiver.class);
            i.putExtra("time", System.currentTimeMillis() + 3000);
            mPendingIntent = PendingIntent.getService(context, 0x123, i,
                    PendingIntent.FLAG_UPDATE_CURRENT);
        }
        if (null == am)
            am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        long time = intent.getLongExtra("time", System.currentTimeMillis());
        am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, mPendingIntent);
    }
}

上述代码中我们使用Intent来传递数据,事实上我们应该使用持久化的数据来存储这个time值,尽量少用甚至不用Intent。
看到这里很多朋友会问是不是OK了啊?很遗憾地告诉你NO!为什么呢?不知道大家是否在开发的过程中遇到这样的问题,你设置的Alarm在应用退出后发现过不了多久居然就没了,特别是在某些深度定制的系统上,上面我们曾提到Receiver如果应用被置为STOPPED状态就再也无法接收到广播,很不幸地告诉你AlarmManager也一样,在AlarmManagerService中有一个BroadcastReceiver,这个BroadcastReceiver会接收上面我们曾说的ACTION_PACKAGE_RESTARTED广播:

class UninstallReceiver extends BroadcastReceiver {
    public UninstallReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
        filter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
        filter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART);
        filter.addDataScheme("package");
        getContext().registerReceiver(this, filter);

        // 省去几行代码……
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        synchronized (mLock) {
            String action = intent.getAction();
            String pkgList[] = null;
            if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) {
                // 省去几行代码……

            } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
                pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
            } else if (Intent.ACTION_USER_STOPPED.equals(action)) {
                // 省去几行代码……

            } else if (Intent.ACTION_UID_REMOVED.equals(action)) {
                // 省去几行代码……

            } else {
                // 省去几行代码……

                Uri data = intent.getData();
                if (data != null) {
                    String pkg = data.getSchemeSpecificPart();
                    if (pkg != null) {
                        pkgList = new String[]{pkg};
                    }
                }
            }
            if (pkgList != null && (pkgList.length > 0)) {
                for (String pkg : pkgList) {
                    removeLocked(pkg);

                    // 省去几行代码……
                }
            }
        }
    }
}

从上述的源码来看当该广播接收者收到ACTION_PACKAGE_RESTARTED广播时会执行removeLocked这个方法,这个方法就像它的名字那样会移除掉与应用相关的Alarm并刷新Alarm的状态:

void removeLocked(String packageName) {
    boolean didRemove = false;
    for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
        Batch b = mAlarmBatches.get(i);
        didRemove |= b.remove(packageName);
        if (b.size() == 0) {
            mAlarmBatches.remove(i);
        }
    }
    for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {
        if (mPendingWhileIdleAlarms.get(i).operation.getTargetPackage().equals(packageName)) {
            mPendingWhileIdleAlarms.remove(i);
        }
    }
    if (didRemove) {
        rebatchAllAlarmsLocked(true);
        rescheduleKernelAlarmsLocked();
        updateNextAlarmClockLocked();
    }
}

因此,对于某些手机可以在清理应用时将其置为STOPPED状态而言,即便设置AlarmManager也是没卵用的。
与AlarmManager类似的在5.0新鲜出炉的JobScheduler相较而言要比AlarmManager好一些,鉴于两者使用原理类似,这里就不再逼逼了。

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

package teach.focus.notificatio;

import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;

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="teach.focus.notificatio">

    <application>
        <service
            android:name=".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所在的进程:

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

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

root@vbox86p:/ # ps|grep notification                                            
u0_a1701 10615 286 762696 27832 sys_epoll_ 00000000 S teach.focus.notification
root@vbox86p:/ # cat /proc/10615/oom_adj
0

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

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="teach.focus.notification">

    <application>
        <service
            android:name=".DaemonService"
            android:process=":service" />
        <service
            android:name=".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>

这种方式唯一的一个缺点就是你需要欺骗用户手动去开启通知权限,需要给用户一个合理的理由,所以对于跟通知权限根本不沾边的应用想想还是算了吧。


以多种方式去相互唤醒相互拉活是目前来说最稳定最安全的方式,各大牛逼点的应用都有类似行为,当然对于很多小应用来说,依然可以使用一些第三方的网络服务,比如使用极光推送+小米推送+华为推送,做多种选择措施,因为小米跟华为rom定制,而他们各自都有推送sdk,可以在进程挂的情况下走系统推送。

最后作为纯净的开发者,奉劝各位,不要制造流氓软件,除非业务特别需要,否则不要常驻,当你的手机里面各种软件没有办法关闭,有的时候,也挺烦人的。

集成了一个以多个方式互相唤醒的demo,地址如下:
http://download.csdn.net/detail/dfskhgalshgkajghljgh/9517643

参考文章:
http://blog.csdn.net/aigestudio/article/details/51348408


如有错误欢迎指出来,一起学习。
在这里插入图片描述

以上是关于Android service进程保护的主要内容,如果未能解决你的问题,请参考以下文章

Android 逆向Android 进程注入工具开发 ( Visual Studio 开发 Android NDK 应用 | Visual Studio 中 SDK 和 NDK 安装位置 )(代码片段

Android 从 Service 到 Client 的通信

java [Intent] Intent片段以启动Activity,Service或发送广播。 #android_snippet #android

Android service 中的stub类是啥意思?

android studio service 中log不输出

Android service ( 二) 远程服务