为了账号安全,请及时绑定邮箱和手机立即绑定

Android 常驻内存

标签:
Android

   APP常驻内存(保活),旧事重提,距离上一次的研究亦有半年有余。最近,用户反馈说多进程守护方案在华为Mate8(7.0)保活效果不是很好,有时候还是不能及时收到消息,于是,又带着怀疑的眼光,重新找回原来的代码进行测试,顺便分析了市场上主流运动类APP保活方法(微信、手Q就算了,富人家的孩子,不具代表性),同时也对系统对内存中APP的管理规则进行了进一步探索。本文便是对最近一周的探索、学习、测试的总结之一,以备将来不时之需。
一、APP保活核心思想归纳
     对于Android6.0及其以上系统APP保活,我觉得主要还是通过这两个方面进行,即:降低omm_adj值,尽量保证进程不被系统杀死;进程被杀死后,通过其他方式将进程复活。但需要明白的是,面对各手机厂商的深度定制和谷歌越来越严格的资源管理机制,这两种方式结合的保活不是永久的,只能是相对存在,不同的机型结果也是不一样的。

由于篇幅限制,本文主要剖析下通过何种方式降低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值也会随着占用物理内存越大而增大,系统进程绝对不会被系统杀死。

二、市场主流运动类APP分析
1. 咕 咚(v 7.17.0)
(1) 一键清理/滑动清理

a. 当"咕咚"处于停止状态,其进程被杀死,通知栏图标被清理,等待几分钟没有 自动重启,当重新进入“咕咚”时,会从欢迎界面重新进入;
b. 当"咕咚"处于运动进行状态,进程死亡,通知栏图标被清除,等待几分钟没有自动重启,但当重新进入“咕咚”时,其直接显示运动界面,而没有从欢迎界面进入,运动时间等状态与被清理时一样;
c. 当"咕咚"处于运动暂停状态,其进程正常存活,通知栏图标正常显示。如果是单独清理,进程死亡,通知栏图标被清除;但当重新进入“咕咚”时,其直接显示运动界面,而没有从欢迎界面进入,运动时间等状态与被清理时一样;
(2) 黑屏/锁屏
a. 当"咕咚"处于停止状态,退到后台,锁屏进入黑屏状态,等待5分钟,进程死亡,通知栏被清除;
b. 当"咕咚"处于运动进行状态,退到后台,锁屏进入黑屏状态,然后再进入系统,“咕咚”跑步界面自动弹出。再次锁屏,等待20分钟,进程没有被杀死,"咕咚"跑步界面自动弹出,运动状态保持不变;
c. 当"咕咚"处于运动暂停状态,退到后台,锁屏进入黑屏状态,然后再进入系统,"咕咚"跑步界面自动弹出。再次锁屏。等待20分钟,进程没有被杀死,"咕咚"跑步界面自动弹出,运动状态保持不变;
* 前提:"手机管家->锁屏清理应用"关闭;
               "手机管家->自启管理"关闭;
                运动状态,禁用返回键,用户只能从Home键退到后台
                运动界面文字闪烁或运动计时
                断网
* 分析:当"咕咚"处于停止状态时,一键清理和黑屏状态会被杀死,说明在没有进入运动界面之前,其保活机制没有被启动(即没有使运动界面切换到后台等)。当“咕咚”处于运动状态时,一键清理和黑屏状态没有被杀死(滑动清理除外),说明已经启动保活机制:①"咕咚"禁止了返回键,以保证运动Activity不被销毁;②不断更新通知栏计时,以保证APP始终在前台,防止被系统回收;③"咕咚"被清理后能够自动重启,通知被删除后自动弹出,说明可能有另外一个东西(进程或Service)监听器运动Service(或进程)存活状态,当Service被销毁时,立马将其拉起来;④“咕咚”被强制停止或清理杀死后,再次进入会直接显示运动界面且能够保持杀死之前的运动状态,说明其可能利用配置文件记录了相关状态;⑤锁屏/解锁后,"咕咚"运动界面会自动弹出,说明其利用了广播机制对锁屏广播进行监听,弹出Activity以保证进程始终在前台;
* 结论:常驻通知栏,双进程守护,广播锁屏,自定义锁屏
* 备注:以上为华为Mate8(7.0)测试结果;其他如三星C9(6.0)保活较好,特别是当一键清理时,"咕咚会自动启动,估计是使用了进程守护策略,而三星使用的是原生系统,因此结果你懂得;360F4(6.0)保活很差,不愧是流氓中的战斗机,以更流氓的方式干掉流氓APP;
2. 乐动力(v7.3.2)
(1) 一键清理 / 滑动清理

      三星C9(6.0):无论何种状态,"乐动力" 进程被杀死,等待几分钟,没有自动启动;
      360F4(6.0):无论何种状态,"乐动力" 进程被杀死,等待几分钟,没有自动启动;
      华为Mate8(7.0):无论何种状态,"乐动力" 进程被杀死,等待几分钟,没有自动启动;
