Android | 进程保活与拉活

Posted Aramis_twoY

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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像素的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 针对LowmemoryKiller所做的进程保活 

LowmemoryKiller会在内存不足的时候扫描所有的用户进程,找到不是太重要的进程杀死,至于LowmemoryKiller杀进程够不够狠,要看当前的内存使用情况,内存越少,下手越狠。在内核中,LowmemoryKiller.c定义了几种内存回收等级如下:(也许不同的版本会有些不同)

static short lowmem_adj[6] = {
    0,
    1,
    6,
    12,
};
static int lowmem_adj_size = 4;

static int lowmem_minfree[6] = {
    3 * 512,    /* 6MB */
    2 * 1024,   /* 8MB */
    4 * 1024,   /* 16MB */
    16 * 1024,  /* 64MB */
};
static int lowmem_minfree_size = 4;

lowmem_adj中各项数值代表阈值的警戒级数,lowmem_minfree代表对应级数的剩余内存,两者一一对应,比如当系统的可用内存小于6MB时,警戒级数为0;当系统可用内存小于8M而大于6M时,警戒级数为1;当可用内存小于64M大于16MB时,警戒级数为12。LowmemoryKiller就是根据当前系统的可用内存多少来获取当前的警戒级数,如果进程的oom_adj大于警戒级数并且占内存最大,将会被优先杀死, 具有相同omm_adj的进程,则杀死占用内存较多的。omm_adj越小,代表进程越重要。一些前台的进程,oom_adj会比较小,而后台的服务,omm_adj会比较大,所以当内存不足的时候,Lowmemorykiller先杀掉的是后台服务而不是前台的进程。对于LowmemoryKiller的杀死,这里有一句话很重要,就是: 具有相同omm_adj的进程,则杀死占用内存较多的,因此,如果我们的APP进入后台,就尽量释放不必要的资源,以降低自己被杀的风险。那么如何释放呢?onTrimeMemory是个不错的时机,而onLowmemory可能是最后的稻草,下面复习一下,LowmemoryKiller如何杀进程的,简单看一下实现源码(4.3):(其他版本原理大同小异)

static int lowmem_shrink(int nr_to_scan, gfp_t gfp_mask)
{
    ...     
    <!--关键点1 获取free内存状况-->
    int other_free = global_page_state(NR_FREE_PAGES);
    int other_file = global_page_state(NR_FILE_PAGES);
    <!--关键点2 找到min_adj -->
    for(i = 0; i < array_size; i++) {
        if (other_free < lowmem_minfree[i] &&
            other_file < lowmem_minfree[i]) {
            min_adj = lowmem_adj[i];
            break;
        }
    }
  <!--关键点3 找到p->oomkilladj>min_adj并且oomkilladj最大,内存最大的进程-->
    for_each_process(p) {
        // 找到第一个大于等于min_adj的,也就是优先级比阈值低的
        if (p->oomkilladj < min_adj || !p->mm)
            continue;
        // 找到tasksize这个是什么呢
        tasksize = get_mm_rss(p->mm);
        if (tasksize <= 0)
            continue;
        if (selected) {
        // 找到优先级最低,并且内存占用大的
            if (p->oomkilladj < selected->oomkilladj)
                continue;
            if (p->oomkilladj == selected->oomkilladj &&
                tasksize <= selected_tasksize)
                continue;
        }
        selected = p;
        selected_tasksize = tasksize;
        lowmem_print(2, "select %d (%s), adj %d, size %d, to kill\\n",
                     p->pid, p->comm, p->oomkilladj, tasksize);
    }
    if(selected != NULL) {...
        force_sig(SIGKILL, selected);
    }
    return rem;
}

这里先看一下关键点1,这里是内核获取当前的free内存状况,并且根据当前空闲内存计算出当前后台杀死的等级(关键点2),之后LowmemoryKiller会遍历所有的进程,找到优先级低并且内存占用较大的进程,如果这个进程的p->oomkilladj>min_adj,就表示这个进程可以杀死,LowmemoryKiller就会送过发送SIGKILL信号杀死就进程,注意,lmkd会先找优先级低的进程,如果多个进程优先级相同,就优先杀死内存占用高的进程,这样就为我们提供了两种进程包活手段

  • 1、提高进程的优先级,其实就是减小进程的p->oomkilladj(越小越重要)
  • 2、降低APP的内存占用量,在oom_adj相同的时候,会优先干掉内存消耗大的进程

