Android进程保活招数概览

Android中的进程保活应该分为两个方面:html

  • 提升进程的优先级,减小被系统杀死的可能性
  • 在进程已经被杀死的状况下,经过一些手段来从新启动应用进程

本文针对这两方面来进程阐述,并给出相应的示例。其实主要也是在前人的基础上作了一个总结,并进行了一些实践。java

阅读本文的时候,能够先clone一份代码 android-process-daemon,这样的话可能理解更清晰。android

1 进程等级与Low Memory Killer

在开始以前,首先有必要了解一下进程等级的概念。Android 系统将尽可能长时间地保持应用进程,但为了新建进程或运行更重要的进程,须要清除旧进程来回收内存。 为了肯定保留或终止哪些进程,系统会对进程进行分类。 须要时,系统会首先消除重要性最低的进程,而后是清除重要性稍低一级的进程,依此类推,以回收系统资源。git

进程等级:github

  • 前台进程面试

    • 与用户正在交互的Activity
    • 前台Activity以bind方式启动的Service
    • Service调用了startForground,绑定了Notification
    • 正在执行生命周期的Service,例如在执行onCreate、onStart、onDestory
    • 正在执行onReceive方法的BroadcastReceiver
  • 可见进程shell

    • 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,若是前台 Activity 启动了一个对话框,容许在其后显示上一 Activity,则有可能会发生这种状况。
    • 托管绑定到可见(或前台)Activity 的 Service。
  • 服务进程 正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,可是它们一般在执行一些用户关心的操做(例如,在后台播放音乐或从网络下载数据)。所以,除非内存不足以维持全部前台进程和可见进程同时运行,不然系统会让服务进程保持运行状态。segmentfault

  • 后台进程 包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 一般会有不少后台进程在运行,所以它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。若是某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,由于当用户导航回该 Activity 时,Activity 会恢复其全部可见状态。缓存

  • 空进程 不含任何活动应用组件的进程。保留这种进程的的惟一目的是用做缓存,以缩短下次在其中运行组件所需的启动时间。 为使整体系统资源在进程缓存和底层内核缓存之间保持平衡,系统每每会终止这些进程。服务器

进程等级参考谷歌官方文档 https://developer.android.google.cn/guide/components/processes-and-threads.html?hl=zh-cn。

系统出于体验和性能上的考虑,app在退到后台时系统并不会真正的kill掉这个进程,而是将其缓存起来。打开的应用越多,后台缓存的进程也越多。在系统内存不足的状况下,系统开始依据自身的一套进程回收机制来判断要kill掉哪些进程,以腾出内存来供给须要的app, 这套杀进程回收内存的机制就叫 Low Memory Killer,它是一种根据 OOM_ADJ 阈值级别触发相应力度的内存回收的机制。

关于 OOM_ADJ 的说明以下:

其中红色部分表明比较容易被杀死的 Android 进程(OOMADJ>=4),绿色部分表示不容易被杀死的 Android 进程,其余表示非 Android 进程(纯 Linux 进程)。在Low Memory Killer 回收内存时会根据进程的级别优先杀死 OOMADJ 比较大的进程,对于优先级相同的进程则进一步受到进程所占内存和进程存活时间的影响。

Android 手机中进程被杀死可能有以下状况:

因此,想要应用下降被杀死的可能性就要尽可能提升进程的优先级,这样才会在系统内存不足的时候减小被杀死的可能性。在这里,咱们只是说减小被杀死的可能性,而不是说必定不会杀死。除了系统应用,或者厂商白名单中的应用,通常的应用都有被杀死的可能性。

咱们能够经过adb命令来查看进程的优先级 首先使用命令:

adb shell ps | grep  packageName

获取进程的PID,而后使用命令获取进程的oom_adj值,这个值越小,表明优先级越高越不容易被杀死:

adb shell cat /proc/PID/oom_adj

好比,先获取adb进程

# adb shell ps |grep com.sososeen09.process
u0_a85    1740  486  1013428 64840 00000000 f7491e65 S com.sososeen09.process.daemon.sample

而后获取oom_adj值:

# adb shell cat /proc/1740/oom_adj
0

