Android之APP保活

lvxiangan發表於2019-01-09

前言

強烈建議不要這麼做,不僅僅從使用者角度考慮,它只會滋生更多的流氓應用,拖垮Android 平臺的流暢性(假如你手機裡裝了支付寶、淘寶、天貓、UC等阿里系的app,那麼你開啟任意一個阿里系的app後,有可能就順便把其他阿里系的app給喚醒了。(只是拿阿里打個比方,其實BAT系都差不多)沒錯,我們的Android手機就是一步一步的被上面這些場景給拖卡機的。)。作為Android開發者也有責任去維護Android的生態環境。現在很多Android開發工程師,主力機居然是iPhone而不是Android裝置,感到相當悲哀。

這種做法其實是撿了芝麻丟了西瓜,最終倒黴的還是安卓開發者!越來越多的人轉向蘋果陣營,到時候你想寫點良心程式碼都不會有人來買你帳了!看看現在公交車上的情景,10個裡有7、8個人是蘋果機,此情此景那部分寫流氓程式碼的安卓程式設計師還能為自己寫了一個使用者怎麼關都關不了的程式而自豪麼?!
 

本文只作技術探討,如果希望找到程式永生的方法,可能要失望了。

 

支付寶、微信如何實現保活常駐系統後臺?

1、與手機廠商溝通好,把它們app放進系統白名單,降低omm_adj值,儘量保證程式不被系統殺死。
2、常用方法:

  • 開啟前臺Service(效果好,推薦)
  • Service中迴圈播放一段無聲音訊(效果較好,但耗電量高,謹慎使用
  • 雙程式守護(Android 5.0前有效)
  • JobScheduler(Android 5.0後引入,8.0後失效)
  • 1 畫素activity保活方案(不推薦)
  • 廣播鎖屏、自定義鎖屏(不推薦)
  • 第三方推送SDK喚醒(效果好,缺點是第三方接入)

PS:不以節能來維持程式保活的手段,都是耍流氓。

 

 

什麼是omm_adj?

Android有一個oom的機制,系統會根據程式的優先順序,給每個程式一個oom權重值,當系統記憶體緊張時,系統會根據這個優先順序去選擇將哪些程式殺掉,以騰出空間保證更高優先順序的程式能正常執行。要想讓程式長期存活,提高優先順序是個不二之選。這個可以在adb中,通過以下命令檢視:su cat /proc/pid/oom_adj   這個值越小,說明程式的優先順序越高,越不容易被程式kill掉。

如果是負數,表示該程式為系統程式,肯定不會被殺掉,
如果是0,表示是前臺程式,即當前使用者正在操作的程式,除非萬不得已,也不會被殺掉,
如果是1,表示是可見程式,通常表示有一個前臺服務,會在通知欄有一個劃不掉的通知,比如放歌,下載檔案什麼的。
再增大,則優先順序逐漸降低,順序為服務程式,快取程式,空程式等等。



常見的保活手段:

1)開啟前臺Service

原理:通過使用 startForeground()方法將當前Service置於前臺來提高Service的優先順序。需要注意的是,對API大於18而言 startForeground()方法需要彈出一個可見通知,如果你覺得不爽,可以開啟另一個Service將通知欄移除,其oom_adj值還是沒變的。實現程式碼如下:

a) DaemonService.java
/**前臺Service,使用startForeground
 * 這個Service儘量要輕,不要佔用過多的系統資源,否則
 * 系統在資源緊張時,照樣會將其殺死
 */