不过大多数情况下,Android对于进程优先级的管理都是比较合理,即使某些场景需要特殊手段提高优先级,Android也是给了参考方案的,比如音频播放,UI隐藏的时候,需要将Sevice进程设置成特定的优先级防止被后台杀死,比如一些备份的进程也需要一些特殊处理,但是这些都是在Android允许的范围内的,所以绝大多数情况下,Android是不建议APP自己提高优先级的,因为这会与Android自身的的进程管理相悖,换句话说就是耍流氓。这里先讨论第二种情况,通过合理的释放内存降低被杀的风险,地主不想被杀,只能交公粮,自裁保身,不过这里也要看自裁的时机,什么时候瘦身比较划算,O(∩_∩)O哈哈~!这里就牵扯到有一个onTrimeMemory函数,该函数是一个系统回调函数,主要是Android系统经过综合评估,给APP一个内存裁剪的等级,比如当内存还算充足,APP退回后台时候,会收到TRIM_MEMORY_UI_HIDDEN等级的裁剪,就是告诉APP,释放一些UI资源,比如大量图片内存,一些引入图片浏览缓存的场景,可能就更加需要释放UI资源,下面来看下onTrimeMemory的回调时机及APP应该做出相应处理。

onTrimeMemory的回调时机及内存裁剪等级

OnTrimMemory是在Android 4.0引入的一个回调接口,其主要作用就是通知应用程序在不同的场景下进行自我瘦身,释放内存,降低被后台杀死的风险,提高用户体验,由于目前APP的适配基本是在14之上,所以不必考虑兼容问题。onTrimeMemory支持不同裁剪等级,比如,APP通过HOME建进入后台时,其优先级(oom_adj)就发生变化,从未触发onTrimeMemory回调,这个时候系统给出的裁剪等级一般是TRIM_MEMORY_UI_HIDDEN,意思是,UI已经隐藏,UI相关的、占用内存大的资源就可以释放了,比如大量的图片缓存等,当然,还会有其他很多场景对应不同的裁剪等级。因此,需要弄清楚两个问题:

  • 1、不同的裁剪等级是如何生成的,其意义是什么
  • 2、APP如何根据不同的裁剪等级释放内存资源,(自裁的程度)

先看下ComponentCallbacks2中定义的不同裁剪等级的意义:这里一共定义了4+3共7个裁剪等级,为什么说是4+3呢?因为有4个是针对后台进程的,还有3个是针对前台(RUNNING)进程的,目标对象不同,具体看下分析

裁剪等级数值目标进程
TRIM_MEMORY_COMPLETE80后台进程
TRIM_MEMORY_MODERATE60后台进程
TRIM_MEMORY_BACKGROUND40后台进程
TRIM_MEMORY_UI_HIDDEN20后台进程
TRIM_MEMORY_RUNNING_CRITICAL15前台RUNNING进程
TRIM_MEMORY_RUNNING_LOW10前台RUNNING进程
TRIM_MEMORY_RUNNING_MODERATE5前台RUNNING进程

其意义如下:

  • TRIM_MEMORY_UI_HIDDEN 当前应用程序的所有UI界面不可见,一般是用户点击了Home键或者Back键,导致应用的UI界面不可见,这时应该释放一些UI相关资源,TRIM_MEMORY_UI_HIDDEN是使用频率最高的裁剪等级。官方文档:the process had been showing a user interface, and is no longer doing so. Large allocations with the UI should be released at this point to allow memory to be better managed

  • TRIM_MEMORY_BACKGROUND 当前手机目前内存吃紧(后台进程数量少),系统开始根据LRU缓存来清理进程,而该程序位于LRU缓存列表的头部位置,不太可能被清理掉的,但释放掉一些比较容易恢复的资源能够提高手机运行效率,同时也能保证恢复速度。官方文档:the process has gone on to the LRU list. This is a good opportunity to clean up resources that can efficiently and quickly be re-built if the user returns to the app

  • TRIM_MEMORY_MODERATE 当前手机目前内存吃紧(后台进程数量少),系统开始根据LRU缓存来清理进程,而该程序位于LRU缓存列表的中间位置,应该多释放一些内存提高运行效率。官方文档:the process is around the middle of the background LRU list; freeing memory can help the system keep other processes running later in the list for better overall performance.

  • TRIM_MEMORY_COMPLETE 当前手机目前内存吃紧 (后台进程数量少),系统开始根据LRU缓存来清理进程,而该程序位于LRU缓存列表的最边缘位置,系统会先杀掉该进程,应尽释放一切可以释放的内存。官方文档:the process is nearing the end of the background LRU list, and if more memory isn’t found soon it will be killed.

