《推送开发全面盘点当前Android后台保活方案的真实运行效果》

 
 
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了...找站长给点建议
全面盘点当前Android后台保活方案的真实运行效果(截止2019年前)
 
 
阅读(15903) | 评论(8)收藏2 淘帖1 2
 
JackJiang Lv.9 精华之王 白金版主 终身成就 11 个月前 |只看大图

本文原作者“minminaya”,作者网站:minminaya.cn,为了提升文章品质,即时通讯网对内容作了幅修订和改动,感谢原作者。


1、引言


对于IM应用和消息推送服务的开发者来说,在Android机型上的后台保活是个相当头疼的问题。

老板一句:“为什么微信、QQ能收到消息,而你写的APP却不行?”,直接让人崩溃,话说老板你这APP要是整成微信、APP那么牛,直接进手机厂商白名单,还要程序员在这瞎忙活?

好了,抱怨归抱怨,活还得干,不然靠谁养活广大苦逼的程序员?

回到正题,Android程序员都知道,随着Android系统的不断完善和升级,Andriod应用的后台保活是一次比一次难(详见《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦),但日子还得过,只能一次次绞尽脑汁想出各种黑科技。但不幸的是,因为Andriod系统的不断升级,各种黑科技也只能适应某些版本的Android系统,无法一劳永逸解决问题。

<ignore_js_op>全面盘点当前Android后台保活方案的真实运行效果(截止2019年前)_333.jpeg
▲ Android各版本都是用“甜品”命名的

正因为Android系统版本的差异,也导致了各种保活黑科技的运行效果大相径庭,所以本文正好借此机会,盘点一下当前主流(截止2019年前)的保活黑科技在市面上各版本Android手机上的运行效果,希望能给大家提供一些客观的参考

2、先总结一下,Android端APP为何要搞保活黑科技?


* 本节内容摘录自即时通讯网整理的《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦》一文

其实Android端APP搞保活的目的倒不是为了干什么见不得人的坏事(但不排除动机不纯的开发者),主要是像IM即时通讯应用和资讯类应用等需要搞后台消息推送、运动类应用需要在后台实时监测用户的运动数据等,因为现在越来越多的手机厂商为了省电策略考虑,基本上如果你的应用没有被加入白名单,一旦处于后台就会被系统限制甚至干掉,但使用APP的用户才不听你这些解释——反正“我”就要你的APP能如期正常运行,开发者也是不得已而为之。

以消息推送为例,当APP处于后台或关闭时,消息推送对于某些应用来说非常有用,比如:

  • 1)IM即时通讯聊天应用:聊天消息通知、音视频聊天呼叫等,典型代表有:微信、QQ、易信、米聊、钉钉、Whatsup、Line;
  • 2)新闻资讯应用:最新资讯通知等,典型代表有:网易新闻客户端、腾讯新闻客户端;
  • 3)SNS社交应用:转发/关注/赞等通知,典型代表有:微博、知乎;
  • 4)邮箱客户端:新邮件通知等,典型代表有:QQ邮箱客户端、Foxmail客户端、网易邮箱大师;
  • 5)金融支付应用:收款通知、转账通知等,典型代表有:支付宝、各大银行的手机银行等;
  •   .... ....

在上述的各种应用中,尤其对于用户接触最多、最平常的IM聊天应用或新闻资讯来说,保活和消息推送简直事关APP的“生死”,消息推送这种能力已经被越来越多的APP作为基础能力之一,因为移动互联网时代下,用户的“全时在线”能力非常诱人和强大,能随时随地即时地将各种重要信息推送给用户,无疑是非常有意义的。

题外话:实际上,对于后台消息推送能力,Android原版系统早就内置了系统级推送服务(跟iOS上的APNs服务是一个东西),它就是GCM服务(现在升级为FCM了),但众所周之的原因,谷哥的服务在国内都是用不了的(你懂的)——无奈啊!

有关GCM的介绍详见:《移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)》、《为何微信、QQ这样的IM工具不使用GCM服务推送消息?》、《求教android消息推送:GCM、XMPP、MQTT三种方案的优劣)。

<ignore_js_op>全面盘点当前Android后台保活方案的真实运行效果(截止2019年前)_11.jpg
▲ 如果Android能有iOS的APNs这么强势的方案存在,那该是多美的事 ...

3、相关文章



4、常见的Android端保活黑科技方案盘点