public class DaemonService extends Service {
    private static final String TAG = "DaemonService";
    public static final int NOTICE_ID = 100;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
        if(Contants.DEBUG)
            Log.d(TAG,"DaemonService---->onCreate被呼叫,啟動前臺service");
        //如果API大於18,需要彈出一個可見通知
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            builder.setContentTitle("KeepAppAlive");
            builder.setContentText("DaemonService is runing...");
            startForeground(NOTICE_ID,builder.build());
            // 如果覺得常駐通知欄體驗不好
            // 可以通過啟動CancelNoticeService,將通知移除,oom_adj值不變
            Intent intent = new Intent(this,CancelNoticeService.class);
            startService(intent);
        }else{
            startForeground(NOTICE_ID,new Notification());
        }
    } 
 
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 如果Service被終止,當資源允許情況下,重啟service
        return START_STICKY;
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
        // 如果Service被殺死,幹掉通知
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){
            NotificationManager mManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
            mManager.cancel(NOTICE_ID);
        }
        if(Contants.DEBUG)
            Log.d(TAG,"DaemonService---->onDestroy,前臺service被殺死");
        // 重啟自己
        Intent intent = new Intent(getApplicationContext(),DaemonService.class);
        startService(intent);
    }
}
講解一下:
       這裡還用到了兩個技巧:一是在onStartCommand方法中返回START_STICKY,其作用是當Service程式被kill後,系統會嘗試重新建立這個Service,且會保留Service的狀態為開始狀態,但不保留傳遞的Intent物件,onStartCommand方法一定會被重新呼叫。其二在onDestory方法中重新啟動自己,也就是說,只要Service在被銷燬時走到了onDestory這裡我們就重新啟動它。

b) CancelNoticeService.java
/** 移除前臺Service通知欄標誌,這個Service選擇性使用 */
public class CancelNoticeService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
    }
 
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if(Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2){
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            startForeground(DaemonService.NOTICE_ID,builder.build());
            // 開啟一條執行緒,去移除DaemonService彈出的通知
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // 延遲1s
                    SystemClock.sleep(1000);
                    // 取消CancelNoticeService的前臺
                    stopForeground(true);
                    // 移除DaemonService彈出的通知
                    NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
                    manager.cancel(DaemonService.NOTICE_ID);
                    // 任務完成,終止自己
                    stopSelf();
                }
            }).start();
        }
        return super.onStartCommand(intent, flags, startId);
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

c) AndroidManifest.xml
<service android:name=".service.DaemonService"
         android:enabled="true"
          android:exported="true"
          android:process=":daemon_service"/>
<service android:name=".service.CancelNoticeService"
            android:enabled="true"
            android:exported="true"
            android:process=":service"/>

 

補充:
同時啟動兩個service,共享同一個NotificationID,並且將他們同時置為前臺狀態,此時會出現兩個前臺服務,但通知管理器裡只有一個關聯的通知。 這時我們在其中一個服務中呼叫 stopForeground(true),這個服務前臺狀態會被取消,同時狀態列通知也被移除。另外一個服務並沒有受到影響,還是前臺服務狀態,但是此時,狀態列通知已經沒了! 這就是支付寶的黑科技。

 

 

 

2)迴圈播放無聲音訊

a) PlayerMusicService.java
/**迴圈播放一段無聲音訊,以提升程式優先順序*/
public class PlayerMusicService extends Service {
    private final static String TAG = "PlayerMusicService";
    private MediaPlayer mMediaPlayer;
 
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
        if(Contants.DEBUG)
            Log.d(TAG,TAG+"---->onCreate,啟動服務");
        mMediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.silent);
        mMediaPlayer.setLooping(true);
    }
 
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                startPlayMusic();
            }
        }).start();
        return START_STICKY;
    }
 
    private void startPlayMusic(){
        if(mMediaPlayer != null){
            if(Contants.DEBUG)
                Log.d(TAG,"啟動後臺播放音樂");
            mMediaPlayer.start();
        }
    }
 
    private void stopPlayMusic(){
        if(mMediaPlayer != null){
            if(Contants.DEBUG)
                Log.d(TAG,"關閉後臺播放音樂");
            mMediaPlayer.stop();
        }
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
        stopPlayMusic();
        if(Contants.DEBUG)
            Log.d(TAG,TAG+"---->onCreate,停止服務");
        // 重啟
        Intent intent = new Intent(getApplicationContext(),PlayerMusicService.class);
        startService(intent);
    }
}
b) AndroidManifest.xml
<service android:name=".service.PlayerMusicService"
          android:enabled="true"
          android:exported="true"
          android:process=":music_service"/>

 


