89

应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)-IM开发/专项技术区 -...

 6 years ago
source link: http://www.52im.net/thread-1138-1-1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)-IM开发/专项技术区 - 即时通讯开发者社区!

想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议

应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)

微信扫一扫关注!

原作者:“裂缝中的阳光dg”,本文由即时通讯网重新修订并整理发布,感谢原作者的无私分享。

最新推荐:Android保活从入门到放弃:乖乖引导用户加白名单吧(附7大机型加白示例)》(此文发布于2020年06月13日)。

在Android 4.4及以后的系统中,应用能否常驻内存,一直以来都是相当头疼的事情,尤其移动端IM、消息推送这类应用,为了保证“全时在线”的概念,真是费尽了心思。虽然APP常驻内存对于用户来说比较”恶心”,但是在诸如IM和消息推送这类场景来说,APP的常驻内存却尤其重要。

APP常驻内存(保活防杀),旧事重提,距离上一次的研究亦有半年有余。最近,用户反馈说多进程守护方案(详见上篇《Android应用保活终极总结(一):Android6.0以下的双进程守护保活实践》)在华为Mate8(Andriod 7.0)保活效果不是很好,有时候还是不能及时收到消息。

于是,又带着怀疑的眼光,重新找回原来的代码进行测试,顺便分析了市场上主流运动类APP保活方法(微信、手Q就算了,富人家的孩子,不具代表性),同时也对系统对内存中APP的管理规则进行了进一步探索。

本文便是对最近一周的Android进程防杀、进程被杀复活的探索、学习、测试的内容总结,以备将来不时之需。因保活防杀和被杀复活涉及内容较多,我将它分成了两篇:即进程防杀篇(本文)和进程被杀复活篇(下篇),本篇将讨论如何实现进程防杀。

说起来比较绕口,总之本文要讨论的内容是如何防止Android应用被系统“杀掉”,下篇讨论的是“被杀掉”后如何让它复活。本文中的进程防杀方法最高适用至Android 7.0系统(版本再高也没有测试手机,无法验证哦)。

特别说明:本文中的Demo源码打包完整下载请至文末,直接从附件下载。

2、系列文章

本文是系列文章中的第2篇,本系列文章的大纲如下:

3、参考资料

Android进程保活详解:一篇文章解决你的所有疑问
微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)
Android P正式版即将到来:后台应用保活、消息推送的真正噩梦
全面盘点当前Android后台保活方案的真实运行效果(截止2019年前)
融云技术分享:融云安卓端IM产品的网络链路保活技术实践
2020年了,Android后台保活还有戏吗?看我如何优雅的实现!
史上最强Android保活思路:深入剖析腾讯TIM的进程永生技术
Android进程永生技术终极揭密:进程被杀底层原理、APP对抗被杀技巧》(* 推荐)
>> 更多同类文章 ……

4、Andriod应用保活核心思想归纳

对于Android6.0及其以上系统APP保活,我觉得主要还是通过这两个方面进行,即:

  • 降低omm_adj值,尽量保证进程不被系统杀死(本文要讨论的内容);
  • 进程被杀死后,通过其他方式将进程复活(将在下篇讨论)。

但需要明白的是,面对各手机厂商的深度定制和谷歌越来越严格的资源管理机制,这两种方式结合的保活不是永久的,只能是相对存在,不同的机型结果也是不一样的。

由于篇幅限制,本文主要剖析下通过何种方式降低oom_adj的值来降低APP被杀的几率,以及oom_adj值是怎样做到的?

接下来,我们需要了解下Android系统回收内存中的进程所依据的规则:
进程在内存中时活动主要有五种状态:即前台进程、可见进程、服务进程、后台进程、空进程,这几种状态的进程优先级由高到低,oom_adj值由低到高(在ProcessList定义)。然后Android系统会根据当前系统资源和进程oom_adj值来回收相应的进程,前台进程一般不会被回收,空进程最容易被回收,这种管理规则就是"传说中"的Low Memory Killer。

为了更直观的了解这套规则,我画了个表:

应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)_20170713111737824.png