主要黑科技方案有:

  • 1)监听广播:监听全局的静态广播,比如时间更新的广播、开机广播、解锁屏、网络状态、解锁加锁亮屏暗屏(3.1版本),高版本需要应用开机后运行一次才能监听这些系统广播,目前此方案失效。可以更换思路,做APP启动后的保活(监听广播启动保活的前台服务);
  • 2)定时器、JobScheduler:假如应用被系统杀死,那么定时器则失效,此方案失效。JobService在5.0,5.1,6.0作用很大,7.0时候有一定影响(可以在电源管理中给APP授权);
  • 3)双进程(NDK方式Fork子进程)、双Service守护:高版本已失效,5.0起系统回收策略改成进程组。双Service方案也改成了应用被杀,任何后台Service无法正常状态运行;
  • 4)提高Service优先级:只能一定程度上缓解Service被立马回收。

针对上述方案,具体的实现思路,通常是这样的:

  • 1)进程拉活:AIDL方式单进程、双进程方式保活Service(最极端的例子就是推送厂商的互相唤醒复活:极光、友盟、以及各大厂商的推送,同派系APP广播互相唤醒:比如今日头条系、阿里系);
  • 2)降低oom_adj的值:常驻通知栏(可通过启动另外一个服务关闭Notification,不对oom_adj值有影响)、使用”1像素“的Activity覆盖在getWindow()的view上(据传某不可言说的IM大厂用过这个方案,虽然他们从未正面承认过)、循环播放无声音频(黑科技,7.0下杀不掉);
  • 3)监听锁屏广播:使Activity始终保持前台;
  • 4)使用自定义锁屏界面:覆盖了系统锁屏界面;
  • 5)创建子进程:通过android:process属性来为Service创建一个进程;
  • 6)白名单:跳转到系统白名单界面让用户自己添加app进入白名单。

5、汇总一下,主要的保活黑科技方案的具体代码实现


5.1黑科技代码实现1:双进程拉活方案的代码实现


使用AIDL绑定方式新建2个Service优先级(防止服务同时被系统杀死)不一样的守护进程互相拉起对方,并在每一个守护进程的ServiceConnection的绑定回调里判断保活Service是否需要重新拉起和对守护线程进行重新绑定。

关于本方案的具体实现,即时通讯网的以下文章有更详细的介绍,您也可以仔细研读:


本方案的具体代码实现,主要由以下4步构成。

1)新建一个AIDL文件:
1
2
3
KeepAliveConnection
interface KeepAliveConnection  {
}

2)新建一个服务类StepService,onBind()方法返回new KeepAliveConnection.Stub()对象,并在ServiceConnection的绑定回调中对守护进程服务类GuardService的启动和绑定:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 主进程 双进程通讯
*
* @author LiGuangMin
* @time Created by 2018/8/17 11:26
*/
public class StepService extends Service {
 
   private final static String TAG = StepService.class.getSimpleName();
   private ServiceConnection mServiceConnection = new ServiceConnection() {
       @Override
       public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
           Logger.d(TAG, "StepService:建立链接");
           boolean isServiceRunning = ServiceAliveUtils.isServiceAlice();
           if (!isServiceRunning) {
               Intent i = new Intent(StepService.this, DownloadService.class);
               startService(i);
           }
       }
 
       @Override
       public void onServiceDisconnected(ComponentName componentName) {
           // 断开链接
           startService(new Intent(StepService.this, GuardService.class));
           // 重新绑定
           bindService(new Intent(StepService.this, GuardService.class), mServiceConnection, Context.BIND_IMPORTANT);
       }
   };
 
   @Nullable
   @Override
   public IBinder onBind(Intent intent) {
       return new KeepAliveConnection.Stub() {
       };
   }
 
   @Override
   public int onStartCommand(Intent intent, int flags, int startId) {
       startForeground(1, new Notification());
       // 绑定建立链接
       bindService(new Intent(this, GuardService.class), mServiceConnection, Context.BIND_IMPORTANT);
       return START_STICKY;
   }
}

3)对守护进程GuardService进行和2一样的处理:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 守护进程 双进程通讯
*
* @author LiGuangMin
* @time Created by 2018/8/17 11:27
*/
public class GuardService extends Service {
   private final static String TAG = GuardService.class.getSimpleName();
   private ServiceConnection mServiceConnection = new ServiceConnection() {
       @Override
       public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
           Logger.d(TAG, "GuardService:建立链接");
           boolean isServiceRunning = ServiceAliveUtils.isServiceAlice();
           if (!isServiceRunning) {
               Intent i = new Intent(GuardService.this, DownloadService.class);
               startService(i);
           }
       }
 