(2) 锁屏/黑屏
 a. 当"乐动力"处于停止状态,退到后台,锁屏,等待5分钟,进程死亡,通知栏被清除;
 b. 当"乐动力"处于运动暂停状态,退到后台,锁屏再开启,运动界面被切换到前台,并强制弹出自定义锁屏界面(覆盖在系统锁屏界面之上);再次锁屏,等待20分钟,应用进程存活;
 c. 当"乐动力"处于运动进行状态,退到后台,锁屏再开启,运动界面被切换到前台,并强制弹出自定义锁屏界面(覆盖在系统锁屏界面之上);再次锁屏,等待20分钟,应用进程存活;
* 前提:"手机管家->锁屏清理应用"关闭;
               "手机管家->自启管理"关闭;
                运动状态,禁用返回键,用户只能从Home键退到后台
                断网
* 分析:当"乐动力"处于停止状态时,黑屏状态下,其在短时间内被系统杀死,说明保活机制没有启用;但当处于运动暂停或进行状态时,"乐动力"在一段时间内没有被杀死,且当锁屏时,"乐动力"会自动将运动界面切换到前台,此外,还会强制弹出自定锁屏界面,这就说明"乐动力"的保活机制很可能是利用监听锁屏广播强制将相关界面切换到前台,以提高"乐动力"在黑屏状态下的存活率。
* 结论:常驻通知栏,广播锁屏,自定义锁屏
3. 悦动圈(v3.1.2.9)
(1) 一键清理 / 滑动清理

      三星C9(6.0):效果与乐动力一致;
      360F4(6.0):效果与乐动力一致;
      华为Mate8(7.0):效果与乐动力一致;
(2) 锁屏/黑屏
a. 当"悦动圈"处于停止状态,退到后台,锁屏,等待3分钟,进程死亡,通知栏被清除;
b. 当"悦动圈"处于运动暂停状态时,自定义锁屏、切换界面到前台与咕咚、乐动力一样,效果一致;
c. 当"悦动圈"处于运动进行状态时,自定义锁屏、切换界面到前台与咕咚、乐动力一样,效果一致;
* 结论:常驻通知栏,广播锁屏,自定义锁屏
三、APP保活方案探讨
    经过上面的讨论分析,"咕咚"、"乐动力"等这类APP主要是通过监听锁屏、网络等系统广播,将进程置于前台以提高进程的级别,从而防止进程不那么轻易被系统干掉。另外,"咕咚"可能还使用了相关的进程被清理复活策略。当然,对于复活策略,我们下一篇文章再探讨,本文主要讨论以上APP是通过哪些方式降低进程omm_adj值,防止其被系统杀死的。
    为了达到与"咕咚"等APP类似效果,我们模拟这么一种场景:当用户登录测试APP后,先不开启保活功能;当用户开始跑步时,开启保活功能,然后再在这基础上做黑屏运行、一键清理、强制停止等功能测试。也就是说,Android项目中SplashActivity、LoginActivity只是配合我们"演戏"的,真正启动APP保活逻辑的是在SportsActivity,它将上演"后宫争宠"戏码。
     好吧,小伙子,开始你的表演!
1. 开启前台Service,“逼君上位”
    将Service置为前台,目的时提高进程Service的oom_adj值,以降低其被系统回收的几率。该方案的原理是,通过使用 startForeground()方法将当前Service置于前台来提高Service的优先级。需要注意的是,对API大于18而言 startForeground()方法需要弹出一个可见通知,如果你觉得不爽,可以开启另一个Service将通知栏移除,其oom_adj值还是没变的。实现代码如下:
a) DaemonService.java

[java] view plain copy

  1. /**前台Service,使用startForeground 

  2.  * 这个Service尽量要轻,不要占用过多的系统资源,否则 

  3.  * 系统在资源紧张时,照样会将其杀死 

  4.  * 

  5.  * Created by jianddongguo on 2017/7/7. 

  6.  * http://blog.csdn.net/andrexpert 

  7.  */  

  8. public class DaemonService extends Service {  

  9.     private static final String TAG = "DaemonService";  

  10.     public static final int NOTICE_ID = 100;  

  11.   

  12.   

  13.     @Nullable  

  14.     @Override  

  15.     public IBinder onBind(Intent intent) {  

  16.         return null;  

  17.     }  

  18.   

  19.   

  20.     @Override  

  21.     public void onCreate() {  

  22.         super.onCreate();  

  23.         if(Contants.DEBUG)  

  24.             Log.d(TAG,"DaemonService---->onCreate被调用,启动前台service");  

  25.         //如果API大于18,需要弹出一个可见通知  

  26.         if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){  

  27.             Notification.Builder builder = new Notification.Builder(this);  

  28.             builder.setSmallIcon(R.mipmap.ic_launcher);  

  29.             builder.setContentTitle("KeepAppAlive");  

  30.             builder.setContentText("DaemonService is runing...");  

  31.             startForeground(NOTICE_ID,builder.build());  

  32.             // 如果觉得常驻通知栏体验不好  

  33.             // 可以通过启动CancelNoticeService,将通知移除,oom_adj值不变  

  34.             Intent intent = new Intent(this,CancelNoticeService.class);  

  35.             startService(intent);  

  36.         }else{  

  37.             startForeground(NOTICE_ID,new Notification());  

  38.         }  

  39.     }  

  40.   

  41.   

  42.     @Override  

  43.     public int onStartCommand(Intent intent, int flags, int startId) {  

  44.         // 如果Service被终止  

  45.         // 当资源允许情况下,重启service  

  46.         return START_STICKY;  

  47.     }  

  48.   

  49.   

  50.     @Override  

  51.     public void onDestroy() {  

  52.         super.onDestroy();  

  53.         // 如果Service被杀死,干掉通知  

  54.         if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){  

  55.             NotificationManager mManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);  

  56.             mManager.cancel(NOTICE_ID);  

  57.         }  

  58.         if(Contants.DEBUG)  

  59.             Log.d(TAG,"DaemonService---->onDestroy,前台service被杀死");  

  60.         // 重启自己  

  61.         Intent intent = new Intent(getApplicationContext(),DaemonService.class);  

  62.         startService(intent);  

  63.     }  

  64. }  

讲解一下:
       这里还用到了两个技巧:一是在onStartCommand方法中返回START_STICKY,其作用是当Service进程被kill后,系统会尝试重新创建这个Service,且会保留Service的状态为开始状态,但不保留传递的Intent对象,onStartCommand方法一定会被重新调用。其二在onDestory方法中重新启动自己,也就是说,只要Service在被销毁时走到了onDestory这里我们就重新启动它。
b) CancelNoticeService.java

