主頁 > 移動端開發 > Android 行程保活方案

Android 行程保活方案

2020-12-25 11:55:51 移動端開發

前言

Android 系統為了保持系統運行流暢,在記憶體吃緊的情況下,會將一些行程給殺掉,以釋放一部分記憶體,然而,對于一些(如:QQ、微信等)比較重要的、我們希望能及時收到訊息的App,需要保持行程持續活躍,那么就需要實施一些保活措施來保證行程能夠持續存活,即 Android 行程保活,

Android 行程保活一般可以從兩個方面進行:

  1. 運行中保活:提高行程優先級,降低被系統殺掉的概率,

  2. 殺掉后拉活:被系統殺掉之后,將行程再拉活(重啟),

一、行程

默認情況下,同一個 App 的所有組件均運行在相同的行程中,但是也可以根據需要,通過在 AndroidManifest.xml 清單檔案中配置來控制某些組件所屬的行程,因此,每一個 Android 應用啟動之后至少對應一個行程,有的應用是多個行程,目前主流的應用基本上都是多個行程,

1.1 行程分類

Android 系統將盡量長時間地保持應用行程,但為了新建行程或運行更重要的行程,最終需要移除舊行程來回收記憶體, 為了確定保留或終止哪些行程,系統會根據行程中正在運行的組件以及這些組件的狀態,將每個行程放入“重要性層次結構”中, 必要時,系統會首先消除重要性最低的行程,然后是重要性略低的行程,依此類推,以回收系統資源,

重要性層次結構一共有 5 級,以下串列按照重要程度列出了各類行程(重要性從高到低):

1. 前臺行程

如果一個行程滿足以下任一條件,即視為前臺行程:

(1) 托管用戶正在互動的 Activity (已呼叫 Activity 的 onResume 方法)

(2) 托管某個 Service , 并且與用戶正在互動的 Activity 系結,

(3) 托管正在前臺運行的 Service(服務已經呼叫 startForeground方法)

(4) 托管正在執行一個生命周期的 Service ( onCreate 、onStart 或 onDestroy)

(5) 托管正在執行其 onReceiver 方法的 BroadcastReceiver

通常在任意給定的時間前臺行程都不是很多,一般系統是不會殺死前臺行程的,只有在記憶體不足以支持它們繼續運行的情況下系統才會殺死它們,

2. 可見行程

沒有任何前臺組件、但仍會影響用戶在螢屏上所見內容的行程, 如果一個行程滿足以下任一條件,即視為可見行程:

(1) 托管不在前臺,但對用戶可見的Activity(已呼叫 onPause 方法),

(2) 托管系結到可見(或前臺)Activity 的 Service ,

用戶正在使用,看得到,但是摸不著,沒有覆寫到整個螢屏,只有螢屏的一部分可見行程不包含任何前臺組件,一般系統也是不會殺死可見行程的,除非要在資源吃緊的情況下,要保持某個或多個前臺行程存活,

3. 服務行程

如果一個行程滿足以下任一條件,即視為服務行程:

(1) 在運行已使用 startService 方法啟動的服務且不屬于上述兩個更高類別行程的行程

在記憶體不足以維持所有前臺行程和可見行程同時運行的情況下,服務行程會被殺死

4. 后臺行程

如果一個行程滿足以下任一條件,即視為后臺行程:

(1) 包含目前對用戶不可見的 Activity 的行程(已呼叫 Activity 的 onStop() 方法),

系統可能隨時終止它們,回收記憶體

5. 空行程

如果一個行程滿足以下任一條件,即視為空行程:

(1) 不含任何活動應用組件的行程 ,

保留這種行程的的唯一目的是用作快取,以縮短下次在其中運行組件所需的啟動時間, 為使總體系統資源在行程快取和底層內核快取之間保持平衡,系統往往會終止這些行程,

1.2 記憶體閾值