       @Override
       public void onServiceDisconnected(ComponentName componentName) {
           // 断开链接
           startService(new Intent(GuardService.this, StepService.class));
           // 重新绑定
           bindService(new Intent(GuardService.this, StepService.class), mServiceConnection, Context.BIND_IMPORTANT);
       }
   };
 
   @Nullable
   @Override
   public IBinder onBind(Intent intent) {
       return new KeepAliveConnection.Stub() {
       };
   }
 
   @Override
   public int onStartCommand(Intent intent, int flags, int startId) {
       startForeground(1, new Notification());
       // 绑定建立链接
       bindService(new Intent(this, StepService.class), mServiceConnection, Context.BIND_IMPORTANT);
       return START_STICKY;
   }
}

4)在Activity中在启动需要保活的DownloadService服务后然后启动保活的双进程:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class MainActivity extends AppCompatActivity {
   private TextView mShowTimeTv;
   private DownloadService.DownloadBinder mDownloadBinder;
   private ServiceConnection mServiceConnection = new ServiceConnection() {
       @Override
       public void onServiceConnected(ComponentName name, IBinder service) {
           mDownloadBinder = (DownloadService.DownloadBinder) service;
           mDownloadBinder.setOnTimeChangeListener(new DownloadService.OnTimeChangeListener() {
               @Override
               public void showTime(final String time) {
                   runOnUiThread(new Runnable() {
                       @Override
                       public void run() {
                           mShowTimeTv.setText(time);
                       }
                   });
               }
           });
       }
 
       @Override
       public void onServiceDisconnected(ComponentName name) {
       }
   };
 
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
 
       Intent intent = new Intent(this, DownloadService.class);
       startService(intent);
       bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
       //双守护线程,优先级不一样
       startAllServices();
   }
 
   @Override
   public void onContentChanged() {
       super.onContentChanged();
       mShowTimeTv = findViewById(R.id.tv_show_time);
   }
 
   @Override
   protected void onDestroy() {
       super.onDestroy();
       unbindService(mServiceConnection);
   }
 
   /**
    * 开启所有守护Service
    */
   private void startAllServices() {
       startService(new Intent(this, StepService.class));
       startService(new Intent(this, GuardService.class));
   }
}

5.2黑科技代码实现2:监听到锁屏广播后使用“1”像素Activity提升优先级


“1”像素保活这么流氓的手段,传说是某IM大厂用过的方案 ...

本方法的具体代码实现主要由以下6步组成。

1)该Activity的View只要设置为1像素然后设置在Window对象上即可。在Activity的onDestroy周期中进行保活服务的存活判断从而唤醒服务。”1像素”Activity如下:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SinglePixelActivity extends AppCompatActivity {
   private static final String TAG = SinglePixelActivity.class.getSimpleName();
 
   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       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);
       ScreenManager.getInstance(this).setSingleActivity(this);
   }
 
   @Override
   protected void onDestroy() {
       if (!SystemUtils.isAppAlive(this, Constant.PACKAGE_NAME)) {
           Intent intentAlive = new Intent(this, DownloadService.class);
           startService(intentAlive);
       }
       super.onDestroy();
   }
}

2)对广播进行监听,封装为一个ScreenReceiverUtil类,进行锁屏解锁的广播动态注册监听:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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);
   }
 
   /**
    * 监听sreen状态对外回调接口
    */
   public interface SreenStateListener {
       void onSreenOn();
 
       void onSreenOff();
 
       void onUserPresent();
   }
 
   public class SreenBroadcastReceiver extends BroadcastReceiver {
       @Override
       public void onReceive(Context context, Intent intent) {
           String action = intent.getAction();
           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();
           }
       }
   }
}