注:优先级1表示最高级,普通进程的oom_adj>=0,系统进程oom_adj<0,系统会根据相应的内存阀值对符合某段oom_adj值的进程进行回收。另外,oom_adj值也会随着占用物理内存越大而增大,系统进程绝对不会被系统杀死。

接下来我将首先分析市面上的主流APP防杀方式,为我接下来的方案提供参考依据,请继续往下阅读。

5、市场主流运动类APP保活分析:咕咚(v 7.17.0)

5.1一键清理/滑动清理

  • a. 当"咕咚"处于停止状态,其进程被杀死,通知栏图标被清理,等待几分钟没有 自动重启,当重新进入“咕咚”时,会从欢迎界面重新进入;
  • b. 当"咕咚"处于运动进行状态,进程死亡,通知栏图标被清除,等待几分钟没有自动重启,但当重新进入“咕咚”时,其直接显示运动界面,而没有从欢迎界面进入,运动时间等状态与被清理时一样;
  • c. 当"咕咚"处于运动暂停状态,其进程正常存活,通知栏图标正常显示。如果是单独清理,进程死亡,通知栏图标被清除;但当重新进入“咕咚”时,其直接显示运动界面,而没有从欢迎界面进入,运动时间等状态与被清理时一样。

5.2黑屏/锁屏

  • a. 当"咕咚"处于停止状态,退到后台,锁屏进入黑屏状态,等待5分钟,进程死亡,通知栏被清除;
  • b. 当"咕咚"处于运动进行状态,退到后台,锁屏进入黑屏状态,然后再进入系统,“咕咚”跑步界面自动弹出。再次锁屏,等待20分钟,进程没有被杀死,"咕咚"跑步界面自动弹出,运动状态保持不变;
  • c. 当"咕咚"处于运动暂停状态,退到后台,锁屏进入黑屏状态,然后再进入系统,"咕咚"跑步界面自动弹出。再次锁屏。等待20分钟,进程没有被杀死,"咕咚"跑步界面自动弹出,运动状态保持不变。

前提:

  • "手机管家->锁屏清理应用"关闭;
  • "手机管家->自启管理"关闭;
  • 运动状态,禁用返回键,用户只能从Home键退到后台;
  • 运动界面文字闪烁或运动计时;

分析:

  • 1)当"咕咚"处于停止状态时,一键清理和黑屏状态会被杀死,说明在没有进入运动界面之前,其保活机制没有被启动(即没有使运动界面切换到后台等)
  • 2)当“咕咚”处于运动状态时,一键清理和黑屏状态没有被杀死(滑动清理除外),说明已经启动保活机制:
      - ①"咕咚"禁止了返回键,以保证运动Activity不被销毁;
      - ②不断更新通知栏计时,以保证APP始终在前台,防止被系统回收;
      - ③"咕咚"被清理后能够自动重启,通知被删除后自动弹出,说明可能有另外一个东西(进程或Service)监听器运动Service(或进程)存活状态,当Service被销毁时,立马将其拉起来;
      - ④“咕咚”被强制停止或清理杀死后,再次进入会直接显示运动界面且能够保持杀死之前的运动状态,说明其可能利用配置文件记录了相关状态;
      - ⑤锁屏/解锁后,"咕咚"运动界面会自动弹出,说明其利用了广播机制对锁屏广播进行监听,弹出Activity以保证进程始终在前台。

结论:
常驻通知栏、双进程守护、广播锁屏、自定义锁屏。

备注:
以上为华为Mate8(Android 7.0)测试结果。其他如三星C9(Android 6.0)保活较好,特别是当一键清理时,"咕咚会自动启动,估计是使用了进程守护策略,而三星使用的是原生系统,因此结果你懂得;360F4(Android 6.0)保活很差,不愧是流氓中的战斗机,以更流氓的方式干掉流氓APP。

6、市场主流运动类APP保活分析:乐动力(v7.3.2)

6.1一键清理 / 滑动清理

  • 三星C9(6.0):无论何种状态,"乐动力" 进程被杀死,等待几分钟,没有自动启动;
  • 360F4(6.0):无论何种状态,"乐动力" 进程被杀死,等待几分钟,没有自动启动;
  • 华为Mate8(7.0):无论何种状态,"乐动力" 进程被杀死,等待几分钟,没有自动启动。