此时该进程运行在前台,它的优先级为0,这种状况下被杀死的可能性很小。当经过Home键把当前引用退回后台的时候,从新查看一下oom_adj,这个值可能会变为6(不一样的rom状况可能不同)。

2 提高进程优先级

2.1 利用Activity提高权限

前面咱们也讲了,当应用切换后后台的时候进程的优先级变得很低,被杀死的可能性就增大了。若是此时用户经过电源键进行息屏了。能够考虑经过监听息屏和解锁的广播,在息屏的时候启动一个只有一个像素的Activity。这样的话,在息屏这段时间,应用的进程优先级很高,不容易被杀死。采用这种方案要注意的是要使用户无感知。

该方案主要解决第三方应用及系统管理工具在检测到锁屏事件后一段时间(通常为5分钟之内)内会杀死后台进程,已达到省电的目的问题。

public class KeepLiveActivity extends Activity {

    private static final String TAG = "KeepLiveActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.e(TAG,"start Keep app activity");
        Window window = getWindow();
        //设置这个act 左上角
        window.setGravity(Gravity.START | Gravity.TOP);
        //宽 高都为1
        WindowManager.LayoutParams attributes = window.getAttributes();
        attributes.width = 1;
        attributes.height = 1;
        attributes.x = 0;
        attributes.y = 0;
        window.setAttributes(attributes);

        KeepLiveManager.getInstance().setKeep(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.e(TAG,"stop keep app activity");
    }
}

为了让无用无感知,Activity要设置的小(只有一个像素),无背景而且是透明的。此外还要注意一点,须要设置Activity的taskAffinity 属性,要与咱们的应用默认的taskAffinity不一样,不然当这个Activity启动的时候,会把咱们的应用所在的任务栈移动到前台,当屏幕解锁以后,会发现咱们的应用移动到前台了。而用户在息屏的时候明明已经把咱们的应用切换到后台了,这会给用户形成困扰。

<activity
    android:name=".keepliveactivity.KeepLiveActivity"
    android:excludeFromRecents="true"
    android:exported="false"
    android:finishOnTaskLaunch="false"
    android:taskAffinity="com.sososeen09.daemon.keep.live"
    android:theme="@style/KeepLiveTheme" />
<style name="KeepLiveTheme">
    <item name="android:windowBackground">@null</item>
    <item name="android:windowIsTranslucent">true</item>
</style>

要有一个BroadcastReceiver,用于监听屏幕的点亮和关闭的广播,在这里咱们使用了Intent.ACTION_USER_PRESENT这个action,它会早于系统发出的Intent.ACTION_SCREEN_OFF 广播。这样能够更早的结束以前息屏的时候启动的Activity。

public class KeepLiveReceiver extends BroadcastReceiver {
    private static final String TAG = "KeepLiveReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        Log.e(TAG, "receive action:" + action);
        //屏幕关闭事件
        if (TextUtils.equals(action, Intent.ACTION_SCREEN_OFF)) {
            //关屏 开启1px activity
            KeepLiveManager.getInstance().startKeepLiveActivity(context);
            // 解锁事件
        } else if (TextUtils.equals(action, Intent.ACTION_USER_PRESENT)) {
            KeepLiveManager.getInstance().finishKeepLiveActivity();
        }

        KeepLiveManager.getInstance().startKeepLiveService(context);
    }
}

2.2 Service绑定一个Notification的方式:

应用启动一个Service,而且Service经过调用startForeground方法来绑定一个前台的通知时,能够有效的提高进程的优先级。

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
    builder.setSmallIcon(R.mipmap.ic_launcher);
    builder.setContentTitle("Foreground");
    builder.setContentText("I am a foreground service");
    builder.setContentInfo("Content Info");
    builder.setWhen(System.currentTimeMillis());
    Intent activityIntent = new Intent(this, MainActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, activityIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    builder.setContentIntent(pendingIntent);
    Notification notification = builder.build();
    startForeground(FOREGROUND_ID, notification);
    return super.onStartCommand(intent, flags, startId);
}

这种方式的话会在通知栏显示一个通知,该方式属于比较文明的。

咱们可使用 命令来查看当前正在运行的服务信息,好比

