天天看點

Android程序保活的一般套路

自己曾經也在這個問題上傷過腦經,前幾日剛好有一個北京的哥們在QQ說在做IM類的項目,問我程序保活如何處理比較恰當,決定去總結一下,網上搜尋一下程序常駐的方案好多好多,但是很多的方案都是不靠譜的或者不是最好的,結合很多資料,今天總結一下Android程序保活的一些方案,都附有完整的實作源碼,有些可能你已經知道,但是有些你可能是第一次聽說,(1像素Activity,前台服務,賬号同步,Jobscheduler,互相喚醒,系統服務捆綁,如果你都了解了,請忽略)經過多方面的驗證,Android系統中在沒有白名單的情況下做一個任何情況下都不被殺死的應用是基本不可能的,但是我們可以做到我們的應用基本不被殺死,如果殺死可以馬上滿血複活,原諒我講的特别含蓄,畢竟現在的技術防不勝防啊,不死應用還是可能的。有幾個問題需要思考,系統為什麼會殺掉程序,殺的為什麼是我的程序,這是按照什麼标準來選擇的,是一次性幹掉多個程序,還是一個接着一個殺,保活套路一堆,如何進行程序保活才是比較恰當......如果這些問題你還還存在,或許這篇文章可以解答。一、程序初步了解每一個Android應用啟動後至少對應一個程序,有的是多個程序,而且主流應用中多個程序的應用比例較大Paste_Image.png1、如何檢視程序解基本資訊對于任何一個程序,我們都可以通過adb shell ps|grep 的方式來檢視它的基本資訊值 解釋u0_a16 USER 程序目前使用者3881 程序ID1223 程序的父程序ID873024 程序的虛拟記憶體大小37108 實際駐留”在記憶體中”的記憶體大小com.wangjing.processlive 程序名2、程序劃分Android中的程序跟封建社會一樣,分了三流九等,Android系統把程序的劃為了如下幾種(重要性從高到低),網上多位大神都詳細總結過(備注:嚴格來說是劃分了6種)。2.1、前台程序(Foreground process)場景:某個程序持有一個正在與使用者互動的Activity并且該Activity正處于resume的狀态。某個程序持有一個Service,并且該Service與使用者正在互動的Activity綁定。某個程序持有一個Service,并且該Service調用startForeground()方法使之位于前台運作。某個程序持有一個Service,并且該Service正在執行它的某個生命周期回調方法,比如onCreate()、 onStart()或onDestroy()。某個程序持有一個BroadcastReceiver,并且該BroadcastReceiver正在執行其onReceive()方法。使用者正在使用的程式,一般系統是不會殺死前台程序的,除非使用者強制停止應用或者系統記憶體不足等極端情況會殺死。2.2、可見程序(Visible process)場景:擁有不在前台、但仍對使用者可見的 Activity(已調用 onPause())。擁有綁定到可見(或前台)Activity 的 Service使用者正在使用,看得到,但是摸不着,沒有覆寫到整個螢幕,隻有螢幕的一部分可見程序不包含任何前台元件,一般系統也是不會殺死可見程序的,除非要在資源吃緊的情況下,要保持某個或多個前台程序存活2.3、服務程序(Service process)場景某個程序中運作着一個Service且該Service是通過startService()啟動的,與使用者看見的界面沒有直接關聯。在記憶體不足以維持所有前台程序和可見程序同時運作的情況下,服務程序會被殺死2.4、背景程序(Background process)場景:在使用者按了"back"或者"home"後,程式本身看不到了,但是其實還在運作的程式,比如Activity調用了onPause方法系統可能随時終止它們,回收記憶體2.5、空程序(Empty process)場景:某個程序不包含任何活躍的元件時該程序就會被置為空程序,完全沒用,殺了它隻有好處沒壞處,第一個幹它!3考慮,app在退到背景時系統并不會真正的kill掉這個程序,而是将其緩存起來。打開的應用越多,背景緩存的程序也越多。在系統記憶體不足的情況下,系統開始依據自身的一套程序回收機制來判斷要kill掉哪些程序,以騰出記憶體來供給需要的app, 這套殺程序回收記憶體的機制就叫 Low Memory Killer。那這個不足怎麼來規定呢,那就是記憶體門檻值,我們可以使用cat /sys/module/lowmemorykiller/parameters/minfree來檢視某個手機的記憶體門檻值。注意這些數字的機關是page. 1 page = 4 kb.上面的六個數字對應的就是(MB): 72,90,108,126,144,180,這些數字也就是對應的記憶體閥值,記憶體門檻值在不同的手機上不一樣,一旦低于該值,Android便開始按順序關閉程序. 是以Android開始結束優先級最低的空程序,即當可用記憶體小于180MB(46080*4/1024)。讀到這裡,你或許有一個疑問,假設現在記憶體不足,空程序都被殺光了,現在要殺背景程序,但是手機中背景程序很多,難道要一次性全部都清理掉?當然不是的,程序是有它的優先級的,這個優先級通過程序的adj值來反映,它是linux核心配置設定給每個系統程序的一個值,代表程序的優先級,程序回收機制就是根據這個優先級來決定是否進行回收,adj值定義在com.android.server.am.ProcessList類中,這個類路徑是${android-sdk-path}\sources\android-23\com\android\server\am\ProcessList.java。oom_adj的值越小,程序的優先級越高,普通程序oom_adj值是大于等于0的,而系統程序oom_adj的值是小于0的,我們可以通過cat /proc/程序id/oom_adj可以看到目前程序的adj值。看到adj值是0,0就代表這個程序是屬于前台程序,我們按下Back鍵,将應用至于背景,再次檢視adj值變成了8,8代表這個程序是屬于不活躍的程序,你可以嘗試其他情況下,oom_adj值是多少,但是每個手機的廠商可能不一樣,oom_adj值主要有這麼幾個,可以參考一下。adj級别 值 解釋UNKNOWN_ADJ 16 預留的最低級别,一般對于緩存的程序才有可能設定成這個級别CACHED_APP_MAX_ADJ 15 緩存程序,空程序,在記憶體不足的情況下就會優先被killCACHED_APP_MIN_ADJ 9 緩存程序,也就是空程序SERVICE_B_ADJ 8 不活躍的程序PREVIOUS_APP_ADJ 7 切換程序HOME_APP_ADJ 6 與Home互動的程序SERVICE_ADJ 5 有Service的程序HEAVY_WEIGHT_APP_ADJ 4 高權重程序BACKUP_APP_ADJ 3 正在備份的程序PERCEPTIBLE_APP_ADJ 2 可感覺的程序,比如那種播放音樂VISIBLE_APP_ADJ 1 可見程序FOREGROUND_APP_ADJ 0 前台程序PERSISTENT_SERVICE_ADJ -11 重要程序PERSISTENT_PROC_ADJ -12 核心程序SYSTEM_ADJ -16 系統程序NATIVE_ADJ -17 系統起的Native程序備注:(上表的數字可能在不同系統會有一定的出入)根據上面的adj值,其實系統在程序回收跟記憶體回收類似也是有一套嚴格的政策,可以自己去了解,大概是這個樣子的,oom_adj越大,占用實體記憶體越多會被最先kill掉,OK,那麼現在對于程序如何保活這個問題就轉化成,如何降低oom_adj的值,以及如何使得我們應用占的記憶體最少。一、程序保活方案1、開啟一個像素的Activity據說這個是手Q的程序保活方案,基本思想,系統一般是不會殺死前台程序的。是以要使得程序常駐,我們隻需要在鎖屏的時候在本程序開啟一個Activity,為了欺騙使用者,讓這個Activity的大小是1像素,并且透明無切換動畫,在開螢幕的時候,把這個Activity關閉掉,是以這個就需要監聽系統鎖屏廣播,我試過了,的确好使,如下。public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}如果直接啟動一個Activity,當我們按下back鍵傳回桌面的時候,oom_adj的值是8,上面已經提到過,這個程序在資源不夠的情況下是容易被回收的。現在造一個一個像素的Activity。public class LiveActivity extends Activity { public static final String TAG = LiveActivity.class.getSimpleName(); public static void actionToLiveActivity(Context pContext) { Intent intent = new Intent(pContext, LiveActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); pContext.startActivity(intent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "onCreate"); setContentView(R.layout.activity_live); Window window = getWindow(); //放在左上角 window.setGravity(Gravity.START | Gravity.TOP); WindowManager.LayoutParams attributes = window.getAttributes(); //寬高設計為1個像素 attributes.width = 1; attributes.height = 1; //起始坐标 attributes.x = 0; attributes.y = 0; window.setAttributes(attributes); ScreenManager.getInstance(this).setActivity(this); } @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy"); }}為了做的更隐藏,最好設定一下這個Activity的主題,當然也無所謂了 在螢幕關閉的時候把LiveActivity啟動起來,在開屏的時候把LiveActivity 關閉掉,是以要監聽系統鎖屏廣播,以接口的形式通知MainActivity啟動或者關閉LiveActivity。public class ScreenBroadcastListener { private Context mContext; private ScreenBroadcastReceiver mScreenReceiver; private ScreenStateListener mListener; public ScreenBroadcastListener(Context context) { mContext = context.getApplicationContext(); mScreenReceiver = new ScreenBroadcastReceiver(); } interface ScreenStateListener { void onScreenOn(); void onScreenOff(); } private class ScreenBroadcastReceiver extends BroadcastReceiver { private String action = null; @Override public void onReceive(Context context, Intent intent) { action = intent.getAction(); if (Intent.ACTION_SCREEN_ON.equals(action)) { // 開屏 mListener.onScreenOn(); } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { // 鎖屏 mListener.onScreenOff(); } } } public void registerListener(ScreenStateListener listener) { mListener = listener; registerListener(); } private void registerListener() { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_SCREEN_OFF); mContext.registerReceiver(mScreenReceiver, filter); }}public class ScreenManager { private Context mContext; private WeakReference mActivityWref; public static ScreenManager gDefualt; public static ScreenManager getInstance(Context pContext) { if (gDefualt == null) { gDefualt = new ScreenManager(pContext.getApplicationContext()); } return gDefualt; } private ScreenManager(Context pContext) { this.mContext = pContext; } public void setActivity(Activity pActivity) { mActivityWref = new WeakReference(pActivity); } public void startActivity() { LiveActivity.actionToLiveActivity(mContext); } public void finishActivity() { //結束掉LiveActivity if (mActivityWref != null) { Activity activity = mActivityWref.get(); if (activity != null) { activity.finish(); } } }}現在MainActivity改成如下public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ScreenManager screenManager = ScreenManager.getInstance(MainActivity.this); ScreenBroadcastListener listener = new ScreenBroadcastListener(this); listener.registerListener(new ScreenBroadcastListener.ScreenStateListener() { @Override public void onScreenOn() { screenManager.finishActivity(); } @Override public void onScreenOff() { screenManager.startActivity(); } }); }}按下back之後,進行鎖屏,現在測試一下oom_adj的值果然将程序的優先級提高了。但是還有一個問題,記憶體也是一個考慮的因素,記憶體越多會被最先kill掉,是以把上面的業務邏輯放到Service中,而Service是在另外一個 程序中,在MainActivity開啟這個服務就行了,這樣這個程序就更加的輕量,public class LiveService extends Service { public static void toLiveService(Context pContext){ Intent intent=new Intent(pContext,LiveService.class); pContext.startService(intent); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { //螢幕關閉的時候啟動一個1像素的Activity,開屏的時候關閉Activity final ScreenManager screenManager = ScreenManager.getInstance(LiveService.this); ScreenBroadcastListener listener = new ScreenBroadcastListener(this); listener.registerListener(new ScreenBroadcastListener.ScreenStateListener() { @Override public void onScreenOn() { screenManager.finishActivity(); } @Override public void onScreenOff() { screenManager.startActivity(); } }); return START_REDELIVER_INTENT; }} OK,通過上面的操作,我們的應用就始終和前台程序是一樣的優先級了,為了省電,系統檢測到鎖屏事件後一段時間内會殺死背景程序,如果采取這種方案,就可以避免了這個問題。但是還是有被殺掉的可能,是以我們還需要做雙程序守護,關于雙程序守護,比較适合的就是aidl的那種方式,但是這個不是完全的靠譜,原理是A程序死的時候,B還在活着,B可以将A程序拉起來,反之,B程序死的時候,A還活着,A可以将B拉起來。是以雙程序守護的前提是,系統殺程序隻能一個個的去殺,如果一次性殺兩個,這種方法也是不OK的。事實上那麼我們先來看看Android5.0以下的源碼,ActivityManagerService是如何關閉在應用退出後清理記憶體的Process.killProcessQuiet(pid);應用退出後,ActivityManagerService就把主程序給殺死了,但是,在Android5.0以後,ActivityManagerService卻是這樣處理的:Process.killProcessQuiet(app.pid); Process.killProcessGroup(app.info.uid, app.pid);在應用退出後,ActivityManagerService不僅把主程序給殺死,另外把主程序所屬的程序組一并殺死,這樣一來,由于子程序和主程序在同一程序組,子程序在做的事情,也就停止了。是以在Android5.0以後的手機應用在程序被殺死後,要采用其他方案。2、前台服務這種大部分人都了解,據說這個微信也用過的程序保活方案,移步微信Android用戶端背景保活經驗分享,這方案實際利用了Android前台service的漏洞。原理如下對于 API level < 18 :調用startForeground(ID, new Notification()),發送空的Notification ,圖示則不會顯示。對于 API level >= 18:在需要提優先級的service A啟動一個InnerService,兩個服務同時startForeground,且綁定同樣的 ID。Stop 掉InnerService ,這樣通知欄圖示即被移除。public class KeepLiveService extends Service { public static final int NOTIFICATION_ID=0x11; public KeepLiveService() { } @Override public IBinder onBind(Intent intent) { throw new UnsupportedOperationException("Not yet implemented"); } @Override public void onCreate() { super.onCreate(); //API 18以下,直接發送Notification并将其置為前台 if (Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN_MR2) { startForeground(NOTIFICATION_ID, new Notification()); } else { //API 18以上,發送Notification并将其置為前台後,啟動InnerService Notification.Builder builder = new Notification.Builder(this); builder.setSmallIcon(R.mipmap.ic_launcher); startForeground(NOTIFICATION_ID, builder.build()); startService(new Intent(this, InnerService.class)); } } public static class InnerService extends Service{ @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); //發送與KeepLiveService中ID相同的Notification,然後将其取消并取消自己的前台顯示 Notification.Builder builder = new Notification.Builder(this); builder.setSmallIcon(R.mipmap.ic_launcher); startForeground(NOTIFICATION_ID, builder.build()); new Handler().postDelayed(new Runnable() { @Override public void run() { stopForeground(true); NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); manager.cancel(NOTIFICATION_ID); stopSelf(); } },100); } }}在沒有采取前台服務之前,啟動應用,oom_adj值是0,按下傳回鍵之後,變成9(不同ROM可能不一樣)在采取前台服務之後,啟動應用,oom_adj值是0,按下傳回鍵之後,變成2(不同ROM可能不一樣),确實程序的優先級有所提高。3、互相喚醒互相喚醒的意思就是,假如你手機裡裝了支付寶、淘寶、天貓、UC等阿裡系的app,那麼你打開任意一個阿裡系的app後,有可能就順便把其他阿裡系的app給喚醒了。這個完全有可能的。此外,開機,網絡切換、拍照、拍視訊時候,利用系統産生的廣播也能喚醒app,不過Android N已經将這三種廣播取消了。LBE安全大師LBE安全大師如果應用想保活,要是QQ,微信願意救你也行,有多少手機上沒有QQ,微信呢?或者像友盟,信鴿這種推送SDK,也存在喚醒app的功能。拉活方法4、JobShedulerJobSheduler是作為程序死後複活的一種手段,native程序方式最大缺點是費電, Native 程序費電的原因是感覺主程序是否存活有兩種實作方式,在 Native 程序中通過死循環或定時器,輪訓判斷主程序是否存活,當主程序不存活時進行拉活。其次5.0以上系統不支援。 但是JobSheduler可以替代在Android5.0以上native程序方式,這種方式即使使用者強制關閉,也能被拉起來,親測可行。 [email protected](Build.VERSION_CODES.LOLLIPOP)public class MyJobService extends JobService { @Override public void onCreate() { super.onCreate(); startJobSheduler(); } public void startJobSheduler() { try { JobInfo.Builder builder = new JobInfo.Builder(1, new ComponentName(getPackageName(), MyJobService.class.getName())); builder.setPeriodic(5); builder.setPersisted(true); JobScheduler jobScheduler = (JobScheduler) this.getSystemService(Context.JOB_SCHEDULER_SERVICE); jobScheduler.schedule(builder.build()); } catch (Exception ex) { ex.printStackTrace(); } } @Override public boolean onStartJob(JobParameters jobParameters) { return false; } @Override public boolean onStopJob(JobParameters jobParameters) { return false; }}5、粘性服務&與系統服務捆綁這個是系統自帶的,onStartCommand方法必須具有一個整形的傳回值,這個整形的傳回值用來告訴系統在服務啟動完畢後,如果被Kill,系統将如何操作,這種方案雖然可以,但是在某些情況or某些定制ROM上可能失效,我認為可以多做一種保保守方案。@Overridepublic int onStartCommand(Intent intent, int flags, int startId) { return START_REDELIVER_INTENT;}START_STICKY如果系統在onStartCommand傳回後被銷毀,系統将會重新建立服務并依次調用onCreate和onStartCommand(注意:根據測試Android2.3.3以下版本隻會調用onCreate根本不會調用onStartCommand,Android4.0可以辦到),這種相當于服務又重新啟動恢複到之前的狀态了)。START_NOT_STICKY如果系統在onStartCommand傳回後被銷毀,如果傳回該值,則在執行完onStartCommand方法後如果Service被殺掉系統将不會重新開機該服務。START_REDELIVER_INTENTSTART_STICKY的相容版本,不同的是其不保證服務被殺後一定能重新開機。相比與粘性服務與系統服務捆綁更厲害一點,這個來自愛哥的研究,這裡說的系統服務很好了解,比如NotificationListenerService,NotificationListenerService就是一個監聽通知的服務,隻要手機收到了通知,NotificationListenerService都能監聽到,即時使用者把程序殺死,也能重新開機,是以說要是把這個服務放到我們的程序之中,那麼就可以呵呵了@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)public class LiveService extends NotificationListenerService { public LiveService() { } @Override public void onNotificationPosted(StatusBarNotification sbn) { } @Override public void onNotificationRemoved(StatusBarNotification sbn) { }}但是這種方式需要權限 是以你的應用要是有消息推送的話,那麼可以用這種方式去欺騙使用者。結束:聽說賬号同步喚醒APP這種機制很不錯,使用者強制停止都殺不起建立一個賬号并設定同步器,建立周期同步,系統會自動調用同步器,這樣就能激活我們的APP,局限是國産機會修改最短同步周期(魅藍NOTE2長達30分鐘),并且需要聯網才能使用。在國内各大ROM"欣欣向榮"的大背景下,關于程序保活,不加入白名單,我也很想知道有沒有一個應用永活的方案,這種方案性能好,不費電,或許做不到,或許有牛人可以,但是,通過上面幾種措施,在絕大部分的機型下,絕大部分使用者手機中,我們的程序壽命确實得到了提高,這篇文章都是圍繞怎麼去提高程序oom_adj的值來進行,關于降低記憶體占用方面,移步Android性能優化的方方面面,是以關于我們的應用更加“健康”,性能優化必不可少。

文章轉至:http://www.apkbus.com/home.php?mod=space&uid=719059&do=blog&id=63191