【Android进阶笔记】进程保活

1,446 阅读9分钟

1. 进程的优先级

developer.android.google.cn/guide/compo…

1.1. 前台进程

指正在与用户进行交互的应用进程,该进程数量较少, 是最高优先级进程, 系统一般不会终止该进程。

  • 进程中包含处于前台的正与用户交互(onResume状态)的Activity。
  • 进程中包含与前台 Activity 绑定的 Service。
  • 进程中包含调用了 startForeground() 方法的 Service。
  • 进程中包含正在执行 onCreate()onStart() onDestroy() 方法的 Service。
  • 进程中包含正在执行 onReceive() 方法的 BroadcastReceiver。

1.2. 可视进程

能被用户看到,但不能根据根据用户的动作做出相应的反馈。

  • 进程中包含可见但不处于前台进程(onPause状态)的 Activity(弹出对话窗时 Activity 处于可见状态,但并不处于前台进程中)。
  • 该进程有一个与可见或前台的 Activity 绑定数据的 Service。
  • 系统正在使用其托管的服务实现用户知晓的特定功能,例如动态壁纸、输入法服务等。

1.3. 服务进程

没有可见界面仍在不断的执行任务的进程,除非在可视进程和前台进程紧缺资源才会被终止。可以被认为是前台进程,但是运行了很长时间(例如 30 分钟或更长时间)的服务的重要性可能会降位。

  • 已使用 startService() 方法启动的 Service。
  • 包含除前台进程和可视进程的 Service 外的 Service 的进程。

1.4. 后台进程

进程中的 Activity 不可见且进程中没有任何启动的 Service,这些进程都是后台进程。通常系统中有大量的后台进程,终止后台进程不会影响用户体验,随时为优先级更高的进程腾出资源而被终止,优先回收长时间没用使用过的进程。

这些后台进程保存在伪 LRU 列表中,列表中的最后一个进程是为了回收内存而终止的第一个进程。

1.5. 空进程

为提高整体系统性能,系统会保存已经完成生命周期的应用程序 , 存在内存中,为下次的启动更加迅速而设计。通常会被定期地终止。当一个进程被杀掉,但进程保留,变成空进程。


2. LowmemoryKiller

LowmemoryKiller(简称lmk)管理所有进程,根据一定策略来杀掉某个进程并释放占用的内存,保证系统的正常运行。

2.1. 原理

所有应用进程从 Zygote 孵化出来的进程都会记录在 ActivityManagerService.mLruProcesses 的列表中,由 ActivityManagerService 进行统一管理,时时更新进程的状态,根据状态计算出进程对应的 OomAdj 值,这个值会传递到 kernel 中去,kernel 有个低内存回收机制,在内存达到一定阈值(不同手机不一样)时会触发清理 OomAdj 值高的进程,从而释放内存空间。

adb shell dumpsys meminfo // 查看每个进程内存使用状况
adb shell su                                        // 启用su权限
cat /sys/module/lowmemorykiller/parameters/minfree  // 查看minfree
// 第一个值:18432(72MB)达到这个值时候,前台进程就会被杀死
// 第二个值:23040(90MB)达到这个值的时候,可见进程就会被杀死
// 第三个值:27648(108MB)达到这个值的时候,服务进程会被杀死
// 第四个值:32256(126MB)达到这个值的时候,后台进程会被杀死
// 第五个值:55296(216MB)达到这个值的时候,ContentProvider会被杀死
// 第六个值:80640(315MB)达到这个值的时候,空进程会被杀死
cat /sys/module/lowmemorykiller/parameters/adj     // 查看手机adj
cat /proc/<pid>/oom_adj                            // 查看进程adj
cat /proc/<pid>/oom_score_adj                      // 查看进程adj

2.2. 划分标准

oom_adj 划分为16级,从-17到16之间,越大优先级越低。