[java] view plain copy

  1. /** 移除前台Service通知栏标志,这个Service选择性使用 

  2.  * 

  3.  * Created by jianddongguo on 2017/7/7. 

  4.  * http://blog.csdn.net/andrexpert 

  5.  */  

  6.   

  7.   

  8. public class CancelNoticeService extends Service {  

  9.     @Nullable  

  10.     @Override  

  11.     public IBinder onBind(Intent intent) {  

  12.         return null;  

  13.     }  

  14.   

  15.   

  16.     @Override  

  17.     public void onCreate() {  

  18.         super.onCreate();  

  19.     }  

  20.   

  21.   

  22.     @Override  

  23.     public int onStartCommand(Intent intent, int flags, int startId) {  

  24.         if(Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2){  

  25.             Notification.Builder builder = new Notification.Builder(this);  

  26.             builder.setSmallIcon(R.mipmap.ic_launcher);  

  27.             startForeground(DaemonService.NOTICE_ID,builder.build());  

  28.             // 开启一条线程,去移除DaemonService弹出的通知  

  29.             new Thread(new Runnable() {  

  30.                 @Override  

  31.                 public void run() {  

  32.                     // 延迟1s  

  33.                     SystemClock.sleep(1000);  

  34.                     // 取消CancelNoticeService的前台  

  35.                     stopForeground(true);  

  36.                     // 移除DaemonService弹出的通知  

  37.                     NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);  

  38.                     manager.cancel(DaemonService.NOTICE_ID);  

  39.                     // 任务完成,终止自己  

  40.                     stopSelf();  

  41.                 }  

  42.             }).start();  

  43.         }  

  44.         return super.onStartCommand(intent, flags, startId);  

  45.     }  

  46.   

  47.   

  48.     @Override  

  49.     public void onDestroy() {  

  50.         super.onDestroy();  

  51.     }  

  52. }  

c) AndroidManifest.xml

[html] view plain copy

  1. <service android:name=".service.DaemonService"  

  2.          android:enabled="true"  

  3.           android:exported="true"  

  4.           android:process=":daemon_service"/>  

  5.   

  6.   

  7. <service android:name=".service.CancelNoticeService"  

  8.             android:enabled="true"  

  9.             android:exported="true"  

  10.             android:process=":service"/>  

讲解一下:

      总所周知,一个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进程号都不一样。

2. 监听锁屏广播,“制造‘1像素’惨案”
a) ScreenReceiverUtil.java

[java] view plain copy

  1. /** 静态监听锁屏、解锁、开屏广播 

  2.  *  a) 当用户锁屏时,将SportsActivity置于前台,同时开启1像素悬浮窗; 

  3.  *  b) 当用户解锁时,关闭1像素悬浮窗; 

  4.  * 

  5.  * Created by jianddongguo on 2017/7/8. 

  6.  * http://blog.csdn.net/andrexpert 

  7.  */  

  8. public class ScreenReceiverUtil {  

  9.     private Context mContext;  

  10.     // 锁屏广播接收器  

  11.     private SreenBroadcastReceiver mScreenReceiver;  

  12.     // 屏幕状态改变回调接口  

  13.     private SreenStateListener mStateReceiverListener;  

  14.   

  15.   

  16.     public ScreenReceiverUtil(Context mContext){  

  17.         this.mContext = mContext;  

  18.     }  

  19.   

  20.   

  21.     public void setScreenReceiverListener(SreenStateListener mStateReceiverListener){  

  22.         this.mStateReceiverListener = mStateReceiverListener;  

  23.         // 动态启动广播接收器  

  24.         this.mScreenReceiver = new SreenBroadcastReceiver();  

  25.         IntentFilter filter = new IntentFilter();  

  26.         filter.addAction(Intent.ACTION_SCREEN_ON);  

  27.         filter.addAction(Intent.ACTION_SCREEN_OFF);  

  28.         filter.addAction(Intent.ACTION_USER_PRESENT);  

  29.         mContext.registerReceiver(mScreenReceiver,filter);  

  30.     }  

  31.   

  32.   

  33.     public void stopScreenReceiverListener(){  

  34.         mContext.unregisterReceiver(mScreenReceiver);  

  35.     }  

  36.   

  37.   

  38.     public  class SreenBroadcastReceiver extends BroadcastReceiver {  

  39.         @Override  

  40.         public void onReceive(Context context, Intent intent) {  

  41.             String action = intent.getAction();  

  42.             Log.d("KeepAppAlive","SreenLockReceiver-->监听到系统广播:"+action);  

  43.             if(mStateReceiverListener == null){  

  44.                 return;  

  45.             }  

  46.             if(Intent.ACTION_SCREEN_ON.equals(action)){         // 开屏  

  47.                 mStateReceiverListener.onSreenOn();  

  48.             }else if(Intent.ACTION_SCREEN_OFF.equals(action)){  // 锁屏  

  49.                 mStateReceiverListener.onSreenOff();  

  50.             }else if(Intent.ACTION_USER_PRESENT.equals(action)){ // 解锁  

  51.                 mStateReceiverListener.onUserPresent();  

  52.             }  

  53.         }  

  54.     }  

  55.   

  56.   

  57.     // 监听sreen状态对外回调接口  

  58.     public interface SreenStateListener {  

  59.         void onSreenOn();  

  60.         void onSreenOff();  

  61.         void onUserPresent();  

  62.     }  

  63. }  