3)对1像素Activity进行防止内存泄露的处理,新建一个ScreenManager类:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class ScreenManager {
   private static final String TAG = ScreenManager.class.getSimpleName();
   private static ScreenManager sInstance;
   private Context mContext;
   private WeakReference<Activity> mActivity;
 
   private ScreenManager(Context mContext) {
       this.mContext = mContext;
   }
 
   public static ScreenManager getInstance(Context context) {
       if (sInstance == null) {
           sInstance = new ScreenManager(context);
       }
       return sInstance;
   }
 
   /** 获得SinglePixelActivity的引用
    * @param activity
    */
   public void setSingleActivity(Activity activity) {
       mActivity = new WeakReference<>(activity);
   }
 
   /**
    * 启动SinglePixelActivity
    */
   public void startActivity() {
       Intent intent = new Intent(mContext, SinglePixelActivity.class);
       intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
       mContext.startActivity(intent);
   }
 
   /**
    * 结束SinglePixelActivity
    */
   public void finishActivity() {
       if (mActivity != null) {
           Activity activity = mActivity.get();
           if (activity != null) {
               activity.finish();
           }
       }
   }
}

4)对1像素的Style进行特殊处理,在style文件中新建一个SingleActivityStyle:
01
02
03
04
05
06
07
08
09
10
<style name="SingleActivityStyle" parent="android:Theme.Holo.Light.NoActionBar">
       <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>
       <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>

5)让SinglePixelActivity使用singleInstance启动模式,在manifest文件中:
1
2
3
4
5
6
7
<activity
           android:name=".activity.SinglePixelActivity"
           android:configChanges="keyboardHidden|orientation|screenSize|navigation|keyboard"
           android:excludeFromRecents="true"
           android:finishOnTaskLaunch="false"
           android:launchMode="singleInstance"
           android:theme="@style/SingleActivityStyle" />

6)在保活服务类DownloadService中对监听的广播进行注册和对SinglePixelActivity进行控制:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
public class DownloadService extends Service {
   public static final int NOTICE_ID = 100;
   private static final String TAG = DownloadService.class.getSimpleName();
   private DownloadBinder mDownloadBinder;
   private NotificationCompat.Builder mBuilderProgress;
   private NotificationManager mNotificationManager;
 
   private ScreenReceiverUtil mScreenListener;
   private ScreenManager mScreenManager;
   private Timer mRunTimer;
 
   private int mTimeSec;
   private int mTimeMin;
   private int mTimeHour;
 
   private ScreenReceiverUtil.SreenStateListener mScreenListenerer = new ScreenReceiverUtil.SreenStateListener() {
       @Override
       public void onSreenOn() {
           mScreenManager.finishActivity();
           Logger.d(TAG, "关闭了1像素Activity");
       }
 
       @Override
       public void onSreenOff() {
           mScreenManager.startActivity();
           Logger.d(TAG, "打开了1像素Activity");
       }
 
       @Override
       public void onUserPresent() {
       }
   };
   private OnTimeChangeListener mOnTimeChangeListener;
 
   @Override
   public void onCreate() {
       super.onCreate();
 
//        注册锁屏广播监听器
       mScreenListener = new ScreenReceiverUtil(this);
       mScreenManager = ScreenManager.getInstance(this);
       mScreenListener.setScreenReceiverListener(mScreenListenerer);
 
       mDownloadBinder = new DownloadBinder();
       mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
   }
 
   @Override
   public int onStartCommand(Intent intent, int flags, int startId) {
       Logger.d(TAG, "onStartCommand");
       startRunTimer();
       return START_STICKY;
   }
 
   @Nullable
   @Override
   public IBinder onBind(Intent intent) {
 
       return mDownloadBinder;
   }
 
   @Override
   public boolean onUnbind(Intent intent) {
       Logger.d(TAG, "onUnbind");
       return super.onUnbind(intent);
   }
 
   @Override
   public void onDestroy() {
       super.onDestroy();
       NotificationManager mManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
       if (mManager == null) {
           return;
       }
       mManager.cancel(NOTICE_ID);
       stopRunTimer();
//        mScreenListener.stopScreenReceiverListener();
   }
 
   private void startRunTimer() {
       TimerTask mTask = new TimerTask() {
           @Override
           public void run() {
               mTimeSec++;
               if (mTimeSec == 60) {
                   mTimeSec = 0;
                   mTimeMin++;
               }
               if (mTimeMin == 60) {
                   mTimeMin = 0;
                   mTimeHour++;
               }
               if (mTimeHour == 24) {
                   mTimeSec = 0;
                   mTimeMin = 0;
                   mTimeHour = 0;
               }
               String time = "时间为:" + mTimeHour + " : " + mTimeMin + " : " + mTimeSec;
               if (mOnTimeChangeListener != null) {
                   mOnTimeChangeListener.showTime(time);
               }
               Logger.d(TAG, time);
           }
       };
       mRunTimer = new Timer();
       // 每隔1s更新一下时间
       mRunTimer.schedule(mTask, 1000, 1000);
   }
 
