Android常见进程保活方法

一、Android进程优先级

  1. 前台进程 ——Foreground process
    用户正在使用的进程,一般系统不会杀死前台进程
    特点
    • 拥有用户正在交互的Activity(onResume())
    • 拥有某个 Service,后者绑定到用户正在交互的 Activity
    • 拥有正在“前台”运行的 Service(服务已调用 startForeground())
    • 拥有正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
    • 拥有正执行其 onReceive() 方法的 BroadcastReceiver
  2. 可见进程——Visible process
    没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。
    特点
    • 拥有不在前台、但仍对用户可见的 Activity(已调用 onPause())。
    • 拥有绑定到可见(或前台)Activity 的 Service
  3. 服务进程 —— Service process
    尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。
    特点
    • 正在运行 startService() 方法启动的服务,且不属于上述两个更高类别进程的进程。
  4. 后台进程 —— Background process
    后台进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU 列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。
    特点
    • 对用户不可见的 Activity 的进程(已调用 Activity的onStop() 方法)
  5. 空进程 —— Empty process
    一个进程不拥有入何active组件。保留这类进程的唯一理由是高速缓存,这样可以提高下一次一个组件要运行它时的启动速度。系统经常为了平衡在进程高速缓存和底层的内核高速缓存之间的整体系统资源而杀死它们。
    特点
    • 不含任何活动应用组件的进程

二、进程回收策略

  1. Low Memory Killer
    通过一些比较复杂的评分机制,对进程进行打分,然后将分数高的进程判定为bad进程,杀死并释放内存
  2. OOM_ODJ:判断进程的优先级

三、进程保活方案

1. 开启一个1像素Activity

监控手机锁屏解锁事件,在屏幕锁屏时启动1个像素的 Activity,在用户解锁时将 Activity 销毁掉。注意该 Activity 需设计成用户无感知。
通过该方案,可以使进程的优先级在屏幕锁屏时间由4提升为最高优先级1。

1.1 适用场景
本方案主要解决第三方应用及系统管理工具在检测到锁屏事件后一段时间(一般为5分钟以内)内会杀死后台进程,已达到省电的目的问题。
1.2方案具体实现
首先定义Activity,并设置Activity的大小为1像素

/**
 * @author: wuchao
 * @date: 2018/5/9 17:23
 * @desciption:
 */
public class KeepLiveActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Window window = getWindow();
        window.setGravity(Gravity.START | Gravity.TOP);
        WindowManager.LayoutParams params = window.getAttributes();
        //起始坐标
        params.x = 0;
        params.y = 0;
        //宽高设计为1个像素
        params.width = 1;
        params.height = 1;
        window.setAttributes(params);
        //用软引用进行管理,方便finish
        KeepLiveActivityManager.getInstance().setActivity(this);
    }
}

其次,从 AndroidManifest 中通过如下属性,排除 Activity 在 RecentTask 中的显示:

<activity
            android:name="KeepLiveActivity"
            android:excludeFromRecents="true"
            android:exported="false"
            android:finishOnTaskLaunch="false"
            android:process=":live"
            android:theme="@style/LiveActivityStyle"/>

android:excludeFromRecents控制在不在recent列表中显示。true时不显示;false显示,默认。recents通俗的讲就是android的多任务键,它可以看到我们最近使用过的应用,通过它可以快速应用切换.
android:finishOnTaskLaunch="true"在配置了该属性为true的activity中按home键返回到[home screen]屏幕后,再点击该应用的图标启动程序时,则系统会调用该activity的[onDestroy]销毁。因为点击应用的图标启动程序时,重新启动了这个任务。
通常情况下,我们可以在activity的标签上使用以下几种属性来清空任务栈
a、clearTaskOnLaunch 属性顾名思义,就是每次返回activity的时候,都将该activity上的所有activity清除,通过这个属性,可以让这个task每次初始化的时候,都只有一个activity
b、finishTaskOnLaunch 这个属性和clearTaskOnLaunch有点类似,只不过clearTaskOnLaunch作用在别人身上,而finishTaskOnLaunch作用在自己身上,通过这个属性,当离开这个activity所处的task,那么用户再返回的时候,该activity会被finish掉
c、alwaysRetainTaskState 给了task一道免死金牌,如果将activity这个属性设置为true,那么该activity所在的task将不接受任何清除命令,一直保持当前task的状态。
最有一点说明就是由于ACTION_SCREEN_OFF和ACTION_SCREEN_ON比较特殊,Receive的注册必须写在代码里面,不能在Manifest.xml里面注册。