讲解一下:
    由于静态注册广播接收器,无法接收到系统的锁屏(Intent.ACTION_SCREEN_OFF)和开屏(Intent.ACTION_SCREEN_ON)广播,因此必须通过动态注册来监听。另外,这里还使用了接口将监听的结果回调给调用者。
b) ScreenManager.java

[java] view plain copy

  1. /**1像素管理类 

  2.  * 

  3.  * Created by jianddongguo on 2017/7/8. 

  4.  * http://blog.csdn.net/andrexpert 

  5.  */  

  6.   

  7.   

  8. public class ScreenManager {  

  9.     private static final String TAG = "ScreenManager";  

  10.     private Context mContext;  

  11.     private static ScreenManager mSreenManager;  

  12.     // 使用弱引用,防止内存泄漏  

  13.     private WeakReference<Activity> mActivityRef;  

  14.   

  15.   

  16.     private ScreenManager(Context mContext){  

  17.         this.mContext = mContext;  

  18.     }  

  19.   

  20.   

  21.     // 单例模式  

  22.     public static ScreenManager getScreenManagerInstance(Context context){  

  23.         if(mSreenManager == null){  

  24.             mSreenManager = new ScreenManager(context);  

  25.         }  

  26.         return mSreenManager;  

  27.     }  

  28.   

  29.   

  30.     // 获得SinglePixelActivity的引用  

  31.     public void setSingleActivity(Activity mActivity){  

  32.         mActivityRef = new WeakReference<>(mActivity);  

  33.     }  

  34.   

  35.   

  36.     // 启动SinglePixelActivity  

  37.     public void startActivity(){  

  38.         if(Contants.DEBUG)  

  39.             Log.d(TAG,"准备启动SinglePixelActivity...");  

  40.         Intent intent = new Intent(mContext,SinglePixelActivity.class);  

  41.         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  

  42.         mContext.startActivity(intent);  

  43.     }  

  44.   

  45.   

  46.     // 结束SinglePixelActivity  

  47.     public void finishActivity(){  

  48.         if(Contants.DEBUG)  

  49.             Log.d(TAG,"准备结束SinglePixelActivity...");  

  50.         if(mActivityRef != null){  

  51.             Activity mActivity = mActivityRef.get();  

  52.             if(mActivity != null){  

  53.                 mActivity.finish();  

  54.             }  

  55.         }  

  56.     }  

  57. }  

讲解一下:
    Java中为对象的引用分了四个级别:强引用、软引用、弱引用、虚引用。这里,我们使用了弱引用WeakReference来防止内存泄漏,为了解释这个问题,我们举这么一个例子:有两个类class A和class B,分别实例化这两个类得到a,b,其中a又作为实例化B时传入的构造参数,代码如下:
A a = new A();
B b = new B(a);
从这两行代码来看,a是对象A的引用,b是对象B的引用,对象B同时依赖于对象A,对象A和对象B之间形成了强引用。当a=null时,a不在指向对象A,通常情况下,对象A在不被其他对象引用时会被GC回收,但是由于B还依赖于对象A,对象A不会被GC回收,从而造成内存泄漏(除非b=null,对象A和对象B才会被GC同时回收)。如果使用弱引用的话,对象A只会被WeakReference所依赖,当a=null时,GC会回收它,从而避免了内存泄漏。
c) SinglePixelActivity.java

