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












网友评论