Android | 进程保活与拉活

2024-02-17 17:20
文章标签 android 进程 保活 拉活

本文主要是介绍Android | 进程保活与拉活,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

进程保活貌似是一个古老的话题,从接触安卓开始就备受关注,国内应用更是各种黑科技手段层出不穷,但随着系统的升级完善保活似乎受到了限制,个人也从未有过具体项目中涉及到这类的技术方案,在最近的面试中和部分公司的项目中会涉及到,所以有必要再梳理一下保活。

关于进程

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像素的ActivityKeepAliveManager.startKeepAlive(context!!)}Intent.ACTION_SCREEN_ON ->{Log.i(TAG,"ACTION_SCREEN_ON")//屏幕开启 关闭1像素的ActivityKeepAliveManager.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.attributesparmas.width=1parmas.height = 1window.attributes =parmasKeepAliveManager.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? = nullfun 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. 未使用1像素保活的情况下,锁屏后进程的oom_adj的值会变大,即优先级变低了。
  2. 使用1像素保活后,锁屏后oom_adj的值不会发生变化。
    部分机型可能存在失效情况。

方案二:前台服务保活

简述:启动一个前台服务,提高进程的优先级。但是API26之后无法隐藏通知。
这种方案只需要编写一个服务即可,

class ForegroundService : Service(){companion object{val SERVICE_ID = 100001val 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.0startForeground(SERVICE_ID,Notification())//这里通过内部服务关闭通知显示startService(Intent(this,InnerService::class.java))}else{//8.0以上 不推荐使用 同一个ID如果已经有了就会拒绝创建val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManagerval 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-permissionandroid:name="android.permission.GET_ACCOUNTS"android:maxSdkVersion="22" />
<uses-permissionandroid: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-dataandroid: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-authenticatorxmlns: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 AccountManagerval 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 {@Overridepublic 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;}@Overridepublic boolean onStopJob(JobParameters params) {Log.i("KeepAliveJobService", "JobService onStopJob 关闭");return false;}public static void startJob(Context context){Log.i("KeepAliveJobService", "JobService startJob");// 创建 JobSchedulerJobScheduler 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;@Overridepublic 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));}}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {return super.onStartCommand(intent, flags, startId);}@Nullable@Overridepublic IBinder onBind(Intent intent) {return myBinder;}class MyBinder extends IMyAidlInterface.Stub {}class MyServiceConnection implements ServiceConnection{@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {}@Overridepublic void onServiceDisconnected(ComponentName name) {//断开连接  互相拉活的关键startService(new Intent(LocalService.this,RemoteService.class));bindService(new Intent(LocalService.this, RemoteService.class), myServiceConnection, BIND_AUTO_CREATE);}@Overridepublic void onBindingDied(ComponentName name) {ServiceConnection.super.onBindingDied(name);}@Overridepublic void onNullBinding(ComponentName name) {ServiceConnection.super.onNullBinding(name);}}class InnerService extends Service{@Overridepublic void onCreate() {super.onCreate();startForeground(16,new Notification());stopSelf();}@Nullable@Overridepublic 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机制要了解,还有对于服务的使用也要了然于胸。

这篇关于Android | 进程保活与拉活的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/718482

相关文章

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO

C#如何优雅地取消进程的执行之Cancellation详解

《C#如何优雅地取消进程的执行之Cancellation详解》本文介绍了.NET框架中的取消协作模型,包括CancellationToken的使用、取消请求的发送和接收、以及如何处理取消事件... 目录概述与取消线程相关的类型代码举例操作取消vs对象取消监听并响应取消请求轮询监听通过回调注册进行监听使用Wa

Android WebView的加载超时处理方案

《AndroidWebView的加载超时处理方案》在Android开发中,WebView是一个常用的组件,用于在应用中嵌入网页,然而,当网络状况不佳或页面加载过慢时,用户可能会遇到加载超时的问题,本... 目录引言一、WebView加载超时的原因二、加载超时处理方案1. 使用Handler和Timer进行超

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Android 10.0 mtk平板camera2横屏预览旋转90度横屏拍照图片旋转90度功能实现

1.前言 在10.0的系统rom定制化开发中,在进行一些平板等默认横屏的设备开发的过程中,需要在进入camera2的 时候,默认预览图像也是需要横屏显示的,在上一篇已经实现了横屏预览功能,然后发现横屏预览后,拍照保存的图片 依然是竖屏的,所以说同样需要将图片也保存为横屏图标了,所以就需要看下mtk的camera2的相关横屏保存图片功能, 如何实现实现横屏保存图片功能 如图所示: 2.mtk

android应用中res目录说明

Android应用的res目录是一个特殊的项目,该项目里存放了Android应用所用的全部资源,包括图片、字符串、颜色、尺寸、样式等,类似于web开发中的public目录,js、css、image、style。。。。 Android按照约定,将不同的资源放在不同的文件夹中,这样可以方便的让AAPT(即Android Asset Packaging Tool , 在SDK的build-tools目