3)雙程式守護

  •  開啟2個服務分別在不同的程式裡面,根據AIDL進行程式之間通訊
  • 本地服務跟遠端服務互相繫結,當本地服務開啟成功,開啟遠端服務,然後跟遠端服務繫結。
  • 反之,當其中一個程式出現異常,另一個程式會馬上把這個出現異常的程式重新啟動。

首先是一個AIDL介面,兩邊的Service都要通過繼承Service_1.Stub來實現AIDL介面中的方法,這裡做一個空實現,目的是為了實現程式通訊。介面宣告如下:
package com.ph.myservice;
interface Service_1 {
    String getName();
}

然後是兩個Service,為了保持連線,內部寫一個內部類實現ServiceConnection的介面,當外部殺了其中一個程式的時候,會進入onDisConnection中,那麼此時要做的就是start和bind另一個程式,因為Service的啟動是可以多次的,所以這樣是沒問題的,程式碼如下:

package com.ph.myservice;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.widget.Toast;

import java.util.List;

public class LocalService extends Service {
    private ServiceConnection conn;
    private MyService myService;

    @Override
    public IBinder onBind(Intent intent) {
        return myService;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        init();
    }

    private void init() {
        if (conn == null) {
            conn = new MyServiceConnection();
        }
        myService = new MyService();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Toast.makeText(getApplicationContext(), "本地程式啟動", Toast.LENGTH_LONG).show();
        Intent intents = new Intent();
        intents.setClass(this, RemoteService.class);
        bindService(intents, conn, Context.BIND_IMPORTANT);
        return START_STICKY;
    }

    class MyService extends Service_1.Stub {
        @Override
        public String getName() throws RemoteException {
            return null;
        }
    }

    class MyServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            System.out.println("獲取連線");
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Toast.makeText(LocalService.this, "遠端連線被幹掉了", Toast.LENGTH_SHORT).show();
            LocalService.this.startService(new Intent(LocalService.this, RemoteService.class));
            LocalService.this.bindService(new Intent(LocalService.this, RemoteService.class), conn, Context.BIND_IMPORTANT);
         }
       }
    }

遠端服務類如下:

package com.ph.myservice;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.widget.Toast;

public class RemoteService extends Service {
    private MyBinder binder;
    private ServiceConnection conn;

    @Override
    public void onCreate() {
        super.onCreate();
        // System.out.println("遠端程式開啟");
        init();
  }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Toast.makeText(getApplicationContext(), "遠端程式啟動", Toast.LENGTH_LONG).show();
        Intent intents = new Intent();
        intents.setClass(this, LocalService.class);
        bindService(intents, conn, Context.BIND_IMPORTANT);
        return START_STICKY;
    }

    private void init() {
        if (conn == null) {
            conn = new MyConnection();
        }
        binder = new MyBinder();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    static class MyBinder extends Service_1.Stub {
        @Override
        public String getName() throws RemoteException {
            return "遠端連線";
        }
    }

    class MyConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            System.out.println("獲取遠端連線");
        }

        @Override
        public void onServiceDisconnected(ComponentName nme) {
            Toast.makeText(RemoteService.this, "本地連線被幹掉了", Toast.LENGTH_SHORT).show();
            RemoteService.this.startService(new Intent(RemoteService.this,
                    LocalService.class));
            RemoteService.this.bindService(new Intent(RemoteService.this,
                    LocalService.class), conn, Context.BIND_IMPORTANT);
        }
    }

}

佈局檔案裡要加上宣告

<service android:name=".LocalService" />
<service android:name=".RemoteService" android:process=":remote" />

