Android後臺資料採集服務保活的探索與發現
github原始碼下載地址:https://github.com/geduo83/TrackDataCollect/blob/sync/app/src/main/java/com/geduo/datacollect/alive/
今天是2018年的最後一天,幸好元旦放假三天,有時間來整理下這陣子研究的一點兒新東西,把它記錄分享出來,本來這篇文章應該是上個周要完成,但是由於工作忙沒有時間寫,就拖到今天了,再過幾個小時就2019年了,願新的一年我們都能心想事成。
在上一篇文章Android車輛運動軌跡大資料採集最佳實踐(https://blog.csdn.net/geduo_83/article/details/84943984)這篇文章中我們講到,資料採集服務是一個持久的操作,當採集服務進入後臺後,有可能被系統殺死的可能,可用通過賬號同步服務SysAdapter來提升程序的優先順序來降低採集服務被後臺殺死的機率。 本文共介紹了6種後臺服務保活方案,下文將逐一介紹。
1.賬號同步服務SysAdapter保活
- 1.1 通過ContentProvider實現資料同步
public class SyncContentProvider extends ContentProvider { public static final String CONTENT_URI_BASE = "content://" + SyncControl.CONTENT_AUTHORITY; public static final String TABLE_NAME = "data"; public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_BASE + "/" + TABLE_NAME); @Override public boolean onCreate() { return true; } @Nullable @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } @Nullable @Override public String getType(Uri uri) { return new String(); } @Nullable @Override public Uri insert(Uri uri, ContentValues values) { return null; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } }
- 1.2 關聯SyncAdapter通訊服務
public class SyncAdapterService extends Service { private static final Object lock = new Object(); private static SyncAdapter mSyncAdapter = null; private static int sync_count = 0; @Override public void onCreate() { synchronized (lock) { if (mSyncAdapter == null) { mSyncAdapter = new SyncAdapter(getApplicationContext(), true); } } } @Override public IBinder onBind(Intent intent) { return mSyncAdapter.getSyncAdapterBinder(); } @Override public boolean onUnbind(Intent intent) { return super.onUnbind(intent); } @Override public void onTaskRemoved(Intent rootIntent) { super.onTaskRemoved(rootIntent); } static class SyncAdapter extends AbstractThreadedSyncAdapter { public SyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { // 具體的同步操作,這裡主要是為了提高程序優先順序 Log.v("MYTAG", "onPerformSync start..."); SyncSharedPreference.getInstance(getContext()).addSyncTime("sync_time"+(++sync_count)); } } }
- 1.3 使用Sync服務
public class SyncControl {
public static final long SYNC_FREQUENCY = 5; // 1 hour (in seconds)
public static final String CONTENT_AUTHORITY = "com.geduo.datacollect";
public static final String ACCOUNT_TYPE = "com.geduo.datacollect.account";
@TargetApi(Build.VERSION_CODES.FROYO)
public static void createSyncAccount(Context context) {
Log.v("MYTAG", "createSyncAccount start...");
Account account = SyncAccountService.getAccount(ACCOUNT_TYPE);
AccountManager accountManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
if (accountManager.addAccountExplicitly(account, null, null)) {
ContentResolver.setIsSyncable(account, CONTENT_AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, CONTENT_AUTHORITY, true);
ContentResolver.addPeriodicSync(account, CONTENT_AUTHORITY, new Bundle(), SYNC_FREQUENCY);
}
}
public static void triggerRefresh() {
Log.v("MYTAG", "triggerRefresh start...");
Bundle b = new Bundle();
// Disable sync backoff and ignore sync preferences. In other words...perform sync NOW!
b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
ContentResolver.requestSync(
SyncAccountService.getAccount(ACCOUNT_TYPE), // Sync account
CONTENT_AUTHORITY, // Content authority
b); // Extras
}
}
2.Notication保活
- 2.1 建立前臺守護服務DaemonService
public class DaemonService extends Service {
private static final String TAG = DaemonService.class.getSimpleName();
public static final int NOTICE_ID = 100;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
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("DataCollect");
builder.setContentText("DataCollect runing...");
startForeground(NOTICE_ID, builder.build());
Intent intent = new Intent(this, CancelService.class);
startService(intent);
} else {
startForeground(NOTICE_ID, new Notification());
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
NotificationManager mManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
mManager.cancel(NOTICE_ID);
}
Intent intent = new Intent(getApplicationContext(), DaemonService.class);
startService(intent);
}
}
- 2.2 建立一個取消通知圖示的服務CancelService,讓使用者在前臺無感知
public class CancelService 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());
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(1000);
stopForeground(true);
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();
}
}
- 2.3 服務的註冊
<service android:name=".alive.notifiy.DaemonService"
android:enabled="true"
android:exported="true"
android:process=":daemon_service"/>
<service android:name=".alive.notifiy.CancelService"
android:enabled="true"
android:exported="true"
android:process=":service"/>
- 2.4 服務開啟
private void startDaemonService() {
Intent intent = new Intent(this, DaemonService.class);
startService(intent);
}
3.通過1畫素Activity保活
- 3.1 建立1畫素透明Activity
public class SinglePixelActivity extends AppCompatActivity {
private static final String TAG = "SinglePixelActivity";
@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 = 300;
attrParams.width = 300;
mWindow.setAttributes(attrParams);
// 繫結SinglePixelActivity到ScreenManager
ScreenManager.getScreenManagerInstance(this).setSingleActivity(this);
}
@Override
protected void onDestroy() {
if(! SystemUtils.isAPPALive(this,"com.geduo.datacollect")){
Intent intentAlive = new Intent(this, MainActivity.class);
intentAlive.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intentAlive);
}
super.onDestroy();
}
}
- 3.2 建立螢幕鎖屏、開屏監聽工具類
public class ScreenReceiverUtil {
private Context mContext;
// 鎖屏廣播接收器
private SreenBroadcastReceiver mScreenReceiver;
// 螢幕狀態改變回調介面
private SreenStateListener mStateReceiverListener;
public ScreenReceiverUtil(Context mContext){
this.mContext = mContext;
}
public void setScreenReceiverListener(SreenStateListener mStateReceiverListener){
mStateReceiverListener = mStateReceiverListener;
// 動態啟動廣播接收器
mScreenReceiver = new SreenBroadcastReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_USER_PRESENT);
mContext.registerReceiver(mScreenReceiver,filter);
}
public void stopScreenReceiverListener(){
mContext.unregisterReceiver(mScreenReceiver);
}
public class SreenBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if(mStateReceiverListener == null){
return;
}
if(Intent.ACTION_SCREEN_ON.equals(action)){ // 開屏
mStateReceiverListener.onSreenOn();
}else if(Intent.ACTION_SCREEN_OFF.equals(action)){ // 鎖屏
mStateReceiverListener.onSreenOff();
}else if(Intent.ACTION_USER_PRESENT.equals(action)){ // 解鎖
mStateReceiverListener.onUserPresent();
}
}
}
// 監聽sreen狀態對外回撥介面
public interface SreenStateListener {
void onSreenOn();
void onSreenOff();
void onUserPresent();
}
}
4.雙服務保活
- 4.1 建立主服務MainService
public class MainService extends Service {
MyBinder binder;
MyConn conn;
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public void onCreate() {
super.onCreate();
Log.v("MYTAG", "main onCreate start...");
binder = new MyBinder();
conn = new MyConn();
//設定該服務未前臺服務
Intent targetIntent = new Intent(this, MainActivity.class);
Notification notification = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
PendingIntent intent = PendingIntent.getActivity(this, 0, targetIntent, 0);
notification = new Notification.Builder(this).setSmallIcon(R.mipmap.ic_launcher).setContentTitle("DataCollect").setContentText("正在進行資料採集").setContentIntent(intent).build();
} else {
notification = new Notification();
notification.flags = Notification.FLAG_ONGOING_EVENT;
notification.flags |= Notification.FLAG_NO_CLEAR;
notification.flags |= Notification.FLAG_FOREGROUND_SERVICE;
}
startForeground(1001, notification);
bindService(new Intent(MainService.this, RemoteService.class), conn, Context.BIND_IMPORTANT);
}
class MyBinder extends IMyAidlInterface.Stub {
@Override
public String getServiceName() throws RemoteException {
return MainService.class.getSimpleName();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
class MyConn implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.v("MYTAG", "main onServiceConnected start...");
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.v("MYTAG", "main onServiceDisconnected start...");
startService(new Intent(MainService.this, RemoteService.class));
bindService(new Intent(MainService.this, RemoteService.class), conn, Context.BIND_IMPORTANT);
}
}
@Override
public void onDestroy() {
Log.v("onDestroy", "main onDestroy start...");
startService(new Intent(MainService.this, RemoteService.class));
bindService(new Intent(MainService.this, RemoteService.class), conn, Context.BIND_IMPORTANT);
}
}
- 4.2 建立守護服務
public class RemoteService extends Service {
MyConn conn;
MyBinder binder;
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public void onCreate() {
super.onCreate();
Log.v("MYTAG", "remote onCreate start...");
conn = new MyConn();
binder = new MyBinder();
bindService(new Intent(this, MainService.class), conn, Context.BIND_IMPORTANT);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
class MyBinder extends IMyAidlInterface.Stub {
@Override
public String getServiceName() throws RemoteException {
return RemoteService.class.getSimpleName();
}
}
class MyConn implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.v("MYTAG", "remote onServiceConnected start...");
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.v("MYTAG", "remote onServiceDisconnected start...");
startService(new Intent(RemoteService.this, MainService.class));
bindService(new Intent(RemoteService.this, MainService.class), conn, Context.BIND_IMPORTANT);
}
}
@Override
public void onDestroy() {
Log.v("MYTAG", "remote onDestroy start...");
}
}
- 4.3 註冊服務
<service
android:name=".alive.doubles.two.MainService"
android:enabled="true"
android:exported="true"
/>
<service
android:name=".alive.doubles.two.RemoteService"
android:enabled="true"
android:exported="true"
android:process=":remote"
/>
- 4.4 呼叫服務
startService (new Intent (this, MainService.class));
startService (new Intent (this, RemoteService.class));
5.定時服務JobService保活
- 5.1 建立定時服務AliveJobService
@TargetApi(21)
public class AliveJobService extends JobService {
private final static String TAG = AliveJobService.class.getSimpleName();
private volatile static Service mKeepAliveService = null;
public static boolean isJobServiceAlive() {
return mKeepAliveService != null;
}
private static final int MESSAGE_ID_TASK = 0x01;
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (SystemUtils.isAPPALive(getApplicationContext(), "com.geduo.datacollect")) {
Toast.makeText(getApplicationContext(), "app alive", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext(), "app die", Toast.LENGTH_SHORT).show();
startActivity(new Intent(AliveJobService.this,MainActivity.class));
}
jobFinished((JobParameters) msg.obj, false);
return true;
}
});
@Override
public boolean onStartJob(JobParameters params) {
Log.d(TAG, "onStartJob start...");
mKeepAliveService = this;
Message msg = Message.obtain(mHandler, MESSAGE_ID_TASK, params);
mHandler.sendMessage(msg);
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
Log.d(TAG, "onStopJob start...");
mHandler.removeMessages(MESSAGE_ID_TASK);
return false;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy start...");
}
}
- 5.2 建立JobSchedulerManager管理定時服務
public class JobSchedulerManager {
private static final int JOB_ID = 1;
private static JobSchedulerManager mJobManager;
private JobScheduler mJobScheduler;
private static Context mContext;
private JobSchedulerManager(Context ctxt){
this.mContext = ctxt;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mJobScheduler = (JobScheduler)ctxt.getSystemService(Context.JOB_SCHEDULER_SERVICE);
}
}
public final static JobSchedulerManager getJobSchedulerInstance(Context ctxt){
if(mJobManager == null){
mJobManager = new JobSchedulerManager(ctxt);
}
return mJobManager;
}
@SuppressLint("MissingPermission")
public void startJobScheduler(){
// 如果JobService已經啟動或API<21,返回
if(AliveJobService.isJobServiceAlive() || isBelowLOLLIPOP()){
return;
}
// 構建JobInfo物件,傳遞給JobSchedulerService
int id = JOB_ID;
mJobScheduler.cancel(id);
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID,new ComponentName(mContext, AliveJobService.class));
if (Build.VERSION.SDK_INT >= 24) {
builder.setMinimumLatency(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS); //執行的最小延遲時間
builder.setOverrideDeadline(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS); //執行的最長延時時間
builder.setMinimumLatency(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS);
builder.setBackoffCriteria(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS, JobInfo.BACKOFF_POLICY_LINEAR);//線性重試方案
} else {
builder.setPeriodic(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS);
}
builder.setPersisted(true); // 設定裝置重啟時,執行該任務
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
builder.setRequiresCharging(false); // 當插入充電器,執行該任務
JobInfo info = builder.build();
mJobScheduler.schedule(info); //開始定時執行該系統任務
}
@TargetApi(21)
public void stopJobScheduler(){
if(isBelowLOLLIPOP())
return;
mJobScheduler.cancelAll();
}
private boolean isBelowLOLLIPOP(){
// API< 21
return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
}
}
6.流氓手段通過無限迴圈播放一個無聲音樂來保活
- 6.1 建立服務PlayerMusicService
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();
Log.d(TAG,"onCreate start...");
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){
Log.d(TAG,"startPlayMusic start...");
mMediaPlayer.start();
}
}
private void stopPlayMusic(){
if(mMediaPlayer != null){
Log.d(TAG,"stopPlayMusic start...");
mMediaPlayer.stop();
}
}
@Override
public void onDestroy() {
super.onDestroy();
stopPlayMusic();
Log.d(TAG,TAG+"onDestroy start...");
// 重啟自己
Intent intent = new Intent(getApplicationContext(),PlayerMusicService.class);
startService(intent);
}
}
- 6.2 註冊服務
<service android:name=".alive.music.PlayerMusicService"
android:enabled="true"
android:exported="true"
android:process=":music_service"/>
探索發現
以上就幾種常見的後臺服務保活常用的解決方案,通過測試發現,Activity保活,Notication保活,雙Services守護,SyncAdapter、JobService,迴圈播放音樂,這些方案不管是明手段還是流氓手段在android5.0下都還算好使,但是在Android6.0以後不管是哪種方案,在app進入後臺,亮屏情況下好用,如果手機黑屏情況下,頂多半個小時程序就被系統殺死了,為此我幾乎把全網關於Android後臺程序保活的文章都看了一遍,方案都大同小異,無外乎就上面所羅列的幾種方案。
那麼在6.0以後應用進入後臺後,當終端處於黑屏模式,也就是Doze模式時,系統到底進行了哪些操作:
- 暫停網路訪問。
- 系統忽略所有的WakeLock。
- 標準的AlarmManager alarms被延緩到下一個maintenance window,但使用AlarmManager的 setAndAllowWhileIdle、setExactAndAllowWhileIdle和setAlarmClock時,alarms定義事件仍會啟動。 在這些alarms啟動前,系統會短暫地退出Doze模式。
- 系統不再進行WiFi掃描。
- 系統不允許sync adapters執行。
- 系統不允許JobScheduler執行。
android6.0以後google對系統安全這塊的保護措施更進一步增強了,程式進入後臺系統為了省電,就認為該應用使用者已經不再使用了,系統就自動清理掉該程序,有的人可能會問微信,支付寶這些應用為什麼使用者殺都殺不掉,原因就在於他們已經進入了系統程序白名單瞭如果你不信你可以去自己看看。 設定白名單有兩種方式,一種就是手機廠商自行設定,另外一種就需要使用者進入手機省電模式自己進行手動設定。
所以最終結論: 在7.0這個版本,如果不通過使用者或廠家設定,至少service是絕對沒有任何辦法保活的,絕對,除非你還能找到未知的BUG。進入程序白名單,然後通過雙服務保活,加上JobService定時任務保駕護航,就可以解決問題了。但我真的很贊同谷歌這樣的做法,如果每個應用都是後臺常駐,整個系統的效能將大打折扣,都說android 手機耗電,現在android可以說是將一切統統殺掉,然後最後的決定權留給使用者。
問題反饋
在使用中有任何問題,歡迎反饋給我,可以用以下聯絡方式跟我交流
- QQ:303704981
- email:[email protected]
- weibo:@geduo_83
關於作者
var geduo_83 = {
nickName : "geduo_83",
site : "http://www.weibo.com/geduo83"
}