ADJ 级别取值解释
UNKNOWN_ADJ16一般指将要会缓存进程,无法获取确定值
CACHED_APP_MAX_ADJ15不可见进程的 adj 最大值
CACHED_APP_MIN_ADJ9不可见进程的 adj 最小值
SERVICE_B_ADJ8B List 中的 Service(较老的、使用可能性更小)
PREVIOUS_APP_ADJ7上一个 App 的进程 (往往通过按返回键)
HOME_APP_ADJ6Home 进程
SERVICE_ADJ5服务进程 (Service process)
HEAVY_WEIGHT_APP_ADJ4后台的重量级进程,system/rootdir/init.rc 文件中设置
BACKUP_APP_ADJ3备份进程
PERCEPTIBLE_APP_ADJ2可感知进程,比如后台音乐播放
VISIBLE_APP_ADJ1可见进程 (Visible process)
FOREGROUND_APP_ADJ0前台进程(Foreground process)
PERSISTENT_SERVICE_ADJ-11关联着系统或 persistent 进程
PERSISTENT_PROC_ADJ-12系统 persistent 进程,比如 telephony
SYSTEM_ADJ-16系统进程
NATIVE_ADJ-17native 进程(不被系统管理)

3. 进程保活

3.1. Activity提权

监控手机锁屏解锁事件,在屏幕锁屏时启动1个像素透明的 Activity,在用户解锁时将 Activity 销毁,从而达到提高进程优先级的作用。

创建Activity

创建一个1像素的 Activity,例如名称为 KeepActivity

Window window = getWindow();
window.setGravity(Gravity.START | Gravity.TOP);
WindowManager.LayoutParams attributes = window.getAttributes();
// 设置宽高为1像素
attributes.width = 1;
attributes.height = 1;
// 设置位置为屏幕左上角
attributes.x = 0;
attributes.y = 0;
window.setAttributes(attributes);

设置主题并注册Activity

// 为Activtiy设置一个透明背景主题
<style name="KeepTheme">
    <item name="android:windowBackground">@null</item>
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:windowNoDisplay">false</item>
    <item name="android:windowDisablePreview">true</item>
</style>

// 注册清单文件
<activity android:name=".activity.KeepActivity"
    android:configChanges="keyboardHidden|orientation|screenSize|navigation|keyboard"
    // 设置为true则这个Activity及后续启动的Activity不会出现在Recent Screens中
    android:excludeFromRecents="true"
    // 指明Task名则会新开一个Task
    android:taskAffinity="com.renpeng.example.keep"
    android:exported="false"
    android:finishOnTaskLaunch="false"
    android:theme="@style/KeepTheme"
/>

动态注册广播接收器

IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
registerReceiver(new BootCompleteReceiver(),filter);

唤醒KeepActivity

public class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if(intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
            // 唤醒KeepActivity
        } else if(intent.getAction().equals(Intent.ACTION_SCREEN_ON)){
            // 退出KeepActivity
        }
    }
}

3.2. Service提权

创建一个前台服务用于提高app的进程优先级。

Service限制

  • Android 8.0 之前,创建前台 Service 的方式通常是先创建一个后台 Service,然后将该 Service 推到前台。
  • Android 8.0 之后,系统不允许后台应用创建后台 Service。在系统创建 Service 后,应用有五秒的时间来调用该 Service 的 startForeground() 方法以显示新 Service 的用户可见通知。 如果应用在此时间限制内未调用 startForeground(),则系统将停止此 Service 并声明此应用为 ANR。

3.3. 广播拉活

在发生特定系统事件时,系统会发出广播,通过在 AndroidManifest 中静态注册对应的广播监听器,即可在发生响应事件时拉活。触发广播的系统事件会导致所有应用快速地连续消耗资源,从而降低用户体验。

  • Android 7.0 之前,时间更新、开机、解锁屏、网络状态等广播都可用于拉活。
  • Android 7.0 开始,对广播进行了限制。在Android 8.0更加严格,可静态注册的隐式广播对拉活没什么帮助了,方案基本失效

3.4. 全家桶拉活

有多个app在用户设备上安装,只要开启其中一个就可以将其他的app也拉活。比如手机里装了手Q、QQ空间、兴趣部落等等,那么打开任意一个app后,其他的app也都会被唤醒。

3.5. 推送拉活

根据终端不同,在小米手机(包括 MIUI)接入小米推送、华为手机接入华为推送。

3.6. Sticky Service拉活