實際情況我個人測試,在5.0以下的模擬器上是沒問題的,不管多次從系統的程式裡kill掉,也還是會重新啟動tos,但是5.0以上這種方法是無效的,5.0以上Android應該是意識到了這種雙程式守護的方式,因此修改了一下原始碼,讓這種雙程式保活應用的方式無效。因此,針對5.0以上,我們採用另一種方案。

 

4)JobScheduler執行任務排程保活

JobScheduler這個類是21版本google新提供的api,參考連結

 

5)一個畫素activity保活方案

一個畫素的方案網上也非常多不做過多的解釋,就是在螢幕關閉的時候開啟一個1px的透明的activity據說是QQ的保活方案,螢幕開啟的時候再去finsh掉這個activty即可,大致程式碼如下:
 /** 一個畫素的保活介面 */
public class LiveActivity extends BaseActivity {

    public static final String TAG="LiveActivity";
    private BroadcastReceiver endReceiver=null;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
        super.onCreate(savedInstanceState, persistentState);
        Window window = getWindow();
        window.setGravity(Gravity.LEFT | Gravity.TOP);
        WindowManager.LayoutParams params = window.getAttributes();
        params.x = 0;
        params.y = 0;
        params.height = 1;
        params.width = 1;
        window.setAttributes(params);
        //結束該頁面的廣播
        endReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                finish();
            }
        };
        registerReceiver(endReceiver, new IntentFilter("finish"));
        //檢查螢幕狀態
        checkScreen();
    }

    @Override
    protected void onResume() {
        super.onResume();
        checkScreen();
    }

    /**  檢查螢幕狀態  isScreenOn為true  螢幕“亮”結束該Activity */
    private void checkScreen() {
        PowerManager pm = (PowerManager) LiveActivity.this.getSystemService(Context.POWER_SERVICE);
        boolean isScreenOn = pm.isScreenOn();
        if (isScreenOn) {
            finish();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (endReceiver!=null){
            unregisterReceiver(endReceiver);
        }
    }
}

當然還需要在設定1畫素Activity的樣式
    <style name="LiveActivity" parent="android:Theme.Holo.Light.NoActionBar">
        <item name="android:windowFrame">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:backgroundDimEnabled">false</item>
        <item name="android:windowBackground">@null</item>
        <item name="android:windowIsTranslucent">true</item>
    </style>


廣播的程式碼:

public class LiveBroadcastReceiver extends BroadcastReceiver {
    public static final String TAG="LiveActivity";
    private KeepLiveManager mKeepLiveManager;

    private HomeActivity mHomeActivity;
    public LiveBroadcastReceiver(HomeActivity homeActivity){
              this.mHomeActivity=homeActivity;
        mKeepLiveManager=mHomeActivity.getKeepLiveManger();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public void onReceive(Context context, Intent intent) {
        switch (intent.getAction()){
            case Intent.ACTION_SCREEN_OFF://螢幕被關閉
                Intent it=new Intent(context, LiveActivity.class);
                it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                context.startActivity(it);
                break;
            case Intent.ACTION_SCREEN_ON://螢幕被開啟
                context.sendBroadcast(new Intent("finish"));
                /**
                 * 以下的程式碼會導致螢幕解鎖後會出現返回主介面的情況
                 */
//                Intent main = new Intent(Intent.ACTION_MAIN);
////                main.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
//                //註釋掉這段避免在再次開啟APP介面時出現的返回主介面的問題
//                //main.addCategory(Intent.CATEGORY_HOME);
//                context.startActivity(main);
                break;
        }
    }
}

 

 

 

 

 

本文綜合以下資料所得:
https://blog.csdn.net/andrexpert/article/details/75045678
http://zhoujianghua.com/2015/07/28/black_technology_in_alipay/
https://blog.csdn.net/zhoukongxiao/article/details/80611059 
https://blog.csdn.net/pan861190079/article/details/72773549 
https://www.zhihu.com/question/29826231/answer/71956560

 

 

相關文章