性能优化 (十) APP 持续运行之进程保活实现

性能优化系列

APP 启动优化html

UI 绘制优化java

内存优化android

图片压缩git

长图优化github

电量优化shell

Dex 加解密性能优化

动态替换 Application服务器

APP 稳定性之热修复原理探索app

APP 持续运行之进程保活实现ide

ProGuard 对代码和资源压缩

APK 极限压缩

简介

如今只要是社交 APP 没有哪一个开发者不想让本身的 APP 永久常驻的,想要永久常驻除非大家家的实力很是雄厚,APP 用户量很是大,那么厂商都会主动来找你,把大家家的 APP 加入白名单。不然永久常驻是不可能甚至都不给你权限后台运行。既然不能永久常驻,那么咱们有没有一个办法可使咱们的 APP 不那么容易被系统杀死勒?或者说是杀死后能主动唤醒,显然是能够的,下面咱们进入主题吧。

怎么使用

代码传送阵

  1. down 代码 github.com/yangkun1992… ,将 live_library 放入本身工程

  2. 在 KeepAliveRuning onRuning 中实现须要保活的代码

    public class KeepAliveRuning implements IKeepAliveRuning {
        /**这里实现 Socket / 推送 等一些保活组件*/
        @Override
        public void onRuning() {
            //TODO--------------------------------------------
            Log.e("runing?KeepAliveRuning", "true");
        }
    
        @Override
        public void onStop() {
            Log.e("runing?KeepAliveRuning", "false");
        }
    }
    复制代码
  3. 开启保活

    public void start() {
            //启动保活服务
            KeepAliveManager.toKeepAlive(
                    getApplication()
                    , HIGH_POWER_CONSUMPTION,
                    "进程保活",
                    "Process: System(哥们儿) 我不想被杀死",
                    R.mipmap.ic_launcher,
                    new ForegroundNotification(
                            //定义前台服务的通知点击事件
                            new ForegroundNotificationClickListener() {
                                @Override
                                public void foregroundNotificationClick(Context context, Intent intent) {
                                    Log.d("JOB-->", " foregroundNotificationClick");
                                }
                            })
            );
        }
    复制代码
  4. 中止保活

    KeepAliveManager.stopWork(getApplication());
    复制代码

最终效果

开启保活

  • 长时间运行,不被杀死,若是被杀死双进程会启动死掉的进程

  • 主动杀掉某一独立运行的进程

    咱们应该知道正常的话点击手机回收垃圾桶后台的应用都会被 kill 掉,还有主动点击 AS Logcat 的进程中止运行的按钮,咱们也会发现进程会自动起来而且 pid 跟上一次不同了。要的就是这种效果,下面咱们来了解下进程保活的知识吧.

未开启保活

进程优先级

官网详细介绍

进程

若是内存不足,须要为其余用户提供更紧急服务的进程又须要内存时,Android 可能会决定在某一时刻关闭某一进程。在被终止进程中运行的应用组件也会随之销毁。 当这些组件须要再次运行时,系统将为它们重启进程。

决定终止哪一个进程时,Android 系统将权衡它们对用户的相对重要程度。例如,相对于托管可见 Activity 的进程而言,它更有可能关闭托管屏幕上再也不可见的 Activity 的进程。 所以,是否终止某个进程的决定取决于该进程中所运行组件的状态。 下面,咱们介绍决定终止进程所用的规则。

进程生命周期

Android 系统将尽可能长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终须要移除旧进程来回收内存。 为了肯定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每一个进程放入“重要性层次结构”中。 必要时,系统会首先消除重要性最低的进程,而后是重要性略逊的进程,依此类推,以回收系统资源。

重要性层次结构一共有 5 级。如下列表按照重要程度列出了各种进程(第一个进程最重要,将是最后一个被终止的进程):

名称 归纳 回收状态
前台进程 正在交互 只有在内存不足以支持它们同时继续运行这一万不得已的状况下,系统才会终止它们
可见进程 没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程 可见进程被视为是极其重要的进程,除非为了维持全部前台进程同时运行而必须终止,不然系统不会终止这些进程。
服务进程 正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。 除非内存不足以维持全部前台进程和可见进程同时运行,不然系统会让服务进程保持运行状态。
后台进程 对用户不可见的 Activity 的进程 系统可能随时终止它们
空进程 不含任何活动应用组件的进程 最容易为杀死

LMK(LowMemoryKiller)

为何引入 LMK ?