以下三个等级针对前台运行应用

  • TRIM_MEMORY_RUNNING_MODERATE 表示该进程是前台或可见进程,正常运行,一般不会被杀掉,但是目前手机有些吃紧(后台及空进程存量不多),系统已经开始清理内存,有必要的话,可以释放一些内存。官方文档:the process is not an expendable background process, but the device is running moderately low on memory. Your running process may want to release some unneeded resources for use elsewhere。

  • TRIM_MEMORY_RUNNING_LOW 表示该进程是前台或可见进程,正常运行,一般不会被杀掉,但是目前手机比较吃紧(后台及空进程被全干掉了一大波),应该去释放掉一些不必要的资源以提升系统性能。 官方文档:the process is not an expendable background process, but the device is running low on memory. Your running process should free up unneeded resources to allow that memory to be used elsewhere.

  • TRIM_MEMORY_RUNNING_CRITICAL 表示该进程是前台或可见进程,但是目前手机比较内存十分吃紧(后台及空进程基本被全干掉了),这时应当尽可能地去释放任何不必要的资源,否则,系统可能会杀掉所有缓存中的进程,并且杀一些本来应当保持运行的进程。官方文档:the process is not an expendable background process, but the device is running extremely low on memory and is about to not be able to keep any background processes running. Your running process should free up as many non-critical resources as it can to allow that memory to be used elsewhere. The next thing that will happen after this is called to report that nothing at all can be kept in the background, a situation that can start to notably impact the user.

以上抽象的说明了一下Android既定参数的意义,下面看一下onTrimeMemory回调的时机及原理,这里采用6.0的代码分析,因为6.0比之前4.3的代码清晰很多:当用户的操作导致APP优先级发生变化,就会调用updateOomAdjLocked去更新进程的优先级,在更新优先级的时候,会扫描一遍LRU进程列表, 重新计算进程的oom_adj,并且参考当前系统状况去通知进程裁剪内存(这里只是针对Android Java层APP),这次操作一般发生在打开新的Activity界面、退回后台、应用跳转切换等等,updateOomAdjLocked代码大概600多行,比较长,尽量精简后如下,还是比较长,这里拆分成一段段梳理:

final void updateOomAdjLocked() {
    final ActivityRecord TOP_ACT = resumedAppLocked();
    <!--关键点1 找到TOP——APP,最顶层显示的APP-->
    final ProcessRecord TOP_APP = TOP_ACT != null ? TOP_ACT.app : null;
    final long oldTime = SystemClock.uptimeMillis() - ProcessList.MAX_EMPTY_TIME;
    mAdjSeq++;
    mNewNumServiceProcs = 0;
    final int emptyProcessLimit;
    final int hiddenProcessLimit;
    <!--关键点2 找到TOP——APP,最顶层显示的APP-->
    // 初始化一些进程数量的限制:
    if (mProcessLimit <= 0) {
        emptyProcessLimit = hiddenProcessLimit = 0;
    } else if (mProcessLimit == 1) {
        emptyProcessLimit = 1;
        hiddenProcessLimit = 0;
    } else {
        // 空进程跟后台非空缓存继承的比例
        emptyProcessLimit = ProcessList.computeEmptyProcessLimit(mProcessLimit);
        cachedProcessLimit = mProcessLimit - emptyProcessLimit;
    }

     <!--关键点3 确定下进程槽 3个槽->
    int numSlots = (ProcessList.HIDDEN_APP_MAX_ADJ - ProcessList.HIDDEN_APP_MIN_ADJ + 1) / 2;
    // 后台进程/前台进程/空进程
    int numEmptyProcs = N - mNumNonCachedProcs - mNumCachedHiddenProcs;
     int emptyFactor = numEmptyProcs/numSlots;
    if (emptyFactor < 1) emptyFactor = 1;
    int hiddenFactor = (mNumHiddenProcs > 0 ? mNumHiddenProcs : 1)/numSlots;
    if (hiddenFactor < 1) hiddenFactor = 1;
    int stepHidden = 0;
    int stepEmpty = 0;
    int numHidden = 0;
    int numEmpty = 0;
    int numTrimming = 0;
    mNumNonHiddenProcs = 0;
    mNumHiddenProcs = 0;
    int i = mLruProcesses.size();
    // 优先级
    int curHiddenAdj = ProcessList.HIDDEN_APP_MIN_ADJ;
    // 初始化的一些值
    int nextHiddenAdj = curHiddenAdj+1;
    // 优先级
    int curEmptyAdj = ProcessList.HIDDEN_APP_MIN_ADJ;
    // 有意思
    int nextEmptyAdj = curEmptyAdj+2;

这前三个关键点主要是做了一些准备工作,关键点1 是单独抽离出TOP_APP,因为它比较特殊,系统只有一个前天进程,关键点2主要是根据当前的配置获取后台缓存进程与空进程的数目限制,而关键点3是将后台进程分为三备份,无论是后台进程还是空进程,会间插的均分6个优先级,一个优先级是可以有多个进程的,而且并不一定空进程的优先级小于HIDDEN进程优先级。

    for (int i=N-1; i>=0; i--) {
            ProcessRecord app = mLruProcesses.get(i);
            if (!app.killedByAm && app.thread != null) {
                app.procStateChanged = false;
                <!--关键点4 计算进程的优先级或者缓存进程的优先级->   
                // computeOomAdjLocked计算进程优先级,但是对于后台进程和empty进程computeOomAdjLocked无效,这部分优先级是AMS自己根据LRU原则分配的
                computeOomAdjLocked(app, ProcessList.UNKNOWN_ADJ, TOP_APP, true, now);
                //还未最终确认,有些进程的优先级,比如只有后台activity或者没有activity的进程,
              <!--关键点5 计算进程的优先级或者缓存进程的优先级->   
                if (app.curAdj >= ProcessList.UNKNOWN_ADJ) {
                    switch (app.curProcState) {
                        case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
                        case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
                            app.curRawAdj = curCachedAdj;
                                    <!--关键点6 根据LRU为后台进程分配优先级-->
                            if (curCachedAdj != nextCachedAdj) {
                                stepCached++;
                                if (stepCached >= cachedFactor) {
                                    stepCached = 0;
                                    curCachedAdj = nextCachedAdj;
                                    nextCachedAdj += 2;
                                    if (nextCachedAdj > ProcessList.CACHED_APP_MAX_ADJ) {
                                        nextCachedAdj = ProcessList.CACHED_APP_MAX_ADJ;
                                    }
                                }
                            }
                            break;
                        default:
                                                                <!--关键点7 根据LRU为后台进程分配优先级-->
                            app.curRawAdj = curEmptyAdj;
                            app.curAdj = app.modifyRawOomAdj(curEmptyAdj);
                            if (curEmptyAdj != nextEmptyAdj) {
                                stepEmpty++;
                                if (stepEmpty >= emptyFactor) {
                                    stepEmpty = 0;
                                    curEmptyAdj = nextEmptyAdj;
                                    nextEmptyAdj += 2;
                                    if (nextEmptyAdj > ProcessList.CACHED_APP_MAX_ADJ) {
                                        nextEmptyAdj = ProcessList.CACHED_APP_MAX_ADJ;
                                    }
                                }
                            }
                            break;
                    }
                }
                <!--关键点8 设置优先级-->
                applyOomAdjLocked(app, true, now, nowElapsed);

上面的这几个关键点主要是为所有进程计算出其优先级oom_adj之类的值,对于非后台进程,比如HOME进程 服务进程,备份进程等都有自己的独特的计算方式,而剩余的后台进程就根据LRU三等分配优先级。

                 <!--关键点9 根据缓存进程的数由AMS选择性杀进程,后台进程太多-->
                switch (app.curProcState) {
                    case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
                    case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
                        mNumCachedHiddenProcs++;
                        numCached++;
                        if (numCached > cachedProcessLimit) {
                            app.kill("cached #" + numCached, true);
                        }
                        break;
                    case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
                        if (numEmpty > ProcessList.TRIM_EMPTY_APPS
                                && app.lastActivityTime < oldTime) {
                            app.kill("empty for "
                                    + ((oldTime + ProcessList.MAX_EMPTY_TIME - app.lastActivityTime)
                                    / 1000) + "s", true);
                        } else {
                            numEmpty++;
                            if (numEmpty > emptyProcessLimit) {
                                app.kill("empty #" + numEmpty, true);
                            }
                        }
                        break;
                    default:
                        mNumNonCachedProcs++;
                        break;
                }
                 <!--关键点10 计算需要裁剪进程的数目-->
                if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
                        && !app.killedByAm) {
                        // 比home高的都需要裁剪,不包括那些等级高的进程
                    numTrimming++;
                }
            }
        }

