美文网首页
性能优化<第九篇>:进程保活方案

性能优化<第九篇>:进程保活方案

作者: NoBugException | 来源:发表于2021-06-28 00:23 被阅读0次

    为什么保证应用某些业务的正常运行,有时候必须要保证进程是存活的,但是目前所有的进程保活方案都不是绝对的,不管如何保活,进程总是存在一定的概率会被系统杀死。
    本文将围绕进程保活方案展开详细讲解。

    1、Android中的进程

    一般情况下,一个应用只有一个进程,整个应用功能将由唯一的一个Application管理。
    在清单文件AndroidManifest.xml中,声明了四大组件,分别是:Activity、Service、Receiver、Provider,它们都可以使用

    android:process=":processName"
    

    将组件分配到其它进程中执行。

    所以,一个应用可以有多个进程,每个进程都分别管理着自己的Application实例。

    在一个应用或者多个应用之间的进程可以相互通信,它们之间的通信由Android自带的IPC机制来完成,但是,想要通信,必须要保证进程活着,如果进程一旦被杀死,那么进程间通信的桥梁就会断开,从而无法通信。

    为了业务的需要,进程保活方案迫在眉睫。

    2、明确保活的切入点

    Android系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,需要清除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会对进程进行分类。 需要时,系统会首先消除重要性最低的进程,然后是清除重要性稍低一级的进程,依此类推,以回收系统资源。

    进程分为:前台进程可见进程服务进程后台进程空进程

    前台进程:即当前正在前台运行的进程,说明用户当前正在与通过该进程与系统进行交互,所以该进程为最重要的进程,
    除非系统的内容已经到不堪重负的情况,否则系统是不会将改进程终止的。
    
    可见进程:一般还是显示在屏幕中,但是用户并没有直接与之进行交互,该进程对用户来说同样是非常重要的进程,
    除非为了保证前台进程的正常运行,否则Android系统一般是不会将该进程终止的。
    
    服务进程:便是拥有Service进程,该进程一般是在后台为用户服务的。一般情况下,Android系统是不会将其中断的,
    除非系统的内容以及达到崩溃的边缘,必须通过释放该进程才能保证前台进程的正常运行时,才可能将其终止。
    
    后台进程:一般对用户的作用不大,缺少该进程并不会影响用户对系统的体验。
    所以如果系统需要终止某个进程才能保证系统正常运行,那么会有非常大的几率将该进程终止。
    
    空进程:对用户没有任何作用的进程,该进程一般是为缓存机制服务的,当系统需要终止某个进程保证系统的正常服务时,会首先将该进程终止。
    

    通过进程的概念,我们知道了进程是被系统杀死的,系统将通过某种机制将进程设置优先级,优先级低的进程最先会被杀死。

    所以,进程保护方案的切入点是: 提高进程优先级,降低进程被杀死的概率

    3、低内存管理机制

    低内存管理机制是系统为了提高性能,选择性的杀死优先级较低的进程,英文是LowMemoryKiller,可以在系统日志中搜索到,简称LMK

    为什么要引入LMK?

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

    LMK基本原理?

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

    低内存阀值?

    如果您的手机已经Root,并且支持LMK,那么可以使用如下命令查看您的低内存阀值:
    
    adb shell
    su
    cat /sys/module/lowmemorykiller/parameters/minfree
    
    如果minfree文件没有读取权限就给它一个权限:
    
        adb shell chmod 777 /sys/module/lowmemorykiller/parameters/minfree
    
    可能会输出下列阈值,如下:
    
    18432,23040,27648,32256,36864,46080
    
    这些阀值可以配置,所以不同的手机,它们的值不一定一样。
    Android将进程分成6个等级,和上面介绍的进程的分类较为类似,这6个等级分别是:
        前台进程(foreground)
        可见进程(visible)
        次要服务(secondary server)
        后台进程(hidden)
        内容供应节点(content provider)
        空进程(empty)
    
    上面6个等级分别和6个阀值一一对应:
        前台进程               -->        18432 page = 73728KB    -->   72M
        可见进程               -->        23040 page = 92160KB    -->   90M
        次要服务               -->        27648 page = 110592KB    -->   108M
        后台进程               -->        32256 page = 129024KB    -->   126M
        内容供应节点           -->        36864 page = 147456KB    -->   144M
        空进程                 -->        46080 page = 184320KB    -->   180M
    
    minfree的单位是page, 1 page = 4KB。
    
    * 当可用内存小于180M时,系统会杀死空进程;
    * 当可用内存小于144M时,系统会杀死内容供应节点进程;
    * 当可用内存小于126M时,系统会杀死后台进程,后台进程较多,优先杀死adj值较高的进程;
    * 当可用内存小于108M时,系统会杀死次要服务进程;
    * 当可用内存小于90M时,系统会杀死可见进程;
    * 当可用内存小于72M时,系统会直接杀死前台进程,前台进程比较敏感,给用户的感觉就是app直接闪退;
    
    另外,进程的adj也有阀值,输入以下命令可以获取adj的阀值:
    
    adb shell
    su
    cat /sys/module/lowmemorykiller/parameters/adj
    
    如果adj文件没有读取权限就给它一个权限:
    
        adb shell chmod 777 /sys/module/lowmemorykiller/parameters/adj
    
    可能会输出下列阈值,如下:
    
    0,58,117,176,529,1000
    
    adj的阀值和低内存阀值一一对应:
    
    前台进程               -->        18432 page = 73728KB    -->   72M    -->   adj(0)
    可见进程               -->        23040 page = 92160KB    -->   90M    -->   adj(58)
    次要服务               -->        27648 page = 110592KB    -->   108M    -->   adj(117)
    后台进程               -->        32256 page = 129024KB    -->   126M    -->   adj(176)
    内容供应节点           -->        36864 page = 147456KB    -->   144M    -->   adj(529)
    空进程                 -->        46080 page = 184320KB    -->   180M    -->   adj(1000)
    
    * 当可用内存小于180M时,系统会杀死adj大于等于1000的进程;
    * 当可用内存小于144M时,系统会杀死adj大于等于529的进程;
    * 当可用内存小于126M时,系统会杀死adj大于等于176的进程;
    * 当可用内存小于108M时,系统会杀死adj大于等于117的进程;
    * 当可用内存小于90M时,系统会杀死adj大于等于58的进程;
    * 当可用内存小于72M时,系统会杀死adj大于等于0的进程;
    
    其中,adj值越大,越优先被系统杀死;
    

    进程的adj值?

    系统为每个进程都分配了一个adj。
    
    输入以下命令可以查看设备中所有的进程:
    
        adb shell
        ps
    
    输入以下命令可以查看指定进程的adj值以及进程打分情况:
        adb shell
        su
        cat /proc/<pid>/oom_adj
        cat /proc/<pid>/oom_score
        cat /proc/<pid>/oom_score_adj
    
    oom_adj是adj值,比如取值为0;(最低取值为-17)
    oom_score是系统打分+用户打分,比如取值为23;(最低取值为0)
    oom_score_adj是用户打分,比如取值为0;(最低取值为-1000)
    
    Native进程,三者的取值分别为:-17、0、-1000,也就是说,Native进程无法被杀死。
    
    
    oom_adj的取值范围是-17~16,进程的优先级通过进程的adj值来反映,它是linux内核分配给每个系统进程的一个值,进程回收机制根据这个值来决定是否进行回收。
    oom_adj的值越小,进程的优先级越高。
    
    oom_adj的取值对应表如下:
    
    adj级别 说明
    UNKNOWN_ADJ 16 预留的最低级别,一般对于缓存的进程才有可能设置成这个级别
    CACHED_APP_MAX_ADJ 15 缓存进程,空进程,在内存不足的情况下就会优先被kill
    CACHED_APP_MIN_ADJ 9 缓存进程,也就是空进程
    SERVICE_B_ADJ 8 不活跃的进程
    PREVIOUS_APP_ADJ 7 切换进程
    HOME_APP_ADJ 6 与Home交互的进程
    SERVICE_ADJ 5 有Service的进程
    HEAVY_WEIGHT_APP_ADJ 4 高权重进程
    BACKUP_APP_ADJ 3 正在备份的进程
    PERCEPTIBLE_APP_ADJ 2 可感知的进程,比如那种播放音乐
    VISIBLE_APP_ADJ 1 可见进程
    FOREGROUND_APP_ADJ 0 前台进程
    PERSISTENT_SERVICE_ADJ -11 重要进程
    PERSISTENT_PROC_ADJ -12 核心进程
    SYSTEM_ADJ -16 系统进程
    NATIVE_ADJ -17 Native进程

    现在打开Android模拟器,进程测试。

    【第一步】 用AS检查当前进程ID

    图片.png

    在AS的Terminal中输入命令:

    adb shell
    su
    cat /proc/6325/oom_adj
    

    当前进程在前台时,ADJ值为0,如图:

    图片.png

    按Home键(或Recent键)将进程切入后台,ADJ值从0变成了11,如图:

    图片.png

    将进程从后台切换到前台,ADJ值又从11变成0;

    4、保活方案一:一像素保活

    方案?

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

    目的?

    启动一个1像素的界面是为了Activity提权,将ADJ值变小。
    

    代码实现?

    1、创建1个像素的Activity
    
    public class KeepActivity extends Activity {
        private static final String TAG = "KeepActivity";
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            Log.e(TAG,"启动Keep");
            Window window = getWindow();
            //设置这个activity在左上角
            window.setGravity(Gravity.START | Gravity.TOP);
            WindowManager.LayoutParams attributes = window.getAttributes();
            //宽高为1
            attributes.width = 1;
            attributes.height = 1;
            //起始位置左上角
            attributes.x = 0;
            attributes.y = 0;
            window.setAttributes(attributes);
    
            KeepManager.getInstance().setKeepActivity(this);
        }
    }
    
    Activity的主题:
    
    <style name="KeepTheme">
        <item name="android:windowBackground">@null</item>
        <item name="android:windowIsTranslucent">true</item>
    </style>
    
    2、广播接收者
    
    public class KeepReceiver extends BroadcastReceiver {
        private static final String TAG = "KeepReceiver";
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            Log.e(TAG, "receive:" + action);
            if (TextUtils.equals(action, Intent.ACTION_SCREEN_OFF)) {
                //灭屏 开启1px activity
                KeepManager.getInstance().startKeep(context);
            } else if (TextUtils.equals(action, Intent.ACTION_SCREEN_ON)) {
                //亮屏 关闭
                KeepManager.getInstance().finishKeep();
            }
        }
    }
    
    3、创建广播注册管理单例类
    
    public class KeepManager {
        private static final KeepManager ourInstance = new KeepManager();
    
        public static KeepManager getInstance() {
            return ourInstance;
        }
    
        private KeepManager() {
        }
        private KeepReceiver keepReceiver;
        private WeakReference<Activity> mKeepActivity;
        /**
         * 注册
         * @param context
         */
        public void registerKeepReceiver(Context context){
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_SCREEN_OFF);
            filter.addAction(Intent.ACTION_SCREEN_ON);
            keepReceiver = new KeepReceiver();
            context.registerReceiver(keepReceiver, filter);
        }
    
        /**
         * 反注册
         * @param context
         */
        public void unRegisterKeepReceiver(Context context){
            if (null != keepReceiver) {
                context.unregisterReceiver(keepReceiver);
            }
        }
    
        /**
         * 启动1个像素的KeepActivity
         * @param context
         */
        public void startKeep(Context context) {
            Intent intent = new Intent(context, KeepActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent);
        }
    
        /**
         * finish1个像素的KeepActivity
         */
        public void finishKeep() {
            if (null != mKeepActivity) {
                Activity activity = mKeepActivity.get();
                if (null != activity) {
                    activity.finish();
                }
                mKeepActivity = null;
            }
        }
    
        public void setKeepActivity(KeepActivity mKeepActivity) {
            this.mKeepActivity = new WeakReference<Activity>(mKeepActivity);
        }
    }
    
    5、保活方案二:Service提权
    1、创建一个前台服务用于提高app在按下home键之后的进程优先级;
    2、Service限制
    
    https://developer.android.google.cn/about/versions/oreo/background#services
    
    startForeground(ID,Notification):使Service成为前台Service。 前台服务需要在通知栏显示一条通知。
    
    
    代码实现:
    
    public class ForegroundService extends Service {
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                NotificationChannel channel = new NotificationChannel("service", "service",
                        NotificationManager.IMPORTANCE_LOW);
                NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
                if (manager == null)
                    return;
                manager.createNotificationChannel(channel);
    
                Notification notification = new NotificationCompat.Builder(this, "service").setAutoCancel(true).setCategory(
                        Notification.CATEGORY_SERVICE).setOngoing(true).setPriority(
                        NotificationManager.IMPORTANCE_LOW).build();
                startForeground(10, notification);
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                //如果 18 以上的设备 启动一个Service startForeground给相同的id
                //然后结束那个Service
                startForeground(10, new Notification());
                startService(new Intent(this, InnnerService.class));
            } else {
                startForeground(10, new Notification());
            }
        }
    
        public static class InnnerService extends Service {
    
            @Override
            public void onCreate() {
                super.onCreate();
                startForeground(10, new Notification());
                stopSelf();
            }
    
            @Nullable
            @Override
            public IBinder onBind(Intent intent) {
                return null;
            }
        }
    }
    
    6、保活方案三:广播拉活
    在发生特定系统事件时,系统会发出广播,通过在 AndroidManifest 中静态注册对应的广播监听器,
    即可在发生响应事件时拉活。但是从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
    
    7、保活方案四:“全家桶”拉活
    有多个app在用户设备上安装,只要开启其中一个就可以将其他的app也拉活。
    比如手机里装了手Q、QQ空间、兴趣部落等等,那么打开任意一个app后,其他的app也都会被唤醒。
    
    8、保活方案五:Service机制(Sticky)拉活
    将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后自动拉活。
    
    START_STICKY:“粘性”。如果service进程被kill掉,保留service的状态为开始状态,但不保留递送的intent对象。
    随后系统会尝试重新创建service,由于服务状态为开始状态,所以创建服务后一定会调用onStartCommand(Intent,int,int)方法。
    如果在此期间没有任何启动命令被传递到service,那么参数Intent将为null。
    
    START_NOT_STICKY:“非粘性的”。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉,系统不会自动重启该服务。
    
    START_REDELIVER_INTENT:重传Intent。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉,系统会自动重启该服务,
    并将Intent的值传入。
    
    START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保证服务被kill后一定能重启。
    
    只要 targetSdkVersion 不小于5,就默认是 START_STICKY。
    但是某些ROM 系统不会拉活。并且经过测试,Service 第一次被异常杀死后很快被重启,第二次会比第一次慢,第三次又会比前一次慢,
    一旦在短时间内 Service 被杀死4-5次,则系统不再拉起。
    
    代码实现如下:
    
    public class StickyService extends Service {
        @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 @StartResult int onStartCommand(Intent intent, @StartArgFlags int flags, int startId) {
        onStart(intent, startId);
        return mStartCompatibility ? START_STICKY_COMPATIBILITY : START_STICKY;
    }
    
    8、保活方案五:账户同步拉活

    相关文章

      网友评论

          本文标题:性能优化<第九篇>:进程保活方案

          本文链接:https://www.haomeiwen.com/subject/yovfultx.html