为了更好的隐藏我们还可以设置透明主题,控制 Activity 为透明:

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

创建完 KeepLiveActivity 之后需要创建一个 Receiver,来监听屏幕状态,在锁屏或解锁是操作 KeepLiveActivity,为了方便管理 KeepLiveActivity 的开启和销毁,可以创建一个管理类:

/**
 * @author: wuchao
 * @date: 2018/5/9 17:26
 * @desciption: KeepLiveActivity 管理类
 */
public class KeepLiveActivityManager {

    private SoftReference<Activity> mActivitySoftReference;

    public static KeepLiveActivityManager getInstance() {
        return Holder.INSTANCE;
    }

    /**
     * 把activity用软引用管理起来
     *
     * @param activity
     */
    public void setActivity(Activity activity) {
        mActivitySoftReference = new SoftReference<>(activity);
    }

    /**
     * 开启1像素Activity
     */
    public void startKeepLiveActivity(Context context) {
        Intent intent = new Intent(context, KeepLiveActivity.class);
        context.startActivity(intent);
    }

    /**
     * 关闭1像素Activity
     */
    public void finishKeepLiveActivity() {
        if (mActivitySoftReference != null) {
            Activity activity = mActivitySoftReference.get();
            if (activity != null) {
                activity.finish();
            }
        }
    }

    private static class Holder {
        private static final KeepLiveActivityManager INSTANCE = new KeepLiveActivityManager();
    }
}

/**
 * @author: wuchao
 * @date: 2018/5/9 18:04
 * @desciption: 屏幕监听Receiver
 */
public class KeepLiveReceiver extends BroadcastReceiver {
    @TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    public void onReceive(Context context, Intent intent) {
        if (Objects.equals(intent.getAction(), Intent.ACTION_SCREEN_OFF)) {//锁屏
            KeepLiveActivityManager.getInstance().startKeepLiveActivity(context);
        } else if (Objects.equals(intent.getAction(), Intent.ACTION_SCREEN_ON)) {//解锁
            KeepLiveActivityManager.getInstance().finishKeepLiveActivity();
        }
    }
}

通过以上配置之后现在MainActivity改成如下

/**
 * @author: wuchao
 * @date: 2018/5/9 18:04
 * @desciption:
 */
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        filter.addAction(Intent.ACTION_SCREEN_ON);
        registerReceiver(new KeepLiveReceiver(),filter);
    }
}

创建 KeepLiveService 来进行业务操作

/**
 * @author: wuchao
 * @date: 2018/5/9 18:14
 * @desciption: 进程保活Service
 */
public class KeepLiveService extends Service {
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        initReceiver();
        return START_STICKY;
    }

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

    /**
     * 注册广播
     */
    private void initReceiver() {
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
        intentFilter.addAction(Intent.ACTION_SCREEN_ON);
        registerReceiver(new KeepLiveReceiver(), intentFilter);
    }
}
<service
            android:name=".KeepLiveService"
            android:process=":live"/>

2. 利用系统Service机制拉活

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

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

onStartCommand中返回值,常用的返回值有START_NOT_STICKYSTART_SICKYSTART_REDELIVER_INTENT,这三个都是静态常理值。
START_NOT_STICKY:表示当Service运行的进程被Android系统强制杀掉之后,不会重新创建该Service,如果想重新实例化该Service,就必须重新调用startService来启动。
使用场景:表示当Service在执行工作中被中断几次无关紧要或者对Android内存紧张的情况下需要被杀掉且不会立即重新创建这种行为也可接受的话,这是可以在onStartCommand返回值中设置该值。如在Service中定时从服务器中获取最新数据
START_STICKY:表示Service运行的进程被Android系统强制杀掉之后,Android系统会将该Service依然设置为started状态(即运行状态),但是不再保存onStartCommand方法传入的intent对象,然后Android系统会尝试再次重新创建该Service,并执行onStartCommand回调方法,这时onStartCommand回调方法的Intent参数为null,也就是onStartCommand方法虽然会执行但是获取不到intent信息
使用场景:如果你的Service可以在任意时刻运行或结束都没什么问题,而且不需要intent信息,那么就可以在onStartCommand方法中返回START_STICKY,比如一个用来播放背景音乐功能的Service就适合返回该值。
START_REDELIVER_INTENT:表示Service运行的进程被Android系统强制杀掉之后,与返回START_STICKY的情况类似,Android系统会将再次重新创建该Service,并执行onStartCommand回调方法,但是不同的是,Android系统会再次将Service在被杀掉之前最后一次传入onStartCommand方法中的Intent再次保留下来并再次传入到重新创建后的Service的onStartCommand方法中,这样我们就能读取到intent参数
使用场景:如果我们的Service需要依赖具体的Intent才能运行(需要从Intent中读取相关数据信息等),并且在强制销毁后有必要重新创建运行,那么这样的Service就适合返回START_REDELIVER_INTENT。

