效能優化 (十) APP 持續執行之程式保活實現

DevYK發表於2019-06-11

效能優化系列

APP 啟動優化

UI 繪製優化

記憶體優化

圖片壓縮

長圖優化

電量優化

Dex 加解密

動態替換 Application

APP 穩定性之熱修復原理探索

APP 持續執行之程式保活實現

ProGuard 對程式碼和資源壓縮

APK 極限壓縮

簡介

現在只要是社交 APP 沒有哪個開發者不想讓自己的 APP 永久常駐的,想要永久常駐除非你們家的實力非常雄厚,APP 使用者量非常大,那麼廠商都會主動來找你,把你們家的 APP 加入白名單。否則永久常駐是不可能甚至都不給你許可權後臺執行。既然不能永久常駐,那麼我們有沒有一個辦法可以使我們的 APP 不那麼容易被系統殺死勒?或者說是殺死後能主動喚醒,顯然是可以的,下面我們進入主題吧。

怎麼使用

程式碼傳送陣

  1. down 程式碼 github.com/yangkun1992… ,將 live_library 放入自己工程

  2. 在 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");
        }
    }
    複製程式碼
  3. 開啟保活

        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");
                                }
                            })
            );
        }
    複製程式碼
  4. 停止保活

       KeepAliveManager.stopWork(getApplication());
    複製程式碼

最終效果

開啟保活

  • 長時間執行,不被殺死,如果被殺死雙程式會啟動死掉的程式

    效能優化 (十)  APP 持續執行之程式保活實現

  • 主動殺掉某一獨立執行的程式

    效能優化 (十)  APP 持續執行之程式保活實現
    我們應該知道正常的話點選手機回收垃圾桶後臺的應用都會被 kill 掉,還有主動點選 AS Logcat 的程式停止執行的按鈕,我們也會發現程式會自動起來並且 pid 跟上一次不一樣了。要的就是這種效果,下面我們來了解下程式保活的知識吧.

未開啟保活

效能優化 (十)  APP 持續執行之程式保活實現

程式優先順序

官網詳細介紹

程式

如果記憶體不足,需要為其他使用者提供更緊急服務的程式又需要記憶體時,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 )

效能優化 (十)  APP 持續執行之程式保活實現

記憶體閾值 記憶體回收閾值 對應程式
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
複製程式碼

效能優化 (十)  APP 持續執行之程式保活實現

檢視程式 adj 值:

adb shell ps
複製程式碼

效能優化 (十)  APP 持續執行之程式保活實現

值越低越不易被回收,0 代表就不會被回收。

記憶體閾值在不同的手機上不一樣,一旦低於該值, Android 便開始按順序關閉程式. 因此 Android 開始結束優先順序最低的空程式,即當可用記憶體小於 180MB (46080)

程式保活方案

Activity 提權

效能優化 (十)  APP 持續執行之程式保活實現

這裡可見 oom_adj 為 0 是不會被回收的

後臺 oom_adj 為 6 記憶體不足會被回收

鎖屏 oom_adj 開啟一畫素 Activity 為 0 相當於可見程式,不易被回收

實現原理:

監控手機鎖屏解鎖事件,在螢幕鎖屏時啟動 1 個畫素透明的 Activity ,在使用者解鎖時將 Activity 銷燬掉,從而達到提高程式優先順序的作用。

程式碼實現

  1. 建立 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");
        }
    複製程式碼
  2. 建立鎖屏開屏廣播接收

        @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),如果有其中一個程式被殺,那麼另外一個程式就會將被殺的程式重新拉起

效能優化 (十)  APP 持續執行之程式保活實現

總結

程式保活就講到這裡了,最後我自己是結合裡面最靠譜的(Activity + Service 提權 + Service 機制拉活 + JobScheduler 定時檢測程式是否執行 + 後臺播放無聲檔案 + 雙程式守護),然後組成了一個程式保活終極方案。 文章中只是部分程式碼,感興趣的可以下載 demo 試下保活效果。

感謝

相關文章