1. 程式人生 > >全面盤點當前Android後臺保活方案的真實執行效果(截止2019年前)

全面盤點當前Android後臺保活方案的真實執行效果(截止2019年前)

本文原作者“minminaya”,作者網站:minminaya.cn,為了提升文章品質,即時通訊網對內容作了幅修訂和改動,感謝原作者。

1、引言

對於IM應用和訊息推送服務的開發者來說,在Android機型上的後臺保活是個相當頭疼的問題。

老闆一句:“為什麼微信、QQ能收到訊息,而你寫的APP卻不行?”,直接讓人崩潰,話說老闆你這APP要是整成微信、APP那麼牛,直接進手機廠商白名單,還要程式設計師在這瞎忙活?

好了,抱怨歸抱怨,活還得幹,不然靠誰養活廣大苦逼的程式設計師?

回到正題,Android程式設計師都知道,隨著Android系統的不斷完善和升級,Andriod應用的後臺保活是一次比一次難(詳見《

Android P正式版即將到來:後臺應用保活、訊息推送的真正噩夢》),但日子還得過,只能一次次絞盡腦汁想出各種黑科技。但不幸的是,因為Andriod系統的不斷升級,各種黑科技也只能適應某些版本的Android系統,無法一勞永逸解決問題。

▲ Android各版本都是用“甜品”命名的

正因為Android系統版本的差異,也導致了各種保活黑科技的執行效果大相徑庭,所以本文正好藉此機會,盤點一下當前主流(截止2019年前)的保活黑科技在市面上各版本Android手機上的執行效果,希望能給大家提供一些客觀的參考。

學習交流:

- 即時通訊/推送技術開發交流4群:101279154

 [推薦]

- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM

(本文同步釋出於:http://www.52im.net/thread-2176-1-1.html

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三種方案的優劣》)。

▲ 如果Android能有iOS的APNs這麼強勢的方案存在,那該是多美的事 ...

3、相關文章

應用保活終極總結(一):Android6.0以下的雙程序守護保活實踐

應用保活終極總結(二):Android6.0及以上的保活實踐(程序防殺篇)

應用保活終極總結(三):Android6.0及以上的保活實踐(被殺復活篇)

Android程序保活詳解:一篇文章解決你的所有疑問

Android端訊息推送總結:實現原理、心跳保活、遇到的問題等

深入的聊聊Android訊息推送這件小事

微信團隊原創分享:Android版微信後臺保活實戰分享(程序保活篇)

Android P正式版即將到來:後臺應用保活、訊息推送的真正噩夢

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是否需要重新拉起和對守護執行緒進行重新繫結。

關於本方案的具體實現,即時通訊網的以下文章有更詳細的介紹,您也可以仔細研讀:

應用保活終極總結(一):Android6.0以下的雙程序守護保活實踐

應用保活終極總結(二):Android6.0及以上的保活實踐(程序防殺篇)

應用保活終極總結(三):Android6.0及以上的保活實踐(被殺復活篇)

本方案的具體程式碼實現,主要由以下4步構成。

1)新建一個AIDL檔案:

KeepAliveConnection

interfaceKeepAliveConnection  {

}

2)新建一個服務類StepService,onBind()方法返回new KeepAliveConnection.Stub()物件,並在ServiceConnection的繫結回撥中對守護程序服務類GuardService的啟動和繫結:

/**

* 主程序 雙程序通訊

*

* @author LiGuangMin

* @time Created by 2018/8/17 11:26

*/

public class StepService extends Service {

   privatefinalstaticString TAG = StepService.class.getSimpleName();

   privateServiceConnection mServiceConnection = newServiceConnection() {

       @Override

       publicvoidonServiceConnected(ComponentName componentName, IBinder iBinder) {

           Logger.d(TAG, "StepService:建立連結");

           booleanisServiceRunning = ServiceAliveUtils.isServiceAlice();

           if(!isServiceRunning) {

               Intent i = newIntent(StepService.this, DownloadService.class);

               startService(i);

           }

       }

 

       @Override

       publicvoidonServiceDisconnected(ComponentName componentName) {

           // 斷開連結

           startService(newIntent(StepService.this, GuardService.class));

           // 重新繫結

           bindService(newIntent(StepService.this, GuardService.class), mServiceConnection, Context.BIND_IMPORTANT);

       }

   };

 

   @Nullable

   @Override

   publicIBinder onBind(Intent intent) {

       returnnewKeepAliveConnection.Stub() {

       };

   }

 

   @Override

   publicintonStartCommand(Intent intent, intflags, intstartId) {

       startForeground(1, newNotification());

       // 繫結建立連結

       bindService(newIntent(this, GuardService.class), mServiceConnection, Context.BIND_IMPORTANT);

       returnSTART_STICKY;

   }

}

3)對守護程序GuardService進行和2一樣的處理:

/**

* 守護程序 雙程序通訊

*

* @author LiGuangMin

* @time Created by 2018/8/17 11:27

*/

publicclassGuardService extendsService {

   privatefinalstaticString TAG = GuardService.class.getSimpleName();

   privateServiceConnection mServiceConnection = newServiceConnection() {

       @Override

       publicvoidonServiceConnected(ComponentName componentName, IBinder iBinder) {

           Logger.d(TAG, "GuardService:建立連結");

           booleanisServiceRunning = ServiceAliveUtils.isServiceAlice();

           if(!isServiceRunning) {

               Intent i = newIntent(GuardService.this, DownloadService.class);

               startService(i);

           }

       }

 

       @Override

       publicvoidonServiceDisconnected(ComponentName componentName) {

           // 斷開連結

           startService(newIntent(GuardService.this, StepService.class));

           // 重新繫結

           bindService(newIntent(GuardService.this, StepService.class), mServiceConnection, Context.BIND_IMPORTANT);

       }

   };

 

   @Nullable

   @Override

   publicIBinder onBind(Intent intent) {

       returnnewKeepAliveConnection.Stub() {

       };

   }

 

   @Override

   publicintonStartCommand(Intent intent, intflags, intstartId) {

       startForeground(1, newNotification());

       // 繫結建立連結

       bindService(newIntent(this, StepService.class), mServiceConnection, Context.BIND_IMPORTANT);

       returnSTART_STICKY;

   }

}

4)在Activity中在啟動需要保活的DownloadService服務後然後啟動保活的雙程序:

public class MainActivity extends AppCompatActivity {

   privateTextView mShowTimeTv;

   privateDownloadService.DownloadBinder mDownloadBinder;

   privateServiceConnection mServiceConnection = newServiceConnection() {

       @Override

       publicvoidonServiceConnected(ComponentName name, IBinder service) {

           mDownloadBinder = (DownloadService.DownloadBinder) service;

           mDownloadBinder.setOnTimeChangeListener(newDownloadService.OnTimeChangeListener() {

               @Override

               publicvoidshowTime(finalString time) {

                   runOnUiThread(newRunnable() {

                       @Override

                       publicvoidrun() {

                           mShowTimeTv.setText(time);

                       }

                   });

               }

           });

       }

 

       @Override

       publicvoidonServiceDisconnected(ComponentName name) {

       }

   };

 

   @Override

   protectedvoidonCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.activity_main);

 

       Intent intent = newIntent(this, DownloadService.class);

       startService(intent);

       bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);

       //雙守護執行緒,優先順序不一樣

       startAllServices();

   }

 

   @Override

   publicvoidonContentChanged() {

       super.onContentChanged();

       mShowTimeTv = findViewById(R.id.tv_show_time);

   }

 

   @Override

   protectedvoidonDestroy() {

       super.onDestroy();

       unbindService(mServiceConnection);

   }

 

   /**

    * 開啟所有守護Service

    */

   privatevoidstartAllServices() {

       startService(newIntent(this, StepService.class));

       startService(newIntent(this, GuardService.class));

   }

}

5.2 黑科技程式碼實現2:監聽到鎖屏廣播後使用“1”畫素Activity提升優先順序

“1”畫素保活這麼流氓的手段,傳說是某IM大廠用過的方案 ...

本方法的具體程式碼實現主要由以下6步組成。

1)該Activity的View只要設定為1畫素然後設定在Window物件上即可。在Activity的onDestroy週期中進行保活服務的存活判斷從而喚醒服務。”1畫素”Activity如下:

public class SinglePixelActivity extends AppCompatActivity {

   private static final String TAG = SinglePixelActivity.class.getSimpleName();

 

   @Override

   protected void onCreate(@NullableBundle 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

   protectedvoidonDestroy() {

       if(!SystemUtils.isAppAlive(this, Constant.PACKAGE_NAME)) {

           Intent intentAlive = newIntent(this, DownloadService.class);

           startService(intentAlive);

       }

       super.onDestroy();

   }

}

2)對廣播進行監聽,封裝為一個ScreenReceiverUtil類,進行鎖屏解鎖的廣播動態註冊監聽:

public class ScreenReceiverUtil {

   privateContext mContext;

   privateSreenBroadcastReceiver mScreenReceiver;

   privateSreenStateListener mStateReceiverListener;

 

   publicScreenReceiverUtil(Context mContext) {

       this.mContext = mContext;

   }

 

   publicvoidsetScreenReceiverListener(SreenStateListener mStateReceiverListener) {

       this.mStateReceiverListener = mStateReceiverListener;

       // 動態啟動廣播接收器

       this.mScreenReceiver = newSreenBroadcastReceiver();

       IntentFilter filter = newIntentFilter();

       filter.addAction(Intent.ACTION_SCREEN_ON);

       filter.addAction(Intent.ACTION_SCREEN_OFF);

       filter.addAction(Intent.ACTION_USER_PRESENT);

       mContext.registerReceiver(mScreenReceiver, filter);

   }

 

   publicvoidstopScreenReceiverListener() {

       mContext.unregisterReceiver(mScreenReceiver);

   }

 

   /**

    * 監聽sreen狀態對外回撥介面

    */

   publicinterfaceSreenStateListener {

       voidonSreenOn();

 

       voidonSreenOff();

 

       voidonUserPresent();

   }

 

   publicclassSreenBroadcastReceiver extendsBroadcastReceiver {

       @Override

       publicvoidonReceive(Context context, Intent intent) {

           String action = intent.getAction();

           if(mStateReceiverListener == null) {

               return;

           }

           if(Intent.ACTION_SCREEN_ON.equals(action)) { // 開屏

               mStateReceiverListener.onSreenOn();

           } elseif(Intent.ACTION_SCREEN_OFF.equals(action)) { // 鎖屏

               mStateReceiverListener.onSreenOff();

           } elseif(Intent.ACTION_USER_PRESENT.equals(action)) { // 解鎖

               mStateReceiverListener.onUserPresent();

           }

       }

   }

}

3)對1畫素Activity進行防止記憶體洩露的處理,新建一個ScreenManager類:

public class ScreenManager {

   privatestaticfinalString TAG = ScreenManager.class.getSimpleName();

   privatestaticScreenManager sInstance;

   privateContext mContext;

   privateWeakReference<Activity> mActivity;

 

   privateScreenManager(Context mContext) {

       this.mContext = mContext;

   }

 

   publicstaticScreenManager getInstance(Context context) {

       if(sInstance == null) {

           sInstance = newScreenManager(context);

       }

       returnsInstance;

   }

 

   /** 獲得SinglePixelActivity的引用

    * @param activity

    */

   publicvoidsetSingleActivity(Activity activity) {

       mActivity = newWeakReference<>(activity);

   }

 

   /**

    * 啟動SinglePixelActivity

    */

   publicvoidstartActivity() {

       Intent intent = newIntent(mContext, SinglePixelActivity.class);

       intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

       mContext.startActivity(intent);

   }

 

   /**

    * 結束SinglePixelActivity

    */

   publicvoidfinishActivity() {

       if(mActivity != null) {

           Activity activity = mActivity.get();

           if(activity != null) {

               activity.finish();

           }

       }

   }

}

4)對1畫素的Style進行特殊處理,在style檔案中新建一個SingleActivityStyle:

<stylename="SingleActivityStyle"parent="android:Theme.Holo.Light.NoActionBar">

       <itemname="android:windowBackground">@android:color/transparent</item>

       <itemname="android:windowFrame">@null</item>

       <itemname="android:windowNoTitle">true</item>

       <itemname="android:windowIsFloating">true</item>

       <itemname="android:windowContentOverlay">@null</item>

       <itemname="android:backgroundDimEnabled">false</item>

       <itemname="android:windowAnimationStyle">@null</item>

       <itemname="android:windowDisablePreview">true</item>

       <itemname="android:windowNoDisplay">false</item>

5)讓SinglePixelActivity使用singleInstance啟動模式,在manifest檔案中:

<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進行控制:

public class DownloadService extends Service {

   publicstaticfinalintNOTICE_ID = 100;

   privatestaticfinalString TAG = DownloadService.class.getSimpleName();

   privateDownloadBinder mDownloadBinder;

   privateNotificationCompat.Builder mBuilderProgress;

   privateNotificationManager mNotificationManager;

 

   privateScreenReceiverUtil mScreenListener;

   privateScreenManager mScreenManager;

   privateTimer mRunTimer;

 

   privateintmTimeSec;

   privateintmTimeMin;

   privateintmTimeHour;

 

   privateScreenReceiverUtil.SreenStateListener mScreenListenerer = newScreenReceiverUtil.SreenStateListener() {

       @Override

       publicvoidonSreenOn() {

           mScreenManager.finishActivity();

           Logger.d(TAG, "關閉了1畫素Activity");

       }

 

       @Override

       publicvoidonSreenOff() {

           mScreenManager.startActivity();

           Logger.d(TAG, "打開了1畫素Activity");

       }

 

       @Override

       publicvoidonUserPresent() {

       }

   };

   privateOnTimeChangeListener mOnTimeChangeListener;

 

   @Override

   publicvoidonCreate() {

       super.onCreate();

 

//        註冊鎖屏廣播監聽器

       mScreenListener = newScreenReceiverUtil(this);

       mScreenManager = ScreenManager.getInstance(this);

       mScreenListener.setScreenReceiverListener(mScreenListenerer);

 

       mDownloadBinder = newDownloadBinder();

       mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

   }

 

   @Override

   publicintonStartCommand(Intent intent, intflags, intstartId) {

       Logger.d(TAG, "onStartCommand");

       startRunTimer();

       returnSTART_STICKY;

   }

 

   @Nullable

   @Override

   publicIBinder onBind(Intent intent) {

 

       returnmDownloadBinder;

   }

 

   @Override

   publicbooleanonUnbind(Intent intent) {

       Logger.d(TAG, "onUnbind");

       returnsuper.onUnbind(intent);

   }

 

   @Override

   publicvoidonDestroy() {

       super.onDestroy();

       NotificationManager mManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

       if(mManager == null) {

           return;

       }

       mManager.cancel(NOTICE_ID);

       stopRunTimer();

//        mScreenListener.stopScreenReceiverListener();

   }

 

   privatevoidstartRunTimer() {

       TimerTask mTask = newTimerTask() {

           @Override

           publicvoidrun() {

               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 = newTimer();

       // 每隔1s更新一下時間

       mRunTimer.schedule(mTask, 1000, 1000);

   }

 

   privatevoidstopRunTimer() {

       if(mRunTimer != null) {

           mRunTimer.cancel();

           mRunTimer = null;

       }

       mTimeSec = 0;

       mTimeMin = 0;

       mTimeHour = 0;

       Logger.d(TAG, "時間為:"+ mTimeHour + " : "+ mTimeMin + " : "+ mTimeSec);

   }

 

   publicinterfaceOnTimeChangeListener {

       voidshowTime(String time);

   }

 

   publicclassDownloadBinder extendsBinder {

       publicvoidsetOnTimeChangeListener(OnTimeChangeListener onTimeChangeListener) {

           mOnTimeChangeListener = onTimeChangeListener;

       }

   }

}

6.3 黑科技程式碼實現3:在後臺播放音樂

後臺播放音樂這種保活方法,親身經歷過:

記得當時用的是某運動記步APP,它為了保活就是這麼幹的。之所以被我發現,是因為在我的Android手機上,每次開啟這個APP居然總能莫名其妙聽到若有若無的環境噪音樣的聲音,尤其安靜的場所下更明顯。我個人估計這個APP裡用的保活音訊檔案,很可能就是程式設計師在簡陋的條件下隨手自已錄製的,雖然也是不得以為之,但做法確實是有點粗糙。

好了,回到正題,本方案的具體程式碼實現主要是以下3步。

1)準備一段無聲的音訊,新建一個播放音樂的Service類,將播放模式改為無限迴圈播放。在其onDestroy方法中對自己重新啟動:

public class PlayerMusicService extends Service {

   privatefinalstaticString TAG = PlayerMusicService.class.getSimpleName();

   privateMediaPlayer mMediaPlayer;

 

   @Nullable

   @Override

   publicIBinder onBind(Intent intent) {

       returnnull;

   }

 

   @Override

   publicvoidonCreate() {

       super.onCreate();

       Logger.d(TAG, TAG + "---->onCreate,啟動服務");

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

       mMediaPlayer.setLooping(true);

   }

 

   @Override

   publicintonStartCommand(Intent intent, intflags, intstartId) {

       newThread(newRunnable() {

           @Override

           publicvoidrun() {

               startPlayMusic();

           }

       }).start();

       returnSTART_STICKY;

   }

 

   privatevoidstartPlayMusic() {

       if(mMediaPlayer != null) {

           Logger.d(TAG, "啟動後臺播放音樂");

           mMediaPlayer.start();

       }

   }

 

   privatevoidstopPlayMusic() {

       if(mMediaPlayer != null) {

           Logger.d(TAG, "關閉後臺播放音樂");

           mMediaPlayer.stop();

       }

   }

 

   @Override

   publicvoidonDestroy() {

       super.onDestroy();

       stopPlayMusic();

       Logger.d(TAG, TAG + "---->onCreate,停止服務");

       // 重啟自己

       Intent intent = newIntent(getApplicationContext(), PlayerMusicService.class);

       startService(intent);

   }

}

2)在保活的DownloadServie服務類的onCreate方法中對PlayerMusicService進行啟動:

Intent intent = newIntent(this, PlayerMusicService.class);

startService(intent);

3)在Manifest檔案中進行註冊:

<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進行存活的判斷來重啟:

public class ScheduleService extends JobService {

   privatestaticfinalString TAG = ScheduleService.class.getSimpleName();

 

   @Override

   publicbooleanonStartJob(JobParameters params) {

 

       booleanisServiceRunning = ServiceAliveUtils.isServiceAlice();

       if(!isServiceRunning) {

           Intent i = newIntent(this, DownloadService.class);

           startService(i);

           Logger.d(TAG, "ScheduleService啟動了DownloadService");

       }

       jobFinished(params, false);

       returnfalse;

   }

 

   @Override

   publicbooleanonStopJob(JobParameters params) {

       returnfalse;

   }

}

2)在DownloadService服務類中進行JobScheduler的註冊和使用:

/**

    * 使用JobScheduler進行保活

    */

   private void useJobServiceForKeepAlive() {

       JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);

       if(jobScheduler == null) {

           return;

       }

       jobScheduler.cancelAll();

       JobInfo.Builder builder =

           newJobInfo.Builder(1024, newComponentName(getPackageName(), ScheduleService.class.getName()));

       //週期設定為了2s

       builder.setPeriodic(1000* 2);

       builder.setPersisted(true);

       builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);

       intschedule = jobScheduler.schedule(builder.build());

       if(schedule <= 0) {

           Logger.w(TAG, "schedule error!");

       }

   }

3)在manifest檔案中進行許可權設定:

<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 類程式碼如下

public class ServiceAliveUtils {

   publicstaticbooleanisServiceAlice() {

       booleanisServiceRunning = false;

       ActivityManager manager =

           (ActivityManager) MyApplication.getMyApplication().getSystemService(Context.ACTIVITY_SERVICE);

       if(manager == null) {

           returntrue;

       }

       for(ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {

           if("demo.lgm.com.keepalivedemo.service.DownloadService".equals(service.service.getClassName())) {

               isServiceRunning = true;

           }

       }

       returnisServiceRunning;

   }

}

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年前)

>> 更多同類文章 ……

(本文同步釋出於:http://www.52im.net/thread-2176-1-1.html