由于篇幅限制,本文主要剖析下通过何种方式降低oom_adj的值来降低APP被杀的几率,以及oom_adj值是怎样做到的?接下来,我们需要了解下Android系统回收内存中的进程所依据的规则:进程在内存中时活动主要有五种状态,即前台进程、可见进程、服务进程、后台进程、空进程,这几种状态的进程优先级由高到低,oom_adj值由低到高(在ProcessList定义),然后Android系统会根据当前系统资源和进程oom_adj值来回收相应的进程,前台进程一般不会被回收,空进程最容易被回收,这种管理规则就是"传说中"的Low Memory Killer。为了更直观的了解这套规则,我画了个表:
注:优先级1表示最高级,普通进程的oom_adj>=0,系统进程oom_adj<0,系统会根据相应的内存阀值对符合某段oom_adj值的进程进行回收。另外,oom_adj值也会随着占用物理内存越大而增大,系统进程绝对不会被系统杀死。
/**前台Service,使用startForeground
* 这个Service尽量要轻,不要占用过多的系统资源,否则
* 系统在资源紧张时,照样会将其杀死
*
* Created by jianddongguo on 2017/7/7.
* http://blog.csdn.net/andrexpert
*/
public class DaemonService extends Service {
private static final String TAG = "DaemonService";
public static final int NOTICE_ID = 100;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
if(Contants.DEBUG)
Log.d(TAG,"DaemonService---->onCreate被调用,启动前台service");
//如果API大于18,需要弹出一个可见通知
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentTitle("KeepAppAlive");
builder.setContentText("DaemonService is runing...");
startForeground(NOTICE_ID,builder.build());
// 如果觉得常驻通知栏体验不好
// 可以通过启动CancelNoticeService,将通知移除,oom_adj值不变
Intent intent = new Intent(this,CancelNoticeService.class);
startService(intent);
}else{
startForeground(NOTICE_ID,new Notification());
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 如果Service被终止
// 当资源允许情况下,重启service
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
// 如果Service被杀死,干掉通知
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){
NotificationManager mManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
mManager.cancel(NOTICE_ID);
}
if(Contants.DEBUG)
Log.d(TAG,"DaemonService---->onDestroy,前台service被杀死");
// 重启自己
Intent intent = new Intent(getApplicationContext(),DaemonService.class);
startService(intent);
}
}
讲解一下:
/** 移除前台Service通知栏标志,这个Service选择性使用
*
* Created by jianddongguo on 2017/7/7.
* http://blog.csdn.net/andrexpert
*/
public class CancelNoticeService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2){
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
startForeground(DaemonService.NOTICE_ID,builder.build());
// 开启一条线程,去移除DaemonService弹出的通知
new Thread(new Runnable() {
@Override
public void run() {
// 延迟1s
SystemClock.sleep(1000);
// 取消CancelNoticeService的前台
stopForeground(true);
// 移除DaemonService弹出的通知
NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
manager.cancel(DaemonService.NOTICE_ID);
// 任务完成,终止自己
stopSelf();
}
}).start();
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
c) AndroidManifest.xml
讲解一下:
总所周知,一个Service没有自己独立的进程,它一般是作为一个线程运行于它所在的应用进程中,且应用进程名称与包名一致。如果希望指定的组件和应用运行在指定的进程中,就需要通过android:process属性来为其创建一个进程,因此android:process=":daemon_service"就是让DaemonService运行在名为“com.jiangdg.keepappalive:daemon_service”进程中;android:enabled属性的作用是Android系统是否实例化应用程序中的组件;android:exported属性的作用是当前组件(Service)是否可以被包含本身以外的应用中的组件启动。
d) 测试结果
接下来,我们观察下KeepAppAlive进程的oom_adj值变化:
首先,adb查看KeepAppAlive进程的进程号;
E:\Android\StudioProject\KeepAppAlive>adb shell
shell@trltechn:/ $ su
root@trltechn:/ # ps | grep jiangdg
其次,观察KeepAppAlive进程在不同状态下的oom_adj值;
root@trltechn:/ # cat /proc/15689/oom_adj
root@trltechn:/ # cat /proc/16033/oom_adj
注意:如果执行su命令,提示"/system/bin/sh: su: not found",说明手机设备没有被root。ps命令用于显示静态进程状态,top命令可以对进程进行实时监控,每次启动KeepAppAlive进程号都不一样。
/** 静态监听锁屏、解锁、开屏广播
* a) 当用户锁屏时,将SportsActivity置于前台,同时开启1像素悬浮窗;
* b) 当用户解锁时,关闭1像素悬浮窗;
*
* Created by jianddongguo on 2017/7/8.
* http://blog.csdn.net/andrexpert
*/
public class ScreenReceiverUtil {
private Context mContext;
// 锁屏广播接收器
private SreenBroadcastReceiver mScreenReceiver;
// 屏幕状态改变回调接口
private SreenStateListener mStateReceiverListener;
public ScreenReceiverUtil(Context mContext){
this.mContext = mContext;
}
public void setScreenReceiverListener(SreenStateListener mStateReceiverListener){
this.mStateReceiverListener = mStateReceiverListener;
// 动态启动广播接收器
this.mScreenReceiver = new SreenBroadcastReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_USER_PRESENT);
mContext.registerReceiver(mScreenReceiver,filter);
}
public void stopScreenReceiverListener(){
mContext.unregisterReceiver(mScreenReceiver);
}
public class SreenBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.d("KeepAppAlive","SreenLockReceiver-->监听到系统广播:"+action);
if(mStateReceiverListener == null){
return;
}
if(Intent.ACTION_SCREEN_ON.equals(action)){ // 开屏
mStateReceiverListener.onSreenOn();
}else if(Intent.ACTION_SCREEN_OFF.equals(action)){ // 锁屏
mStateReceiverListener.onSreenOff();
}else if(Intent.ACTION_USER_PRESENT.equals(action)){ // 解锁
mStateReceiverListener.onUserPresent();
}
}
}
// 监听sreen状态对外回调接口
public interface SreenStateListener {
void onSreenOn();
void onSreenOff();
void onUserPresent();
}
}
讲解一下:
/**1像素管理类
*
* Created by jianddongguo on 2017/7/8.
* http://blog.csdn.net/andrexpert
*/
public class ScreenManager {
private static final String TAG = "ScreenManager";
private Context mContext;
private static ScreenManager mSreenManager;
// 使用弱引用,防止内存泄漏
private WeakReference mActivityRef;
private ScreenManager(Context mContext){
this.mContext = mContext;
}
// 单例模式
public static ScreenManager getScreenManagerInstance(Context context){
if(mSreenManager == null){
mSreenManager = new ScreenManager(context);
}
return mSreenManager;
}
// 获得SinglePixelActivity的引用
public void setSingleActivity(Activity mActivity){
mActivityRef = new WeakReference<>(mActivity);
}
// 启动SinglePixelActivity
public void startActivity(){
if(Contants.DEBUG)
Log.d(TAG,"准备启动SinglePixelActivity...");
Intent intent = new Intent(mContext,SinglePixelActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
// 结束SinglePixelActivity
public void finishActivity(){
if(Contants.DEBUG)
Log.d(TAG,"准备结束SinglePixelActivity...");
if(mActivityRef != null){
Activity mActivity = mActivityRef.get();
if(mActivity != null){
mActivity.finish();
}
}
}
}
讲解一下:
/**1像素Activity
*
* Created by jianddongguo on 2017/7/8.
*/
public class SinglePixelActivity extends AppCompatActivity {
private static final String TAG = "SinglePixelActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(Contants.DEBUG)
Log.d(TAG,"onCreate--->启动1像素保活");
// 获得activity的Window对象,设置其属性
Window mWindow = getWindow();
mWindow.setGravity(Gravity.LEFT | Gravity.TOP);
WindowManager.LayoutParams attrParams = mWindow.getAttributes();
attrParams.x = 0;
attrParams.y = 0;
attrParams.height = 1;
attrParams.width = 1;
mWindow.setAttributes(attrParams);
// 绑定SinglePixelActivity到ScreenManager
ScreenManager.getScreenManagerInstance(this).setSingleActivity(this);
}
@Override
protected void onDestroy() {
if(Contants.DEBUG)
Log.d(TAG,"onDestroy--->1像素保活被终止");
if(! SystemUtils.isAppAlive(this,Contants.PACKAGE_NAME)){
Intent intentAlive = new Intent(this, SportsActivity.class);
intentAlive.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intentAlive);
Log.i(TAG,"SinglePixelActivity---->APP被干掉了,我要重启它");
}
super.onDestroy();
}
}
讲解一下:
/** 运动界面,启动监听锁屏广播,判断是否开关1像素界面
*
* Created by jianddongguo on 2017/7/7.
* http://blog.csdn.net/andrexpert
*/
public class SportsActivity extends AppCompatActivity {
// 动态注册锁屏等广播
private ScreenReceiverUtil mScreenListener;
// 1像素Activity管理类
private ScreenManager mScreenManager;
// 代码省略...
private ScreenReceiverUtil.SreenStateListener mScreenListenerer = new ScreenReceiverUtil.SreenStateListener() {
@Override
public void onSreenOn() {
// 移除"1像素"
mScreenManager.finishActivity();
}
@Override
public void onSreenOff() {
// 接到锁屏广播,将SportsActivity切换到可见模式
// "咕咚"、"乐动力"、"悦动圈"就是这么做滴
// Intent intent = new Intent(SportsActivity.this,SportsActivity.class);
// startActivity(intent);
// 如果你觉得,直接跳出SportActivity很不爽
// 那么,我们就制造个"1像素"惨案
mScreenManager.startActivity();
}
@Override
public void onUserPresent() {
// 解锁,暂不用,保留
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sports);
if(Contants.DEBUG)
Log.d(TAG,"--->onCreate");
// 1. 注册锁屏广播监听器
mScreenListener = new ScreenReceiverUtil(this);
mScreenManager = ScreenManager.getScreenManagerInstance(this);
mScreenListener.setScreenReceiverListener(mScreenListenerer);
}
// 代码省略...
}
e) AndroidManifest.xml
讲解一下:
f) 测试结果
监听锁屏广播,锁屏时将SportActivity置于前台(可见)
监听锁屏广播,锁屏时开启SinglePixelActivity(1像素)
3 .循环播放一段无声音频,"打造金刚不坏之身"
对于三星C9、Note4和华为4X来说,结合前台Service和悬浮界面(1像素)的保活方式,在用户不主动清理或强杀的情况下,测试APP的保活效果还是非常不错的。但是,对于华为Mate8来说,效果还是差强人意,尤其是当使用一键清理内存时,测试APP基本无法幸存。然后,"咕咚"却奇妙的活了下来,一键清理怎么也清不掉,正当自己百思不得其"姐"时,一个"恶心"的界面出现在我面前。尼玛!看到下面的红框框没,"咕咚"居然在后台循环播放一个无声音乐,难怪生命力这么旺盛,但是耗电也是杠杠的。好吧,不纠结这么多,这里只是从学技术的角度出发而研究,毕竟用户对耗电量还是很敏感的,不到万不得已还是收敛点,不要这么"风骚",用户体验很重要,一不小心就"泻"了你。
a) PlayerMusicService.java
/**循环播放一段无声音频,以提升进程优先级
*
* Created by jianddongguo on 2017/7/11.
* http://blog.csdn.net/andrexpert
*/
public class PlayerMusicService extends Service {
private final static String TAG = "PlayerMusicService";
private MediaPlayer mMediaPlayer;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
if(Contants.DEBUG)
Log.d(TAG,TAG+"---->onCreate,启动服务");
mMediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.silent);
mMediaPlayer.setLooping(true);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
startPlayMusic();
}
}).start();
return START_STICKY;
}
private void startPlayMusic(){
if(mMediaPlayer != null){
if(Contants.DEBUG)
Log.d(TAG,"启动后台播放音乐");
mMediaPlayer.start();
}
}
private void stopPlayMusic(){
if(mMediaPlayer != null){
if(Contants.DEBUG)
Log.d(TAG,"关闭后台播放音乐");
mMediaPlayer.stop();
}
}
@Override
public void onDestroy() {
super.onDestroy();
stopPlayMusic();
if(Contants.DEBUG)
Log.d(TAG,TAG+"---->onCreate,停止服务");
// 重启
Intent intent = new Intent(getApplicationContext(),PlayerMusicService.class);
startService(intent);
}
}
b) AndroidManifest.xml
4. 测试结果
(2)三星C9(6.0):开启前台Service和1像素,KeepAppAlive在黑屏后台模式下存活9个小时以上,看样子原生系统还是温柔些;开启后台播放音频服务,用户一键清理最近应用成功保活;
(3)华为4X(6.0):效果同C9;
(4) 三星Note4(5.0):效果同C9;
注:Mate8循环播放一段无声音频,当用户点击一键清理最近应用时,KeepAppAlive不会被干掉,但是如果用户只选择清理KeepAppAlive时,也会被杀死,这与"咕咚"保活效果一致。
华为Mate8(7.0):运行Demo,黑屏和一键清理保活效果