6.2锁屏/黑屏

  • a. 当"乐动力"处于停止状态,退到后台,锁屏,等待5分钟,进程死亡,通知栏被清除;
  • b. 当"乐动力"处于运动暂停状态,退到后台,锁屏再开启,运动界面被切换到前台,并强制弹出自定义锁屏界面(覆盖在系统锁屏界面之上);再次锁屏,等待20分钟,应用进程存活;
  • c. 当"乐动力"处于运动进行状态,退到后台,锁屏再开启,运动界面被切换到前台,并强制弹出自定义锁屏界面(覆盖在系统锁屏界面之上);再次锁屏,等待20分钟,应用进程存活。

前提:

  • "手机管家->锁屏清理应用"关闭;
  • "手机管家->自启管理"关闭;
  • 运动状态,禁用返回键,用户只能从Home键退到后台;

分析:

  • 当"乐动力"处于停止状态时,黑屏状态下,其在短时间内被系统杀死,说明保活机制没有启用;
  • 但当处于运动暂停或进行状态时,"乐动力"在一段时间内没有被杀死,且当锁屏时,"乐动力"会自动将运动界面切换到前台,此外,还会强制弹出自定锁屏界面,这就说明"乐动力"的保活机制很可能是利用监听锁屏广播强制将相关界面切换到前台,以提高"乐动力"在黑屏状态下的存活率。

结论:
常驻通知栏、广播锁屏、自定义锁屏。

7、市场主流运动类APP保活分析:悦动圈(v3.1.2.9)

7.1一键清理 / 滑动清理

  • 三星C9(6.0):效果与乐动力一致;
  • 360F4(6.0):效果与乐动力一致;
  • 华为Mate8(7.0):效果与乐动力一致。

6.2锁屏/黑屏

  • a. 当"悦动圈"处于停止状态,退到后台,锁屏,等待3分钟,进程死亡,通知栏被清除;
  • b. 当"悦动圈"处于运动暂停状态时,自定义锁屏、切换界面到前台与咕咚、乐动力一样,效果一致;
  • c. 当"悦动圈"处于运动进行状态时,自定义锁屏、切换界面到前台与咕咚、乐动力一样,效果一致。

结论:
常驻通知栏、广播锁屏、自定义锁屏。

8、本文的APP进程防杀方案原理

经过上面的讨论分析,"咕咚"、"乐动力"等这类APP主要是通过监听锁屏、网络等系统广播,将进程置于前台以提高进程的级别,从而防止进程不那么轻易被系统干掉。另外,"咕咚"可能还使用了相关的进程被清理复活策略。当然,对于复活策略,我们下一篇文章再探讨,本文主要讨论以上APP是通过哪些方式降低进程omm_adj值,防止其被系统杀死的。

为了达到与"咕咚"等APP类似效果,我们模拟这么一种场景:当用户登录测试APP后,先不开启保活功能;当用户开始跑步时,开启保活功能,然后再在这基础上做黑屏运行、一键清理、强制停止等功能测试。也就是说,Android项目中SplashActivity、LoginActivity只是配合我们"演戏"的,真正启动APP保活逻辑的是在SportsActivity,它将上演"后宫争宠"戏码。

9、APP进程防杀方案第一阶:开启前台Service,“逼君上位”

将Service置为前台,目的时提高进程Service的oom_adj值,以降低其被系统回收的几率。该方案的原理是,通过使用 startForeground()方法将当前Service置于前台来提高Service的优先级。需要注意的是,对API大于18而言 startForeground()方法需要弹出一个可见通知,如果你觉得不爽,可以开启另一个Service将通知栏移除,其oom_adj值还是没变的。实现代码如下。

DaemonService.java:

/**前台Service,使用startForeground
* 这个Service尽量要轻,不要占用过多的系统资源,否则
* 系统在资源紧张时,照样会将其杀死
*
* Created by jianddongguo on 2017/7/7.
*/ 
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); 
}

讲解一下,这里还用到了两个技巧:

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

CancelNoticeService.java:

