一、前言
说到进程保活,大家往往联想到hacking和“流氓”软件。这是一些不负责任的开发者滥用进程保活,导致了用户的反感和抵触情绪。实际上大部分软件是不需要常驻进程的,开发人员应该充分考虑常驻进程对手机性能的影响和用户情感的伤害。对于系统而言,没有哪个App可以做到“永生”的。尤其在现在手机产品创新不足,性能至上的大环境下,你的处心积虑和不择手段最后只能是手机厂商发布会上性能优化背后的炮灰。但对一些特殊应用,确实需要常驻进程来完成一些连续性工作才能给用户带来完美的体验。我们有必要研究让进程尽可能长时间存活的方法,但不能指望它真能永久存在,毕竟,对于系统而言,一切App都在“裸奔”。聪明的做法是一面尽量延长进程存活时间,一面做好进程真被杀死后的“善后”工作。至于延长进程存活时间的方法,应该尽量利用Android自身的进程生命周期管理规则,过于投机取巧和“耍流氓”只会引来安全软件毫不留情的“封杀”。
二、LowMemoryKiller机制
想要好好活着,就应该研究如何死去;想要进程保活,首先应该研究Android中进程的生命周期。Android 系统会尽量长时间地保持应用进程。除非进程被主动kill掉,用户应用退出后,该进程还会在系统中缓存,这样用户再次启动 App 时,会加速启动。随着启动的应用越来越多,系统内存越来越少,当没有足够内存打开新进程时,就需要移除旧进程来回收内存。为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。 必要时,系统会首先消除重要性最低的进程,然后是重要性略逊的进程,依此类推,以回收系统资源,这就是Android的LowMemoryKiller机制,也就是系统用于判定是否需要杀进程和杀哪些进程的一个机制。
(1)进程优先级划分
笼统地说,Android将进程优先级分为5级,优先级越低,越早被kill:
- 前台进程——用户当前操作所必需的进程
- 可见进程——没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程
- 服务进程——正在运行已使用
startService()
方法启动的服务且不属于上述两个更高类别进程的进程 - 后台进程——包含目前对用户不可见的 Activity 的进程(已调用 Activity 的
onStop()
方法) - 空进程——不含任何活动应用组件的进程
实际上,android对进程的优先级划分要细致得多,我们可以称之为OOM Adjustment列表,具体定义在platform/frameworks/base/services/core/java/com/android/server/am/ProcessList.java中:
// Adjustment used in certain places where we don't know it yet. // (Generally this is something that is going to be cached, but we // don't know the exact value in the cached range to assign yet.) static final int UNKNOWN_ADJ = 1001; // This is a process only hosting activities that are not visible, // so it can be killed without any disruption. static final int CACHED_APP_MAX_ADJ = 906; static final int CACHED_APP_MIN_ADJ = 900; // The B list of SERVICE_ADJ -- these are the old and decrepit // services that aren't as shiny and interesting as the ones in the A list. static final int SERVICE_B_ADJ = 800; // This is the process of the previous application that the user was in. // This process is kept above other things, because it is very common to // switch back to the previous app. This is important both for recent // task switch (toggling between the two top recent apps) as well as normal // UI flow such as clicking on a URI in the e-mail app to view in the browser, // and then pressing back to return to e-mail. static final int PREVIOUS_APP_ADJ = 700; // This is a process holding the home application -- we want to try // avoiding killing it, even if it would normally be in the background, // because the user interacts with it so much. static final int HOME_APP_ADJ = 600; // This is a process holding an application service -- killing it will not // have much of an impact as far as the user is concerned. static final int SERVICE_ADJ = 500; // This is a process with a heavy-weight application. It is in the // background, but we want to try to avoid killing it. Value set in // system/rootdir/init.rc on startup. static final int HEAVY_WEIGHT_APP_ADJ = 400; // This is a process currently hosting a backup operation. Killing it // is not entirely fatal but is generally a bad idea. static final int BACKUP_APP_ADJ = 300; // This is a process only hosting components that are perceptible to the // user, and we really want to avoid killing them, but they are not // immediately visible. An example is background music playback. static final int PERCEPTIBLE_APP_ADJ = 200; // This is a process only hosting activities that are visible to the // user, so we'd prefer they don't disappear. static final int VISIBLE_APP_ADJ = 100; static final int VISIBLE_APP_LAYER_MAX = PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1; // This is the process running the current foreground app. We'd really // rather not kill it! static final int FOREGROUND_APP_ADJ = 0; // This is a process that the system or a persistent process has bound to, // and indicated it is important. static final int PERSISTENT_SERVICE_ADJ = -700; // This is a system persistent process, such as telephony. Definitely // don't want to kill it, but doing so is not completely fatal. static final int PERSISTENT_PROC_ADJ = -800; // The system process runs at the default adjustment. static final int SYSTEM_ADJ = -900; // Special code for native processes that are not being managed by the system (so // don't have an oom adj assigned by the system). static final int NATIVE_ADJ = -1000;
我们整理成列表:
ADJ | Value | Note |
---|---|---|
UNKNOWN_ADJ | 1001 | 无法获取adj值,一般指即将缓存的进程 |
CACHED_APP_MAX_ADJ | 906 | 不可见进程adj最大值 |
CACHED_APP_MIN_ADJ | 900 | 不可见进程adj最小值 |
SERVICE_B_ADJ | 800 | 拥有较老的、使用可能性更小的services的进程 |
PREVIOUS_APP_ADJ | 700 | 上一个App的进程(往往通过按返回键) |
HOME_APP_ADJ | 600 | Home进程 |
SERVICE_ADJ | 500 | 服务进程(Service process) |
HEAVY_WEIGHT_APP_ADJ | 400 | 在system/rootdir/init.rc中定义的重量级后台进程 |
BACKUP_APP_ADJ | 300 | 正在备份操作的进程 |
PERCEPTIBLE_APP_ADJ | 200 | 可感知进程,比如后台音乐播放 |
VISIBLE_APP_ADJ | 100 | 可见进程(Visible process) |
FOREGROUND_APP_ADJ | 0 | 前台进程(Foreground process) |
PERSISTENT_SERVICE_ADJ | -700 | 被系统进程或persistent进程绑定的进程 |
PERSISTENT_PROC_ADJ | -800 | persistent进程,比如telephony |
SYSTEM_ADJ | -900 | 系统进程 |
NATIVE_ADJ | -1000 | native进程(不被系统管理) |
系统会为每一个进程记录它对应的adj值,具体目录在:/proc/进程id/oom_adj
比如,我们先通过procrank命令查看com.tencent.news对应的pid是27931
PID Vss Rss Pss Uss Swap PSwap USwap ZSwap cmdline 21005 4751032K 361032K 251168K 238692K 0K 0K 0K 0K com.android.systemui 1685 4853360K 316904K 205238K 191832K 0K 0K 0K 0K system_server 27931 1996008K 247720K 169506K 150628K 0K 0K 0K 0K com.tencent.news
然后cat /proc/27931/oom_score_adj(另一个oom_adj文件中存储的是kernel层adj值)
/proc/27931 # cat oom_score_adj
900
因为刚刚打开过,所以com.tencent.news进程的adj是900,表示后台进程中优先级最高的。
一个进程的adj值会随着进程的状态变化而变化,而进程的状态又依赖于它所包含的各个组件的状态,所以adj值跟新的时机一般发生在各组件的生命周期函数回调,比如Activity启动活销毁。
仍以com.tencent.news为例:
- 当我们打开腾讯新闻客户端时,其adj值为0,即前台进程;
- 当按下Home键后,其adj变为700,即上一个App进程;
- 当双击返回键退出后,其adj变为900,即后台进程。
(2)进程kill节点
知道了进程的优先级表现为其oom_adj值,那么进程究竟什么时候被系统kill掉呢?
首先,我们看两个配置文件:
/sys/module/lowmemorykiller/parameters/minfree
/sys/module/lowmemorykiller/parameters # cat minfree
55296,69120,82944,96768,165888,241920
/sys/module/lowmemorykiller/parameters/adj
/sys/module/lowmemorykiller/parameters # cat adj
0,100,200,300,900,906
其中,minfree中配置的是进程kill节点,单位是page(1page = 4kb);adj文件配置的是对应被kill掉的进程的adj值。比如,被当剩余内存小于241920(即945MB)时,kill掉adj大于906的进程,以此类推。
这样我们就得出一个结论:尽量让自己的进程保持高优先级,也就是降低adj值,就可以更晚被杀死!
三、进程保活
下面介绍几种常用的进程保活方案。还是那句话,没有哪个方案是万能的:这个版本管用,可能下个版本又不管用了;这个型号的手机管用,可能其他型号的手机又不管用了。任何方案都需要不断升级,协同作用。
注:以下方案测试结果均基于Lenovo JD2019(Android P)。
(1)单像素Activity
前台进程被杀死的几率是很小的,如果能让我们的进程尽量保持在前台,那必然会大大增加进程存活时间。基于这个思路,我们可以在灭屏时启动一个单像素的Activity,这样在灭屏后我们就成了前台进程。
首先,我们创建一个单像素Activity
public class SinglePixelActivity extends Activity { public static void actionToSinglePixelActivity(Context pContext) { Intent intent = new Intent(pContext, SinglePixelActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); pContext.startActivity(intent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_singlepixel); Window window = getWindow(); //放在左上角 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); //保存实例,亮屏后finish ScreenManager.getInstance(this).setActivity(this); } }
然后,创建一个单例类ScreenManager来管理此单像素Activity
public class ScreenManager { private Context mContext; private WeakReference<Activity> mActivityWref; public static ScreenManager gDefualt; public static ScreenManager getInstance(Context pContext) { if (gDefualt == null) { gDefualt = new ScreenManager(pContext.getApplicationContext()); } return gDefualt; } private ScreenManager(Context pContext) { this.mContext = pContext; } //启动SinglePixelActivity public void startActivity() { SinglePixelActivity.actionToSinglePixelActivity(mContext); } //保存SinglePixelActivity实例 public void setActivity(Activity pActivity) { mActivityWref = new WeakReference<Activity>(pActivity); } //结束掉SinglePixelActivity public void finishActivity() { if (mActivityWref != null) { Activity activity = mActivityWref.get(); if (activity != null) { activity.finish(); } } } }
最后,我们创建ScreenBroadcastListener累来监听屏幕状态
public class ScreenBroadcastListener { private Context mContext; private ScreenBroadcastReceiver mScreenReceiver; private ScreenStateListener mListener; //定义屏幕监听接口 interface ScreenStateListener { void onScreenOn(); void onScreenOff(); } public ScreenBroadcastListener(Context context) { mContext = context.getApplicationContext(); //注册监听屏幕状态 mScreenReceiver = new ScreenBroadcastReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_SCREEN_OFF); mContext.registerReceiver(mScreenReceiver, filter); } private class ScreenBroadcastReceiver extends BroadcastReceiver { private String action = null; @Override public void onReceive(Context context, Intent intent) { action = intent.getAction(); if (Intent.ACTION_SCREEN_ON.equals(action)) { //亮屏 mListener.onScreenOn(); } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { //灭屏 mListener.onScreenOff(); } } } public void registerListener(ScreenStateListener listener) { mListener = listener; } }
在MainActivity注册监听:
public class MainActivity extends AppCompatActivity { private static final String TAG = "debug_a"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ScreenManager screenManager = ScreenManager.getInstance(MainActivity.this); ScreenBroadcastListener listener = new ScreenBroadcastListener(this); //注册监听 listener.registerListener(new ScreenBroadcastListener.ScreenStateListener() { @Override public void onScreenOn() { Log.i(TAG, "onScreenOn"); screenManager.finishActivity(); } @Override public void onScreenOff() { Log.i(TAG, "onScreenOff"); screenManager.startActivity(); } }); } }
App退出后,adj变为900,锁屏后adj再次变为0:
#/proc/4225 # cat oom_score_adj 900 #/proc/4225 # cat oom_score_adj 0
(2)隐形通知
这次,我们将目标定在adj=200的可感知应用(PERCEPTIBLE_APP_ADJ),天气和音乐类App总是希望能保持一个常驻通知,即使应用退出,依然保持前台服务(关于前台服务请自行查阅)的优先级,避免进入后台后被过早杀死。对于其他应用来说,在用户眼皮底下保持一个常驻通知不太现实。现在我们就来实现不保留通知的前提下依然保持前台服务的优先级。
首先,我们创建一个KeepLiveService服务,这是一个前台服务,发送一个常驻通知,可以保证我们App退出后,adj不超过200:
public class KeepLiveService extends Service { public static final int NOTIFICATION_ID = 0x11; static String CHANNEL_ID = "DEBUG_CHANNEL_ID"; static String CHANNEL_NAME = "DEBUG_CHANNEL_NAME"; NotificationChannel notificationChannel = null; @Override public IBinder onBind(Intent intent) { throw new UnsupportedOperationException("Not yet implemented"); } @TargetApi(Build.VERSION_CODES.O) @Override public void onCreate() { super.onCreate(); //Android各版本发通知的方式是不同的,需要适配,限于篇幅,这里仅O以上版本为例 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { notificationChannel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); //IMPORTANCE_LOW不会有声音提醒 notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); manager.createNotificationChannel(notificationChannel); } Notification notification = new Notification.Builder(this).setChannelId(CHANNEL_ID) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle("") .setContentText("") .build(); notification.flags |= Notification.FLAG_NO_CLEAR; startForeground(NOTIFICATION_ID, notification); //Android O以上版本启动service 5s内必须调用startForeground } }
同时,记得添加权限:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
然后,我们再创建一个HideNotificationService服务,隐藏KeepLiveService的常驻通知:
public class HideNotificationService extends Service { public static final int NOTIFICATION_ID = 0x11; static String CHANNEL_ID = "DEBUG_CHANNEL_ID"; @Override public IBinder onBind(Intent intent) { return null; } @TargetApi(Build.VERSION_CODES.O) @Override public void onCreate() { super.onCreate(); //判断CHANNEL_ID是否已经存在 final NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); NotificationChannel channel = manager.getNotificationChannel(CHANNEL_ID); if (channel == null) { Log.i("opop","KeepLiveService is started, return!"); stopSelf(); return; } //发送与KeepLiveService中ID相同的Notification,“抵消”原通知,然后取消自己的前台显示 Notification notification = new Notification.Builder(this).setChannelId(CHANNEL_ID) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle("") .setContentText("") .build(); notification.flags |= Notification.FLAG_NO_CLEAR; startForeground(NOTIFICATION_ID, notification); //Android O以上版本启动service 5s内必须调用startForeground new Handler().postDelayed(new Runnable() { @Override public void run() { stopForeground(true); //取消前台显示 manager.deleteNotificationChannel(CHANNEL_ID); //删除通知 stopSelf(); //结束服务 } }, 100); } }
最后,同样在MainActivity中调用:
public class MainActivity extends AppCompatActivity { private static final String TAG = "debug_a"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onResume() { super.onResume(); //启动隐藏通知服务 startService(); } private void startService() { startService(new Intent(this, KeepLiveService.class)); startService(new Intent(this, HideNotificationService.class)); } }
测试结果,退出App后,adj=50,继续打开其他应用后,adj=200:
#:/proc/12095 # cat oom_score_adj 0 #:/proc/12095 # cat oom_score_adj 50 #:/proc/12095 # cat oom_score_adj 200
(3)家族系应用互拉
这种方式比较流氓,但确实有效,比如百度系App,只要有一个启动,就可以拉起旗下任意App。用到的技术就是一个App调用另一个App,一般采用Intent隐式启动,有三种方式:
- Package
- Action
- Uri
通过Package启动:
Intent intent = new Intent(); intent.setClassName("com.xibeixue.debugapp", "com.xibeixue.debugapp.MainActivity"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent);
通过Action启动:
<intent-filter> <action android:name="xibeixue.main" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> Intent intent = new Intent(); intent.setAction("xibeixue.main");//这个值一定要和B应用的action一致,否则会报错 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent);
通过Uri启动:
<intent-filter> <data android:host="com.xibeixue.debugapp" android:path="/main" android:scheme="xibeixue" /> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> </intent-filter>
Intent intent = new Intent(); Uri uri = Uri.parse("xibeixue://com.xibeixue.debugapp/main"); intent.putExtra("", "");//也可以直接在URI中传递参数:"scheme://host/path?xx=xx" intent.setData(uri); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent);
(4) 蹭广播
与接收系统广播类似,不同的是该方案为接收第三方 Top 应用广播。没有家族系的背景,只能“拿来主义”了。通过反编译第三方 Top 应用,如:手机QQ、微信、支付宝、UC浏览器等,以及友盟、信鸽、个推等 SDK,找出它们外发的广播,在应用中进行监听,这样当这些应用发出广播时,就会将我们的应用拉活。
(5)双进程保活
Android 5.0之后,用双进程方式保活,已经非常困难,可以参考猫九爷果果的文章。
(6)JobScheduler
在Android5.0以上系统中即使应用被杀掉,JobScheduler也能在符合一定条件时唤醒应用;但是亲测后发现,国内定制系统(尤其7.0之后)基本给堵死了,进程被杀或系统重启,都无法执行JobService,也无法拉起应用。如果只在进程活着的时候才能执行JobService,那它还有什么意义呢?
(7)账户同步
Android 系统的账号同步机制会定期进行同步账号,该方案目的在于利用同步机制进行进程的拉活,包括被 forestop 掉的进程,但是Android N之后不再有效。
(8)三方推送唤醒App
根据机型接入不同厂商或三方推送平台,依赖三方推送唤醒自己的App。
总结
随着Android版本的升级,以及国内厂商对性能的苛求,进程保活变得越来越困难,总结为三个特点:
- 复活越来越困难——一旦被杀死,想再复活变得及其困难,以前很灵的方案在更高版本上都失效了
- 保活越来越凸显——既然复活很难,就应该十分珍惜用户“恩赐”的点开,尽可能长延长存活时间
- 白名单越来越重要——App的生命越来越握在厂商系统手上,研究各厂商的白名单规则,通过友好的提醒,借助用户之手获得免死金牌也是不错的手段
总之,单一手段很难再保证进程的永久存活,多管齐下,协调合作,因机制宜,才能在严密布防下获得一线生机~