效能優化系列
簡介
現在只要是社交 APP 沒有哪個開發者不想讓自己的 APP 永久常駐的,想要永久常駐除非你們家的實力非常雄厚,APP 使用者量非常大,那麼廠商都會主動來找你,把你們家的 APP 加入白名單。否則永久常駐是不可能甚至都不給你許可權後臺執行。既然不能永久常駐,那麼我們有沒有一個辦法可以使我們的 APP 不那麼容易被系統殺死勒?或者說是殺死後能主動喚醒,顯然是可以的,下面我們進入主題吧。
怎麼使用
-
down 程式碼 github.com/yangkun1992… ,將 live_library 放入自己工程
-
在 KeepAliveRuning onRuning 中實現需要保活的程式碼
public class KeepAliveRuning implements IKeepAliveRuning { /**這裡實現 Socket / 推送 等一些保活元件*/ @Override public void onRuning() { //TODO-------------------------------------------- Log.e("runing?KeepAliveRuning", "true"); } @Override public void onStop() { Log.e("runing?KeepAliveRuning", "false"); } } 複製程式碼
-
開啟保活
public void start() { //啟動保活服務 KeepAliveManager.toKeepAlive( getApplication() , HIGH_POWER_CONSUMPTION, "程式保活", "Process: System(哥們兒) 我不想被殺死", R.mipmap.ic_launcher, new ForegroundNotification( //定義前臺服務的通知點選事件 new ForegroundNotificationClickListener() { @Override public void foregroundNotificationClick(Context context, Intent intent) { Log.d("JOB-->", " foregroundNotificationClick"); } }) ); } 複製程式碼
-
停止保活
KeepAliveManager.stopWork(getApplication()); 複製程式碼
最終效果
開啟保活
-
長時間執行,不被殺死,如果被殺死雙程式會啟動死掉的程式
-
主動殺掉某一獨立執行的程式
我們應該知道正常的話點選手機回收垃圾桶後臺的應用都會被 kill 掉,還有主動點選 AS Logcat 的程式停止執行的按鈕,我們也會發現程式會自動起來並且 pid 跟上一次不一樣了。要的就是這種效果,下面我們來了解下程式保活的知識吧.
未開啟保活
程式優先順序
程式
如果記憶體不足,需要為其他使用者提供更緊急服務的程式又需要記憶體時,Android 可能會決定在某一時刻關閉某一程式。在被終止程式中執行的應用元件也會隨之銷燬。 當這些元件需要再次執行時,系統將為它們重啟程式。
決定終止哪個程式時,Android 系統將權衡它們對使用者的相對重要程度。例如,相對於託管可見 Activity 的程式而言,它更有可能關閉託管螢幕上不再可見的 Activity 的程式。 因此,是否終止某個程式的決定取決於該程式中所執行元件的狀態。 下面,我們介紹決定終止程式所用的規則。
程式生命週期
Android 系統將盡量長時間地保持應用程式,但為了新建程式或執行更重要的程式,最終需要移除舊程式來回收記憶體。 為了確定保留或終止哪些程式,系統會根據程式中正在執行的元件以及這些元件的狀態,將每個程式放入“重要性層次結構”中。 必要時,系統會首先消除重要性最低的程式,然後是重要性略遜的程式,依此類推,以回收系統資源。
重要性層次結構一共有 5 級。以下列表按照重要程度列出了各類程式(第一個程式最重要,將是最後一個被終止的程式):
名稱 | 概括 | 回收狀態 |
---|---|---|
前臺程式 | 正在互動 | 只有在記憶體不足以支援它們同時繼續執行這一萬不得已的情況下,系統才會終止它們 |
可見程式 | 沒有任何前臺元件、但仍會影響使用者在螢幕上所見內容的程式 | 可見程式被視為是極其重要的程式,除非為了維持所有前臺程式同時執行而必須終止,否則系統不會終止這些程式。 |
服務程式 | 正在執行已使用 startService() 方法啟動的服務且不屬於上述兩個更高類別程式的程式。 |
除非記憶體不足以維持所有前臺程式和可見程式同時執行,否則系統會讓服務程式保持執行狀態。 |
後臺程式 | 對使用者不可見的 Activity 的程式 | 系統可能隨時終止它們 |
空程式 | 不含任何活動應用元件的程式 | 最容易為殺死 |
LMK(LowMemoryKiller)
為什麼引入 LMK ?
程式的啟動分冷啟動和熱啟動,當使用者退出某一個程式的時候,並不會真正的將程式退出,而是將這個程式放到後臺,以便下次啟動的時候可以馬上啟動起來,這個過程名為熱啟動,這也是Android 的設計理念之一。這個機制會帶來一個問題,每個程式都有自己獨立的記憶體地址空間,隨著應用開啟數量的增多, 系統已使用的記憶體越來越大,就很有可能導致系統記憶體不足。為了解決這個問題,系統引入 LowmemoryKiller (簡稱 lmk ) 管理所有程式,根據一定策略來 kill 某個程式並釋放佔用的記憶體,保證系統的正常執行。
LMK 基本原理
所有應用程式都是從 zygote 孵化出來的,記錄在 AMS 中mLruProcesses 列表中,由 AMS 進行統一管理,AMS 中會根據程式的狀態更新程式對應的 oom_adj 值,這個值會通過檔案傳遞到 kernel 中去,kernel 有個低記憶體回收機制,在記憶體達到一定閥值時會觸發清理 oom_adj 值高的程式騰出更多的記憶體空間
LMK 殺程式標準
minfree : 存放6個數值,單位記憶體頁面數 ( 一個頁面 4kb )
記憶體閾值 | 記憶體回收閾值 | 對應程式 |
---|---|---|
18432 | 72 M | .前臺程式(foreground) |
23040 | 90 M | 可見程式(visible) |
27648 | 108 M | 次要服務(secondary server) |
32256 | 126 M | 後臺程式(hidden) |
36864 | 144 M | 內容供應節點(content provider) |
46080 | 180 M | 空程式(empty) |
當記憶體到 180 M的時候會將空程式進行回收,當記憶體到 144 M 的時候把空程式回收完以後開始對內容供應節點進行回收,並不是所有的內容供應節點都回收,而是通過判斷它的優先順序進行回收,優先順序是用 oom_adj 的值來表示,值越大回收的機率越高
adj 檢視:
cat /sys/module/lowmemorykiller/parameters/adj
複製程式碼
檢視程式 adj 值:
adb shell ps
複製程式碼
值越低越不易被回收,0 代表就不會被回收。
記憶體閾值在不同的手機上不一樣,一旦低於該值, Android 便開始按順序關閉程式. 因此 Android 開始結束優先順序最低的空程式,即當可用記憶體小於 180MB (46080)
程式保活方案
Activity 提權
這裡可見 oom_adj 為 0 是不會被回收的
後臺 oom_adj 為 6 記憶體不足會被回收
鎖屏 oom_adj 開啟一畫素 Activity 為 0 相當於可見程式,不易被回收
實現原理:
監控手機鎖屏解鎖事件,在螢幕鎖屏時啟動 1 個畫素透明的 Activity ,在使用者解鎖時將 Activity 銷燬掉,從而達到提高程式優先順序的作用。
程式碼實現
-
建立 onePxActivity
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //設定一畫素的activity Window window = getWindow(); window.setGravity(Gravity.START | Gravity.TOP); WindowManager.LayoutParams params = window.getAttributes(); params.x = 0; params.y = 0; params.height = 1; params.width = 1; window.setAttributes(params); //在一畫素activity裡註冊廣播接受者 接受到廣播結束掉一畫素 br = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { finish(); } }; registerReceiver(br, new IntentFilter("finish activity")); checkScreenOn("onCreate"); } 複製程式碼
-
建立鎖屏開屏廣播接收
@Override public void onReceive(final Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { //螢幕關閉的時候接受到廣播 appIsForeground = IsForeground(context); try { Intent it = new Intent(context, OnePixelActivity.class); it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); it.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); context.startActivity(it); } catch (Exception e) { e.printStackTrace(); } //通知螢幕已關閉,開始播放無聲音樂 context.sendBroadcast(new Intent("_ACTION_SCREEN_OFF")); } else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { //螢幕開啟的時候傳送廣播 結束一畫素 context.sendBroadcast(new Intent("finish activity")); if (!appIsForeground) { appIsForeground = false; try { Intent home = new Intent(Intent.ACTION_MAIN); home.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); home.addCategory(Intent.CATEGORY_HOME); context.getApplicationContext().startActivity(home); } catch (Exception e) { e.printStackTrace(); } } //通知螢幕已點亮,停止播放無聲音樂 context.sendBroadcast(new Intent("_ACTION_SCREEN_ON")); } } 複製程式碼
Service 提權
建立一個前臺服務用於提高 app 在按下 home 鍵之後的程式優先順序
private void startService(Context context) {
try {
Log.i(TAG, "---》啟動雙程式保活服務");
//啟動本地服務
Intent localIntent = new Intent(context, LocalService.class);
//啟動守護程式
Intent guardIntent = new Intent(context, RemoteService.class);
if (Build.VERSION.SDK_INT >= 26) {
startForegroundService(localIntent);
startForegroundService(guardIntent);
} else {
startService(localIntent);
startService(guardIntent);
}
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}
複製程式碼
注意如果開啟 startForegroundService 前臺服務,那麼必須在 5 s內開啟一個前臺程式的服務通知欄,不然會報 ANR
startForeground(KeepAliveConfig.FOREGROUD_NOTIFICATION_ID, notification);
複製程式碼
廣播拉活(在 8.0 以下很受用)
在發生特定系統事件時,系統會發出廣播,通過在 AndroidManifest 中靜態註冊對應的廣播監聽器,即可在發生響應事件時拉活。但是從android 7.0 開始,對廣播進行了限制,而且在 8.0 更加嚴格。
以靜態廣播的形式註冊
<receiver android:name=".receive.NotificationClickReceiver">
<intent-filter>
<action android:name="CLICK_NOTIFICATION"></action>
</intent-filter>
</receiver>
複製程式碼
全家桶 拉活
有多個 app 在使用者裝置上安裝,只要開啟其中一個就可以將其他的app 也拉活。比如手機裡裝了手 Q、QQ 空間、興趣部落等等,那麼開啟任意一個 app 後,其他的 app 也都會被喚醒。
Service 機制拉活
將 Service 設定為 START_STICKY,利用系統機制在 Service 掛掉後自動拉活
只要 targetSdkVersion 不小於5,就預設是 START_STICKY。 但是某些 ROM 系統不會拉活。並且經過測試,Service 第一次被異常殺死後很快被重啟,第二次會比第一次慢,第三次又會比前一次慢,一旦在短時間內 Service 被殺死 4-5 次,則系統不再拉起。
賬號同步拉活(只做瞭解,不靠譜)
手機系統設定裡會有 “帳戶” 一項功能,任何第三方 APP 都可以通過此功能將資料在一定時間內同步到伺服器中去。系統在將 APP 帳戶同步時,會將未啟動的 APP 程式拉活
JobScheduler 拉活(靠譜,8.0 官方推薦)
JobScheduler 允許在特定狀態與特定時間間隔週期執行任務。可以利用它的這個特點完成保活的功能,效果即開啟一個定時器,與普通定時器不同的是其排程由系統完成。
注意 setPeriodic 方法 在 7.0 以上如果設定小於 15 min 不起作用,可以使用setMinimumLatency 設定延時啟動,並且輪詢
public static void startJob(Context context) {
try {
mJobScheduler = (JobScheduler) context.getSystemService(
Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(10,
new ComponentName(context.getPackageName(),
JobHandlerService.class.getName())).setPersisted(true);
/**
* I was having this problem and after review some blogs and the official documentation,
* I realised that JobScheduler is having difference behavior on Android N(24 and 25).
* JobScheduler works with a minimum periodic of 15 mins.
*
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//7.0以上延遲1s執行
builder.setMinimumLatency(KeepAliveConfig.JOB_TIME);
} else {
//每隔1s執行一次job
builder.setPeriodic(KeepAliveConfig.JOB_TIME);
}
mJobScheduler.schedule(builder.build());
} catch (Exception e) {
Log.e("startJob->", e.getMessage());
}
}
複製程式碼
推送拉活
根據終端不同,在小米手機(包括 MIUI)接入小米推送、華為手機接入華為推送。
Native 拉活
Native fork 子程式用於觀察當前 app 主程式的存亡狀態。對於 5.0以上成功率極低。
後臺迴圈播放一條無聲檔案
//如果選擇流氓模式,就預設接收了耗電的缺點,但是保活效果很好。
if (mediaPlayer == null && KeepAliveConfig.runMode == RunMode.HIGH_POWER_CONSUMPTION) {
mediaPlayer = MediaPlayer.create(this, R.raw.novioce);
mediaPlayer.setVolume(0f, 0f);
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
Log.i(TAG, "迴圈播放音樂");
play();
}
});
play();
}
複製程式碼
雙程式守護 (靠譜)
兩個程式相互繫結 (bindService),如果有其中一個程式被殺,那麼另外一個程式就會將被殺的程式重新拉起
總結
程式保活就講到這裡了,最後我自己是結合裡面最靠譜的(Activity + Service 提權 + Service 機制拉活 + JobScheduler 定時檢測程式是否執行 + 後臺播放無聲檔案 + 雙程式守護),然後組成了一個程式保活終極方案。 文章中只是部分程式碼,感興趣的可以下載 demo 試下保活效果。