[java] view plain copy

  1. /**1像素Activity 

  2.  * 

  3.  * Created by jianddongguo on 2017/7/8. 

  4.  */  

  5.   

  6.   

  7. public class SinglePixelActivity extends AppCompatActivity {  

  8.     private static final String TAG = "SinglePixelActivity";  

  9.   

  10.   

  11.     @Override  

  12.     protected void onCreate(@Nullable Bundle savedInstanceState) {  

  13.         super.onCreate(savedInstanceState);  

  14.         if(Contants.DEBUG)  

  15.             Log.d(TAG,"onCreate--->启动1像素保活");  

  16.         // 获得activity的Window对象,设置其属性  

  17.         Window mWindow = getWindow();  

  18.         mWindow.setGravity(Gravity.LEFT | Gravity.TOP);  

  19.         WindowManager.LayoutParams attrParams = mWindow.getAttributes();  

  20.         attrParams.x = 0;  

  21.         attrParams.y = 0;  

  22.         attrParams.height = 1;  

  23.         attrParams.width = 1;  

  24.         mWindow.setAttributes(attrParams);  

  25.         // 绑定SinglePixelActivity到ScreenManager  

  26.         ScreenManager.getScreenManagerInstance(this).setSingleActivity(this);  

  27.     }  

  28.   

  29.   

  30.     @Override  

  31.     protected void onDestroy() {  

  32.         if(Contants.DEBUG)  

  33.             Log.d(TAG,"onDestroy--->1像素保活被终止");  

  34.         if(! SystemUtils.isAppAlive(this,Contants.PACKAGE_NAME)){  

  35.             Intent intentAlive = new Intent(this, SportsActivity.class);  

  36.             intentAlive.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  

  37.             startActivity(intentAlive);  

  38.             Log.i(TAG,"SinglePixelActivity---->APP被干掉了,我要重启它");  

  39.         }  

  40.         super.onDestroy();  

  41.     }  

  42. }  

讲解一下:
    在UI界面架构中,每个Activity都包含一个Window对象,在Android中Window对象通常由PhoneWindow来实现,PhoneWindow将一个DecorView设置为整个应用窗口的根View,它作为窗口界面的顶层视图,封装了很多通用操作窗口的方法...好了,不扯远了,既然我们已经知道Window对象在一个Activity中的位置,这里我们通过getWindow方法来获得SinglePixelActivity 的Window对象,然后为其设置相关属性,比如窗体的大小、位置、坐标等,来达到所需的"1像素"界面效果。
d) SportsActivity.java

[java] view plain copy

  1. /** 运动界面,启动监听锁屏广播,判断是否开关1像素界面 

  2.  * 

  3.  * Created by jianddongguo on 2017/7/7. 

  4.  * http://blog.csdn.net/andrexpert 

  5.  */  

  6.   

  7. public class SportsActivity extends AppCompatActivity {  

  8.     // 动态注册锁屏等广播  

  9.     private ScreenReceiverUtil mScreenListener;  

  10.     // 1像素Activity管理类  

  11.     private ScreenManager mScreenManager;  

  12.     // 代码省略...  

  13.   

  14.   

  15.     private ScreenReceiverUtil.SreenStateListener mScreenListenerer = new ScreenReceiverUtil.SreenStateListener() {  

  16.         @Override  

  17.         public void onSreenOn() {  

  18.             // 移除"1像素"  

  19.             mScreenManager.finishActivity();  

  20.         }  

  21.   

  22.   

  23.         @Override  

  24.         public void onSreenOff() {  

  25.             // 接到锁屏广播,将SportsActivity切换到可见模式  

  26.             // "咕咚"、"乐动力"、"悦动圈"就是这么做滴  

  27. //            Intent intent = new Intent(SportsActivity.this,SportsActivity.class);  

  28. //            startActivity(intent);  

  29.             // 如果你觉得,直接跳出SportActivity很不爽  

  30.             // 那么,我们就制造个"1像素"惨案  

  31.             mScreenManager.startActivity();  

  32.         }  

  33.   

  34.   

  35.         @Override  

  36.         public void onUserPresent() {  

  37.             // 解锁,暂不用,保留  

  38.         }  

  39.     };  

  40.   

  41.   

  42.     @Override  

  43.     protected void onCreate(@Nullable Bundle savedInstanceState) {  

  44.         super.onCreate(savedInstanceState);  

  45.         setContentView(R.layout.activity_sports);  

  46.         if(Contants.DEBUG)  

  47.             Log.d(TAG,"--->onCreate");  

  48.         // 1. 注册锁屏广播监听器  

  49.         mScreenListener = new ScreenReceiverUtil(this);  

  50.         mScreenManager = ScreenManager.getScreenManagerInstance(this);  

  51.         mScreenListener.setScreenReceiverListener(mScreenListenerer);  

  52.     }  

  53.     // 代码省略...  

  54. }  