将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后自动拉活。

  • START_STICKY:“粘性”。如果 service 进程被 kill 掉,保留 service 的状态为开始状态,但不保留递送的 intent 对象。随后系统会尝试重新创建 service,由于服务状态为开始状态,所以创建服务后一定会调用 onStartCommand(Intent,int,int) 方法。如果在此期间没有任何启动命令被传递到 service,那么参数 Intent 将为 null

  • START_NOT_STICKY:“非粘性的”。使用这个返回值时,如果在执行完 onStartCommand 后,服务被异常 kill 掉,系统不会自动重启该服务。

  • START_REDELIVER_INTENT:重传 Intent。使用这个返回值时,如果在执行完 onStartCommand 后,服务被异常 kill 掉,系统会自动重启该服务,并将 Intent 的值传入。

  • START_STICKY_COMPATIBILITY:START_STICKY 的兼容版本,但不保证服务被 kill 后一定能重启。

  • 只要 targetSdkVersion 不小于5,就默认是 START_STICKY。

  • 但是某些ROM系统不会拉活。并且经过测试,Service 第一次被异常杀死后很快被重启,第二次会比第一次慢,第三次又会比前一次慢,一旦在短时间内 Service 被杀死4-5次,则系统不再拉起。

3.7. JobScheduler 拉活

JobScheduler 允许在特定状态与特定时间间隔周期执行任务。可以利用它的这个特点完成保活的功能,效果即开启一个定时器,与普通定时器不同的是其调度由系统完成。也比较可靠稳定,但是会受到白名单等模式的影响,在某些ROM中甚至无法拉活

创建JobService

public class MyJobService extends JobService {
    private static final String TAG = "MyJobService";

    public static void startJob(Context context) {
        JobScheduler jobScheduler = (JobScheduler) context.getSystemService(
                Context.JOB_SCHEDULER_SERVICE);
        JobInfo.Builder builder = new JobInfo.Builder(10,
                new ComponentName(context.getPackageName(),
                MyJobService.class.getName())).setPersisted(true);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            // 7.0以上如果设置小于15min不起作用
            // 可以使用setMinimumLatency设置延时启动,并且轮询
            builder.setMinimumLatency(1000);
        } else {
            //每隔1s执行一次job
            builder.setPeriodic(1000);
        }
        jobScheduler.schedule(builder.build());
    }

    @Override
    public boolean onStartJob(JobParameters jobParameters) {
        Log.e(TAG, "开启job");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            startJob(this);
        }
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters jobParameters) { return false; }
}

注册JobService

<service
    android:name=".MyJobService"
    android:permission="android.permission.BIND_JOB_SERVICE" />

启动JobSchedule

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    MyJobService.startJob(this);
}

3.8. 系统账户拉活

手机系统设置里会有 Account 帐户功能,任何APP都可以通过它将我们自己的 APP 注册到这个 Account 帐户中,并且将数据在一定时间内同步到服务器中去。系统在将 APP 帐户同步时,自动将未启动的 APP 进程拉活。开发者可以随意重写 onPerformSync() 的内容。Android8.0及以上无法拉活

3.9. 双进程守护

利用了系统 Binder 机制并结合前台服务提权,两个进程共同运行,如果有其中一个进程被杀,那么另外一个进程就会将被杀的进程重新拉起。目前此种方式也是成功率很高的一种方式。实现步骤点击进入

3.10. 自启动白名单

国产ROM很多都有自己的安全管理软件,里面可以管理自启动白名单。如果能引导用户去这里把我们的APP加入到白名单,是最稳定最成功最有效的进程保活方式。

查看自启动管理Activity

相同手机厂商、不同的系统版本,跳转的自启动管理页面的Activity都不一定相同。所以先进入到自启动管理的Activity,再执行adb命令查看。

查看当前可视的 Activity:adb shell dumpsys activity top

如:Samsung S9+ 10.0系统,ACTIVITY为:com.samsung.android.sm_cn/.autorun.ui.AutoRunActivity

跳转到对应Activity

ComponentName com = new ComponentName(
    "com.samsung.android.sm_cn", // 包名
    "com.samsung.android.sm_cn.autorun.ui.AutoRunActivity"// Activity全路径名
);
Intent intent = new Intent();
intent.setComponent(com);
startAvtivity(intent);

如果抛异常java.lang.SecurityException: Permission Denial: starting Intent,则表示 AutoRunActivity 没有在厂商工程的清单文件中被设置为,因此我们不能直接跳转到自启动管理页,只能跳转到安全管家首页。

// 根据包名获取跳转到该APP的Launch页面的Intent
Intent intent = getPackageManager().getLaunchIntentForPackage("com.samsung.android.sm_cn");
startActivity(intent);