上面的两个关键点是看当前后台进程是否过多或者过老,如果存在过多或者过老的后台进程,AMS是有权利杀死他们的。之后才是我们比较关心的存活进程的裁剪:

        final int numCachedAndEmpty = numCached + numEmpty;
        int memFactor;
         <!--关键点11 根据后台进程数目确定当前系统的内存使用状况 ,确立内存裁剪等级(内存因子)memFactor,android的理念是准许存在一定数量的后台进程,并且只有内存不够的时候,才会缩减后台进程-->
        if (numCached <= ProcessList.TRIM_CACHED_APPS
                && numEmpty <= ProcessList.TRIM_EMPTY_APPS) {
            // 等级高低 ,杀的越厉害,越少,需要约紧急的时候才杀
            if (numCachedAndEmpty <= ProcessList.TRIM_CRITICAL_THRESHOLD) {//3
                memFactor = ProcessStats.ADJ_MEM_FACTOR_CRITICAL;
            } else if (numCachedAndEmpty <= ProcessList.TRIM_LOW_THRESHOLD) { //5
                memFactor = ProcessStats.ADJ_MEM_FACTOR_LOW;
            } else {
                memFactor = ProcessStats.ADJ_MEM_FACTOR_MODERATE;
            }
        } else {
            // 后台进程数量足够说明内存充足
            memFactor = ProcessStats.ADJ_MEM_FACTOR_NORMAL;
        }
       <!--关键点12 根据内存裁剪等级裁剪内存 Android认为后台进程不足的时候,内存也不足-->
        if (memFactor != ProcessStats.ADJ_MEM_FACTOR_NORMAL) {
            if (mLowRamStartTime == 0) {
                mLowRamStartTime = now;
            }
            int step = 0;
            int fgTrimLevel;
         // 内存不足的时候,也要通知前台或可见进程进行缩减
            switch (memFactor) {
                case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
                    break;
                case ProcessStats.ADJ_MEM_FACTOR_LOW:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
                    break;
                default:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;
                    break;
            }
            int factor = numTrimming/3;
            int minFactor = 2;
            if (mHomeProcess != null) minFactor++;
            if (mPreviousProcess != null) minFactor++;
            if (factor < minFactor) factor = minFactor;
            int curLevel = ComponentCallbacks2.TRIM_MEMORY_COMPLETE;

关键点11这里不太好理解:Android系统根据后台进程的数目来确定当前系统内存的状况,后台进程越多,越说明内存并不紧张,越少,说明越紧张,回收等级也就越高,如果后台进程的数目较多,内存裁剪就比较宽松是ProcessStats.ADJ_MEM_FACTOR_NORMAL,如果不足,则再根据缓存数目划分等级。以6.0源码来说:

  • 如果后台进程数量(包含空进程)< 3 ,就说明内存非常紧张,内存裁剪因子就是ProcessStats.ADJ_MEM_FACTOR_CRITICAL
  • 如果后台进程数量(包含空进程)< 5 ,就说明内存非常紧张,内存裁剪因子就是ProcessStats.ADJ_MEM_FACTOR_LOW
  • 如果比上面两个多,但是仍然不足正常的后台数目 ,内存裁剪因子就是ProcessStats.ADJ_MEM_FACTOR_MODERATE

与之对应的关键点12,是确立前台RUNNING进程(也不一定是前台显示)的裁剪等级。

  • ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
  • ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
  • ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;

之后就真正开始裁剪APP,这里先看后台进程不足的情况的裁剪,这部分相对复杂一些:

            <!--裁剪后台进程-->
            for (int i=N-1; i>=0; i--) {
                ProcessRecord app = mLruProcesses.get(i);
                if (allChanged || app.procStateChanged) {
                    setProcessTrackerStateLocked(app, trackerMemFactor, now);
                    app.procStateChanged = false;
                }   
                //  PROCESS_STATE_HOME = 12;  
                //PROCESS_STATE_LAST_ACTIVITY = 13; 退到后台的就会用
                // 优先级比较低,回收等级比较高ComponentCallbacks2.TRIM_MEMORY_COMPLETE
                //  当curProcState > 12且没有被am杀掉的情况;上面的update的时候,在kill的时候,是会设置app.killedByAm的
                //裁剪的话,如果 >= ActivityManager.PROCESS_STATE_HOME,老的裁剪等级较高,不重要,越新鲜的进程,裁剪等级越低

                if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
                        && !app.killedByAm) {
                     // 先清理最陈旧的 ,最陈旧的那个遭殃
                    if (app.trimMemoryLevel < curLevel && app.thread != null) {
                        try {
                            app.thread.scheduleTrimMemory(curLevel);
                        } catch (RemoteException e) {
                        }
                    }
                    app.trimMemoryLevel = curLevel;
                    step++; 
                    // 反正一共就三个槽,将来再次刷新的 时候,要看看是不是从一个槽里面移动到另一个槽,
                    // 没有移动,就不需要再次裁剪,等级没变
                    if (step >= factor) {
                        step = 0;
                        switch (curLevel) {
                            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
                                curLevel = ComponentCallbacks2.TRIM_MEMORY_MODERATE;
                                break;
                            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
                                curLevel = ComponentCallbacks2.TRIM_MEMORY_BACKGROUND;
                                break;
                        }
                    }
                }

上面的这部分是负责 app.curProcState >= ActivityManager.PROCESS_STATE_HOME这部分进程裁剪,主要是针对后台缓存进程,一般是oom_adj在9-11之间的进程,根据LRU确定不同的裁减等级。

                else {
                    if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
                            || app.systemNoUi) && app.pendingUiClean) {
                        // 释放UI
                        final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;
                        if (app.trimMemoryLevel < level && app.thread != null) {
                            try {
                                app.thread.scheduleTrimMemory(level);
                            } catch (RemoteException e) {
                            }
                        }
                        app.pendingUiClean = false;
                    }
                    // 启动的时候会回调一遍,如果有必要,启动APP的时候,app.trimMemoryLevel=0
                    if (app.trimMemoryLevel < fgTrimLevel && app.thread != null) {
                        try {
                            app.thread.scheduleTrimMemory(fgTrimLevel);
                        } catch (RemoteException e) {
                        }
                    }
                    app.trimMemoryLevel = fgTrimLevel;
                }
            }
        } 

