Android程序保活招數概覽
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}
[email protected]598cd 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
更多閱讀:
不懂技術的人不要對懂技術的人說這很容易實現 歡迎關注我的微信公眾號:終端研發部,一起學習和交流