有以上可知,我们可以在 onStartCommand 中返回 START_STICKY,但是这种方案有如下缺陷:

  • Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
  • 进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。

3. 利用 JobScheduler 机制拉活

Android5.0 以后系统对 Native 进程等加强了管理,Native 拉活方式失效。系统在 Android5.0 以上版本提供了 JobScheduler 接口,系统会定时调用该进程以使应用进行一些逻辑操作。

/**
 * @author: wuchao
 * @date: 2018/5/9 18:14
 * @desciption: 进程保活Service
 */
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class KeepLiveService extends JobService {

    @Override
    public void onCreate() {
        super.onCreate();
        startJobService();
    }

    /**
     * 启动JobScheduler拉活
     * 适用范围:用于Android5.0以后版本进程保活,对被"强制停止"有效
     */
    private void startJobService() {
        int jobId=1;
        JobInfo.Builder builder = new JobInfo.Builder(jobId,
                new ComponentName(getPackageName(), KeepLiveService.class.getName()));
        //设置间隔时间
        builder.setPeriodic(5);
        //设置重启之后你的任务是否还有继续执行
        builder.setPersisted(true);
        JobScheduler jobScheduler = (JobScheduler) this.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        if (jobScheduler != null) {
            jobScheduler.schedule(builder.build());
        }
    }

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

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

4. 利用 Notification 提升权限

  1. 方案设计思想:Android 中 Service 的优先级为4,通过 setForeground 接口可以将后台 Service 设置为前台 Service,使进程的优先级由4提升为2,从而使进程的优先级仅仅低于用户当前正在交互的进程,与可见进程优先级一致,使进程被杀死的概率大大降低
  2. 方案实现:通过实现一个内部 Service,在 KeepLiveService 和其NotifyService 中同时发送具有相同 ID 的 Notification,然后将内部 Service 结束掉。随着内部 Service 的结束,Notification 将会消失,但系统优先级依然保持为2。
    对于 API level < 18:调用startForeground(ID, new Notification()),发送空的Notification ,图标则不会显示。
    对于 API level >= 18:在调用startForeground将Service设置为前台Service时,必须发送一条通知,也就是前台 Service 与一条可见的通知时绑定在一起的。因此需要提优先级的service 启动一个NotifyService,两个服务同时startForeground,且绑定同样的 ID。Stop 掉NotifyService,这样通知栏图标即被移除
  3. 方案具体实现
public class KeepLiveService extends Service {

    /** 通知id*/
    private int NOTIFY_ID = 101;

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2){
            startForeground(NOTIFY_ID,new Notification());
        } else {
        //发送Notification并将其置为前台后,启动NotifyService
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            startForeground(NOTIFY_ID,builder.build());
            startService(new Intent(this,NotifyService.class));
        }
        return START_STICKY;
    }
}
public class NotifyService extends Service {

    /** 通知id*/
    private int NOTIFY_ID = 101;

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
     //发送与KeepLiveService 中ID相同的Notification,然后将其取消并取消自己的前台显示
        Notification.Builder builder = new Notification.Builder(this);
        builder.setSmallIcon(R.mipmap.ic_launcher_round);
        startForeground(NOTIFY_ID,builder.build());
        stopSelf();
        return super.onStartCommand(intent, flags, startId);
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,028评论 25 707
  • 【Android Service】 Service 简介(★★★) 很多情况下,一些与用户很少需要产生交互的应用程...
    Rtia阅读 3,148评论 1 21
  • 2.1 Activity 2.1.1 Activity的生命周期全面分析 典型情况下的生命周期:在用户参与的情况下...
    AndroidMaster阅读 3,038评论 0 8
  • 11.1玉民日志 1.6:00起床 2.6:30-7:30晨跑 3.9:00-17:00工作,去协和看看眼睛 4....
    Mr玉民阅读 142评论 1 2
  • 二十年后遇见你,我拿什么爱你? 还记得年少时的梦吗,像朵永不凋零的花。年少时的我也是个文学青年,那时的我多愁善感,...
    人生如梦001阅读 277评论 0 2