系統出于體驗和性能上的考慮,app 在退到后臺時系統并不會真正的 kill 掉這個行程,而是將其快取起來,打開的應用越多,后臺快取的行程也越多,在系統記憶體不足的情況下,系統開始依據自身的一套行程回識訓制來判斷要 kill 掉哪些行程,以騰出記憶體來供給需要的 app 使用, 這套殺行程回收記憶體的機制就叫 Low Memory Killer,那這個不足怎么來規定呢,那就是記憶體閾值,當記憶體小于記憶體閾值就開始殺行程,記憶體閾值是存放在 /sys/module/lowmemorykiller/parameters/minfree 檔案中的,這個需要root權限才能查看,因此這里就不繼續研究記憶體閾值了,

讀到這里,如果現在記憶體不足了,要開始殺空行程了,如果空行程不止一個,難道要一次性將空行程全部殺掉嗎?當然不是的,行程是有它的優先級的,這個優先級通過行程的 oom_adj 值來反映,它是 linux 內核分配給每個行程的一個值,代表行程的優先級,行程回識訓制就是根據這個優先級來決定是否進行回收,oom_adj 的值越小,行程的優先級越高,普通行程 oom_adj 值是大于等于 0 的,而系統行程 oom_adj 的值是小于0的,

我們可以通過 cat /proc/行程id/oom_adj 可以看到當前行程的 oom_adj 值,如下圖所示:

看到 oom_adj 值為 0, 0 就是表這個行程屬于前臺行程,我們按下 Back 鍵,將應用切換到后臺,再次查看 oom_adj 值,如下所示:

oom_adj 值是多少,每個手機的廠商可能都不一樣,具體含義也就不做分析了,只用知道 oom_adj 越大,行程的優先級就越低,就越容易被系統殺掉

二、行程保活方案

2.1 開啟一個像素的Activity

基本思想,系統一般不會殺死前臺行程,所以要使得行程常駐,我們只需要在鎖屏的時候在本行程中開啟一個 Activity ,為了欺騙用戶,我們讓這個 Activity 的大小是 1 像素,并且透明無切換影片,在開螢屏的時候,把這個 Activity 關閉掉,所以這個就需要監聽系統鎖屏廣播,

創建一個一像素的 AliveActivity ,在螢屏關閉的時候把 AliveActivity 啟動起來,在開屏的時候把 AliveActivity 關閉掉,所以要監聽系統鎖屏廣播,以回呼的形式通知 MainActivity 啟動或者關閉 AliveActivity,

public interface ScreenStateCallback {
    /**
     * 開屏
     */
    void onScreenOn();
?
    /**
     * 鎖屏
     */
    void onScreenOff();
}public class ScreenStateManager {
    private Context mContext;
    private WeakReference<AliveActivity> mAliveActivityWrf;
    private static volatile ScreenStateManager instance;
    private ScreenStateReceiver mReceiver;
    private ScreenStateCallback mCallback;
?
    private ScreenStateManager(Context context) {
        this.mContext = context;
        mReceiver = new ScreenStateReceiver();
    }
?
    /**
     * 獲取ScreenStateManager物件
     *
     * @param context
     * @return
     */
    public static ScreenStateManager getInstance(Context context) {
        if (instance == null) {
            synchronized (ScreenStateManager.class) {
                if (instance == null) {
                    instance = new ScreenStateManager(context.getApplicationContext());
                }
            }
        }
        return instance;
    }
?
    /**
     * 設定監聽
     *
     * @param callback
     */
    public void setScreenStateCallback(ScreenStateCallback callback) {
        this.mCallback = callback;
    }
?
?
    /**
     * 注冊螢屏狀態廣播
     */
    public void registerScreenStateBroadcastReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_SCREEN_ON);
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        mContext.registerReceiver(mReceiver, filter);
    }
?
    /**
     * 解注冊廣播
     */
    public void unregisterScreenStateBroadcastReceiver() {
        mContext.unregisterReceiver(mReceiver);
    }
?
?
    /**
     * 設定AliveActivity
     *
     * @param aliveActivity
     */
    public void setAliveActivity(AliveActivity aliveActivity) {
        this.mAliveActivityWrf = new WeakReference<>(aliveActivity);
    }
