1. 程式人生 > >Android之D面試題②程序保活的一般套路(1畫素Activity/賬號同步/Jobscheduler/系統服務捆綁)

Android之D面試題②程序保活的一般套路(1畫素Activity/賬號同步/Jobscheduler/系統服務捆綁)

       讀到這裡,你或許有一個疑問,假設現在記憶體不足,空程序都被殺光了,現在要殺後臺程序,但是手機中後臺程序很多,難道要一次性全部都清理掉?當然不是的,程序是有它的優先順序的,這個優先順序通過程序的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_ADJ16預留的最低級別,一般對於快取的程序才有可能設定成這個級別
CACHED_APP_MAX_ADJ15快取程序,空程序,在記憶體不足的情況下就會優先被kill
CACHED_APP_MIN_ADJ9快取程序,也就是空程序
SERVICE_B_ADJ8不活躍的程序
PREVIOUS_APP_ADJ7切換程序
HOME_APP_ADJ6與Home互動的程序
SERVICE_ADJ5有Service的程序
HEAVY_WEIGHT_APP_ADJ4高權重程序
BACKUP_APP_ADJ3正在備份的程序
PERCEPTIBLE_APP_ADJ2可感知的程序,比如那種播放音樂
VISIBLE_APP_ADJ1可見程序
FOREGROUND_APP_ADJ0前臺程序
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的主題,當然也無所謂了

   <style name="LiveStyle">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowAnimationStyle">@null</item>
        <item name="android:windowNoTitle">true</item>
   </style>

在螢幕關閉的時候把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();
    }

    /**
     * screen狀態廣播接收者
     */
    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<Activity> 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<Activity>(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;
    }
}
      <service android:name=".LiveService"
            android:process=":live_service"/>

       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、JobSheduler

JobSheduler是作為程序死後復活的一種手段,native程序方式最大缺點是費電, Native 程序費電的原因是感知主程序是否存活有兩種實現方式,在 Native 程序中通過死迴圈或定時器,輪訓判斷主程序是否存活,當主程序不存活時進行拉活。其次5.0以上系統不支援。 但是JobSheduler可以替代在Android5.0以上native程序方式,這種方式即使使用者強制關閉,也能被拉起來,親測可行。

  JobSheduler@TargetApi(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上可能失效,我認為可以多做一種保保守方案

@Override
public 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_INTENT
    START_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) {
    }
}

但是這種方式需要許可權

  <service
            android:name=".LiveService"
            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
            <intent-filter>
                <action android:name="android.service.notification.NotificationListenerService" />
            </intent-filter>
        </service>

所以你的應用要是有訊息推送的話,那麼可以用這種方式去欺騙使用者。

結束:
聽說賬號同步喚醒APP這種機制很不錯,使用者強制停止都殺不起建立一個賬號並設定同步器,建立週期同步,系統會自動呼叫同步器,這樣就能啟用我們的APP,侷限是國產機會修改最短同步週期(魅藍NOTE2長達30分鐘),並且需要聯網才能使用。在國內各大ROM"欣欣向榮"的大背景下,關於程序保活,不加入白名單,我也很想知道有沒有一個應用永活的方案,這種方案效能好,不費電,或許做不到,或許有牛人可以,但是,通過上面幾種措施,在絕大部分的機型下,絕大部分使用者手機中,我們的程序壽命確實得到了提高,這篇文章都是圍繞怎麼去提高程序oom_adj的值來進行,關於降低記憶體佔用方面,移步
Android效能優化的方方面面,所以關於我們的應用更加“健康”,效能優化必不可少。