e) AndroidManifest.xml

[html] view plain copy

  1. <activity android:name=".SportsActivity"  

  2.             android:launchMode="singleTask"/>  

  3. <activity android:name=".SinglePixelActivity"  

  4.             android:configChanges="keyboardHidden|orientation|screenSize|navigation|keyboard"  

  5.             android:excludeFromRecents="true"  

  6.             android:finishOnTaskLaunch="false"  

  7.             android:launchMode="singleInstance"  

  8.             android:theme="@style/SingleActivityStyle"/>  

讲解一下:
     android:launchMode属性用于指定activity的启动模式,总共分为四种,即standar模式,每次启动activity都会创建其实例,并加入到任务栈的栈顶;singleTop模式,每次启动activity如果栈顶时该activity则无需创建,其余情况都要创建该activity的实例;singleTask模式,如果被启动的activity的实例存在栈中,则不需要创建,只需要把此activity加入到栈顶,并把该activity以上的activity实例全部pop;singleInstance模式,将创建的activity实例放入单独的栈中,该栈只能存储这个实例,且是作为共享实例存在;
     android:configChanges属性用于捕获手机状态的改变,即当手机状态(如切换横竖屏、屏幕大小)改变时会保存当前活动状态重启Activity,由于SinglePixelActivity肩负着保活的特殊使命,这里使用android:configChanges属性防止Activity重启,它只是调用了onConfigurationChanged(Configuration newConfig)来通知手机状态的改变;
     android:excludeFromRecents属性用于控制SinglePixelActivity不在最近任务列表中显示;
     android:finishOnTaskLaunch属性用于标记当用户再起启动应用(TASK)时是否关闭已经存在的Activity的实例,false表示不关闭;
     android:theme属性用于指定Activity显示主题,这里我们自定义主题SingleActivityStyle

[html] view plain copy

  1.         <style name="SingleActivityStyle" parent="horizontal_slide">  

  2.         <!-- 窗体背景颜色为透明 -->  

  3. <item name="android:windowBackground">@android:color/transparent</item>  

  4.         <!-- 窗体没有边框 -->  

  5.        <item name="android:windowFrame">@null</item>  

  6.        <!-- 窗体不包含标题栏 -->  

  7.        <item name="android:windowNoTitle">true</item>  

  8.        <!-- 窗体悬浮 -->  

  9.        <item name="android:windowIsFloating">true</item>  

  10.        <!-- 自定义TitleBar时去掉多余的阴影-->  

  11.        <item name="android:windowContentOverlay">@null</item>  

  12.        <!-- 不允许窗体背景变暗-->  

  13.        <item name="android:backgroundDimEnabled">false</item>  

  14.        <!-- 窗体切换无动画-->  

  15.        <item name="android:windowAnimationStyle">@null</item>  

  16.        <!-- 禁用窗口的预览动画-->  

  17.        <item name="android:windowDisablePreview">true</item>  

  18.        <item name="android:windowNoDisplay">false</item>  

  19.    </style>  

f) 测试结果

   监听锁屏广播,锁屏时将SportActivity置于前台(可见) 

   监听锁屏广播,锁屏时开启SinglePixelActivity(1像素)

3 .循环播放一段无声音频,"打造金刚不坏之身"
    对于三星C9、Note4和华为4X来说,结合前台Service和悬浮界面(1像素)的保活方式,在用户不主动清理或强杀的情况下,测试APP的保活效果还是非常不错的。但是,对于华为Mate8来说,效果还是差强人意,尤其是当使用一键清理内存时,测试APP基本无法幸存。然后,"咕咚"却奇妙的活了下来,一键清理怎么也清不掉,正当自己百思不得其"姐"时,一个"恶心"的界面出现在我面前。尼玛!看到下面的红框框没,"咕咚"居然在后台循环播放一个无声音乐,难怪生命力这么旺盛,但是耗电也是杠杠的。好吧,不纠结这么多,这里只是从学技术的角度出发而研究,毕竟用户对耗电量还是很敏感的,不到万不得已还是收敛点,不要这么"风骚",用户体验很重要,一不小心就"泻"了你。