?
    /**
     * 開啟AliveActivity
     */
    public void openAliveActivity() {
        AliveActivity.openAliveActivity(mContext);
    }
?
?
    /**
     * 關閉AliveActivity
     */
    public void closeAliveActivity() {
        if (mAliveActivityWrf != null) {
            AliveActivity aliveActivity = mAliveActivityWrf.get();
            if (aliveActivity != null) {
                aliveActivity.finish();
            }
        }
    }
?
    /**
     * 螢屏狀態廣播
     */
    private class ScreenStateReceiver extends BroadcastReceiver {
?
        @Override
        public void onReceive(Context context, Intent intent) {
            if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
                // 開屏
                if (mCallback != null) {
                    mCallback.onScreenOn();
                }
            } else if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
                // 鎖屏
                if (mCallback != null) {
                    mCallback.onScreenOff();
                }
            }
        }
    }
public class AliveActivity extends Activity {
  private static final String TAG = AliveActivity.class.getSimpleName();
?
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_alive);
?
      Log.d(TAG, "AliveActivity onCreate");
?
      // 設定為1像素
      Window window = getWindow();
      // 放在左上角
      window.setGravity(Gravity.START | Gravity.TOP);
      WindowManager.LayoutParams params = window.getAttributes();
      // 寬高設定為1個像素
      params.width = 1;
      params.height = 1;
      // 起始坐標
      params.x = 0;
      params.y = 0;
      window.setAttributes(params);
       
      ScreenStateManager.getInstance(this).setAliveActivity(this);
?
  }
?
  /**
    * 打開AliveActivity
    *
    * @param context
    */
  public static void openAliveActivity(Context context) {
      Intent intent = new Intent(context, AliveActivity.class);
      intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
      context.startActivity(intent);
  }
?
  @Override
  protected void onDestroy() {
      super.onDestroy();
      Log.d(TAG, "AliveActivity onDestroy");
  }
}

MainActivity 的修改如下:

public class MainActivity extends AppCompatActivity {
    private ScreenStateManager manager;
?
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
?
?
        manager = ScreenStateManager.getInstance(this);
        // 注冊廣播
        manager.registerScreenStateBroadcastReceiver();
        manager.setScreenStateCallback(new ScreenStateCallback() {
            @Override
            public void onScreenOn() {
                manager.closeAliveActivity();
            }
?
            @Override
            public void onScreenOff() {
                manager.openAliveActivity();
            }
        });
    }
?
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (manager != null) {
            manager.unregisterScreenStateBroadcastReceiver();
        }
    }
}

運行之后,先點擊home鍵,再進行鎖屏,結果如下

由以上結果可以看出,在鎖屏的時候 AliveActivity 已經啟動,行程的優先級也提高了,

然后再開屏,結果如下所示:

當我們開屏時 ,AliveActivity 也退出了,

缺點: 存在一個AliveActivity 不夠干凈,同時需要在鎖屏時才能監聽到,如果用戶一直處于亮屏狀態,oom_adj 的值不會變小,如果系統記憶體不足,還是會被殺死,

2.2 前臺服務

原理是通過呼叫 startForeground 方法提示 Service 的優先級,讓 Services 變為前臺服務,

對于 API level < 18:呼叫 startForeground(ID,new Notification()),發送空的Notification ,圖示則不會顯示,

對于 API level >= 18 并且 API leve < 26:

呼叫 startForeground(ID,new Notification()),發送空的Notification ,圖示卻會顯示,

因此在需要提優先級的 service A 啟動一個 InnerService,兩個服務同時 startForeground,且系結同樣的 ID,Stop 掉 InnerService ,這樣通知欄圖示即被移除,這種方式在 API leve >=26 以上就不可以生效了,即使 Stop 調 InnerServices ,通知欄依然會有顯示,

代碼如下:

public class AliveService extends Service {
    public static final int NOTIFICATION_ID = 0x001;
    private static final String NOTIFICATION_CHANNEL_ID = "alive_notification_id";
?
    @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 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            //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));
        } 
    }