进程的启动分冷启动和热启动,当用户退出某一个进程的时候,并不会真正的将进程退出,而是将这个进程放到后台,以便下次启动的时候能够立刻启动起来,这个过程名为热启动,这也是Android 的设计理念之一。这个机制会带来一个问题,每一个进程都有本身独立的内存地址空间,随着应用打开数量的增多, 系统已使用的内存愈来愈大,就颇有可能致使系统内存不足。为了解决这个问题,系统引入 LowmemoryKiller (简称 lmk ) 管理全部进程,根据必定策略来 kill 某个进程并释放占用的内存,保证系统的正常运行。

LMK 基本原理

全部应用进程都是从 zygote 孵化出来的,记录在 AMS 中mLruProcesses 列表中,由 AMS 进行统一管理,AMS 中会根据进程的状态更新进程对应的 oom_adj 值,这个值会经过文件传递到 kernel 中去,kernel 有个低内存回收机制,在内存达到必定阀值时会触发清理 oom_adj 值高的进程腾出更多的内存空间

LMK 杀进程标准

minfree : 存放6个数值,单位内存页面数 ( 一个页面 4kb )

内存阈值 内存回收阈值 对应进程
18432 72 M .前台进程(foreground)
23040 90 M 可见进程(visible)
27648 108 M 次要服务(secondary server)
32256 126 M 后台进程(hidden)
36864 144 M 内容供应节点(content provider)
46080 180 M 空进程(empty)

当内存到 180 M的时候会将空进程进行回收,当内存到 144 M 的时候把空进程回收完之后开始对内容供应节点进行回收,并非全部的内容供应节点都回收,而是经过判断它的优先级进行回收,优先级是用 oom_adj 的值来表示,值越大回收的概率越高

adj 查看:

cat /sys/module/lowmemorykiller/parameters/adj
复制代码

查看进程 adj 值:

adb shell ps
复制代码

值越低越不易被回收,0 表明就不会被回收。

内存阈值在不一样的手机上不同,一旦低于该值, Android 便开始按顺序关闭进程. 所以 Android 开始结束优先级最低的空进程,即当可用内存小于 180MB (46080)

进程保活方案

Activity 提权

这里可见 oom_adj 为 0 是不会被回收的

后台 oom_adj 为 6 内存不足会被回收

锁屏 oom_adj 开启一像素 Activity 为 0 至关于可见进程,不易被回收

实现原理:

监控手机锁屏解锁事件,在屏幕锁屏时启动 1 个像素透明的 Activity ,在用户解锁时将 Activity 销毁掉,从而达到提升进程优先级的做用。

