Android中的進程保活應該分爲兩個方面:html
本文針對這兩方面來進程闡述,並給出相應的示例。其實主要也是在前人的基礎上作了一個總結,並進行了一些實踐。java
閱讀本文的時候,能夠先clone一份代碼 android-process-daemon,這樣的話可能理解更清晰。android
在開始以前,首先有必要了解一下進程等級的概念。Android 系統將盡可能長時間地保持應用進程,但爲了新建進程或運行更重要的進程,須要清除舊進程來回收內存。 爲了肯定保留或終止哪些進程,系統會對進程進行分類。 須要時,系統會首先消除重要性最低的進程,而後是清除重要性稍低一級的進程,依此類推,以回收系統資源。git
進程等級:github
前臺進程面試
可見進程shell
服務進程 正在運行已使用 startService() 方法啓動的服務且不屬於上述兩個更高類別進程的進程。儘管服務進程與用戶所見內容沒有直接關聯,可是它們一般在執行一些用戶關心的操做(例如,在後臺播放音樂或從網絡下載數據)。所以,除非內存不足以維持全部前臺進程和可見進程同時運行,不然系統會讓服務進程保持運行狀態。segmentfault
後臺進程 包含目前對用戶不可見的 Activity 的進程(已調用 Activity 的 onStop() 方法)。這些進程對用戶體驗沒有直接影響,系統可能隨時終止它們,以回收內存供前臺進程、可見進程或服務進程使用。 一般會有不少後臺進程在運行,所以它們會保存在 LRU (最近最少使用)列表中,以確保包含用戶最近查看的 Activity 的進程最後一個被終止。若是某個 Activity 正確實現了生命週期方法,並保存了其當前狀態,則終止其進程不會對用戶體驗產生明顯影響,由於當用戶導航回該 Activity 時,Activity 會恢復其全部可見狀態。緩存
空進程 不含任何活動應用組件的進程。保留這種進程的的惟一目的是用做緩存,以縮短下次在其中運行組件所需的啓動時間。 爲使整體系統資源在進程緩存和底層內核緩存之間保持平衡,系統每每會終止這些進程。服務器
進程等級參考谷歌官方文檔 https://developer.android.google.cn/guide/components/processes-and-threads.html?hl=zh-cn。
系統出於體驗和性能上的考慮,app在退到後臺時系統並不會真正的kill掉這個進程,而是將其緩存起來。打開的應用越多,後臺緩存的進程也越多。在系統內存不足的狀況下,系統開始依據自身的一套進程回收機制來判斷要kill掉哪些進程,以騰出內存來供給須要的app, 這套殺進程回收內存的機制就叫 Low Memory Killer,它是一種根據 OOM_ADJ 閾值級別觸發相應力度的內存回收的機制。
關於 OOM_ADJ 的說明以下:
其中紅色部分表明比較容易被殺死的 Android 進程(OOMADJ>=4),綠色部分表示不容易被殺死的 Android 進程,其餘表示非 Android 進程(純 Linux 進程)。在Low Memory Killer 回收內存時會根據進程的級別優先殺死 OOMADJ 比較大的進程,對於優先級相同的進程則進一步受到進程所佔內存和進程存活時間的影響。
Android 手機中進程被殺死可能有以下狀況:
因此,想要應用下降被殺死的可能性就要儘可能提升進程的優先級,這樣纔會在系統內存不足的時候減小被殺死的可能性。在這裏,咱們只是說減小被殺死的可能性,而不是說必定不會殺死。除了系統應用,或者廠商白名單中的應用,通常的應用都有被殺死的可能性。
咱們能夠經過adb命令來查看進程的優先級 首先使用命令:
adb shell ps | grep packageName
獲取進程的PID,而後使用命令獲取進程的oom_adj值,這個值越小,表明優先級越高越不容易被殺死:
adb shell cat /proc/PID/oom_adj
好比,先獲取adb進程
# adb shell ps |grep com.sososeen09.process u0_a85 1740 486 1013428 64840 00000000 f7491e65 S com.sososeen09.process.daemon.sample
而後獲取oom_adj值:
# adb shell cat /proc/1740/oom_adj 0
此時該進程運行在前臺,它的優先級爲0,這種狀況下被殺死的可能性很小。當經過Home鍵把當前引用退回後臺的時候,從新查看一下oom_adj,這個值可能會變爲6(不一樣的rom狀況可能不同)。
前面咱們也講了,當應用切換後後臺的時候進程的優先級變得很低,被殺死的可能性就增大了。若是此時用戶經過電源鍵進行息屏了。能夠考慮經過監聽息屏和解鎖的廣播,在息屏的時候啓動一個只有一個像素的Activity。這樣的話,在息屏這段時間,應用的進程優先級很高,不容易被殺死。採用這種方案要注意的是要使用戶無感知。
該方案主要解決第三方應用及系統管理工具在檢測到鎖屏事件後一段時間(通常爲5分鐘之內)內會殺死後臺進程,已達到省電的目的問題。
public class KeepLiveActivity extends Activity { private static final String TAG = "KeepLiveActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.e(TAG,"start Keep app activity"); Window window = getWindow(); //設置這個act 左上角 window.setGravity(Gravity.START | Gravity.TOP); //寬 高都爲1 WindowManager.LayoutParams attributes = window.getAttributes(); attributes.width = 1; attributes.height = 1; attributes.x = 0; attributes.y = 0; window.setAttributes(attributes); KeepLiveManager.getInstance().setKeep(this); } @Override protected void onDestroy() { super.onDestroy(); Log.e(TAG,"stop keep app activity"); } }
爲了讓無用無感知,Activity要設置的小(只有一個像素),無背景而且是透明的。此外還要注意一點,須要設置Activity的taskAffinity 屬性,要與咱們的應用默認的taskAffinity不一樣,不然當這個Activity啓動的時候,會把咱們的應用所在的任務棧移動到前臺,當屏幕解鎖以後,會發現咱們的應用移動到前臺了。而用戶在息屏的時候明明已經把咱們的應用切換到後臺了,這會給用戶形成困擾。
<activity android:name=".keepliveactivity.KeepLiveActivity" android:excludeFromRecents="true" android:exported="false" android:finishOnTaskLaunch="false" android:taskAffinity="com.sososeen09.daemon.keep.live" android:theme="@style/KeepLiveTheme" />
<style name="KeepLiveTheme"> <item name="android:windowBackground">@null</item> <item name="android:windowIsTranslucent">true</item> </style>
要有一個BroadcastReceiver,用於監聽屏幕的點亮和關閉的廣播,在這裏咱們使用了Intent.ACTION_USER_PRESENT
這個action,它會早於系統發出的Intent.ACTION_SCREEN_OFF
廣播。這樣能夠更早的結束以前息屏的時候啓動的Activity。
public class KeepLiveReceiver extends BroadcastReceiver { private static final String TAG = "KeepLiveReceiver"; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); Log.e(TAG, "receive action:" + action); //屏幕關閉事件 if (TextUtils.equals(action, Intent.ACTION_SCREEN_OFF)) { //關屏 開啓1px activity KeepLiveManager.getInstance().startKeepLiveActivity(context); // 解鎖事件 } else if (TextUtils.equals(action, Intent.ACTION_USER_PRESENT)) { KeepLiveManager.getInstance().finishKeepLiveActivity(); } KeepLiveManager.getInstance().startKeepLiveService(context); } }
應用啓動一個Service,而且Service經過調用startForeground方法來綁定一個前臺的通知時,能夠有效的提高進程的優先級。
@Override public int onStartCommand(Intent intent, int flags, int startId) { NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setSmallIcon(R.mipmap.ic_launcher); builder.setContentTitle("Foreground"); builder.setContentText("I am a foreground service"); builder.setContentInfo("Content Info"); builder.setWhen(System.currentTimeMillis()); Intent activityIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, activityIntent, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(pendingIntent); Notification notification = builder.build(); startForeground(FOREGROUND_ID, notification); return super.onStartCommand(intent, flags, startId); }
這種方式的話會在通知欄顯示一個通知,該方式屬於比較文明的。
咱們可使用 命令來查看當前正在運行的服務信息,好比
adb shell dumpsys activity services com.sososeen09.process
能夠獲得結果:
ACTIVITY MANAGER SERVICES (dumpsys activity services) User 0 active services: * ServiceRecord{d18c80d u0 com.sososeen09.process.daemon.sample/.service.WhiteService} intent={cmp=com.sososeen09.process.daemon.sample/.service.WhiteService} packageName=com.sososeen09.process.daemon.sample processName=com.sososeen09.process.daemon.sample:white baseDir=/data/app/com.sososeen09.process.daemon.sample-2/base.apk dataDir=/data/data/com.sososeen09.process.daemon.sample app=ProcessRecord{696d809 2478:com.sososeen09.process.daemon.sample:white/u0a85} isForeground=true foregroundId=1001 foregroundNoti=Notification(pri=0 contentView=com.sososeen09.process.daemon.sample/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE) createTime=-44s879ms startingBgTimeout=-- lastActivity=-44s860ms restartTime=-44s860ms createdFromFg=true startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1 * ServiceRecord{e4782a4 u0 com.sososeen09.process.daemon.sample/.service.NormalService} intent={cmp=com.sososeen09.process.daemon.sample/.service.NormalService} packageName=com.sososeen09.process.daemon.sample processName=com.sososeen09.process.daemon.sample:normal baseDir=/data/app/com.sososeen09.process.daemon.sample-2/base.apk dataDir=/data/data/com.sososeen09.process.daemon.sample app=ProcessRecord{2402ea0e 2459:com.sososeen09.process.daemon.sample:normal/u0a85} createTime=-48s510ms startingBgTimeout=-- lastActivity=-48s479ms restartTime=-48s479ms createdFromFg=true startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1 Connection bindings to services: * ConnectionRecord{3b4eb582 u0 CR DEAD com.sososeen09.process.daemon.sample/.acount.AuthenticationService:@2a1598cd} binding=AppBindRecord{d621c2f com.sososeen09.process.daemon.sample/.acount.AuthenticationService:system} conn=android.app.LoadedApk$ServiceDispatcher$InnerConnection@2a1598cd flags=0x1
能夠看到,調用了startForeground方法的Service是一個前臺進程了,有一個屬性是isForeground=true。
在這種狀況下,當應用所在進程退回到後臺時,oom_adj的值爲1,不容易被殺死。
前面講的startForeground,會在通知欄中顯示一個通知。有一種方式利用了系統漏洞,把通知欄給隱藏,讓用戶無感知。不過這種方式跟版本有關:
@Override public int onStartCommand(Intent intent, int flags, int startId) { try { Notification notification = new Notification(); if (Build.VERSION.SDK_INT < 18) { startForeground(NOTIFICATION_ID, notification); } else { startForeground(NOTIFICATION_ID, notification); // start InnerService startService(new Intent(this, InnerService.class)); } } catch (Throwable e) { e.printStackTrace(); } return super.onStartCommand(intent, flags, startId); }
而後在InnerService中關閉Notification
@Override public void onCreate() { super.onCreate(); try { startForeground(NOTIFICATION_ID, new Notification()); } catch (Throwable throwable) { throwable.printStackTrace(); } stopSelf(); }
其實咱們能夠發現,在Tinker中,因爲在Patch的過程是在另外一個服務進程中,爲了保證這個服務進程不被幹掉,Tinker也利用了這個系統的漏洞。具體能夠查看TinkerPatchService
上面講了提高進程優先級的方式了來減小應用被殺死的可能性,可是當應用真的被殺死的時候,咱們就要想辦法來拉活進行了。
這個在推送中比較常見,當幾個App都集成了同一家的推送,只要有一個App起來,就會發送一個廣播,這樣其它的App接收到這個廣播以後,開啓一個服務,就把進程給啓動起來了。各大廠家的全家桶也是這樣的。
public class WakeReceiver extends BroadcastReceiver { private final static int NOTIFICATION_ID = 1001; public final static String ACTION_WAKE = "com.sososeen09.wake"; private final static String TAG = "WakeReceiver"; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action != null && action.equals(ACTION_WAKE)) { context.startService(new Intent(context, WakeService.class)); Log.e(TAG, "onReceive: " + "收到廣播,兄弟們要起來了。。。"); } } public static class WakeService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { try { Notification notification = new Notification(); if (Build.VERSION.SDK_INT < 18) { startForeground(NOTIFICATION_ID, notification); } else { startForeground(NOTIFICATION_ID, notification); // start InnerService startService(new Intent(this, WakeInnerService.class)); } } catch (Throwable e) { e.printStackTrace(); } Log.e(TAG, "onReceive: " + "我是 WakeService,我起來了,謝謝兄弟。。。" + ProcessUtils.getProcessName(this)); return super.onStartCommand(intent, flags, startId); } } public static class WakeInnerService extends Service { @Override public void onCreate() { super.onCreate(); try { startForeground(NOTIFICATION_ID, new Notification()); } catch (Throwable throwable) { throwable.printStackTrace(); } stopSelf(); } @Override public void onDestroy() { stopForeground(true); super.onDestroy(); } } }
其實也能夠監聽系統的廣播來達到啓動應用進程的方式,可是從android 7.0開始,對廣播進行了限制,並且在8.0更加嚴格https://developer.android.google.cn/about/versions/oreo/background.html#broadcasts
可靜態註冊廣播列表: https://developer.android.google.cn/guide/components/broadcast-exceptions.html
將 Service 設置爲 START_STICKY,利用系統機制在 Service 掛掉後自動拉活。
STARTSTICKY: 「粘性」。若是service進程被kill掉,保留service的狀態爲開始狀態,但不保留遞送的intent對象。隨後系統會嘗試從新建立service,因爲服務狀態爲開始狀態,因此建立服務後必定會調用onStartCommand(Intent,int,int)方法。若是在此期間沒有任何啓動命令被傳遞到service,那麼參數Intent將爲null。 STARTNOTSTICKY: 「非粘性的」。使用這個返回值時,若是在執行完onStartCommand後,服務被異常kill掉,系統不會自動重啓該服務。 STARTREDELIVERINTENT: 重傳Intent。使用這個返回值時,若是在執行完onStartCommand後,服務被異常kill掉,系統會自動重啓該服務,並將Intent的值傳入。 STARTSTICKYCOMPATIBILITY: STARTSTICKY的兼容版本,但不保證服務被kill後必定能重啓。 只要 targetSdkVersion 不小於5,就默認是 START_STICKY。 可是某些ROM 系統不會拉活。而且通過測試,Service 第一次被異常殺死後很快被重啓,第二次會比第一次慢,第三次又會比前一次慢,一旦在短期內 Service 被殺死4-5次,則系統再也不拉起。
手機系統設置裏會有「賬戶」一項功能,任何第三方APP均可以經過此功能將數據在必定時間內同步到服務器中去。系統在將APP賬戶同步時,會將未啓動的APP進程拉活。 如何利用帳戶同步能夠參考 https://github.com/googlesamples/android-BasicSyncAdapter
可是帳戶同步這個東西,在不一樣的手機上可能在同步時間不一樣。
關於這種方式,這裏就很少講了,有興趣的能夠搜索相關文章,在示例代碼中也有相關的介紹。](https://github.com/sososeen09/android-process-daemon)中也有相關的介紹。)
JobScheduler容許在特定狀態與特定時間間隔週期執行任務。能夠利用它的這個特色完成保活的功能,效果相似開啓一個定時器,與普通定時器不一樣的是其調度由系統完成。它是在Android5.0以後推出的,在5.0以前沒法使用。
首先寫一個Service類繼承自JobService,在小於7.0的系統上,JobInfo能夠週期性的執行,可是在7.0以上的系統上,不能週期性的執行了。所以能夠在JobService的onStartJob回調方法中繼續開啓一個任務來執行。
@SuppressLint("NewApi") public class MyJobService extends JobService { private static final String TAG = "MyJobService"; public static void startJob(Context context) { JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); JobInfo.Builder builder = new JobInfo.Builder(10, new ComponentName(context.getPackageName(), MyJobService.class.getName())).setPersisted(true); //小於7.0 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // 每隔1s 執行一次 job builder.setPeriodic(1_000); } else { //延遲執行任務 builder.setMinimumLatency(1_000); } if (jobScheduler != null) { jobScheduler.schedule(builder.build()); } } @Override public boolean onStartJob(JobParameters params) { Log.e(TAG, "start job schedule"); //若是7.0以上 輪訓 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { startJob(this); } return false; } @Override public boolean onStopJob(JobParameters params) { return false; } }
AndroidManifest.xml並須要聲明權限。
<service android:name=".jobschedule.MyJobService" android:permission="android.permission.BIND_JOB_SERVICE" />
不過在某些ROM可能並不能達到須要的效果(某米)
咱們都直到Service能夠以bind方式啓動,當Service被系統殺死的時候,會在ServiceConnection的onServiceDisconnected方法中會收到回調。利用這個原理,能夠在主進程中進行有一個LocalService,在子進程中有RemoteService。LocalService中以bind和start方式啓動RemoteService,同時RemoteService以bind和start方式啓動LocalService。而且在它們各自的ServiceConnection的onServiceDisconnected方法中從新bind和start。
這種Java層經過Service這種雙進程守護的方式,能夠有效的保證進程的存活能力。
public class LocalService extends Service { private final static int NOTIFICATION_ID = 1003; private static final String TAG = "LocalService"; private ServiceConnection serviceConnection; @Override public IBinder onBind(Intent intent) { return new MyBinder(); } @Override public void onCreate() { super.onCreate(); serviceConnection = new LocalServiceConnection(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { return super.onStartCommand(intent, flags, startId); } class LocalServiceConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { //服務鏈接後回調 } @Override public void onServiceDisconnected(ComponentName name) { Log.e(TAG, "remote service died,make it alive"); //鏈接中斷後回調 startService(new Intent(LocalService.this, RemoteService.class)); bindService(new Intent(LocalService.this, RemoteService.class), serviceConnection, BIND_AUTO_CREATE); } } static class MyBinder extends IMyAidlInterface.Stub { @Override public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException { } } }
RemoteService也相似
public class RemoteService extends Service { private final static int NOTIFICATION_ID = 1002; private static final String TAG = "RemoteService"; private ServiceConnection serviceConnection; @Override public IBinder onBind(Intent intent) { return new MyBinder(); } @Override public void onCreate() { super.onCreate(); serviceConnection = new RemoteServiceConnection(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { return super.onStartCommand(intent, flags, startId); } class RemoteServiceConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { //服務鏈接後回調 } @Override public void onServiceDisconnected(ComponentName name) { Log.e(TAG, "main process local service died,make it alive"); //鏈接中斷後回調 startService(new Intent(RemoteService.this, LocalService.class)); bindService(new Intent(RemoteService.this, LocalService.class), serviceConnection, BIND_AUTO_CREATE); } } static class MyBinder extends IMyAidlInterface.Stub { @Override public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException { } } }
爲了提升Service所在進程的優先級,能夠結合咱們以前講的startForground來開啓一個Notification的方式,提升進程的優先級,以下降被殺風險。
其它咱們還可使用推送拉活,根據終端不一樣,在小米手機(包括 MIUI)接入小米推送、華爲手機接入華爲推送,這樣也能夠保證進程能夠被推送喚醒。
Native拉活,Native fork子進程用於觀察當前app主進程的存亡狀態。這種在5.0之前的系統上效果比較高,對於5.0以上成功率極低。
提高進程優先級的方式
Activity提權,監聽屏幕的息屏和解鎖,使用一個1個像素的Activity
Service提權,Service經過startForground方法來開啓一個Notification
進程拉活
經過廣播的方式
經過Service在onStartCommand的返回值,START_STICK,由系統拉活,在短期內若是屢次被殺死可能就再也啓動不了了
經過帳戶同步拉活
經過JobSchedule拉活
經過Service的bind啓動的方式,雙進程守護拉活
推送拉活
Native fork子進程的方式拉活
更多詳情,請查看 android-process-daemon
做者:sososeen09 連接:https://www.jianshu.com/p/c1a9e3e86666
更多閱讀:
不懂技術的人不要對懂技術的人說這很容易實現 歡迎關注個人微信公衆號:終端研發部,一塊兒學習和交流