?
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        return null;
    }
?
    @Override
    public void onDestroy() {
        super.onDestroy();
        stopForeground(true);
    }
?
    public static class InnerService extends Service {
        @Override
        public void onCreate() {
            super.onCreate();
                //發送與ALiveService中ID相同的Notification,然后將其取消并取消自己的前臺顯示
                Notification.Builder builder = new Notification.Builder(this);
                builder.setSmallIcon(R.mipmap.ic_launcher);
                startForeground(NOTIFICATION_ID, builder.build());
?
                // 100ms 銷毀
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        stopForeground(true);
                        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                        manager.cancel(NOTIFICATION_ID);
                        stopSelf();
                    }
                }, 100);
            } else {
                //發送與ALiveService中ID相同的Notification,然后將其取消并取消自己的前臺顯示
                Notification notification = new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
                        .setSmallIcon(R.mipmap.ic_launcher)
                        .build();
                notification.flags |= Notification.FLAG_NO_CLEAR;
                startForeground(NOTIFICATION_ID, notification);
?
                // 100ms 銷毀
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        stopForeground(true);
                        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                        manager.cancel(NOTIFICATION_ID);
                        stopSelf();
                    }
                }, 100);
            }
        }
?
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    }
}
在沒有采取前臺服務之前,啟動應用,oom_adj 值是 0,按下回傳鍵之后,變成 8(不同ROM可能不一樣),如下圖所示:

在采取前臺服務之后,啟動應用,oom_adj 值是 0,按下回傳鍵之后,變成 1(不同ROM可能不一樣),確實行程的優先級有所提高,

三、行程拉活

3.1 相互喚醒

相互喚醒的意思就是,假如你手機里裝了支付寶、淘寶、天貓、UC等阿里系的 app,那么你打開任意一個阿里系的 app 后,有可能就順便把其他阿里系的 app 給喚醒了,這個完全有可能的,

如果應用想保活,要是 QQ,微信愿意救你也行,有多少手機上沒有 QQ,微信呢?或者像友盟、小米、華為、信鴿這種推送 SDK,也存在喚醒 app 的功能,

3.2 廣播拉活

通過接收系統廣播去拉活行程,但是 Android 在7.0之后對廣播增加了一些限制,在8.0以后就更加嚴格了,現在接收系統廣播的拉活方式基本上已經用不了了,

3.3 JobScheduler

JobScheduler 簡單來說就是一個系統定時任務,在 app 達到一定條件時可以指定執行任務,且如果 app 被強迫終止,此前預定的任務還可執行,與普通定時器不同的是其調度由系統來完成,因此可以用來做行程保活,

JobSchedule 需要在android 5.0系統以上才能使用,

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

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class AliveJobService extends JobService {
    private static final String TAG = AliveJobService.class.getSimpleName();
?
    public static void startJobScheduler(Context context) {
        try {
            JobScheduler jobScheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE);
            JobInfo.Builder builder = new JobInfo.Builder(1,
                    new ComponentName(context.getPackageName(), AliveJobService.class.getName()));
?
            // 設定設備重啟依然執行
            builder.setPersisted(true);
?
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                //7.0以上延遲1s執行
                builder.setMinimumLatency(1000);
            } else {
                //每隔1s執行一次job
                builder.setPeriodic(1000);
            }
?
            jobScheduler.schedule(builder.build());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
?
    @Override
    public boolean onStartJob(JobParameters params) {
        Log.e(TAG, "開啟job");
        // 7.0 以上開啟輪詢
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            startJobScheduler(this);
        }
        return false;
    }
?
    @Override
    public boolean onStopJob(JobParameters params) {
        return false;
    }
?
}

在 AndroidManifest.xml 中配置 Service ,

<service
    android:name=".service.AliveJobService"
    android:enabled="true"
    android:exported="true"
    android:permission="android.permission.BIND_JOB_SERVICE" />

在 MainActivity 的 onCreate 方法中呼叫,