adb shell dumpsys activity services com.sososeen09.process

能够获得结果:

ACTIVITY MANAGER SERVICES (dumpsys activity services)
  User 0 active services:
  * ServiceRecord{d18c80d u0 com.sososeen09.process.daemon.sample/.service.WhiteService}
    intent={cmp=com.sososeen09.process.daemon.sample/.service.WhiteService}
    packageName=com.sososeen09.process.daemon.sample
    processName=com.sososeen09.process.daemon.sample:white
    baseDir=/data/app/com.sososeen09.process.daemon.sample-2/base.apk
    dataDir=/data/data/com.sososeen09.process.daemon.sample
    app=ProcessRecord{696d809 2478:com.sososeen09.process.daemon.sample:white/u0a85}
    isForeground=true foregroundId=1001 foregroundNoti=Notification(pri=0 contentView=com.sososeen09.process.daemon.sample/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE)
    createTime=-44s879ms startingBgTimeout=--
    lastActivity=-44s860ms restartTime=-44s860ms createdFromFg=true
    startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

  * ServiceRecord{e4782a4 u0 com.sososeen09.process.daemon.sample/.service.NormalService}
    intent={cmp=com.sososeen09.process.daemon.sample/.service.NormalService}
    packageName=com.sososeen09.process.daemon.sample
    processName=com.sososeen09.process.daemon.sample:normal
    baseDir=/data/app/com.sososeen09.process.daemon.sample-2/base.apk
    dataDir=/data/data/com.sososeen09.process.daemon.sample
    app=ProcessRecord{2402ea0e 2459:com.sososeen09.process.daemon.sample:normal/u0a85}
    createTime=-48s510ms startingBgTimeout=--
    lastActivity=-48s479ms restartTime=-48s479ms createdFromFg=true
    startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

  Connection bindings to services:
  * ConnectionRecord{3b4eb582 u0 CR DEAD com.sososeen09.process.daemon.sample/.acount.AuthenticationService:@2a1598cd}
    binding=AppBindRecord{d621c2f com.sososeen09.process.daemon.sample/.acount.AuthenticationService:system}
    conn=android.app.LoadedApk$ServiceDispatcher$InnerConnection@2a1598cd flags=0x1

能够看到,调用了startForeground方法的Service是一个前台进程了,有一个属性是isForeground=true。

在这种状况下,当应用所在进程退回到后台时,oom_adj的值为1,不容易被杀死。

2.3 隐藏Notification的Service

前面讲的startForeground,会在通知栏中显示一个通知。有一种方式利用了系统漏洞,把通知栏给隐藏,让用户无感知。不过这种方式跟版本有关:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    try {
        Notification notification = new Notification();
        if (Build.VERSION.SDK_INT < 18) {
            startForeground(NOTIFICATION_ID, notification);
        } else {
            startForeground(NOTIFICATION_ID, notification);
            // start InnerService
            startService(new Intent(this, InnerService.class));
        }
    } catch (Throwable e) {
        e.printStackTrace();
    }

    return super.onStartCommand(intent, flags, startId);
}

而后在InnerService中关闭Notification

