1. 进程的优先级
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_ADJ | 16 | 一般指将要会缓存进程,无法获取确定值 |
CACHED_APP_MAX_ADJ | 15 | 不可见进程的 adj 最大值 |
CACHED_APP_MIN_ADJ | 9 | 不可见进程的 adj 最小值 |
SERVICE_B_ADJ | 8 | B List 中的 Service(较老的、使用可能性更小) |
PREVIOUS_APP_ADJ | 7 | 上一个 App 的进程 (往往通过按返回键) |
HOME_APP_ADJ | 6 | Home 进程 |
SERVICE_ADJ | 5 | 服务进程 (Service process) |
HEAVY_WEIGHT_APP_ADJ | 4 | 后台的重量级进程,system/rootdir/init.rc 文件中设置 |
BACKUP_APP_ADJ | 3 | 备份进程 |
PERCEPTIBLE_APP_ADJ | 2 | 可感知进程,比如后台音乐播放 |
VISIBLE_APP_ADJ | 1 | 可见进程 (Visible process) |
FOREGROUND_APP_ADJ | 0 | 前台进程(Foreground process) |
PERSISTENT_SERVICE_ADJ | -11 | 关联着系统或 persistent 进程 |
PERSISTENT_PROC_ADJ | -12 | 系统 persistent 进程,比如 telephony |
SYSTEM_ADJ | -16 | 系统进程 |
NATIVE_ADJ | -17 | native 进程(不被系统管理) |
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的进程优先级。
- 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);