而这里的裁剪主要是一些优先级较高的进程,其裁剪一般是 ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN ,由于这部分进程比较重要,裁剪等级较低,至于前台进程的裁剪,一般是在启动的时候,这个时候app.pendingUiClean==false,只会裁剪当前进程:

        else {
              <!--关键点13 内存充足的时候,进程的裁剪-->
             ... 
            for (int i=N-1; i>=0; i--) {
                ProcessRecord app = mLruProcesses.get(i);
                // 在resume的时候,都是设置成true,所以退回后台的时候app.pendingUiClean==true是满足的,
                // 因此缩减一次,但是不会再次走这里的分支缩减即使优先级变化,但是已经缩减过
                // 除非走上面的后台流程,那个时候这个进程的等级已经很低了,
                if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
                        || app.systemNoUi) && app.pendingUiClean) {
                    if (app.trimMemoryLevel < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
                            && app.thread != null) {
                        try {
                            app.thread.scheduleTrimMemory(
                                    ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
                        } catch (RemoteException e) {
                        }
                    }
                    // clean一次就弄成false
                    app.pendingUiClean = false;
                }
                // 基本算没怎么裁剪
                app.trimMemoryLevel = 0;
            }
        }
 }

最后这部分是后台进程数量充足的时候,系统只会针对app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND的进程进行裁剪,而裁剪等级也较低:ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,因此根据裁剪等级APP可以大概知道系统当前的内存状况,同时也能知道系统希望自己如何裁剪,之后APP做出相应的瘦身即可。不过,上面的进程裁剪的优先级是完全根据后台进程数量来判断的,但是,不同的ROM可能进行了改造,所以裁剪等级不一定完全准确,比如在开发者模式打开限制后台进程数量的选项,限制后台进程数目不超过2个,那么这个时候的裁剪等级就是不太合理的,因为内存可能很充足,但是由于限制了后台进程的数量,导致裁剪等级过高。因此在使用的时候,最好结合裁剪等级与当前内存数量来综合考量。

通过“流氓”手段提高oom_adj,降低被杀风险,化身流氓进程

进程优先级的计算Android是有自己的一条准则的,某些特殊场景的需要额外处理进程的oom_adj Android也是给了参考方案的。但是,那对于流氓来说,并没有任何约束效力。 “流氓”仍然能够参照oom_adj(优先级)的计算规则,利用其漏洞,提高进程的oom_adj,以降低被杀的风险。如果单单降低被杀风险还好,就怕那种即不想死,又想占用资源的APP,累积下去就会导致系统内存不足,导致整个系统卡顿。