@Override
public void onCreate() {
    super.onCreate();
    try {
        startForeground(NOTIFICATION_ID, new Notification());
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    stopSelf();
}

其实咱们能够发现,在Tinker中,因为在Patch的过程是在另外一个服务进程中,为了保证这个服务进程不被干掉,Tinker也利用了这个系统的漏洞。具体能够查看TinkerPatchService

3 进程保活

上面讲了提高进程优先级的方式了来减小应用被杀死的可能性,可是当应用真的被杀死的时候,咱们就要想办法来拉活进行了。

3.1 利用广播拉活

这个在推送中比较常见,当几个App都集成了同一家的推送,只要有一个App起来,就会发送一个广播,这样其它的App接收到这个广播以后,开启一个服务,就把进程给启动起来了。各大厂家的全家桶也是这样的。

public class WakeReceiver extends BroadcastReceiver {
    private final static int NOTIFICATION_ID = 1001;
    public final static String ACTION_WAKE = "com.sososeen09.wake";
    private final static String TAG = "WakeReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (action != null && action.equals(ACTION_WAKE)) {
            context.startService(new Intent(context, WakeService.class));

            Log.e(TAG, "onReceive: " + "收到广播,兄弟们要起来了。。。");
        }
    }

    public static class WakeService extends Service {
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            try {
                Notification notification = new Notification();
                if (Build.VERSION.SDK_INT < 18) {
                    startForeground(NOTIFICATION_ID, notification);
                } else {
                    startForeground(NOTIFICATION_ID, notification);
                    // start InnerService
                    startService(new Intent(this, WakeInnerService.class));
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
            Log.e(TAG, "onReceive: " + "我是 WakeService,我起来了,谢谢兄弟。。。" + ProcessUtils.getProcessName(this));
            return super.onStartCommand(intent, flags, startId);

        }
    }

    public static class WakeInnerService extends Service {

        @Override
        public void onCreate() {
            super.onCreate();
            try {
                startForeground(NOTIFICATION_ID, new Notification());
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
            stopSelf();
        }

        @Override
        public void onDestroy() {
            stopForeground(true);
            super.onDestroy();
        }
    }
}

其实也能够监听系统的广播来达到启动应用进程的方式,可是从android 7.0开始,对广播进行了限制,并且在8.0更加严格https://developer.android.google.cn/about/versions/oreo/background.html#broadcasts

可静态注册广播列表: https://developer.android.google.cn/guide/components/broadcast-exceptions.html

3.2 系统Service机制拉活

将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后自动拉活。

STARTSTICKY: “粘性”。若是service进程被kill掉,保留service的状态为开始状态,但不保留递送的intent对象。随后系统会尝试从新建立service,因为服务状态为开始状态,因此建立服务后必定会调用onStartCommand(Intent,int,int)方法。若是在此期间没有任何启动命令被传递到service,那么参数Intent将为null。 STARTNOTSTICKY: “非粘性的”。使用这个返回值时,若是在执行完onStartCommand后,服务被异常kill掉,系统不会自动重启该服务。 STARTREDELIVERINTENT: 重传Intent。使用这个返回值时,若是在执行完onStartCommand后,服务被异常kill掉,系统会自动重启该服务,并将Intent的值传入。 STARTSTICKYCOMPATIBILITY: STARTSTICKY的兼容版本,但不保证服务被kill后必定能重启。 只要 targetSdkVersion 不小于5,就默认是 START_STICKY。 可是某些ROM 系统不会拉活。而且通过测试,Service 第一次被异常杀死后很快被重启,第二次会比第一次慢,第三次又会比前一次慢,一旦在短期内 Service 被杀死4-5次,则系统再也不拉起。

3.3 使用帐户同步拉活

手机系统设置里会有“账户”一项功能,任何第三方APP均可以经过此功能将数据在必定时间内同步到服务器中去。系统在将APP账户同步时,会将未启动的APP进程拉活。 如何利用帐户同步能够参考 https://github.com/googlesamples/android-BasicSyncAdapter

可是帐户同步这个东西,在不一样的手机上可能在同步时间不一样。

关于这种方式,这里就很少讲了,有兴趣的能够搜索相关文章,在示例代码中也有相关的介绍。](https://github.com/sososeen09/android-process-daemon)中也有相关的介绍。)

3.4 使用JobSchedule拉活

JobScheduler容许在特定状态与特定时间间隔周期执行任务。能够利用它的这个特色完成保活的功能,效果相似开启一个定时器,与普通定时器不一样的是其调度由系统完成。它是在Android5.0以后推出的,在5.0以前没法使用。

首先写一个Service类继承自JobService,在小于7.0的系统上,JobInfo能够周期性的执行,可是在7.0以上的系统上,不能周期性的执行了。所以能够在JobService的onStartJob回调方法中继续开启一个任务来执行。

@SuppressLint("NewApi")
public class MyJobService extends JobService {
    private static final String TAG = "MyJobService";

    public static void startJob(Context context) {
        JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);

        JobInfo.Builder builder = new JobInfo.Builder(10, new ComponentName(context.getPackageName(), MyJobService.class.getName())).setPersisted(true);

        //小于7.0
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            // 每隔1s 执行一次 job
            builder.setPeriodic(1_000);
        } else {
            //延迟执行任务
            builder.setMinimumLatency(1_000);
        }

        if (jobScheduler != null) {
            jobScheduler.schedule(builder.build());
        }
    }

    @Override
    public boolean onStartJob(JobParameters params) {
        Log.e(TAG, "start job schedule");
        //若是7.0以上 轮训
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            startJob(this);
        }
        return false;
    }

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