   private void stopRunTimer() {
       if (mRunTimer != null) {
           mRunTimer.cancel();
           mRunTimer = null;
       }
       mTimeSec = 0;
       mTimeMin = 0;
       mTimeHour = 0;
       Logger.d(TAG, "时间为:" + mTimeHour + " : " + mTimeMin + " : " + mTimeSec);
   }
 
   public interface OnTimeChangeListener {
       void showTime(String time);
   }
 
   public class DownloadBinder extends Binder {
       public void setOnTimeChangeListener(OnTimeChangeListener onTimeChangeListener) {
           mOnTimeChangeListener = onTimeChangeListener;
       }
   }
}

6.3黑科技代码实现3:在后台播放音乐


后台播放音乐这种保活方法,亲身经历过:

记得当时用的是某运动记步APP,它为了保活就是这么干的。之所以被我发现,是因为在我的Android手机上,每次打开这个APP居然总能莫名其妙听到若有若无的环境噪音样的声音,尤其安静的场所下更明显。我个人估计这个APP里用的保活音频文件,很可能就是程序员在简陋的条件下随手自已录制的,虽然也是不得以为之,但做法确实是有点粗糙。


好了,回到正题,本方案的具体代码实现主要是以下3步。

1)准备一段无声的音频,新建一个播放音乐的Service类,将播放模式改为无限循环播放。在其onDestroy方法中对自己重新启动:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class PlayerMusicService extends Service {
   private final static String TAG = PlayerMusicService.class.getSimpleName();
   private MediaPlayer mMediaPlayer;
 
   @Nullable
   @Override
   public IBinder onBind(Intent intent) {
       return null;
   }
 
   @Override
   public void onCreate() {
       super.onCreate();
       Logger.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) {
           Logger.d(TAG, "启动后台播放音乐");
           mMediaPlayer.start();
       }
   }
 
   private void stopPlayMusic() {
       if (mMediaPlayer != null) {
           Logger.d(TAG, "关闭后台播放音乐");
           mMediaPlayer.stop();
       }
   }
 
   @Override
   public void onDestroy() {
       super.onDestroy();
       stopPlayMusic();
       Logger.d(TAG, TAG + "---->onCreate,停止服务");
       // 重启自己
       Intent intent = new Intent(getApplicationContext(), PlayerMusicService.class);
       startService(intent);
   }
}

2)在保活的DownloadServie服务类的onCreate方法中对PlayerMusicService进行启动:
1
2
Intent intent = new Intent(this, PlayerMusicService.class);
startService(intent);

3)在Manifest文件中进行注册:
1
2
3
4
5
<service
           android:name=".service.PlayerMusicService"
           android:enabled="true"
           android:exported="true"
           android:process=":music_service" />

6.4黑科技代码实现4:使用JobScheduler唤醒Service


本方案代码实现由以下3步组成。

1)新建一个继承自JobService的ScheduleService类,在其onStartJob回调中对DownloadService进行存活的判断来重启:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class ScheduleService extends JobService {
   private static final String TAG = ScheduleService.class.getSimpleName();
 
   @Override
   public boolean onStartJob(JobParameters params) {
 
       boolean isServiceRunning = ServiceAliveUtils.isServiceAlice();
       if (!isServiceRunning) {
           Intent i = new Intent(this, DownloadService.class);
           startService(i);
           Logger.d(TAG, "ScheduleService启动了DownloadService");
       }
       jobFinished(params, false);
       return false;
   }
 
   @Override
   public boolean onStopJob(JobParameters params) {
       return false;
   }
}

2)在DownloadService服务类中进行JobScheduler的注册和使用:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
/**
    * 使用JobScheduler进行保活
    */
   private void useJobServiceForKeepAlive() {
       JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
       if (jobScheduler == null) {
           return;
       }
       jobScheduler.cancelAll();
       JobInfo.Builder builder =
           new JobInfo.Builder(1024, new ComponentName(getPackageName(), ScheduleService.class.getName()));
       //周期设置为了2s
       builder.setPeriodic(1000 * 2);
       builder.setPersisted(true);
       builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
       int schedule = jobScheduler.schedule(builder.build());
       if (schedule <= 0) {
           Logger.w(TAG, "schedule error!");
       }
   }