/** 移除前台Service通知栏标志,这个Service选择性使用
*
* Created by jianddongguo on 2017/7/7.
*/ 
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(); 
}

AndroidManifest.xml:

<service android:name=".service.DaemonService" 
android:enabled="true" 
android:exported="true" 
android:process=":daemon_service"/> 
<service android:name=".service.CancelNoticeService" 
android:enabled="true" 
android:exported="true" 
android:process=":service"/>

讲解一下:
从所周知,一个Service没有自己独立的进程,它一般是作为一个线程运行于它所在的应用进程中,且应用进程名称与包名一致。如果希望指定的组件和应用运行在指定的进程中,就需要通过android:process属性来为其创建一个进程,因此android:process=":daemon_service"就是让DaemonService运行在名为“com.jiangdg.keepappalive:daemon_service”进程中;android:enabled属性的作用是Android系统是否实例化应用程序中的组件;android:exported属性的作用是当前组件(Service)是否可以被包含本身以外的应用中的组件启动。

测试结果:
接下来,我们观察下KeepAppAlive进程的oom_adj值变化。

首先,adb查看KeepAppAlive进程的进程号:

E:\Android\StudioProject\KeepAppAlive>adb shell
shell@trltechn:/ $ su
root@trltechn:/ # ps | grep jiangdg
应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)_1.png



其次,观察KeepAppAlive进程在不同状态下的oom_adj值:

root@trltechn:/ # cat /proc/15689/oom_adj
root@trltechn:/ # cat /proc/16033/oom_adj
应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)_2.png

注意:如果执行su命令,提示"/system/bin/sh: su: not found",说明手机设备没有被root。ps命令用于显示静态进程状态,top命令可以对进程进行实时监控,每次启动KeepAppAlive进程号都不一样。

9、APP进程防杀方案第二阶:监听锁屏广播,“制造‘1像素’惨案”

先看测试Demo的源码。

ScreenManager.java:

/**1像素管理类
*
* Created by jianddongguo on 2017/7/8.
*/ 
public class ScreenManager { 
private static final String TAG = "ScreenManager"
private Context mContext; 
private static ScreenManager mSreenManager; 
// 使用弱引用,防止内存泄漏 
private WeakReference<Activity> 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(); 
}

讲解一下:
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会回收它,从而避免了内存泄漏。

SinglePixelActivity.java:

/**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(); 
}

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

SportsActivity.java:

/** 运动界面,启动监听锁屏广播,判断是否开关1像素界面
*
* Created by jianddongguo on 2017/7/7.
*/ 
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); 
// 代码省略... 
}

AndroidManifest.xml:

<activity android:name=".SportsActivity" 
android:launchMode="singleTask"/> 
<activity android:name=".SinglePixelActivity" 
android:configChanges="keyboardHidden|orientation|screenSize|navigation|keyboard" 
android:excludeFromRecents="true" 
android:finishOnTaskLaunch="false" 
android:launchMode="singleInstance" 
android:theme="@style/SingleActivityStyle"/>

讲解一下:

  • 1)android:launchMode属性:用于指定activity的启动模式,总共分为四种,即:
       - standar模式,每次启动activity都会创建其实例,并加入到任务栈的栈顶;
       - singleTop模式,每次启动activity如果栈顶时该activity则无需创建,其余情况都要创建该activity的实例;
       - singleTask模式,如果被启动的activity的实例存在栈中,则不需要创建,只需要把此activity加入到栈顶,并把该activity以上的activity实例全部pop;
       - singleInstance模式:将创建的activity实例放入单独的栈中,该栈只能存储这个实例,且是作为共享实例存在。
  • 2)android:configChanges属性:用于捕获手机状态的改变,即当手机状态(如切换横竖屏、屏幕大小)改变时会保存当前活动状态重启Activity,由于SinglePixelActivity肩负着保活的特殊使命,这里使用android:configChanges属性:防止Activity重启,它只是调用了onConfigurationChanged(Configuration newConfig)来通知手机状态的改变;
  • 3)android:excludeFromRecents属性:用于控制SinglePixelActivity不在最近任务列表中显示;
  • 4)android:finishOnTaskLaunch属性:用于标记当用户再起启动应用(TASK)时是否关闭已经存在的Activity的实例,false表示不关闭;
  • 5)android:theme属性:用于指定Activity显示主题,这里我们自定义主题SingleActivityStyle。
