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