优先级的计算逻辑比较复杂,这里只简述非缓存进程,因为一旦沦为缓存进程,其优先级就只能依靠LRU来计算,不可控。而流氓是不会让自己沦为缓存进程的,非缓存进程是以下进程中的一种,并且,优先级越高(数值越小),越不易被杀死:

ADJ优先级优先级进程类型
SERVICE_ADJ5服务进程(Service process)
HEAVY_WEIGHT_APP_ADJ4后台的重量级进程,system/rootdir/init.rc文件中设置
BACKUP_APP_ADJ3备份进程(这个不太了解)
PERCEPTIBLE_APP_ADJ2可感知进程,比如后台音乐播放 ,通过startForeground设置的进程
VISIBLE_APP_ADJ1可见进程(可见,但是没能获取焦点,比如新进程仅有一个悬浮Activity,其后面的进程就是Visible process)
FOREGROUND_APP_ADJ0前台进程(正在展示是APP,存在交互界面,Foreground process)

* 第一种提高到FOREGROUND_APP_ADJ

我们从低到高看:如何让进程编程FOREGROUND_APP_ADJ进程,也就是前台进程,这个没有别的办法,只有TOP activity进程才能是算前台进程。正常的交互逻辑下,这个是无法实现的,锁屏的时候倒是可以启动一个Activity,但是需要屏幕点亮的时候再隐藏,容易被用户感知,得不偿失,所以基本是无解,所以之前传说的QQ通过一个像素来保活的应该不是这种方案,而通过WindowManager往主屏幕添加View的方式也并未阻止进程被杀,到底是否通过一像素实现进程包活,个人还未得到解答,希望能有人解惑。

  • 第二种,提高到VISIBLE_APP_ADJ或者PERCEPTIBLE_APP_ADJ(不同版本等级可能不同 “4.3 = PERCEPTIBLE_APP_ADJ” 而 “> 5.0 = VISIBLE_APP_ADJ”)

先看一下源码中对两种优先级的定义,VISIBLE_APP_ADJ是含有可见但是非交互Activity的进程,PERCEPTIBLE_APP_ADJ是用户可感知的进程,如后台音乐播放等

    // This is a process only hosting components that are perceptible to the
    // user, and we really want to avoid killing them, but they are not
    // immediately visible. An example is background music playback.
    static final int PERCEPTIBLE_APP_ADJ = 2;

    // This is a process only hosting activities that are visible to the
    // user, so we'd prefer they don't disappear.
    static final int VISIBLE_APP_ADJ = 1;

这种做法是相对温和点的,Android官方曾给过类似的方案,比如音乐播放时后,通过设置前台服务的方式来保活,这里就为流氓进程提供了入口,不过显示一个常住服务会在通知栏上有个运行状态的图标,会被用户感知到。但是Android恰恰还有个漏洞可以把该图标移除,真不知道是不是Google故意的。这里可以参考微信的保活方案:双Service强制前台进程保活

startForeground(ID, new Notification()),可以将Service变成前台服务,所在进程就算退到后台,优先级只会降到PERCEPTIBLE_APP_ADJ或者VISIBLE_APP_ADJ,一般不会被杀掉,Android的有个漏洞,如果两个Service通过同样的ID设置为前台进程,而其一通过stopForeground取消了前台显示,结果是保留一个前台服务,但不在状态栏显示通知,这样就不会被用户感知到耍流氓,这种手段是比较常用的流氓手段。优先级提高后,AMS的killBackgroundProcesses已经不能把进程杀死了,它只会杀死oom_adj大于ProcessList.SERVICE_ADJ的进程,而最近的任务列表也只会清空Activity,无法杀掉进程。 因为后台APP的优先级已经提高到了PERCEPTIBLE_APP_ADJ或ProcessList.VISIBLE_APP_ADJ,可谓流氓至极,如果再占据着内存不释放,那就是泼皮无赖了,这里有个遗留疑问:startForeground看源码只会提升到PERCEPTIBLE_APP_ADJ,但是在5.0之后的版本提升到了VISIBLE_APP_ADJ,这里看源码,没找到原因,希望有人能解惑。具体做法如下:

public class RogueBackGroundService extends Service {