AndroidManifest.xml并须要声明权限。

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

不过在某些ROM可能并不能达到须要的效果(某米)

3.5 双进程守护

咱们都直到Service能够以bind方式启动,当Service被系统杀死的时候,会在ServiceConnection的onServiceDisconnected方法中会收到回调。利用这个原理,能够在主进程中进行有一个LocalService,在子进程中有RemoteService。LocalService中以bind和start方式启动RemoteService,同时RemoteService以bind和start方式启动LocalService。而且在它们各自的ServiceConnection的onServiceDisconnected方法中从新bind和start。

这种Java层经过Service这种双进程守护的方式,能够有效的保证进程的存活能力。

public class LocalService extends Service {
    private final static int NOTIFICATION_ID = 1003;
    private static final String TAG = "LocalService";
    private ServiceConnection serviceConnection;

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

    @Override
    public void onCreate() {
        super.onCreate();
        serviceConnection = new LocalServiceConnection();
    }

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

    class LocalServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //服务链接后回调
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(TAG, "remote service died,make it alive");
            //链接中断后回调
            startService(new Intent(LocalService.this, RemoteService.class));
            bindService(new Intent(LocalService.this, RemoteService.class), serviceConnection,
                    BIND_AUTO_CREATE);
        }
    }

    static class MyBinder extends IMyAidlInterface.Stub {
        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

        }
    }
}

RemoteService也相似

public class RemoteService extends Service {
    private final static int NOTIFICATION_ID = 1002;
    private static final String TAG = "RemoteService";
    private ServiceConnection serviceConnection;

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

    @Override
    public void onCreate() {
        super.onCreate();
        serviceConnection = new RemoteServiceConnection();
    }

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

    class RemoteServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //服务链接后回调
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(TAG, "main process local service died,make it alive");
            //链接中断后回调
            startService(new Intent(RemoteService.this, LocalService.class));
            bindService(new Intent(RemoteService.this, LocalService.class), serviceConnection,
                    BIND_AUTO_CREATE);
        }
    }

    static class MyBinder extends IMyAidlInterface.Stub {
        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

        }
    }
}

为了提升Service所在进程的优先级,能够结合咱们以前讲的startForground来开启一个Notification的方式,提升进程的优先级,以下降被杀风险。

3.6 其它方式拉活

其它咱们还可使用推送拉活,根据终端不一样,在小米手机(包括 MIUI)接入小米推送、华为手机接入华为推送,这样也能够保证进程能够被推送唤醒。

Native拉活,Native fork子进程用于观察当前app主进程的存亡状态。这种在5.0之前的系统上效果比较高,对于5.0以上成功率极低。

4 总结

提高进程优先级的方式

  • Activity提权,监听屏幕的息屏和解锁,使用一个1个像素的Activity

  • Service提权,Service经过startForground方法来开启一个Notification

进程拉活

  • 经过广播的方式

  • 经过Service在onStartCommand的返回值,START_STICK,由系统拉活,在短期内若是屡次被杀死可能就再也启动不了了

  • 经过帐户同步拉活

  • 经过JobSchedule拉活

  • 经过Service的bind启动的方式,双进程守护拉活

  • 推送拉活

  • Native fork子进程的方式拉活

更多详情,请查看 android-process-daemon

参考

做者:sososeen09 连接:https://www.jianshu.com/p/c1a9e3e86666

更多阅读:

一份用心整理的Android面试总结

Android 目前最稳定和高效的UI适配方案

很值得收藏的安卓开源控件库

不懂技术的人不要对懂技术的人说这很容易实现  欢迎关注个人微信公众号:终端研发部,一块儿学习和交流

相关文章
相关标签/搜索