进程优先级原理
https://developer.android.google.cn/guide/components/processes-and-threads.html?hl=zh-cn
LMK(LowMemoryKiller)
- 为什么引入LMK?
进程的启动分冷启动和热启动,当用户退出某一个进程的时候,并不会真正的将进程退出,而是将这个进程放到后台,以便下次启动的时候可以马上启动起来,这个过程名为热启动,这也是Android的设计理念之一。这个机制会带来一个问题,每个进程都有自己独立的内存地址空间,随着应用打开数量的增多,系统已使用的内存越来越大,就很有可能导致系统内存不足。为了解决这个问题,系统引入LowmemoryKiller(简称lmk)管理所有进程,根据一定策略来kill某个进程并释放占用的内存,保证系统的正常运行 - LMK基本原理
所有应用进程都是从zygote孵化出来的,记录在AMS中mLruProcesses列表中,由AMS进行统一管理,AMS中会根据进程的状态更新进程对应的oom_adj值,这个值会通过文件传递到kernel中去,kernel有个低内存回收机制,在内存达到一定阀值时会触发清理oom_adj值高的进程腾出更多的内存空间 - LMK杀进程标准
minfree(存放6个数值,单位内存页面数(一个页面4kb))
adb shell
su
cat /sys/module/lowmemorykiller/parameters/minfree
adj
cat /sys/module/lowmemorykiller/parameters/adj
查看进程的adj值
/proc/<pid>/oom_adj
/proc/<pid>/oom_score_adj
内存阈值在不同的手机上不一样,一旦低于该值,Android便开始按顺序关闭进程. 因此Android开始结束优先级最低的空进程,即当可用内存小于180MB(46080)
常用的保活方式
- Activity提权
- Service提权
- 广播拉活
- “全家桶”拉活
- Service机制(Sticky)拉活
- 账户同步拉活
- JobScheduler拉活
- 推送拉活
- Native拉活(NDK)
- 双进程守护(JobScheduler和两个Service结合用)
Activity提权
- 原理
监控手机锁屏解锁事件,在屏幕锁屏时启动1个像素透明的 Activity,在用户解锁时将 Activity 销毁掉,从而达到提高进程优先级的作用。 - 代码实现
- 创建1个像素的Activity KeepActivity
Window window = getWindow();
window.setGravity(Gravity.START | Gravity.TOP);
WindowManager.LayoutParams attributes = window.getAttributes();
attributes.width = 1;
attributes.height = 1;
attributes.x = 0;
attributes.y = 0;
window.setAttributes(attributes);
manifest注册
styles添加主题样式
- 创建广播接收者KeepReceiver
if (TextUtils.equals(action, Intent.ACTION_SCREEN_OFF)) {
//灭屏 开启1px activity
KeepManager.getInstance().startKeep(context);
} else if (TextUtils.equals(action, Intent.ACTION_SCREEN_ON)) {
//亮屏 关闭
KeepManager.getInstance().finishKeep();
}
- 创建广播注册管理单例类KeepManager
注册/反注册广播
启动/关闭keepActivity
设置keepActivity弱引用
Service提权
- 创建一个前台服务用于提高app在按下home键之后的进程优先级
- Service限制https://developer.android.google.cn/about/versions/oreo/background#services
- startForeground(ID,Notification):使Service成为前台Service。 前台服务需要在通知栏显示一条通知
广播拉活
- 在发生特定系统事件时,系统会发出广播,通过在 AndroidManifest 中静态注册对应的广播监听器,即可在发生响应事件时拉活。但是从android 7.0开始,对广播进行了限制,而且在8.0更加严格
- 可静态注册广播列表https://developer.android.google.cn/guide/components/broadcast-exceptions.html
“全家桶”拉活
- 有多个app在用户设备上安装,只要开启其中一个就可以将其他的app也拉活。比如手机里装了手Q、QQ空间、兴趣部落等等,那么打开任意一个app后,其他的app也都会被唤醒。
Service机制(Sticky)拉活
- 将 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次,则系统不再拉起。
账户同步拉活
- 手机系统设置里会有“帐户”一项功能,任何第三方APP都可以通过此功能将数据在一定时间内同步到服务器中去。系统在将APP帐户同步时,会将未启动的APP进程拉活https://github.com/googlesamples/android-BasicSyncAdapter.比较繁琐,效果还不一定好,这里就不研究了.
JobScheduler拉活(为后续双进程守护做准备)
- JobScheduler允许在特定状态与特定时间间隔周期执行任务。可以利用它的这个特点完成保活的功能,效果即开启一个定时器,与普通定时器不同的是其调度由系统完成。
- 创建一个JobService
注意setPeriodic方法
在7.0以上如果设置小于15min不起作用,可以使用setMinimumLatency设置延时启动,并且轮询
推送拉活
- 根据终端不同,在小米手机(包括 MIUI)接入小米推送、华为手机接入华为推送。
Native拉活(NDK)
- Native fork子进程用于观察当前app主进程的存亡状态。对于5.0以上成功率极低。
双进程守护(最后一个当然是重点啦)
- 两个进程共同运行,如果有其中一个进程被杀,那么另外一个进程就会将被杀的进程重新拉起,结合JobScheduler一起拉活,这种效率是最高的。会附上核心代码。
public class LocalService extends Service {
private MyBinder myBinder;
class MyBinder extends IMyAidlInterface.Stub{
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString) throws RemoteException {
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return myBinder;
}
@Override
public void onCreate() {
super.onCreate();
myBinder = new MyBinder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("deamon", "deamon",
NotificationManager.IMPORTANCE_LOW);
NotificationManager manager = (NotificationManager) getSystemService(
Context.NOTIFICATION_SERVICE);
if (manager == null)
return;
manager.createNotificationChannel(channel);
Notification notification = new NotificationCompat.Builder(this,
"deamon").setAutoCancel(true).setCategory(
Notification.CATEGORY_SERVICE).setOngoing(true).setPriority(
NotificationManager.IMPORTANCE_LOW).build();
startForeground(10, notification);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
//如果 18 以上的设备 启动一个Service startForeground给相同的id
//然后结束那个Service
startForeground(10, new Notification());
startService(new Intent(this, InnnerService.class));
} else {
startForeground(10, new Notification());
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
bindService(new Intent(this, RemoteService.class), new MyServiceConnection(),
BIND_AUTO_CREATE);
return super.onStartCommand(intent, flags, startId);
}
private class MyServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
//回调
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
//
startService(new Intent(LocalService.this, RemoteService.class));
bindService(new Intent(LocalService.this, RemoteService.class), new MyServiceConnection(),
BIND_AUTO_CREATE);
}
}
public static class InnnerService extends Service {
@Override
public void onCreate() {
super.onCreate();
startForeground(10, new Notification());
stopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
}
public class RemoteService extends Service {
private MyBinder myBinder;
class MyBinder extends IMyAidlInterface.Stub {
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString) throws RemoteException {
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return myBinder;
}
@Override
public void onCreate() {
super.onCreate();
myBinder = new MyBinder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("deamon", "deamon",
NotificationManager.IMPORTANCE_LOW);
NotificationManager manager = (NotificationManager) getSystemService(
Context.NOTIFICATION_SERVICE);
if (manager == null)
return;
manager.createNotificationChannel(channel);
Notification notification = new NotificationCompat.Builder(this,
"deamon").setAutoCancel(true).setCategory(
Notification.CATEGORY_SERVICE).setOngoing(true).setPriority(
NotificationManager.IMPORTANCE_LOW).build();
startForeground(10, notification);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
//如果 18 以上的设备 启动一个Service startForeground给相同的id
//然后结束那个Service
startForeground(10, new Notification());
startService(new Intent(this, InnnerService.class));
} else {
startForeground(10, new Notification());
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
bindService(new Intent(this, LocalService.class), new MyServiceConnection(),
BIND_AUTO_CREATE);
return super.onStartCommand(intent, flags, startId);
}
private class MyServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
startService(new Intent(RemoteService.this, LocalService.class));
bindService(new Intent(RemoteService.this, LocalService.class),
new MyServiceConnection(), BIND_AUTO_CREATE);
}
}
public static class InnnerService extends Service {
@Override
public void onCreate() {
super.onCreate();
startForeground(10, new Notification());
stopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
}
@SuppressLint("NewApi")
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);
/**
* I was having this problem and after review some blogs and the official documentation,
* I realised that JobScheduler is having difference behavior on Android N(24 and 25).
* JobScheduler works with a minimum periodic of 15 mins.
*
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//7.0以上延迟1s执行
builder.setMinimumLatency(1000);
} else {
//每隔1s执行一次job
builder.setPeriodic(1000);
}
jobScheduler.schedule(builder.build());
}
@Override
public boolean onStartJob(JobParameters jobParameters) {
Log.e(TAG, "开启job");
//7.0以上轮询
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
startJob(this);
}
//判断服务是否在运行
boolean isLocalServiceRun = isServiceRunning(this, LocalService.class.getName());
boolean isRemoteServiceRun = isServiceRunning(this, RemoteService.class.getName());
if (!isLocalServiceRun || !isRemoteServiceRun) {
startService(new Intent(this, LocalService.class));
startService(new Intent(this, RemoteService.class));
}
return false;
}
private boolean isServiceRunning(Context context, String serviceName) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningServiceInfo> runningServices = am.getRunningServices(10);
for (ActivityManager.RunningServiceInfo runningService : runningServices) {
if (TextUtils.equals(runningService.service.getClassName(), serviceName)) {
return true;
}
}
return false;
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
return false;
}
}
总结:目前没有说百分之百保证能拉活的一些方法,除非你叫厂商给你开白名单。你不是大厂的APP并且有几千万用户的没这个必要,一般来说你给不起厂商这个钱。以上说的这些拉活其实是些流氓软件常常用到的,有风险。并且慎用,这些APP装在手机上面虽然功能实现了,但是可能会造成内存不足(保活了就释放不了了嘛,根据需求)。
应用场景:
比如你的APP对于用户地理位置很敏感,需要5分钟上传一次给服务器记录每个用户的地理位置,按照普通的做法没保活的话那肯定实现不了嘛,用这种方式是可以的。
自身经历:
几年前因为业务需要,跟着我师父他们公司做了一个短信扣费的功能的,也就是传说中的SP吧,属于打擦边球的,也算是属于和中国xx合作。我用的广播的形式,只要用户一启动APP,就自动获取手机信息,包括SIM卡,上传到服务器,然后服务器根据这些信息直接去扣费,或者服务器给客户端发送什么指令,要用户根据指令发送短信(目前也可以实现),如果是在2012年左右,这项业务是非常非常赚钱的(可惜那时候我还没学安卓).如果是4.4手机系统的话,收到扣费信息都可以屏蔽掉,现在都是高版本的手机一般来说屏蔽不了短信了。那时候各项机制都不是特别健全。所以很多这种。所以,少去不正经的网站下载小游戏,一不小心,你口袋里面20块可能就没了。