AliveJobService.startJobScheduler(this);

運行,然后在 kill ,最后 Jobschedule 依然在運行,而且行程重新啟動,結果如下所示:

3.4 雙行程守護

雙行程守護本質上是開啟兩個行程,一個主行程(包含一個本地服務)和一個子行程(包含一個遠程服務),當其中一個行程被殺死時,另一個行程會自動的把被殺死的那個行程拉活,原理圖如下所示:

(1) 實作一個 AIDL 檔案

此處僅僅是為了拉活,不需要遠程呼叫某些功能,可以不用具體實作,但是不能缺少,創建程序如下所示:

代碼如下所示:

// IAliveAidlInterface.aidl
package com.lx.keep.alive;

// Declare any non-default types here with import statements

interface IAliveAidlInterface {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

(2) 實作主行程的本地服務

public class LocalAliveService extends Service {
    private static final String TAG = LocalAliveService.class.getSimpleName();
    private ServiceConnection mConnection;
    private LocalAliveBinder mBinder;

    @Override
    public void onCreate() {
        super.onCreate();
        mBinder = new LocalAliveBinder();
        mConnection = new LocalAliveServiceConnect();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //系結本地守護Service
        bindService(new Intent(this, RemoteAliveService.class), mConnection, BIND_AUTO_CREATE);
        return super.onStartCommand(intent, flags, startId);
    }

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

    class LocalAliveServiceConnect implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //服務連接后回呼
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(TAG, "遠程子行程可能被干掉了,拉活");
            //連接中斷后回呼,再啟動子行程所在的Service,并進行系結,通過啟動子行程的遠程服務強行拉活
            startService(new Intent(LocalAliveService.this, RemoteAliveService.class));
            bindService(new Intent(LocalAliveService.this, RemoteAliveService.class), mConnection,
                    BIND_AUTO_CREATE);
        }
    }

    class LocalAliveBinder extends IAliveAidlInterface.Stub {

        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

        }
    }
}

(3) 實作子行程的遠程服務

public class RemoteAliveService extends Service {
    private static final String TAG = RemoteAliveService.class.getSimpleName();
    private ServiceConnection mConnection;
    private RemoteAliveBinder mBinder;

    @Override
    public void onCreate() {
        super.onCreate();
        mBinder = new RemoteAliveBinder();
        mConnection = new RemoteAliveServiceConnect();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //系結本地守護Service,必須實作AIDL否則bindService在這沒有作用
        bindService(new Intent(this, LocalAliveService.class), mConnection, BIND_AUTO_CREATE);
        return super.onStartCommand(intent, flags, startId);
    }

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

    class RemoteAliveServiceConnect implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //服務連接后回呼
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(TAG, "本地主行程可能被干掉了,拉活");
            //連接中斷后回呼,再啟動主行程所在的Service,并進行系結,通過啟動主行程的本地服務強行拉活
            startService(new Intent(RemoteAliveService.this, LocalAliveService.class));
            bindService(new Intent(RemoteAliveService.this, LocalAliveService.class), mConnection,
                    BIND_AUTO_CREATE);
        }
    }

    class RemoteAliveBinder extends IAliveAidlInterface.Stub {

        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

        }
    }
}

(4) AndroidManifest.xml 配置 Service 如下:

<service
    android:name=".service.RemoteAliveService"
    android:enabled="true"
    android:exported="true"
    android:process=":remote"></service>
<service
    android:name=".service.LocalAliveService"
    android:enabled="true"
    android:exported="true"/>

(5) 開啟雙行程守護

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            //雙行程Service守護
            startService(new Intent(this, LocalAliveService.class));//啟動主執行緒守護服務
            startService(new Intent(this, RemoteAliveService.class));//啟動主執行緒守護服務
        }
    }
}

說明: 在 Android O之后,當應用進入后臺時,過一段時間就會變成 idle 狀態,這時就不能通過 startService 啟動一個服務,不然會報如下錯誤:

在 Android O 之后啟動服務通過 startForegroundService() 啟動一個前臺服務,并且在系統創建 Service 后,需要在一定時間內呼叫startForeground( )讓 Service 為用戶可見通知,否則則系統將停止此 Service,拋出 ANR,但是會在通知欄有提示框,上文有說明,

因此兼容方案就根據實際情況而選擇了,

運行結果如下所示:

3.5 賬戶同步拉活

手機系統設定里會有 Account 賬戶一項功能,任何第三方 App 都可以通過此功能將我們自己的 App 注冊到這個 Account 賬戶中,并且將資料在一定時間內同步到服務器中去,系統在將 App 賬戶同步時,自動將為開啟的 App 行程拉活

(1) 開啟賬戶服務

AuthenticationService 繼承自 Service 本質上是一個 AIDL ,提供給其他的行程使用的,主要我們實作并且宣告了之后,android 系統會通過 android.accounts.AccountAuthenticator 這個 Action 找到它,并通過它來把我們自己的賬號注冊到系統設定界面,其中AccountAuthenticator 是一個繼承自 AbstractAccountAuthenticator 的類,而 AbstractAccountAuthenticator 是用于實作對手機系統設定里“賬號與同步”中 Account 的添加、洗掉和驗證等一些基本功能,很明顯 AbstractAccountAuthenticator 里面有個繼承于IAccountAuthenticator.Stub 的內部類,以用來對 AbstractAccountAuthenticator 的遠程介面呼叫進行包裝,所以可以通過AbstractAccountAuthenticator 的 getIBinder() 方法,回傳內部類的 IBinder 形式,

public class AuthenticationService extends Service {
    private AccountAuthenticator accountAuthenticator;


    @Override
    public void onCreate() {
        super.onCreate();
        accountAuthenticator = new AccountAuthenticator(this);
    }

    @Override
    public IBinder onBind(Intent intent) {
        // 回傳操作資料的Binder
        return accountAuthenticator.getIBinder();
    }

    /**
     * 賬戶操作類
     */
    static class AccountAuthenticator extends AbstractAccountAuthenticator {

        public AccountAuthenticator(Context context) {
            super(context);
        }

        @Override
        public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
            return null;
        }

        @Override
        public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
            return null;
        }

        @Override
        public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
            return null;
        }

        @Override
        public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
            return null;
        }

        @Override
        public String getAuthTokenLabel(String authTokenType) {
            return null;
        }

        @Override
        public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
            return null;
        }

        @Override
        public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
            return null;
        }
    }
}

在 AndroidManifest.xml 中配置 service,

<service
    android:name=".service.AuthenticationService"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.accounts.AccountAuthenticator"/>
    </intent-filter>
    <meta-data
        android:name="android.accounts.AccountAuthenticator"
        android:resource="@xml/authenticator"/>
</service>

在 xml 中添加 authenticator.xml ,

其中icon、label分別是Account串列中的圖示和顯示名稱,而accountType則是操作用戶所必須的引數之一

<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.lx.keep.alive.account"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name" />

<!--accountType表示賬戶型別,必須唯一-->

經過以上步驟之后,安裝 Apk,再次打開設定中賬戶 -> 添加賬戶,你會發現原來的 Account 串列多了一行資料,說明我們的 App 也可以支持這個Account 系統了,

(2) 添加賬戶

public class AccountHelper {
    // 與 authenticator.xml 中  accountType 一致
    private static final String ACCOUNT_TYPE = "com.lx.keep.alive.account";

    /**
     * 添加賬戶 需要 "android.permission.GET_ACCOUNTS"權限
     * @param context
     */
    public static void addAccount(Context context){
        AccountManager accountManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
        Account[] accounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
        if(accounts.length >0 ){
            // 賬戶已存在
            return;
        }

        Account account = new Account("test",ACCOUNT_TYPE);
        // 添加賬戶
        accountManager.addAccountExplicitly(account,"123456",new Bundle());
    }
}

呼叫 addAccount 這個方法之后就會在系統設定的 Account 界面多了一個 Account ,如下圖所示:

(3) 同步服務

創建一個 Service 作為同步 Service,并且在 onBind 回傳 AbstractThreadedSyncAdapter 的 getSyncAdapterBinder,

public class SyncAccountService extends Service {
    private static final String TAG = SyncAccountService.class.getSimpleName();
    private SyncAdapter syncAdapter;

    @Override
    public void onCreate() {
        super.onCreate();
        syncAdapter = new SyncAdapter(this, true);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return syncAdapter.getSyncAdapterBinder();
    }

    static class SyncAdapter extends AbstractThreadedSyncAdapter {

        public SyncAdapter(Context context, boolean autoInitialize) {
            super(context, autoInitialize);
        }

        @Override
        public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
            Log.d(TAG, "賬戶同步了");
        }
    }
}

在 AndroidManifest.xml 中配置 service,

<service
    android:name=".service.SyncAccountService"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.content.SyncAdapter" />
    </intent-filter>
    <meta-data
        android:name="android.content.SyncAdapter"
        android:resource="@xml/sync_account_adapter" />
</service>

在 xml 中添加 sync_account_adapter.xml ,

<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.lx.keep.alive.account"
    android:allowParallelSyncs="true"
    android:contentAuthority="com.lx.keep.alive.provider"
    android:isAlwaysSyncable="true"
    android:supportsUploading="false"
    android:userVisible="false" />

<!--contentAuthority 系統在進行賬戶同步的時候會查找 此auth的ContentProvider-->
<!--accountType表示賬戶型別,與authenticator.xml里要一致-->
<!-- userVisible 是否在“設定”中顯示-->
<!-- supportsUploading 是否必須notifyChange通知才能同步-->
<!-- allowParallelSyncs 允許多個賬戶同時同步-->
<!--isAlwaysSyncable 設定所有賬號的isSyncable為1-->

(4) 創建 ContentProvider

public class AccountProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
}

在 AndroidManifest.xml 中配置 ContentProvider ,

<provider
    android:name=".helper.AccountProvider"
    android:authorities="com.lx.keep.alive.provider"
    android:exported="false" />

(5) 開啟同步

為了達到行程保活的效果,可以開啟自動同步,時間間隔雖然設定了1s,但是 Android 本身為了考慮同步所帶來的消耗和減少喚醒設備的次數,1s只是一個參考時間

public class AccountHelper {
    // 與 authenticator.xml 中  accountType 一致
    private static final String ACCOUNT_TYPE = "com.lx.keep.alive.account";
    private static final String CONTENT_AUTHORITY = "com.lx.keep.alive.provider";

    // ...

    /**
     * 設定賬戶同步,即告知系統我們需要系統為我們來進行賬戶同步,只有設定了之后系統才會自動去
     * 觸發SyncAccountAdapter#onPerformSync方法
     */
    public static void autoSyncAccount(){
        Account account = new Account("test",ACCOUNT_TYPE);
        // 設定同步
        ContentResolver.setIsSyncable(account,CONTENT_AUTHORITY,1);
        // 設定自動同步
        ContentResolver.setSyncAutomatically(account,CONTENT_AUTHORITY,true);
        // 設定同步周期
        ContentResolver.addPeriodicSync(account,CONTENT_AUTHORITY,new Bundle(),1);
    }

}

最后在 MainActivity 的 onCreate 中呼叫

AccountHelper.addAccount(this);
AccountHelper.autoSyncAccount();

測驗, 將 App 運行到手機之后,然后殺掉行程,當賬戶開始同步時,行程又啟動了,結果如下:

以上就是利用賬戶同步進行拉活的主要核心思想,測驗程序中發現,不同系統表現不同,至于同步周期完全是由系統進行控制的,雖然比較穩定但是周期不可控,

掃描下方二維碼關注公眾號,獲取更多技術干貨,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/240083.html

標籤:其他

上一篇:gradle版本和插件以及buildToolsVersion之間的對應關系

下一篇:Android studio如何查看本地資料庫

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more