草庐IT

Android | 进程保活与拉活

Aramis_twoY 2023-04-03 原文

前言

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

关于进程

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. 未使用1像素保活的情况下,锁屏后进程的oom_adj的值会变大,即优先级变低了。
  2. 使用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机制要了解,还有对于服务的使用也要了然于胸。

有关Android | 进程保活与拉活的更多相关文章

  1. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  2. ruby - 通过 ruby​​ 进程共享变量 - 2

    我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

  3. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  4. ruby - 无法在 Ruby 中将 ffmpeg 作为子进程运行 - 2

    我正在尝试使用以下代码通过将ffmpeg实用程序作为子进程运行并获取其输出并解析它来确定视频分辨率:IO.popen'ffmpeg-i'+path_to_filedo|ffmpegIO|#myparsegoeshereend...但是ffmpeg输出仍然连接到标准输出并且ffmepgIO.readlines是空的。ffmpeg实用程序是否需要一些特殊处理?或者还有其他方法可以获得ffmpeg输出吗?我在WinXP和FedoraLinux下测试了这段代码-结果是一样的。 最佳答案 要跟进mouviciel的评论,您需要使用类似pope

  5. Ruby 守护进程导致 ActiveRecord 记录器 IOError - 2

    我目前正在用Ruby编写一个项目,它使用ActiveRecordgem进行数据库交互,我正在尝试使用ActiveRecord::Base.logger记录所有数据库事件具有以下代码的属性ActiveRecord::Base.logger=Logger.new(File.open('logs/database.log','a'))这适用于迁移等(出于某种原因似乎需要启用日志记录,因为它在禁用时会出现NilClass错误)但是当我尝试运行包含调用ActiveRecord对象的线程守护程序的项目时脚本失败并出现以下错误/System/Library/Frameworks/Ruby.frame

  6. ruby - 在 ruby​​ 中生成一个进程,捕获 stdout,stderr,获取退出状态 - 2

    我想从ruby​​rake脚本运行一个可执行文件,比如foo.exe我希望将foo.exe的STDOUT和STDERR输出直接写入我正在运行rake任务的控制台.当进程完成时,我想将退出代码捕获到一个变量中。我如何实现这一目标?我一直在玩backticks、process.spawn、system但我无法获得我想要的所有行为,只有部分更新:我在Windows上,在标准命令提示符下,而不是cygwin 最佳答案 system获取您想要的STDOUT行为。它还返回true作为零退出代码,这可能很有用。$?填充了有关最后一次system调

  7. ruby-on-rails - 如何用不同的用户运行nginx主进程 - 2

    A/ctohttp://wiki.nginx.org/CoreModule#usermaster进程曾经以root用户运行,是否可以以不同的用户运行nginxmaster进程? 最佳答案 只需以非root身份运行init脚本(即/etc/init.d/nginxstart),就可以用不同的用户运行nginxmaster进程。如果这真的是你想要做的,你将需要确保日志和pid目录(通常是/var/log/nginx&/var/run/nginx.pid)对该用户是可写的,并且您所有的listen调用都是针对大于1024的端口(因为绑定(

  8. Ruby 守护进程和 JRuby - 备选方案 - 2

    我有一个应用程序正在从Ruby迁移到JRuby(由于需要通过Java提供更好的Web服务安全支持)。我使用的gem之一是daemons创建后台作业。问题在于它使用fork+exec来创建后台进程,但这对JRuby来说是禁忌。那么-是否有用于创建后台作业的替代gem/wrapper?我目前的想法是只从shell脚本调用rake并让rake任务永远运行......提前致谢,克里斯。更新我们目前正在使用几个与Java线程相关的包装器,即https://github.com/jmettraux/rufus-scheduler和https://github.com/philostler/acts

  9. ruby-on-rails - Rails - Carrierwave 进程抛出 ArgumentError : no images in this image list - 2

    在尝试实现应用auto_orient的过程之后!对于我的图片,我收到此错误:ArgumentError(noimagesinthisimagelist):app/uploaders/image_uploader.rb:36:in`fix_exif_rotation'app/controllers/posts_controller.rb:12:in`create'Carrierwave在没有进程的情况下工作正常,但在添加进程后尝试上传图像时抛出错误。流程如下:process:fix_exif_rotationdeffix_exif_rotationmanipulate!do|image|

  10. ruby-on-rails - Ruby 长时间运行的进程对队列事件使用react - 2

    我有一个将某些事件写入队列的Rails3应用。现在我想在服务器上创建一个服务,每x秒轮询一次队列,并按计划执行其他任务。除了创建ruby​​脚本并通过cron作业运行它之外,还有其他稳定的替代方案吗? 最佳答案 尽管启动基于Rails的持久任务是一种选择,但您可能希望查看更有序的系统,例如delayed_job或Starling管理您的工作量。我建议不要在cron中运行某些东西,因为启动整个Rails堆栈的开销可能很大。每隔几秒运行一次它是不切实际的,因为Rails上的启动时间通常为5-15秒,具体取决于您的硬件。不过,每天这样做几

随机推荐