a) PlayerMusicService.Java

[java] view plain copy

  1. /**循环播放一段无声音频,以提升进程优先级 

  2.  * 

  3.  * Created by jianddongguo on 2017/7/11. 

  4.  * http://blog.csdn.net/andrexpert 

  5.  */  

  6. public class PlayerMusicService extends Service {  

  7.     private final static String TAG = "PlayerMusicService";  

  8.     private MediaPlayer mMediaPlayer;  

  9.   

  10.   

  11.     @Nullable  

  12.     @Override  

  13.     public IBinder onBind(Intent intent) {  

  14.         return null;  

  15.     }  

  16.   

  17.   

  18.     @Override  

  19.     public void onCreate() {  

  20.         super.onCreate();  

  21.         if(Contants.DEBUG)  

  22.             Log.d(TAG,TAG+"---->onCreate,启动服务");  

  23.         mMediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.silent);  

  24.         mMediaPlayer.setLooping(true);  

  25.     }  

  26.   

  27.   

  28.     @Override  

  29.     public int onStartCommand(Intent intent, int flags, int startId) {  

  30.         new Thread(new Runnable() {  

  31.             @Override  

  32.             public void run() {  

  33.                 startPlayMusic();  

  34.             }  

  35.         }).start();  

  36.         return START_STICKY;  

  37.     }  

  38.   

  39.   

  40.     private void startPlayMusic(){  

  41.         if(mMediaPlayer != null){  

  42.             if(Contants.DEBUG)  

  43.                 Log.d(TAG,"启动后台播放音乐");  

  44.             mMediaPlayer.start();  

  45.         }  

  46.     }  

  47.   

  48.   

  49.     private void stopPlayMusic(){  

  50.         if(mMediaPlayer != null){  

  51.             if(Contants.DEBUG)  

  52.                 Log.d(TAG,"关闭后台播放音乐");  

  53.             mMediaPlayer.stop();  

  54.         }  

  55.     }  

  56.   

  57.   

  58.     @Override  

  59.     public void onDestroy() {  

  60.         super.onDestroy();  

  61.         stopPlayMusic();  

  62.         if(Contants.DEBUG)  

  63.             Log.d(TAG,TAG+"---->onCreate,停止服务");  

  64.         // 重启  

  65.         Intent intent = new Intent(getApplicationContext(),PlayerMusicService.class);  

  66.         startService(intent);  

  67.     }  

  68. }  

b) AndroidManifest.xml

[html] view plain copy

  1. <service android:name=".service.PlayerMusicService"  

  2.           android:enabled="true"  

  3.           android:exported="true"  

  4.           android:process=":music_service"/>  

4. 测试结果
    这里在cmd窗口使用"ps | grep jiangdg"命令,如果进程在内存中存在,则打印进程信息;如果不存在,则没有信息。各机型测试情况如下:
    (1)华为Mate8(7.0):将测试APP置于后台,前台Service在黑屏状态下1分钟之内被干掉,"1像素"悬浮Activity在黑屏状态下测试2小时依然存活,效果还可以。但是,当用户一键清理最近应用时,会被杀死,当在后台开启Serive循环播放一段无声音频时,一键清理依然存活,在置于后台的黑屏模式下存活12小时以上;


    (2)三星C9(6.0):开启前台Service和1像素,KeepAppAlive在黑屏后台模式下存活9个小时以上,看样子原生系统还是温柔些;开启后台播放音频服务,用户一键清理最近应用成功保活;
    (3)华为4X(6.0):效果同C9;
    (4) 三星Note4(5.0):效果同C9;
注:Mate8循环播放一段无声音频,当用户点击一键清理最近应用时,KeepAppAlive不会被干掉,但是如果用户只选择清理KeepAppAlive时,也会被杀死,这与"咕咚"保活效果一致。

三星C9(6.0):运行Demo,后台黑屏保活效果

华为Mate8(7.0):运行Demo,黑屏和一键清理保活效果


Github项目地址:https://github.com/jiangdongguo/KeepingAppAlive


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消