3)在manifest文件中进行权限设置:
1
2
3
4
5
<service
           android:name=".service.ScheduleService"
           android:enabled="true"
           android:exported="true"
    android:permission="android.permission.BIND_JOB_SERVICE" />

7、总结一下,以上方案在当前主流手机上的运行效果


【1】双进程守护方案(基于onStartCommand() return START_STICKY):

  • 1)原生5.0、5.1:原生任务栏滑动清理app,Service会被杀掉,然后被拉起,接着一直存活;
  • 2)金立F100(5.1):一键清理直接杀掉整个app,包括双守护进程。不手动清理情况下,经测试能锁屏存活至少40分钟;
  • 3)华为畅享5x(6.0):一键清理直接杀掉整个app,包括双守护进程。不手动清理下,锁屏只存活10s。结论:双进程守护方案失效;
  • 4)美图m8s(7.1.1):一键清理直接杀掉整个app,包括双守护进程。不清理情况下,锁屏会有被杀过程(9分钟左右被杀),之后重新复活,之后不断被干掉然后又重新复活。结论:双守护进程可在后台不断拉起Service;
  • 5)原生7.0:任务栏清除APP后,Service存活。使用此方案后Service照样存活;
  • 6)LG V30+(7.1.2):不加双进程守护的时候,一键清理无法杀掉服务。加了此方案之后也不能杀掉服务,锁屏存活(测试观察大于50分钟);
  • 7)小米8(8.1):一键清理直接干掉app并且包括双守护进程。不清理情况下,不加守护进程方案与加守护进程方案Service会一直存活,12分钟左右closed。结论:此方案没有起作用。

▲ 结论:除了华为此方案无效以及未更改底层的厂商不起作用外(START_STICKY字段就可以保持Service不被杀)。此方案可以与其他方案混合使用

【2】监听锁屏广播打开1像素Activity(基于onStartCommand() return START_STICKY):

  • 1)原生5.0、5.1:锁屏后3s服务被干掉然后重启(START_STICKY字段起作用);
  • 2)华为畅享5x(6.0):锁屏只存活4s。结论:方案失效;
  • 3)美图m8s(7.1.1):同原生5.0;
  • 4)原生7.0:同美图m8s;
  • 5)LG V30+(7.1.2):锁屏后情况跟不加情况一致,服务一致保持运行,结论:此方案不起作用;
  • 6)小米8(8.1):关屏过2s之后app全部被干掉。结论:此方案没有起作用。

▲ 结论:此方案无效果

【3】故意在后台播放无声的音乐(基于onStartCommand() return START_STICKY):

  • 1)原生5.0、5.1:锁屏后3s服务被干掉然后重启(START_STICKY字段起作用);
  • 2)华为畅享5x(6.0):一键清理后服务依然存活,需要单独清理才可杀掉服务,锁屏8分钟后依然存活。结论:此方案适用;
  • 3)美图m8s(7.1.1):同5.0;
  • 4)原生7.0:任务管理器中关闭APP后服务被干掉,大概过3s会重新复活(同仅START_STICKY字段模式)。结论:看不出此方案有没有其作用;
  • 5)LG V30+(7.1.2):使用此方案前后效果一致。结论:此方案不起作用;
  • 6)小米8(8.1):一键清理可以杀掉服务。锁屏后保活超过20分钟。

▲ 结论:成功对华为手机保活。小米8下也成功突破20分钟

【4】使用JobScheduler唤醒Service(基于onStartCommand() return START_STICKY):

  • 1)原生5.0、5.1:任务管理器中干掉APP,服务会在周期时间后重新启动。结论:此方案起作用;
  • 2)华为畅享5x(6.0):一键清理直接杀掉APP,过12s左右会自动重启服务,JobScheduler起作用;
  • 3)美图m8s(7.1.1):一键清理直接杀掉APP,无法自动重启;
  • 4)原生7.0:同美图m8s(7.1.1);
  • 5)小米8(8.1):同美图m8s(7.1.1)。

▲ 结论:只对5.0,5.1、6.0起作用