代码实现

  1. 建立 onePxActivity

    @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            //设定一像素的activity
            Window window = getWindow();
            window.setGravity(Gravity.START | Gravity.TOP);
            WindowManager.LayoutParams params = window.getAttributes();
            params.x = 0;
            params.y = 0;
            params.height = 1;
            params.width = 1;
            window.setAttributes(params);
            //在一像素activity里注册广播接受者 接受到广播结束掉一像素
            br = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    finish();
                }
            };
            registerReceiver(br, new IntentFilter("finish activity"));
            checkScreenOn("onCreate");
        }
    复制代码
  2. 建立锁屏开屏广播接收

    @Override
        public void onReceive(final Context context, Intent intent) {
            if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {    //屏幕关闭的时候接受到广播
                appIsForeground = IsForeground(context);
                try {
                    Intent it = new Intent(context, OnePixelActivity.class);
                    it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    it.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
                    context.startActivity(it);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //通知屏幕已关闭,开始播放无声音乐
                context.sendBroadcast(new Intent("_ACTION_SCREEN_OFF"));
            } else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {   //屏幕打开的时候发送广播 结束一像素
                context.sendBroadcast(new Intent("finish activity"));
                if (!appIsForeground) {
                    appIsForeground = false;
                    try {
                        Intent home = new Intent(Intent.ACTION_MAIN);
                        home.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        home.addCategory(Intent.CATEGORY_HOME);
                        context.getApplicationContext().startActivity(home);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                //通知屏幕已点亮,中止播放无声音乐
                context.sendBroadcast(new Intent("_ACTION_SCREEN_ON"));
            }
        }
    复制代码

Service 提权

建立一个前台服务用于提升 app 在按下 home 键以后的进程优先级

private void startService(Context context) {
        try {
            Log.i(TAG, "---》启动双进程保活服务");
            //启动本地服务
            Intent localIntent = new Intent(context, LocalService.class);
            //启动守护进程
            Intent guardIntent = new Intent(context, RemoteService.class);
            if (Build.VERSION.SDK_INT >= 26) {
                startForegroundService(localIntent);
                startForegroundService(guardIntent);
            } else {
                startService(localIntent);
                startService(guardIntent);
            }
        } catch (Exception e) {
            Log.e(TAG, e.getMessage());
        }
    }
复制代码

注意若是开启 startForegroundService 前台服务,那么必须在 5 s内开启一个前台进程的服务通知栏,否则会报 ANR

startForeground(KeepAliveConfig.FOREGROUD_NOTIFICATION_ID, notification);
复制代码

广播拉活(在 8.0 如下很受用)

在发生特定系统事件时,系统会发出广播,经过在 AndroidManifest 中静态注册对应的广播监听器,便可在发生响应事件时拉活。可是从android 7.0 开始,对广播进行了限制,并且在 8.0 更加严格。

以静态广播的形式注册

<receiver android:name=".receive.NotificationClickReceiver">
<intent-filter>
<action android:name="CLICK_NOTIFICATION"></action>
</intent-filter>
</receiver>
复制代码

全家桶 拉活

有多个 app 在用户设备上安装,只要开启其中一个就能够将其余的app 也拉活。好比手机里装了手 Q、QQ 空间、兴趣部落等等,那么打开任意一个 app 后,其余的 app 也都会被唤醒。

Service 机制拉活

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

只要 targetSdkVersion 不小于5,就默认是 START_STICKY。 可是某些 ROM 系统不会拉活。而且通过测试,Service 第一次被异常杀死后很快被重启,第二次会比第一次慢,第三次又会比前一次慢,一旦在短期内 Service 被杀死 4-5 次,则系统再也不拉起。

帐号同步拉活(只作了解,不靠谱)

手机系统设置里会有 “账户” 一项功能,任何第三方 APP 均可以经过此功能将数据在必定时间内同步到服务器中去。系统在将 APP 账户同步时,会将未启动的 APP 进程拉活

JobScheduler 拉活(靠谱,8.0 官方推荐)

JobScheduler 容许在特定状态与特定时间间隔周期执行任务。能够利用它的这个特色完成保活的功能,效果即开启一个定时器,与普通定时器不一样的是其调度由系统完成。

注意 setPeriodic 方法 在 7.0 以上若是设置小于 15 min 不起做用,可使用setMinimumLatency 设置延时启动,而且轮询

public static void startJob(Context context) {
        try {
            mJobScheduler = (JobScheduler) context.getSystemService(
                    Context.JOB_SCHEDULER_SERVICE);
            JobInfo.Builder builder = new JobInfo.Builder(10,
                    new ComponentName(context.getPackageName(),
                            JobHandlerService.class.getName())).setPersisted(true);
            /** * I was having this problem and after review some blogs and the official documentation, * I realised that JobScheduler is having difference behavior on Android N(24 and 25). * JobScheduler works with a minimum periodic of 15 mins. * */
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                //7.0以上延迟1s执行
                builder.setMinimumLatency(KeepAliveConfig.JOB_TIME);
            } else {
                //每隔1s执行一次job
                builder.setPeriodic(KeepAliveConfig.JOB_TIME);
            }
            mJobScheduler.schedule(builder.build());

        } catch (Exception e) {
            Log.e("startJob->", e.getMessage());
        }
    }
复制代码

推送拉活

根据终端不一样,在小米手机(包括 MIUI)接入小米推送、华为手机接入华为推送。

Native 拉活

Native fork 子进程用于观察当前 app 主进程的存亡状态。对于 5.0以上成功率极低。

后台循环播放一条无声文件

//若是选择流氓模式,就默认接收了耗电的缺点,可是保活效果很好。 
if (mediaPlayer == null && KeepAliveConfig.runMode == RunMode.HIGH_POWER_CONSUMPTION) {
            mediaPlayer = MediaPlayer.create(this, R.raw.novioce);
            mediaPlayer.setVolume(0f, 0f);
            mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mediaPlayer) {
                    Log.i(TAG, "循环播放音乐");
                    play();
                }
            });
            play();
        }
复制代码

双进程守护 (靠谱)

两个进程相互绑定 (bindService),若是有其中一个进程被杀,那么另一个进程就会将被杀的进程从新拉起

总结

进程保活就讲到这里了,最后我本身是结合里面最靠谱的(Activity + Service 提权 + Service 机制拉活 + JobScheduler 定时检测进程是否运行 + 后台播放无声文件 + 双进程守护),而后组成了一个进程保活终极方案。 文章中只是部分代码,感兴趣的能够下载 demo 试下保活效果。

感谢