    private static int ROGUE_ID = 1;
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Intent intent = new Intent(this, RogueIntentService.class);
        startService(intent);
        startForeground(ROGUE_ID, new Notification());
    }
    public static class RogueIntentService extends IntentService {

        //流氓相互唤醒Service
        public RogueIntentService(String name) {
            super(name);
        }

        public RogueIntentService() {
            super("RogueIntentService");
        }

        @Override
        protected void onHandleIntent(Intent intent) {

        }
        @Override
        public void onCreate() {
            super.onCreate();
            startForeground(ROGUE_ID, new Notification());
        }   
       @Override
        public void onDestroy() {
            stopForeground(true);//这里不写也没问题,好像会自动停止
            super.onDestroy();
        }
    }
}

双Service守护进程保活(这个也很流氓,不过如果不提高优先级(允许被杀),也算稍微良心)

前文我们分析过Android Binder的讣告机制:如果Service Binder实体的进程挂掉,系统会向Client发送讣告,而这个讣告系统就给进程保活一个可钻的空子。可以通过两个进程中启动两个binder服务,并且互为C/S,一旦一个进程挂掉,另一个进程就会收到讣告,在收到讣告的时候,唤起被杀进程。逻辑如下下:

双服务保活.jpg

首先编写两个binder实体服务PairServiceA ,PairServiceB,并且在onCreate的时候相互绑定,并在onServiceDisconnected收到讣告的时候再次绑定。

public class PairServiceA extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new AliveBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        bindService(new Intent(PairServiceA.this, PairServiceB.class), mServiceConnection, BIND_AUTO_CREATE);
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            bindService(new Intent(PairServiceA.this, PairServiceB.class), mServiceConnection, BIND_AUTO_CREATE);
            ToastUtil.show("bind A");
        }
    };

与之配对的B

public class PairServiceB extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new AliveBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        bindService(new Intent(PairServiceB.this, PairServiceA.class), mServiceConnection, BIND_AUTO_CREATE);
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            bindService(new Intent(PairServiceB.this, PairServiceA.class), mServiceConnection, BIND_AUTO_CREATE);
        }
    };
}

之后再Manifest中注册,注意要进程分离

    <service android:name=".service.alive.PairServiceA"/>
    <service
        android:name=".service.alive.PairServiceB"
        android:process=":alive"/>

之后再Application或者Activity中启动一个Service即可。

startService(new Intent(MainActivity.this, PairServiceA.class));

这个方案一般都没问题,因为Binder讣告是系统中Binder框架自带的,除非一次性全部杀死所有父子进程,这个没测试过。

广播或者Service原地复活的进程保活

还有一些比较常见的进程保活手段是通过注册BroadcastReceiver来实现的比如:

  • 开机
  • 网络状态切换
  • 相机
  • 一些国内推送SDK(内含一些)

另外也能依靠Service的自启动特性,通过onStartCommand的START_STICKY来实现,相比上面的不死,这些算比较柔和的启动了,毕竟这两种都是允许后台杀死的前提下启动的:

public class BackGroundService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }
}

总结

所有流氓手段的进程保活,都是下策,建议不要使用,本文只是分析实验用。当APP退回后台,优先级变低,就应该适时释放内存,以提高系统流畅度,依赖流氓手段提高优先级,还不释放内存,保持不死的,都是作死。

Android后台杀死系列之一:FragmentActivity及PhoneWindow后台杀死处理机制
Android后台杀死系列之二:ActivityManagerService与App现场恢复机制
Android后台杀死系列之三:LowMemoryKiller原理(4.3-6.0)
Android后台杀死系列之四:Binder讣告原理
Android后台杀死系列之五:Android进程保活-自“裁”或者耍流氓

仅供参考,欢迎指正

参考文档

谷歌文档Application
Android四大组件与进程启动的关系
Android 7.0 ActivityManagerService(8) 进程管理相关流程分析(2) updateOomAdjLocked
Android 7.0 ActivityManagerService(9) 进程管理相关流程分析(3) computeOomAdjLocked 精
Android代码内存优化建议-OnTrimMemory优化 精
微信Android客户端后台保活经验分享
按”Home”键回到桌面的过程
Android low memory killer 机制
应用内存优化之OnLowMemory&OnTrimMemory

以上是关于Android | 进程保活与拉活的主要内容,如果未能解决你的问题,请参考以下文章

Android进程保活招式大全

腾讯Bugly干货分享Android 进程保活招式大全

Android进程保活-自“裁”或者耍流氓

Android后台进程保活方案

Android应用自启动保活手段与安全现状分析

Android应用自启动保活手段与安全现状分析