Android中的进程保活应该分为两个方面:
- 提高进程的优先级,减少被系统杀死的可能性
- 在进程已经被杀死的情况下,通过一些手段来重新启动应用进程
本文针对这两方面来进程阐述,并给出相应的示例。其实主要也是在前人的基础上做了一个总结,并进行了一些实践。
阅读本文的时候,可以先clone一份代码 android-process-daemon,这样的话可能理解更清晰。
1 进程等级与Low Memory Killer
在开始之前,首先有必要了解一下进程等级的概念。Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,需要清除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会对进程进行分类。 需要时,系统会首先消除重要性最低的进程,然后是清除重要性稍低一级的进程,依此类推,以回收系统资源。
进程等级:
-
前台进程
- 与用户正在交互的Activity
- 前台Activity以bind方式启动的Service
- Service调用了startForground,绑定了Notification
- 正在执行生命周期的Service,例如在执行onCreate、onStart、onDestory
- 正在执行onReceive方法的BroadcastReceiver
-
可见进程
- 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。
- 托管绑定到可见(或前台)Activity 的 Service。
-
服务进程
正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。 -
后台进程
包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。 -
空进程
不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。
进程等级参考谷歌官方文档 https://developer.android.google.cn/guide/components/processes-and-threads.html?hl=zh-cn。
系统出于体验和性能上的考虑,app在退到后台时系统并不会真正的kill掉这个进程,而是将其缓存起来。打开的应用越多,后台缓存的进程也越多。在系统内存不足的情况下,系统开始依据自身的一套进程回收机制来判断要kill掉哪些进程,以腾出内存来供给需要的app, 这套杀进程回收内存的机制就叫 Low Memory Killer,它是一种根据 OOM_ADJ 阈值级别触发相应力度的内存回收的机制。
关于 OOM_ADJ 的说明如下:
图片摘自——腾讯Bugly干货分享】Android 进程保活招式大全.png
其中红色部分代表比较容易被杀死的 Android 进程(OOM_ADJ>=4),绿色部分表示不容易被杀死的 Android 进程,其他表示非 Android 进程(纯 Linux 进程)。在Low Memory Killer 回收内存时会根据进程的级别优先杀死 OOM_ADJ 比较大的进程,对于优先级相同的进程则进一步受到进程所占内存和进程存活时间的影响。
Android 手机中进程被杀死可能有如下情况:
图片摘自——腾讯Bugly干货分享】Android 进程保活招式大全.png
所以,想要应用降低被杀死的可能性就要尽量提高进程的优先级,这样才会在系统内存不足的时候减少被杀死的可能性。在这里,我们只是说减少被杀死的可能性,而不是说一定不会杀死。除了系统应用,或者厂商白名单中的应用,一般的应用都有被杀死的可能性。
我们可以通过adb命令来查看进程的优先级
首先使用命令:
adb shell ps | grep packageName
获取进程的PID,然后使用命令获取进程的oom_adj值,这个值越小,代表优先级越高越不容易被杀死:
adb shell cat /proc/PID/oom_adj
比如,先获取adb进程
# adb shell ps |grep com.sososeen09.process
u0_a85 1740 486 1013428 64840 00000000 f7491e65 S com.sososeen09.process.daemon.sample
然后获取oom_adj值:
# adb shell cat /proc/1740/oom_adj
0
此时该进程运行在前台,它的优先级为0,这种情况下被杀死的可能性很小。当通过Home键把当前引用退回后台的时候,重新查看一下oom_adj,这个值可能会变为6(不同的rom情况可能不一样)。
2 提升进程优先级
2.1 利用Activity提升权限
前面我们也讲了,当应用切换后后台的时候进程的优先级变得很低,被杀死的可能性就增大了。如果此时用户通过电源键进行息屏了。可以考虑通过监听息屏和解锁的广播,在息屏的时候启动一个只有一个像素的Activity。这样的话,在息屏这段时间,应用的进程优先级很高,不容易被杀死。采用这种方案要注意的是要使用户无感知。
该方案主要解决第三方应用及系统管理工具在检测到锁屏事件后一段时间(一般为5分钟以内)内会杀死后台进程,已达到省电的目的问题。
public class KeepLiveActivity extends Activity {
private static final String TAG = "KeepLiveActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG,"start Keep app activity");
Window window = getWindow();
//设置这个act 左上角
window.setGravity(Gravity.START | Gravity.TOP);
//宽 高都为1
WindowManager.LayoutParams attributes = window.getAttributes();
attributes.width = 1;
attributes.height = 1;
attributes.x = 0;
attributes.y = 0;
window.setAttributes(attributes);
KeepLiveManager.getInstance().setKeep(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.e(TAG,"stop keep app activity");
}
}
为了让无用无感知,Activity要设置的小(只有一个像素),无背景并且是透明的。此外还要注意一点,需要设置Activity的taskAffinity 属性,要与我们的应用默认的taskAffinity不同,否则当这个Activity启动的时候,会把我们的应用所在的任务栈移动到前台,当屏幕解锁之后,会发现我们的应用移动到前台了。而用户在息屏的时候明明已经把我们的应用切换到后台了,这会给用户造成困扰。
<activity
android:name=".keepliveactivity.KeepLiveActivity"
android:excludeFromRecents="true"
android:exported="false"
android:finishOnTaskLaunch="false"
android:taskAffinity="com.sososeen09.daemon.keep.live"
android:theme="@style/KeepLiveTheme" />
<style name="KeepLiveTheme">
<item name="android:windowBackground">@null</item>
<item name="android:windowIsTranslucent">true</item>
</style>
要有一个BroadcastReceiver,用于监听屏幕的点亮和关闭的广播,在这里我们使用了Intent.ACTION_USER_PRESENT
这个action,它会早于系统发出的Intent.ACTION_SCREEN_OFF
广播。这样可以更早的结束之前息屏的时候启动的Activity。
public class KeepLiveReceiver extends BroadcastReceiver {
private static final String TAG = "KeepLiveReceiver";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.e(TAG, "receive action:" + action);
//屏幕关闭事件
if (TextUtils.equals(action, Intent.ACTION_SCREEN_OFF)) {
//关屏 开启1px activity
KeepLiveManager.getInstance().startKeepLiveActivity(context);
// 解锁事件
} else if (TextUtils.equals(action, Intent.ACTION_USER_PRESENT)) {
KeepLiveManager.getInstance().finishKeepLiveActivity();
}
KeepLiveManager.getInstance().startKeepLiveService(context);
}
}
2.2 Service绑定一个Notification的方式:
应用启动一个Service,并且Service通过调用startForeground方法来绑定一个前台的通知时,可以有效的提升进程的优先级。
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentTitle("Foreground");
builder.setContentText("I am a foreground service");
builder.setContentInfo("Content Info");
builder.setWhen(System.currentTimeMillis());
Intent activityIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, activityIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
Notification notification = builder.build();
startForeground(FOREGROUND_ID, notification);
return super.onStartCommand(intent, flags, startId);
}
这种方式的话会在通知栏显示一个通知,该方式属于比较文明的。
我们可以使用 命令来查看当前正在运行的服务信息,比如
adb shell dumpsys activity services com.sososeen09.process
可以得到结果:
ACTIVITY MANAGER SERVICES (dumpsys activity services)
User 0 active services:
* ServiceRecord{d18c80d u0 com.sososeen09.process.daemon.sample/.service.WhiteService}
intent={cmp=com.sososeen09.process.daemon.sample/.service.WhiteService}
packageName=com.sososeen09.process.daemon.sample
processName=com.sososeen09.process.daemon.sample:white
baseDir=/data/app/com.sososeen09.process.daemon.sample-2/base.apk
dataDir=/data/data/com.sososeen09.process.daemon.sample
app=ProcessRecord{696d809 2478:com.sososeen09.process.daemon.sample:white/u0a85}
isForeground=true foregroundId=1001 foregroundNoti=Notification(pri=0 contentView=com.sososeen09.process.daemon.sample/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE)
createTime=-44s879ms startingBgTimeout=--
lastActivity=-44s860ms restartTime=-44s860ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1
* ServiceRecord{e4782a4 u0 com.sososeen09.process.daemon.sample/.service.NormalService}
intent={cmp=com.sososeen09.process.daemon.sample/.service.NormalService}
packageName=com.sososeen09.process.daemon.sample
processName=com.sososeen09.process.daemon.sample:normal
baseDir=/data/app/com.sososeen09.process.daemon.sample-2/base.apk
dataDir=/data/data/com.sososeen09.process.daemon.sample
app=ProcessRecord{2402ea0e 2459:com.sososeen09.process.daemon.sample:normal/u0a85}
createTime=-48s510ms startingBgTimeout=--
lastActivity=-48s479ms restartTime=-48s479ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1
Connection bindings to services:
* ConnectionRecord{3b4eb582 u0 CR DEAD com.sososeen09.process.daemon.sample/.acount.AuthenticationService:@2a1598cd}
binding=AppBindRecord{d621c2f com.sososeen09.process.daemon.sample/.acount.AuthenticationService:system}
conn=android.app.LoadedApk$ServiceDispatcher$InnerConnection@2a1598cd flags=0x1
可以看到,调用了startForeground方法的Service是一个前台进程了,有一个属性是isForeground=true。
在这种情况下,当应用所在进程退回到后台时,oom_adj的值为1,不容易被杀死。
2.3 隐藏Notification的Service
前面讲的startForeground,会在通知栏中显示一个通知。有一种方式利用了系统漏洞,把通知栏给隐藏,让用户无感知。不过这种方式跟版本有关:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
try {
Notification notification = new Notification();
if (Build.VERSION.SDK_INT < 18) {
startForeground(NOTIFICATION_ID, notification);
} else {
startForeground(NOTIFICATION_ID, notification);
// start InnerService
startService(new Intent(this, InnerService.class));
}
} catch (Throwable e) {
e.printStackTrace();
}
return super.onStartCommand(intent, flags, startId);
}
然后在InnerService中关闭Notification
@Override
public void onCreate() {
super.onCreate();
try {
startForeground(NOTIFICATION_ID, new Notification());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
stopSelf();
}
其实我们可以发现,在Tinker中,由于在Patch的过程是在另一个服务进程中,为了保证这个服务进程不被干掉,Tinker也利用了这个系统的漏洞。具体可以查看TinkerPatchService
tinker中Service进程保活.png
3 进程保活
上面讲了提升进程优先级的方式了来减少应用被杀死的可能性,但是当应用真的被杀死的时候,我们就要想办法来拉活进行了。
3.1 利用广播拉活
这个在推送中比较常见,当几个App都集成了同一家的推送,只要有一个App起来,就会发送一个广播,这样其它的App接收到这个广播之后,开启一个服务,就把进程给启动起来了。各大厂家的全家桶也是这样的。
public class WakeReceiver extends BroadcastReceiver {
private final static int NOTIFICATION_ID = 1001;
public final static String ACTION_WAKE = "com.sososeen09.wake";
private final static String TAG = "WakeReceiver";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action != null && action.equals(ACTION_WAKE)) {
context.startService(new Intent(context, WakeService.class));
Log.e(TAG, "onReceive: " + "收到广播,兄弟们要起来了。。。");
}
}
public static class WakeService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
try {
Notification notification = new Notification();
if (Build.VERSION.SDK_INT < 18) {
startForeground(NOTIFICATION_ID, notification);
} else {
startForeground(NOTIFICATION_ID, notification);
// start InnerService
startService(new Intent(this, WakeInnerService.class));
}
} catch (Throwable e) {
e.printStackTrace();
}
Log.e(TAG, "onReceive: " + "我是 WakeService,我起来了,谢谢兄弟。。。" + ProcessUtils.getProcessName(this));
return super.onStartCommand(intent, flags, startId);
}
}
public static class WakeInnerService extends Service {
@Override
public void onCreate() {
super.onCreate();
try {
startForeground(NOTIFICATION_ID, new Notification());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
stopSelf();
}
@Override
public void onDestroy() {
stopForeground(true);
super.onDestroy();
}
}
}
其实也可以监听系统的广播来达到启动应用进程的方式,但是从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
3.2 系统Service机制拉活
将 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次,则系统不再拉起。
3.3 使用账户同步拉活
手机系统设置里会有“帐户”一项功能,任何第三方APP都可以通过此功能将数据在一定时间内同步到服务器中去。系统在将APP帐户同步时,会将未启动的APP进程拉活。 如何利用账户同步可以参考 https://github.com/googlesamples/android-BasicSyncAdapter
但是账户同步这个东西,在不同的手机上可能在同步时间不同。
关于这种方式,这里就不多讲了,有兴趣的可以搜索相关文章,在示例代码中也有相关的介绍。](https://github.com/sososeen09/android-process-daemon)中也有相关的介绍。)
3.4 使用JobSchedule拉活
JobScheduler允许在特定状态与特定时间间隔周期执行任务。可以利用它的这个特点完成保活的功能,效果类似开启一个定时器,与普通定时器不同的是其调度由系统完成。它是在Android5.0之后推出的,在5.0之前无法使用。
首先写一个Service类继承自JobService,在小于7.0的系统上,JobInfo可以周期性的执行,但是在7.0以上的系统上,不能周期性的执行了。因此可以在JobService的onStartJob回调方法中继续开启一个任务来执行。
@SuppressLint("NewApi")
public class MyJobService extends JobService {
private static final String TAG = "MyJobService";
public static void startJob(Context context) {
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(10, new ComponentName(context.getPackageName(), MyJobService.class.getName())).setPersisted(true);
//小于7.0
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
// 每隔1s 执行一次 job
builder.setPeriodic(1_000);
} else {
//延迟执行任务
builder.setMinimumLatency(1_000);
}
if (jobScheduler != null) {
jobScheduler.schedule(builder.build());
}
}
@Override
public boolean onStartJob(JobParameters params) {
Log.e(TAG, "start job schedule");
//如果7.0以上 轮训
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
startJob(this);
}
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}
AndroidManifest.xml并需要声明权限。
<service
android:name=".jobschedule.MyJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />
不过在某些ROM可能并不能达到需要的效果(某米)
3.5 双进程守护
我们都直到Service可以以bind方式启动,当Service被系统杀死的时候,会在ServiceConnection的onServiceDisconnected方法中会收到回调。利用这个原理,可以在主进程中进行有一个LocalService,在子进程中有RemoteService。LocalService中以bind和start方式启动RemoteService,同时RemoteService以bind和start方式启动LocalService。并且在它们各自的ServiceConnection的onServiceDisconnected方法中重新bind和start。
这种Java层通过Service这种双进程守护的方式,可以有效的保证进程的存活能力。
public class LocalService extends Service {
private final static int NOTIFICATION_ID = 1003;
private static final String TAG = "LocalService";
private ServiceConnection serviceConnection;
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
@Override
public void onCreate() {
super.onCreate();
serviceConnection = new LocalServiceConnection();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
class LocalServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//服务连接后回调
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.e(TAG, "remote service died,make it alive");
//连接中断后回调
startService(new Intent(LocalService.this, RemoteService.class));
bindService(new Intent(LocalService.this, RemoteService.class), serviceConnection,
BIND_AUTO_CREATE);
}
}
static class MyBinder extends IMyAidlInterface.Stub {
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
}
}
}
RemoteService也类似
public class RemoteService extends Service {
private final static int NOTIFICATION_ID = 1002;
private static final String TAG = "RemoteService";
private ServiceConnection serviceConnection;
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
@Override
public void onCreate() {
super.onCreate();
serviceConnection = new RemoteServiceConnection();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
class RemoteServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//服务连接后回调
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.e(TAG, "main process local service died,make it alive");
//连接中断后回调
startService(new Intent(RemoteService.this, LocalService.class));
bindService(new Intent(RemoteService.this, LocalService.class), serviceConnection,
BIND_AUTO_CREATE);
}
}
static class MyBinder extends IMyAidlInterface.Stub {
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
}
}
}
为了提高Service所在进程的优先级,可以结合我们之前讲的startForground来开启一个Notification的方式,提高进程的优先级,以降低被杀风险。
3.6 其它方式拉活
其它我们还可以使用推送拉活,根据终端不同,在小米手机(包括 MIUI)接入小米推送、华为手机接入华为推送,这样也可以保证进程可以被推送唤醒。
Native拉活,Native fork子进程用于观察当前app主进程的存亡状态。这种在5.0以前的系统上效果比较高,对于5.0以上成功率极低。
4 总结
提升进程优先级的方式
-
Activity提权,监听屏幕的息屏和解锁,使用一个1个像素的Activity
-
Service提权,Service通过startForground方法来开启一个Notification
进程拉活
-
通过广播的方式
-
通过Service在onStartCommand的返回值,START_STICK,由系统拉活,在短时间内如果多次被杀死可能就再也启动不了了
-
通过账户同步拉活
-
通过JobSchedule拉活
-
通过Service的bind启动的方式,双进程守护拉活
-
推送拉活
-
Native fork子进程的方式拉活
更多详情,请查看 android-process-daemon