<style name="SingleActivityStyle" parent="horizontal_slide"
<!-- 窗体背景颜色为透明 --> 
<item name="android:windowBackground">@android:color/transparent</item
<!-- 窗体没有边框 --> 
<item name="android:windowFrame">@null</item
<!-- 窗体不包含标题栏 --> 
<item name="android:windowNoTitle">true</item
<!-- 窗体悬浮 --> 
<item name="android:windowIsFloating">true</item
<!-- 自定义TitleBar时去掉多余的阴影--> 
<item name="android:windowContentOverlay">@null</item
<!-- 不允许窗体背景变暗--> 
<item name="android:backgroundDimEnabled">false</item
<!-- 窗体切换无动画--> 
<item name="android:windowAnimationStyle">@null</item
<!-- 禁用窗口的预览动画--> 
<item name="android:windowDisablePreview">true</item
<item name="android:windowNoDisplay">false</item
</style

测试结果:

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

应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)_a.png



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

应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)_b.png

9、APP进程防杀方案第三阶:循环播放一段无声音频,"打造金刚不坏之身"

对于三星C9、Note4和华为4X来说,结合前台Service和悬浮界面(1像素)的保活方式,在用户不主动清理或强杀的情况下,测试APP的保活效果还是非常不错的。

但是,对于华为Mate8来说,效果还是差强人意,尤其是当使用一键清理内存时,测试APP基本无法幸存。然后,"咕咚"却奇妙的活了下来,一键清理怎么也清不掉,正当自己百思不得其"姐"时,一个"恶心"的界面出现在我面前。尼玛!看到下面的红框框没,"咕咚"居然在后台循环播放一个无声音乐,难怪生命力这么旺盛,但是耗电也是杠杠的。

好吧,不纠结这么多,这里只是从学技术的角度出发而研究,毕竟用户对耗电量还是很敏感的,不到万不得已还是收敛点,不要这么"风骚",用户体验很重要,一不小心就"泻"了你。

看咕咚这无声音乐播放保活方式,够不要脸吧:

应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)_c.png



PlayerMusicService.java:

/**循环播放一段无声音频,以提升进程优先级
*
* Created by jianddongguo on 2017/7/11.
*/ 
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); 
}

AndroidManifest.xml:

<service android:name=".service.PlayerMusicService" 
android:enabled="true" 
android:exported="true" 
android:process=":music_service"/>

测试结果:
这里在cmd窗口使用"ps | grep jiangdg"命令,如果进程在内存中存在,则打印进程信息;如果不存在,则没有信息。

各机型测试情况如下:

  • 1)华为Mate8(Android 7.0):将测试APP置于后台,前台Service在黑屏状态下1分钟之内被干掉,"1像素"悬浮Activity在黑屏状态下测试2小时依然存活,效果还可以。但是,当用户一键清理最近应用时,会被杀死,当在后台开启Serive循环播放一段无声音频时,一键清理依然存活,在置于后台的黑屏模式下存活12小时以上;
  • 2)三星C9(Android 6.0):开启前台Service和1像素,KeepAppAlive在黑屏后台模式下存活9个小时以上,看样子原生系统还是温柔些;开启后台播放音频服务,用户一键清理最近应用成功保活;
  • 3)华为4X(Android 6.0):效果同C9;
  • 4)三星Note4(Android 5.0):效果同C9。

注:Mate8循环播放一段无声音频,当用户点击一键清理最近应用时,KeepAppAlive不会被干掉,但是如果用户只选择清理KeepAppAlive时,也会被杀死,这与"咕咚"保活效果一致。

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

应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)_d.gif



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

应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)_e.gif

10、源码下载

zip.gifKeepingAppAlive-master(52im.net).zip

(1.29 MB , 下载次数: 1647 , 售价: 3 金币)

11、下篇预告

下篇将介绍Android6.0及以上版本的APP进程被杀后的复活方法,敬请期待!

(原文链接:点此进入


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK