Android中的進程保活應該分爲兩個方面:
- 提高進程的優先級,減少被系統殺死的可能性
- 在進程已經被殺死的情況下,通過一些手段來重新啓動應用進程
本文針對這兩方面來進程闡述,並給出相應的示例。其實主要也是在前人的基礎上做了一個總結,並進行了一些實踐。
閱讀本文的時候,可以先clone一份代碼 android-process-daemon,這樣的話可能理解更清晰。
1 進程等級與Low Memory Killer
在開始之前,首先有必要了解一下進程等級的概念。Android 系統將盡量長時間地保持應用進程,但爲了新建進程或運行更重要的進程,需要清除舊進程來回收內存。 爲了確定保留或終止哪些進程,系統會對進程進行分類。 需要時,系統會首先消除重要性最低的進程,然後是清除重要性稍低一級的進程,依此類推,以回收系統資源。
進程等級:
前臺進程
- 與用戶正在交互的Activity
- 前臺Activity以bind方式啓動的Service
- Service調用了startForground,綁定了Notification
- 正在執行生命週期的Service,例如在執行onCreate、onStart、onDestory
- 正在執行onReceive方法的BroadcastReceiver
可見進程
- 託管不在前臺、但仍對用戶可見的 Activity(已調用其 onPause() 方法)。例如,如果前臺 Activity 啓動了一個對話框,允許在其後顯示上一 Activity,則有可能會發生這種情況。
- 託管綁定到可見(或前臺)Activity 的 Service。
服務進程 正在運行已使用 startService() 方法啓動的服務且不屬於上述兩個更高類別進程的進程。儘管服務進程與用戶所見內容沒有直接關聯,但是它們通常在執行一些用戶關心的操作(例如,在後臺播放音樂或從網絡下載數據)。因此,除非內存不足以維持所有前臺進程和可見進程同時運行,否則系統會讓服務進程保持運行狀態。
後臺進程 包含目前對用戶不可見的 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情況可能不一樣)。
2 提升進程優先級
2.1 利用Activity提升權限
前面我們也講了,當應用切換後後臺的時候進程的優先級變得很低,被殺死的可能性就增大了。如果此時用戶通過電源鍵進行息屏了。可以考慮通過監聽息屏和解鎖的廣播,在息屏的時候啓動一個只有一個像素的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);
}
}
2.2 Service綁定一個Notification的方式:
應用啓動一個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,不容易被殺死。
2.3 隱藏Notification的Service
前面講的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
3 進程保活
上面講了提升進程優先級的方式了來減少應用被殺死的可能性,但是當應用真的被殺死的時候,我們就要想辦法來拉活進行了。
3.1 利用廣播拉活
這個在推送中比較常見,當幾個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
3.2 系統Service機制拉活
將 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次,則系統不再拉起。
3.3 使用賬戶同步拉活
手機系統設置裏會有“帳戶”一項功能,任何第三方APP都可以通過此功能將數據在一定時間內同步到服務器中去。系統在將APP帳戶同步時,會將未啓動的APP進程拉活。 如何利用賬戶同步可以參考 https://github.com/googlesamples/android-BasicSyncAdapter
但是賬戶同步這個東西,在不同的手機上可能在同步時間不同。
關於這種方式,這裏就不多講了,有興趣的可以搜索相關文章,在示例代碼中也有相關的介紹。](https://github.com/sososeen09/android-process-daemon)中也有相關的介紹。)
3.4 使用JobSchedule拉活
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可能並不能達到需要的效果(某米)
3.5 雙進程守護
我們都直到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的方式,提高進程的優先級,以降低被殺風險。
3.6 其它方式拉活
其它我們還可以使用推送拉活,根據終端不同,在小米手機(包括 MIUI)接入小米推送、華爲手機接入華爲推送,這樣也可以保證進程可以被推送喚醒。
Native拉活,Native fork子進程用於觀察當前app主進程的存亡狀態。這種在5.0以前的系統上效果比較高,對於5.0以上成功率極低。
4 總結
提升進程優先級的方式
Activity提權,監聽屏幕的息屏和解鎖,使用一個1個像素的Activity
Service提權,Service通過startForground方法來開啓一個Notification
進程拉活
通過廣播的方式
通過Service在onStartCommand的返回值,START_STICK,由系統拉活,在短時間內如果多次被殺死可能就再也啓動不了了
通過賬戶同步拉活
通過JobSchedule拉活
通過Service的bind啓動的方式,雙進程守護拉活
推送拉活
Native fork子進程的方式拉活
更多詳情,請查看 android-process-daemon
參考
- https://developer.android.google.cn/about/versions/oreo/background#broadcasts
- https://developer.android.google.cn/guide/components/broadcast-exceptions.html
- https://github.com/googlesamples/android-BasicSyncAdapter
- 【騰訊Bugly乾貨分享】Android 進程保活招式大全
- 關於 Android 進程保活,你所需要知道的一切
作者:sososeen09 鏈接:https://www.jianshu.com/p/c1a9e3e86666
更多閱讀:
不懂技術的人不要對懂技術的人說這很容易實現 歡迎關注我的微信公衆號:終端研發部,一起學習和交流