前言
进程保活貌似是一个古老的话题,从接触安卓开始就备受关注,国内应用更是各种黑科技手段层出不穷,但随着系统的升级完善保活似乎受到了限制,个人也从未有过具体项目中涉及到这类的技术方案,在最近的面试中和部分公司的项目中会涉及到,所以有必要再梳理一下保活。
关于进程
Low memory killer
在安卓中进程是受系统限制和管理的,正常情况下应用退出到后台是不会立即被Kill掉的,而是将其缓存起来,随着进程的增加系统会考虑到内存性能上的压力而根据自身的回收机制Kill掉进程,这套机制就是low memory killer。
进程的优先级
● 关键优先级:前台进程
● 搞优先级:可见进程、服务进程
● 低优先级:后台进程、空进程
通过oom-adj值判断优先级,值越小越不容易被杀死。
通过命令查看进程信息:adb shell ps
通过命令查看进程优先级:cat proc/917/oom_adj
Kill的触发
存在一个内存阈值,不同的手机阈值不同,一旦低于阈值会触发Kill。手机root后可以通过命令查看。会获取五个值分别对应上面说到的进程分类。
关于前台、可见、服务、后台、空进程的理解
正常的APP进程都逃不出这几种转变,要想实现进程保活就要对这几种进程有清晰的概念,他们符合什么样的特征,尤其是前台进程,这是保活的目标进程。
前台进程
满足:
● 正在交互的Activity
● 包含绑定到正在交互的Ac的Service
● 包含正在运行的前台Service,startForeground
● 包含正在执行生命周期回调的Service
● 包含正在执行onReceive()方法的BroadcastReceive
可见进程
没有任何相关联的前台组件,但会影响屏幕可见内容的进程,即不在前台但是可见,如调用了一个对话框进程,但是可见发起的AC。
服务进程
正在运行已使用startService()方法启动的服务且不属于上述两个更高级别进程的进程。
后台进程
比如正常的APP从正常的AC点击HOME键回到桌面,此时的AC调用onPause-onStop,这个时候进程就包含了一个不可见但没有调用onDestroy方法的AC,这就是一个从前台进程变为后台进程的场景。
如何保活
从上面的梳理来看,优先级越低越容易被杀死。而保活的目的就是要提高进程的优先级,之前看过的博客和网上的主流方法有两种:
方案一:1像素保活
简述:关闭屏幕时创建一个空视图的AC,让应用成为前台进程,打开屏幕是关闭AC。
具体实现:
创建一个广播接收器,用于监听屏幕锁屏和开启的事件,他的作用是打开和关闭1像素页面。
class KeepAliveReceive :BroadcastReceiver() {
companion object{
const val TAG = "KeepAliveReceive"
}
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_SCREEN_OFF->{
Log.i(TAG,"ACTION_SCREEN_OFF")
//锁屏 打开1像素的Activity
KeepAliveManager.startKeepAlive(context!!)
}
Intent.ACTION_SCREEN_ON ->{
Log.i(TAG,"ACTION_SCREEN_ON")
//屏幕开启 关闭1像素的Activity
KeepAliveManager.finishKeepAliveActivity()
}
}
}
}
创建1像素的页面
class KeepAliveActivity: AppCompatActivity() {
private val TAG = "KeepAliveActivity"
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
Log.i(TAG,"KeepAliveActivity start")
window.setGravity(Gravity.START)
val parmas = window.attributes
parmas.width=1
parmas.height = 1
window.attributes =parmas
KeepAliveManager.stepKeepAliveActivity(this)
}
在注册文件中进行注册并设置透明背景的主题
<activity android:name=".keepalive.KeepAliveActivity"
android:excludeFromRecents="true"
android:taskAffinity="com.rotate.keep"
android:theme="@style/KeepAliveTheme">
</activity>
<!--添加自定义主题 保证activity背景透明-->
<style name="KeepAliveTheme" parent="AppTheme">
<item name="android:windowBackground">@null</item>
<item name="android:windowIsTranslucent">true</item>
</style>
编写一个单列管理类
object KeepAliveManager {
private val keepAliveReceive by lazy {
KeepAliveReceive()
}
private var keepAliveActivity :KeepAliveActivity? = null
fun startKeepAlive(context: Context) {
if (keepAliveActivity == null) {
context.startActivity(Intent(context,KeepAliveActivity::class.java))
}
}
fun finishKeepAliveActivity() {
if(keepAliveActivity?.isFinishing == false){
keepAliveActivity!!.finish()
}
}
fun stepKeepAliveActivity(activity: KeepAliveActivity) {
this.keepAliveActivity = activity
}
fun registerKeepListener(context: Context) {
val intentFilter = IntentFilter()
intentFilter.apply {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
}
context.registerReceiver(keepAliveReceive,intentFilter)
}
fun unRegisterKeepAliveListener(context: Context) {
context.unregisterReceiver(keepAliveReceive)
}
}
注册使用
KeepAliveManager.registerKeepListener(this)
测试是否生效,如果手机Root的话可以用adb命令去观察进程的优先级:
- 未使用1像素保活的情况下,锁屏后进程的oom_adj的值会变大,即优先级变低了。
- 使用1像素保活后,锁屏后oom_adj的值不会发生变化。
部分机型可能存在失效情况。
方案二:前台服务保活
简述:启动一个前台服务,提高进程的优先级。但是API26之后无法隐藏通知。
这种方案只需要编写一个服务即可,
class ForegroundService : Service(){
companion object{
val SERVICE_ID = 100001
val TAG = "ForegroundService"
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
Log.i(TAG,"onCreate")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
//4.3 以下不会显示通知 用户无感知
startForeground(SERVICE_ID, Notification())
}else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
//4.3 - 7.0
startForeground(SERVICE_ID,Notification())
//这里通过内部服务关闭通知显示
startService(Intent(this,InnerService::class.java))
}else{
//8.0以上 不推荐使用 同一个ID如果已经有了就会拒绝创建
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel("channel","demo",NotificationManager.IMPORTANCE_HIGH)
manager?.let {
it.createNotificationChannel(channel)
startForeground(SERVICE_ID,NotificationCompat.Builder(this,"channel").build())
}
}
}
class InnerService : Service() {
override fun onCreate() {
super.onCreate()
startForeground(SERVICE_ID,Notification())
stopSelf()
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
}
}
上述两个方案都无法百分百的保活成功,只是提高应用程序的存活率。
如何拉活
与保活不同拉活是在程序挂了的情况下想法设法的救活他。主流的方案在网上都有相关实现和讲解,主要是梳理和整合。
广播拉活
通过静态注册广播监听器,在发生系统事件时做出响应,这种方式很难在高版本中再生效,7.0以及8.0之后都做了很严格的控制。
其次是通过全家桶式的拉活,依靠其他APP拉活,还是通过广播,这种方法一般都是大公司有活跃度高的多种产品。
Service系统机制拉活
onStartCommand是关键的方法,需要关注他的返回值,可以参考博客:https://blog.csdn.net/fenggering/article/details/82535154
当返回START_STICKY,如果Service进程被kill掉,保留service的状态为开始状态,但不保留Intent数据,随后系统会尝试重新创建service,如果此期间没有任何调用命令被传递,则参数intent为null.
START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保证服务被Kill一定会重启。
这种方法也比较简单实现起来,声明注册调用即可:
class StickyService :Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId)
}
}
但这种方案不稳定,有些机型可能会直接失效。
账户拉活
手机系统的设置会有Account账户功能,任何第三方的APP都可以将自己注册到账户中,并将数据在一定时间内同步到服务器中,系统将账户同步时,自动将未启动的APP进程拉活。
实现方法:
1.创建Service并实现AbstractAccountAuthenticator(账户验证器)用于返回Binder.
class AuthenticationService :Service(){
//账户验证器
private val accountAuthenticator by lazy {
AccountAuthenticator(this)
}
override fun onBind(intent: Intent?): IBinder? {
return accountAuthenticator.iBinder
}
class AccountAuthenticator(val context: Context) :AbstractAccountAuthenticator(context){
override fun editProperties(
response: AccountAuthenticatorResponse?,
accountType: String?
): Bundle {
TODO("Not yet implemented")
}
override fun addAccount(
response: AccountAuthenticatorResponse?,
accountType: String?,
authTokenType: String?,
requiredFeatures: Array<out String>?,
options: Bundle?
): Bundle {
TODO("Not yet implemented")
}
override fun confirmCredentials(
response: AccountAuthenticatorResponse?,
account: Account?,
options: Bundle?
): Bundle {
TODO("Not yet implemented")
}
override fun getAuthToken(
response: AccountAuthenticatorResponse?,
account: Account?,
authTokenType: String?,
options: Bundle?
): Bundle {
TODO("Not yet implemented")
}
override fun getAuthTokenLabel(authTokenType: String?): String {
TODO("Not yet implemented")
}
override fun updateCredentials(
response: AccountAuthenticatorResponse?,
account: Account?,
authTokenType: String?,
options: Bundle?
): Bundle {
TODO("Not yet implemented")
}
override fun hasFeatures(
response: AccountAuthenticatorResponse?,
account: Account?,
features: Array<out String>?
): Bundle {
TODO("Not yet implemented")
}
}
}
2.在注册文件中配置Service
<!--涉及的两个权限声明-->
<uses-permission
android:name="android.permission.GET_ACCOUNTS"
android:maxSdkVersion="22" />
<uses-permission
android:name="android.permission.AUTHENTICATE_ACCOUNTS"
android:maxSdkVersion="22" />
<service android:name=".keepalive.accountAlive.AuthenticationService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android
这里要在res/xml中定义显示在Account列表中的资源:
<?xml version ="1.0" encoding ="utf-8"?><!-- Learn More about how to use App Actions: https://developer.android.com/guide/actions/index.html -->
<account-authenticator
xmlns:android="http://schemas.android.com/apk/res/android"
//这里的type要和后续的代码注册的相同
android:accountType="com.example.rotateimageview"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
</account-authenticator>
3.编写添加账号的工具类
object AccountHelp {
private const val ACCOUNT_TYPE = "com.example.rotateimageview"
@SuppressLint("MissingPermission")
fun addAccount(context: Context) {
val accountManager = context.getSystemService(Context.ACCOUNT_SERVICE) as AccountManager
val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE)
if (accounts.isNotEmpty()) {
//已存在
return
}
val account = Account("demoRotateImage", ACCOUNT_TYPE)
accountManager.addAccountExplicitly(account,"123", Bundle())
}
}
再调用addAccount方法,去查看账号列表就可以发现这个RotateImage账号了。
这个方案被很多应用采用,主要还是利用了系统自动同步账号数据这一点,但时间是不确定的。
官方的Demo
通过JobScheduler
JobScheduler相当于一个定时器,可以特定时间间隔的执行任务,其调用是由系统完成的,某些ROM可能并不能达到预期的效果,存在不确定性,且这种方案比较耗费性能占用内存。
具体实现也很简单
public class KeepAliveJobService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
Log.i("KeepAliveJobService", "JobService onStartJob 开启");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
// 如果当前设备大于 7.0 , 延迟 5 秒 , 再次执行一次
startJob(this);
}
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
Log.i("KeepAliveJobService", "JobService onStopJob 关闭");
return false;
}
public static void startJob(Context context){
Log.i("KeepAliveJobService", "JobService startJob");
// 创建 JobScheduler
JobScheduler jobScheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
// 第一个参数指定任务 ID
// 第二个参数指定任务在哪个组件中执行
// setPersisted 方法需要 android.permission.RECEIVE_BOOT_COMPLETED 权限
// setPersisted 方法作用是设备重启后 , 依然执行 JobScheduler 定时任务
JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(10,
new ComponentName(context.getPackageName(), KeepAliveJobService.class.getName()))
.setPersisted(true);
// 7.0 以下的版本, 可以每隔 5000 毫秒执行一次任务
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N){
jobInfoBuilder.setPeriodic(5_000);
}else{
// 7.0 以上的版本 , 设置延迟 5 秒执行
// 该时间不能小于 JobInfo.getMinLatencyMillis 方法获取的最小值
jobInfoBuilder.setMinimumLatency(5_000);
}
// 开启定时任务
jobScheduler.schedule(jobInfoBuilder.build());
}
}
注册文件中声明,调用startJob即可
<!--Jobscheduler拉活-->
<service android:name=".keepalive.jobscheduler.KeepAliveJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true">
</service>
经过测试不同ROM上确实有区别,在一加五真机上跑貌似只会执行一次任务,在模拟器上正常的循环调用。
双进程拉活
两个进程同时运行,如果一个被杀死,那么另一个协助拉起,相互保护。
核心是编写两个Service,通常的定义为远端服务和本地服务,在代码实现上基本一致
public class LocalService extends Service {
private MyBinder myBinder;
private MyServiceConnection myServiceConnection;
@Override
public void onCreate() {
super.onCreate();
//用于进程间通信
myBinder = new MyBinder();
myServiceConnection = new MyServiceConnection();
startForeground(16,new Notification());
if(Build.VERSION.PREVIEW_SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){
startService(new Intent(this,InnerService.class));
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return myBinder;
}
class MyBinder extends IMyAidlInterface.Stub {
}
class MyServiceConnection implements ServiceConnection{
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
}
@Override
public void onServiceDisconnected(ComponentName name) {
//断开连接 互相拉活的关键
startService(new Intent(LocalService.this,RemoteService.class));
bindService(new Intent(LocalService.this, RemoteService.class), myServiceConnection, BIND_AUTO_CREATE);
}
@Override
public void onBindingDied(ComponentName name) {
ServiceConnection.super.onBindingDied(name);
}
@Override
public void onNullBinding(ComponentName name) {
ServiceConnection.super.onNullBinding(name);
}
}
class InnerService extends Service{
@Override
public void onCreate() {
super.onCreate();
startForeground(16,new Notification());
stopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
}
注册的时候有区别的:
<service android:name=".keepalive.process.LocalService$InnerService"
android:exported="true">
</service>
<service android:name=".keepalive.process.RemoteService$InnerService"
android:exported="true"
android:process=":remote">
</service>
总结
实际工作中几乎没有接触过类似需求,对于保活和拉活没有稳定完全可靠的方案,方案的目的都是提高进程的优先级,方案的实现貌似都没有涉及过多的代码,基本都是模板代码,重点在于理解原理,对于进程的优先级和LMK机制要了解,还有对于服务的使用也要了然于胸。