看过很多相关进程保活得文章,但是在项目中没有实际应用,也只是了解了一些方法,具体哪些能用具体怎么用都没有实际操作,正好本期作业是进程保活,在整理得过程中也实际操作了一下。
一、进程初步了解
进程保活主要是从两个方向来考虑:
A、提升进程优先级,进而降低被杀死的概率
B、进程被杀死后,想办法进行拉活
下面就着两个方向进行总结
1、进程优先级
要想提高进程优先级,首先需要知道进程 划分,按照重要性从高到低,分为以下5种:
1.1 前台进程(Foreground process)
用户正在使用的进程,一般系统是不会杀死前台进程的。
场景:
A、某个进程持有一个正在与当前用户交互的Activity并且该Activity处于resume状态
B、某个进程持有一个Service,并且该Service与用户正在交互的Activity绑定
C、某个进程持有一个Service,并且该Service调用startForground()方法使之位于前台
D、某个进程持有一个Service,并且该Service正在执行它的某个生命周期,比如OnCreate()、OnStart()和OnDestroy()
E、某个进程持有一个BroadcastReceiver,并且该receiver正在执行OnReceive()方法
1.2 可见进程(Visible process)
用户正在使用,并且看的到,但是无法操作,一般系统不会杀死可见进程,除非在资源吃紧的情况下。
场景:
A、拥有不在前台、但是可见的Activity,此时Activity处于pause状态
B、拥有绑定到可见或者前台Activity的Service
1.3 服务进程( Service process)
在内存不足以维持前台进程和可见进行时,会杀死服务进程。
场景:
A、某个进程中运行着一个Service,并且该Service通过startService()启动,且与用户看得见的界面没有直接联系
1.4 后台进程(Background process)
系统随时可以终止后台进程
场景:
A、程序看不到,但是还在后台运行,比如按了”home”键,例如已经调用了onStop()的Activity
1.5 空进程(Empty process)
不包含任何活动组件的进程
2、进程回收策咯
Android中对内存的回收主要依靠 Lowmemorykiller 来完成,是一种根据 OOM_ADJ 阈值级别出发相应力度的内存回收机制。大概是oom_adj越大,占用物理内存越多会最先被系统回收。这里不做过多的描述,想了解的可以搜索 OOM_ADJ 具体了解下。
二、进程具体保活方案
1、开启一个1像素Activity
因为前台进程最不容易被杀死,所以为了进程常驻我们可以监听手机的锁屏状态,并开启一个1像素的Activity来使进程转换为前台进程,并且在解锁时finish掉这个activity,达到欺骗用户的效果。因此需要注册一个广播来监听手机锁屏状态,具体实现方式如下:
public class EmptyActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
//起始坐标
params.x = 0;
params.y = 0;
//宽高设计为1个像素
params.height = 1;
params.width = 1;
window.setAttributes(params);
//用软引用进行管理,方便finish
KeepLiveActivityManager.getInstance(this).setActivity(this);
}
}
如上 EmptyActivity 是一个1像素的Activity,为了更好的隐藏我们还可以设置透明主题,当然不设置也没有影响。
<style name="KeepLiveStyle">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowFrame">@null</item>
</style>
创建完 EmptyActivity 之后需要创建一个 Receiver,来监听屏幕状态,在锁屏或解锁是操作 EmptyActivity,为了方便管理 EmptyActivity 的开启和销毁,可以创建一个管理类:
/**
* 屏幕监听Receive
*/
public class KeepLiveReceive extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(Intent.ACTION_SCREEN_OFF)){//锁屏
KeepLiveActivityManager.getInstance(context).startEmptyActivity();
} else if(intent.getAction().equals(Intent.ACTION_SCREEN_ON)){//解锁
KeepLiveActivityManager.getInstance(context).closeEmptyActivity();
}
}
}
/**
* EmptyActivity 管理类
*/
public class KeepLiveActivityManager {
private Context context;
private static KeepLiveActivityManager instance;
private SoftReference<Activity> activitySoftReference;
public static KeepLiveActivityManager getInstance(Context context){
if(instance == null){
instance = new KeepLiveActivityManager(context);
}
return instance;
}
private KeepLiveActivityManager(Context context){
this.context = context;
}
/***
* 把Activity用软引用管理起来
* @param activity
*/
public void setActivity(Activity activity){
activitySoftReference = new SoftReference<Activity>(activity);
}
/**
* 开启1像素Activity
*/
public void startEmptyActivity(){
Intent intent = new Intent(context, EmptyActivity.class);
context.startActivity(intent);
}
/***
* 关闭1像素Activity
*/
public void closeEmptyActivity(){
if(activitySoftReference != null){
Activity activity = activitySoftReference.get();
if(activity != null){
activity.finish();
}
}
}
}
通过以上配置之后现在MainActivity改成如下
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
registerReceiver(new KeepLiveReceive(),filter);
}
}
这样就可以简单的实现锁屏时开启 EmptyActivity,并且将当前进程提升为前台进程。当然初次之外内存也需要考虑,因为内存占用越多用容易被kill掉,因为可以把上面的业务伙计放到 Service 中,创建 KeepLiveService 来进行业务操作
/**
* 进行保活service
*/
public class KeepLiveService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
initReceive();
return START_STICKY;
}
/** 注册广播*/
public void initReceive(){
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
registerReceiver(new KeepLiveReceive(),filter);
}
}
<service android:name=".service.KeepLiveService"
android:process=":remote"/>
通过上面的操作,我们的应用就始终和前台进程是一样的优先级了,为了省电,系统检测到锁屏事件后一段时间内会杀死后台进程,如果采取这种方案,就可以避免了这个问题。功能虽然实现了,但是如果我们的进程不是最近操作的进程,解锁屏幕之后会发现我们的进程在最近任务里面的第一个,因此我们需要进行如下优化,在manifest里面这样配置 EmptyActivity:
<activity android:name=".EmptyActivity"
android:theme="@style/KeepLiveStyle"
android:finishOnTaskLaunch="false"
android:excludeFromRecents="true"
android:process=":remote"/>
这里用到了两个不常用得 excludeFromRecents 和 finishOnTaskLaunch 属性。
excludeFromRecents 是和recents相关得一个属性,recents通俗的讲就是android的多任务键,它可以看到我们最近使用过的应用,通过它可以快速应用切换。excludeFromRecents 默认为 false ,设置为true时代表不在列表中显示。
finishOnTaskLaunch 是和清空任务栈相关的一个属性,通常情况下,我们可以在activity的标签上使用以下几种属性来清空任务栈
a、clearTaskOnLaunch 属性顾名思义,就是每次返回activity的时候,都将该activity上的所有activity清除,通过这个属性,可以让这个task每次初始化的时候,都只有一个activity
b、finishTaskOnLaunch 这个属性和clearTaskOnLaunch有点类似,只不过clearTaskOnLaunch作用在别人身上,而finishTaskOnLaunch作用在自己身上,通过这个属性,当离开这个activity所处的task,那么用户再返回的时候,该activity会被finish掉
c、alwaysRetainTaskState 给了task一道免死金牌,如果将activity这个属性设置为true,那么该activity所在的task将不接受任何清除命令,一直保持当前task的状态。
最有一点说明就是由于ACTION_SCREEN_OFF和ACTION_SCREEN_ON比较特殊,Receive的注册必须写在代码里面,不能在Manifest.xml里面注册。
2、利用系统Service机制拉活
这个是系统自带的,onStartCommand方法必须具有一个整形的返回值,这个整形的返回值用来告诉系统在服务启动完毕后,如果被Kill,系统将如何操作
@Override
public int onStartCommand(Intent intent,int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
onStartCommand中返回值,常用的返回值有:START_NOT_STICKY、START_SICKY 和 START_REDELIVER_INTENT,这三个都是静态常理值。
START_NOT_STICKY:表示当Service运行的进程被Android系统强制杀掉之后,不会重新创建该Service,如果想重新实例化该Service,就必须重新调用startService来启动。
使用场景:表示当Service在执行工作中被中断几次无关紧要或者对Android内存紧张的情况下需要被杀掉且不会立即重新创建这种行为也可接受的话,这是可以在onStartCommand返回值中设置该值。如在Service中定时从服务器中获取最新数据
START_STICKY:表示Service运行的进程被Android系统强制杀掉之后,Android系统会将该Service依然设置为started状态(即运行状态),但是不再保存onStartCommand方法传入的intent对象,然后Android系统会尝试再次重新创建该Service,并执行onStartCommand回调方法,这时onStartCommand回调方法的Intent参数为null,也就是onStartCommand方法虽然会执行但是获取不到intent信息。
使用场景:如果你的Service可以在任意时刻运行或结束都没什么问题,而且不需要intent信息,那么就可以在onStartCommand方法中返回START_STICKY,比如一个用来播放背景音乐功能的Service就适合返回该值。
START_REDELIVER_INTENT:表示Service运行的进程被Android系统强制杀掉之后,与返回START_STICKY的情况类似,Android系统会将再次重新创建该Service,并执行onStartCommand回调方法,但是不同的是,Android系统会再次将Service在被杀掉之前最后一次传入onStartCommand方法中的Intent再次保留下来并再次传入到重新创建后的Service的onStartCommand方法中,这样我们就能读取到intent参数。
使用场景:如果我们的Service需要依赖具体的Intent才能运行(需要从Intent中读取相关数据信息等),并且在强制销毁后有必要重新创建运行,那么这样的Service就适合返回START_REDELIVER_INTENT。
有以上可知,我们可以在 onStartCommand 中返回 START_STICKY,但是这种方案有如下缺陷:
a. Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
b. 进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
3、前台服务
这方案实际利用了Android前台service的漏洞。 Android 中 Service 的优先级为4,通过 setForeground 接口可以将后台 Service 设置为前台 Service,使进程的优先级由4提升为2,从而使进程的优先级仅仅低于用户当前正在交互的进程,与可见进程优先级一致,使进程被杀死的概率大大降低。
实现方式如下 :
对于 API level < 18:调用startForeground(ID, new Notification()),发送空的Notification ,图标则不会显示。
对于 API level >= 18:在调用startForeground将Service设置为前台Service时,必须发送一条通知,也就是前台 Service 与一条可见的通知时绑定在一起的。因此需要提优先级的service 启动一个NotifyService,两个服务同时startForeground,且绑定同样的 ID。Stop 掉NotifyService,这样通知栏图标即被移除。
public class KeepLiveService extends Service {
/** 通知id*/
private int NOTIFY_ID = 101;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2){
startForeground(NOTIFY_ID,new Notification());
} else {
//发送Notification并将其置为前台后,启动NotifyService
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
startForeground(NOTIFY_ID,builder.build());
startService(new Intent(this,NotifyService.class));
}
return START_STICKY;
}
}
public class NotifyService extends Service {
/** 通知id*/
private int NOTIFY_ID = 101;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//发送与KeepLiveService 中ID相同的Notification,然后将其取消并取消自己的前台显示
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher_round);
startForeground(NOTIFY_ID,builder.build());
stopSelf();
return super.onStartCommand(intent, flags, startId);
}
}
4、JobSheduler
JobSheduler是作为进程死后复活的一种手段,native进程方式最大缺点是费电, Native 进程费电的原因是感知主进程是否存活有两种实现方式,在 Native 进程中通过死循环或定时器,轮训判断主进程是否存活,当主进程不存活时进行拉活。其次5.0以上系统不支持。 但是JobSheduler可以替代在Android5.0以上native进程方式,这种方式即使用户强制关闭,也能被拉起来
JobSheduler@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class MyJobService extends JobService {
@Override
public void onCreate() {
super.onCreate();
startJobSheduler();
}
public void startJobSheduler() {
try {
JobInfo.Builder builder = new JobInfo.Builder(1, new ComponentName(getPackageName(), MyJobService.class.getName()));
builder.setPeriodic(5);//设置间隔时间
builder.setPersisted(true);//设备重启之后你的任务是否还要继续执行
JobScheduler jobScheduler = (JobScheduler) this.getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(builder.build());
} catch (Exception ex) {
ex.printStackTrace();
}
}
@Override
public boolean onStartJob(JobParameters jobParameters) {
return false;
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
return false;
}
}
5、利用Native进程拉活
Android5.0 以后系统对 Native 进程等加强了管理,Native 拉活方式失效,因此没有研究。
6、后台播放音乐
启动一个Service在后台循环播放一个音乐文件,并将音量设置为0,后续可以尝试下
7、系统广播拉活
通过监听系统广播来进行进程拉活,比如监听网络状态变化、文件挂在、开机广播、应用安装卸载等一些列广播。思路可行但是具体实施时会发现会被一些系统自带安全软件禁用。
8、相互唤醒
相互唤醒的意思就是,假如你手机里装了支付宝、淘宝、天猫、UC等阿里系的app,那么你打开任意一个阿里系的app后,有可能就顺便把其他阿里系的app给唤醒了,该方案总的设计思想与接收系统广播类似,不同的是该方案为接收第三方 Top 应用广播。
9、利用账号同步机制拉活
Android 系统的账号同步机制会定期同步账号进行,该方案目的在于利用同步机制进行进程的拉活。
总结:
在实现过程中发现华为手机前两种方法都不起作用,因为一锁屏华为会直接把锁屏开启的进程杀死;在魅族手机上也会有一些瑕疵,魅族手机会有一个锁屏下显示界面的提示,这都是用户无法接受的。
在前四种方式的实现过程中,对进程保活有了进一步的了解,同时对一些涉及的小知识点也有了新的认识。后续也会对后面的5种方式进程逐步尝试,并且积累相关知识点。当然没有任何方式是能适应任何手机的,这些方式都是只能增加存活概率,而且应该合理的利用进程保活。