【5】混合使用的效果,并且在通知栏弹出通知:

  • 1)原生5.0、5.1:任务管理器中干掉APP,服务会在周期时间后重新启动。锁屏超过11分钟存活;
  • 2)华为畅享5x(6.0):一键清理后服务依然存活,需要单独清理才可杀掉服务。结论:方案适用;
  • 3)美图m8s(7.1.1):一键清理APP会被杀掉。正常情况下锁屏后服务依然存活;
  • 4)原生7.0:任务管理器中关闭APP后服务被干掉,过2s会重新复活;
  • 5)小米8(8.1):一键清理可以杀掉服务,锁屏下后台保活时间超过38分钟;
  • 6)荣耀10(8.0):一键清理杀掉服务,锁屏下后台保活时间超过23分钟。

▲ 结论:高版本情况下可以使用弹出通知栏、双进程、无声音乐提高后台服务的保活概率

8、补充:ServiceAliveUtils 类代码如下


01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class ServiceAliveUtils {
 
   public static boolean isServiceAlice() {
       boolean isServiceRunning = false;
       ActivityManager manager =
           (ActivityManager) MyApplication.getMyApplication().getSystemService(Context.ACTIVITY_SERVICE);
       if (manager == null) {
           return true;
       }
       for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
           if ("demo.lgm.com.keepalivedemo.service.DownloadService".equals(service.service.getClassName())) {
               isServiceRunning = true;
           }
       }
       return isServiceRunning;
   }
}

9、写在最后


Android P(即Android 9)已于2018年8月7日的正式发布,此版本的Android省电策略等限制,对于APP的后台保活来说将更为困难。预计2019年Android P将会成为Android设备的主流系统,到那时才是真正噩梦的开始

关于Android P在保活方面的问题,请详细阅读《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦》。

附录:更多有关IM/推送的心跳保活处理的文章


应用保活终极总结(一):Android6.0以下的双进程守护保活实践
应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)
应用保活终极总结(三):Android6.0及以上的保活实践(被杀复活篇)
Android进程保活详解:一篇文章解决你的所有疑问
Android端消息推送总结:实现原理、心跳保活、遇到的问题等
深入的聊聊Android消息推送这件小事
为何基于TCP协议的移动端IM仍然需要心跳保活机制?
微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)
微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)
移动端IM实践:实现Android版微信的智能心跳机制
移动端IM实践:WhatsApp、Line、微信的心跳策略分析
Android P正式版即将到来:后台应用保活、消息推送的真正噩梦
全面盘点当前Android后台保活方案的真实运行效果(截止2019年前)
>> 更多同类文章 ……

即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

 
 
 上一篇:技术干货:从零开始,教你设计一个百万级的消息推送系统▪下一篇:求教基于Netty4的消息推送系统的实时性问题,要求200万并发、5秒推完 

本帖已收录至以下技术专辑

推荐方案
 
 
评论 8
 
 
2 楼: winlmh Lv.1 11 个月前 |
我去试试效果
 
 
 
 
 
 
 
3 楼: sappublic Lv.1 11 个月前 |
不错,学习了,回去试试
 
 
 
 
 
 
 
4 楼: hsc456 Lv.1 7 个月前 |
总结得很好
 
 
 
 
 
 
 
5 楼: hc111 Lv.1 2 个月前 |
总结的很好,现在真的很难保活
 
 
 
 
 
 
 
6 楼: JackJiang Lv.9 精华之王 白金版主 终身成就 楼主 2 个月前 |
引用:hc111 发表于 2019-09-24 16:48
总结的很好,现在真的很难保活

这条路不通的
 
 
签名: 《极致优化,iOS版微信编译速度3倍提升的实践总结》http://www.52im.net/thread-2873-1-1.html
 
 
 
 
7 楼: jevensonv Lv.2 论坛新秀 29 天前 |
学习
 
 
 
 
 
 
 
8 楼: elan Lv.1 22 天前 |
正文第一个超链错了
 
 
签名: testoo哈哈~
 
 
 
 
9 楼: JackJiang Lv.9 精华之王 白金版主 终身成就 楼主 22 天前 |
引用:elan 发表于 2019-11-28 20:08
正文第一个超链错了

感谢纠错,已修改。
 
 
签名: 《极致优化,iOS版微信编译速度3倍提升的实践总结》http://www.52im.net/thread-2873-1-1.html
 
 
 
 
 
 
原文地址:https://www.cnblogs.com/cx2016/p/12075144.html