1. 程式人生 > >[Android Framework] 8.1 Doze模式分析(一)——Doze簡介和DeviceIdleController的啟動

[Android Framework] 8.1 Doze模式分析(一)——Doze簡介和DeviceIdleController的啟動

概述

Doze模式,官方翻譯為低電耗模式,是Andoriod6.0增加的一項系統服務,主要目的是為了優化電池效能,增加電池續航時間,Doze模式又分兩種模式:深度Doze模式(Deep Doze)和輕度Doze模式(Light Doze),如果使用者長時間沒有主動使用其裝置,處於靜止狀態且螢幕已關閉,則系統會使裝置進入Doze模式,也就是深度Doze模式。如果使用者關閉裝置螢幕但仍處於移動狀態時,則裝置進入輕度Doze模式,此外,輕度Doze模式只適合Android7.0及以上版本。

當用戶長時間未使用裝置時,裝置進入Doze模式,Doze模式會延遲應用後臺 CPU 和網路活動,從而延長電池續航時間。處於Doze模式的裝置會定期進入維護時段,在此期間,應用可以完成待進行的活動。然後,Doze模式會使裝置重新進入較長時間的休眠狀態,接著進入下一個維護時段。在達到幾個小時的休眠時間上限之前,平臺會周而復始地重複Doze模式休眠/維護的序列,且每一次都會延長Doze模式時長。處於Doze模式的裝置始終可以感知到動作,且會在檢測到動作時立即退出Doze模式。整個Doze圖示如下:
這裡寫圖片描述

Deep Doze 和Light Doze模式對比如下:

操作 低電耗模式 輕度低電耗模式
觸發因素 螢幕關閉、電池供電、靜止 螢幕關閉、電池供電(未插電)
時間 隨維護時段依次增加 隨維護時段反覆持續 N 分鐘
限制 無法進行網路訪問、喚醒鎖忽略、 GPS/WLAN無法掃描、鬧鐘和SyncAdapter/JobScheduler被延遲。 無法進行網路訪問、SyncAdapter/JobScheduler
行為 僅接收優先順序較高的推送通知訊息。 接收所有實時訊息(即時訊息、來電等)。優先順序較高的推送通知訊息可以暫時訪問網路。
退出 裝置有移動、和使用者有互動、螢幕開啟、鬧鐘響鈴 螢幕開啟。

接下來就分析Doze的實現原理。Doze模式是通過DeviceIdleController來實現的。

1.DeviceIdleController的啟動流程

DeviceIdleController(以下簡稱DIC)繼承於SystemService,因此也是一個系統服務,在分析PMS的時候說過,繼承於SytemService的服務啟動有以下幾個共同點:

  • 1.在SystemServer中例項化並啟動,啟動時會執行SytemService的生命週期方法:
    Constructor()->onStart()->onBootPhase()
  • 2.內部維護一個Binder和其他服務進行IPC通訊;
  • 3.內部維護一個Internal類用於和System程序進行互動;

下面就從SystemServer開始分析DIC的啟動流程。

在SystemServer啟動其他服務時啟動DIC:

private void startOtherServices() {
    .............
mSystemServiceManager.startService(DeviceIdleController.class);
............
}

在SystemServiceManager中,通過反射的方式獲取了DIC物件,並且呼叫了onStart()方法:

public <T extends SystemService> T startService(Class<T> serviceClass) {
    try {
        ....................
        final String name = serviceClass.getName();
        Slog.i(TAG, "Starting " + name);
        final T service;
        try {
            //通過反射獲取例項
            Constructor<T> constructor = serviceClass.getConstructor(Context.class);
            service = constructor.newInstance(mContext);
        } catch (InstantiationException ex) {
        }
        //呼叫同名過載方法
        startService(service);
        return service;
    } 
    ....................
}
public void startService(@NonNull final SystemService service) {
    // 加入到mServices列表中
    mServices.add(service);
    long time = SystemClock.elapsedRealtime();
    try {
        //呼叫onStart()開始服務
        service.onStart();
    } catch (RuntimeException ex) {
    }
}

執行到這裡,DIC的啟動就開始了,再來看看最後一個生命週期方法onBootPhase()的呼叫,這個方法表示啟動服務的過程,在SystemServer中會呼叫多次,從而在不同的啟動階段完成不同的工作,程式碼如下:

private void startOtherServices() {
    .............
    mSystemServiceManager.startBootPhase(SystemService.
             PHASE_LOCK_SETTINGS_READY);
    .............
    mSystemServiceManager.startBootPhase(SystemService.
             PHASE_SYSTEM_SERVICES_READY);
    .............
mSystemServiceManager.startBootPhase(
        SystemService.PHASE_ACTIVITY_MANAGER_READY);
    .............
    mSystemServiceManager.startBootPhase(
        SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
    .............
}

在SystemServiceManager中的startBootPhase()方法中,遍歷已啟動的服務列表,呼叫onBootPhase():

public void startBootPhase(final int phase) {
    if (phase <= mCurrentPhase) {
        throw new IllegalArgumentException("Next phase must be larger than previous");
    }
    mCurrentPhase = phase;
    Slog.i(TAG, "Starting phase " + mCurrentPhase);
    try {
        for (int i = 0; i < serviceLen; i++) {
            final SystemService service = mServices.get(i);
            long time = SystemClock.elapsedRealtime();
            try {
                //呼叫每個service的onBootPhase()
                service.onBootPhase(mCurrentPhase);
            } catch (Exception ex) {
            }
        }
    } 
}

當這個方法執行完畢後,DIC的啟動就完成了,下面我們看看DIC的生命週期方法中做了什麼。首先看其構造方法:

public DeviceIdleController(Context context) {
    super(context);
    //建立可以執行原子操作的檔案,/data/system/deviceidle.xml
    mConfigFile = new AtomicFile(new File(getSystemDir(), "deviceidle.xml"));
    //建立Handler,和BackgroundThread的Looper進行繫結
    mHandler = new MyHandler(BackgroundThread.getHandler().getLooper());
}

構造方法中,首先建立在/data/sytem下建立了deviceidle.xml檔案,然後建立了一個Handler,並將BackgroundThread中Handler的Looper和該Handler進行繫結。BackgroundThread是一個單例模式實現的後臺執行緒,可以用於任何一個程序。

再來看看DIC的onStart()方法:

@Override
public void onStart() {
    final PackageManager pm = getContext().getPackageManager();

    synchronized (this) {
        //Light doze模式和Deep Doze是否可用,預設不可用
        mLightEnabled = mDeepEnabled = getContext().getResources().getBoolean(
                com.android.internal.R.bool.config_enableAutoPowerModes);
        //獲取系統全域性配置資訊,如許可權
        SystemConfig sysConfig = SystemConfig.getInstance();
        //從配置檔案中讀取省電模式下的白名單列表且不在在idle狀態的白名單列表,即列表中的app能夠在省電模式下在後臺執行,
        ArraySet<String> allowPowerExceptIdle = sysConfig.getAllowInPowerSaveExceptIdle();
        for (int i=0; i<allowPowerExceptIdle.size(); i++) {
            String pkg = allowPowerExceptIdle.valueAt(i);
            try {
                //獲取白名單列表中的系統應用
                ApplicationInfo ai = pm.getApplicationInfo(pkg,
                        PackageManager.MATCH_SYSTEM_ONLY);
                int appid = UserHandle.getAppId(ai.uid);
                //新增到Map中,表示這些應用在省電模式下可後臺執行,但在Doze下後臺不可執行
                mPowerSaveWhitelistAppsExceptIdle.put(ai.packageName, appid);
                //新增到SpareArray中,表示這是處於powersave白名單但不處於doze白名單的系統應用                
                mPowerSaveWhitelistSystemAppIdsExceptIdle.put(appid, true);
            } catch (PackageManager.NameNotFoundException e) {
            }
        }
        //從配置檔案中讀取時能夠在省電模式白名單列表,即包括不在doze白名單中的應用,也包括所有的白名單上的應用
        ArraySet<String> allowPower = sysConfig.getAllowInPowerSave();
        for (int i=0; i<allowPower.size(); i++) {
            String pkg = allowPower.valueAt(i);
            try {
                ApplicationInfo ai = pm.getApplicationInfo(pkg,
                        PackageManager.MATCH_SYSTEM_ONLY);
                int appid = UserHandle.getAppId(ai.uid);
                mPowerSaveWhitelistAppsExceptIdle.put(ai.packageName, appid);
                mPowerSaveWhitelistSystemAppIdsExceptIdle.put(appid, true);
                //新增到DIC中的白名單列表中
                mPowerSaveWhitelistApps.put(ai.packageName, appid);
                //新增到DIC中的系統應用白名單列表中
                mPowerSaveWhitelistSystemAppIds.put(appid, true);
            } catch (PackageManager.NameNotFoundException e) {
            }
        }
        //設定Contants內容觀察者
        mConstants = new Constants(mHandler, getContext().getContentResolver());
        //讀取/data/system/deviceidle.xml檔案,將讀取的app新增到表示使用者設定的白名單中
        readConfigFileLocked();
        //更新白名單列表
        updateWhitelistAppIdsLocked();
        //網路是否連線
        mNetworkConnected = true;
        //螢幕是否保持常亮
        mScreenOn = true;
        // Start out assuming we are charging.  If we aren't, we will at least get
        // a battery update the next time the level drops.
        //是否充電
        mCharging = true;
        mState = STATE_ACTIVE;//裝置保持活動狀態,深度Doze的初始值
        mLightState = LIGHT_STATE_ACTIVE;//裝置保持活動狀態,輕度Doze的初始值
        mInactiveTimeout = mConstants.INACTIVE_TIMEOUT;
    }
    mBinderService = new BinderService();
    //釋出遠端服務
    publishBinderService(Context.DEVICE_IDLE_CONTROLLER, mBinderService);
    //釋出本地服務
    publishLocalService(LocalService.class, new LocalService());
}

在onStart()方法中,首先通過SystemConfig讀取了兩類白名單列表:在低電量模式下後臺允許執行的應用的白名單、在低電量模式和Doze模式下都允許後臺執行的應用白名單;
然後讀取配置檔案/data/system/deviceidle.xml,將讀取的應用包名新增到mPowerSaveWhiteListUserApps這個Map中,表示使用者新增到白名單中的應用。接下來更新白名單列表,然後給成員變數進行初始化。
最後,釋出了遠端服務和本地服務,從而可以和其他服務進行互動。釋出後其他服務中就可以通過這種獲取到DIC的BinderService從而進行互動:

IDeviceIdleController dic = IDeviceIdleController.Stub.asInterface(
        ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));

獲取本地服務通過如下方式:

mLocalDeviceIdleController
        = LocalServices.getService(DeviceIdleController.LocalService.class);

在onStart()方法中還呼叫了用來更新白名單列表的方法,來看下這個方法:


private void updateWhitelistAppIdsLocked() {
    //處於省電模式白名單但不處於Idle狀態白名單的app
    mPowerSaveWhitelistExceptIdleAppIdArray = 
            buildAppIdArray(mPowerSaveWhitelistAppsExceptIdle,
            mPowerSaveWhitelistUserApps, mPowerSaveWhitelistExceptIdleAppIds);
    //處於所有的白名單的app,包括使用者新增的
    mPowerSaveWhitelistAllAppIdArray = buildAppIdArray(mPowerSaveWhitelistApps,
            mPowerSaveWhitelistUserApps, mPowerSaveWhitelistAllAppIds);
    //使用者新增的白名單列表應用
    mPowerSaveWhitelistUserAppIdArray = buildAppIdArray(null,
            mPowerSaveWhitelistUserApps, mPowerSaveWhitelistUserAppIds);
    if (mLocalActivityManager != null) {
        //將適用於所有情況的白名單列表通知給AMS
        mLocalActivityManager.setDeviceIdleWhitelist
               (mPowerSaveWhitelistAllAppIdArray);
    }
    if (mLocalPowerManager != null) {
        //將適用於所有情況的白名單列表通知給PMS
        mLocalPowerManager.setDeviceIdleWhitelist(
                   mPowerSaveWhitelistAllAppIdArray);
    }
    if (mLocalAlarmManager != null) {
        //將使用者新增到白名單列表中的應用通知給AlarmManagerService
       mLocalAlarmManager.setDeviceIdleUserWhitelist
                    (mPowerSaveWhitelistUserAppIdArray);
    }
}

在這個方法中,會將三類白名單列表中的應用id新增到一個int陣列中,該方法在新增應用到白名單列表、將應用移除白名單列表,都會呼叫該方法來更新白名單列表。

執行完onStart()方法後,就開始執行最後一個生命週期方法onBootPhase()方法了,該方法如下:


@Override
public void onBootPhase(int phase) {
    if (phase == PHASE_SYSTEM_SERVICES_READY) {
        synchronized (this) {
            mAlarmManager = (AlarmManager) getContext().getSystemService
                    (Context.ALARM_SERVICE);
            mBatteryStats = BatteryStatsService.getService();
            mLocalActivityManager = getLocalService(ActivityManagerInternal.class);
            mLocalPowerManager = getLocalService(PowerManagerInternal.class);
            mPowerManager = getContext().getSystemService(PowerManager.class);
            //申請一個wakelock鎖保持CPU喚醒
            mActiveIdleWakeLock = mPowerManager.newWakeLock
                     (PowerManager.PARTIAL_WAKE_LOCK,"deviceidle_maint");
            //設定wakelock鎖為非計數鎖,只要執行一次release()就能釋所有非計數鎖
            mActiveIdleWakeLock.setReferenceCounted(false);
            mGoingIdleWakeLock = mPowerManager.newWakeLock
                   (PowerManager.PARTIAL_WAKE_LOCK,"deviceidle_going_idle");
            //設定wakelock鎖為計數鎖,一次申請對應一次釋放
            mGoingIdleWakeLock.setReferenceCounted(true);
            mConnectivityService = (ConnectivityService)ServiceManager.getService(
                    Context.CONNECTIVITY_SERVICE);
            mLocalAlarmManager = getLocalService(AlarmManagerService.
                    LocalService.class);
            mNetworkPolicyManager = INetworkPolicyManager.Stub.asInterface(
                    ServiceManager.getService
                    (Context.NETWORK_POLICY_SERVICE));
            mSensorManager = (SensorManager)getContext().getSystemService
                   (Context.SENSOR_SERVICE);
            //可用於自動省電模式時的感測器id,0表示沒有可用感測器
            int sigMotionSensorId = getContext().getResources().getInteger(
                    com.android.internal.R.integer.config_
                    autoPowerModeAnyMotionSensor);
            if (sigMotionSensorId > 0) {
                //根據感測器id獲取感測器
                mMotionSensor = mSensorManager.getDefaultSensor
                     (sigMotionSensorId, true);
            }
            //如果沒有指定任一感測器&&在沒有指定感測器情況下首選WristTilt
            //感測器配置為true
            if (mMotionSensor == null && getContext().getResources().getBoolean(
                    com.android.internal.R.bool.config_
                    autoPowerModePreferWristTilt)) {
                //獲取一個WristTilt感測器(手腕抖動)
                mMotionSensor = mSensorManager.getDefaultSensor(
                        Sensor.TYPE_WRIST_TILT_GESTURE, true);
            }
            if (mMotionSensor == null) {
                //如果以上條件都不滿足,則獲取一個SMD感測器
                mMotionSensor = mSensorManager.getDefaultSensor(
                        Sensor.TYPE_SIGNIFICANT_MOTION, true);
            }
            //是否在進入Doze模式時預先獲取位置
            if (getContext().getResources().getBoolean(
                    com.android.internal.R.bool
                    .config_autoPowerModePrefetchLocation)) {
                mLocationManager = (LocationManager) getContext().
                        getSystemService(Context.LOCATION_SERVICE);
                mLocationRequest = new LocationRequest()
                    .setQuality(LocationRequest.ACCURACY_FINE)
                    .setInterval(0)
                    .setFastestInterval(0)
                    .setNumUpdates(1);
            }
            //自動省電模式下感測器檢測的閾值度
            float angleThreshold = getContext().getResources().getInteger(
                    com.android.internal.R.integer.config_
                    autoPowerModeThresholdAngle) / 100f;
            //用於檢測裝置是否已靜止
            mAnyMotionDetector = new AnyMotionDetector(
                    (PowerManager) getContext().getSystemService
                    (Context.POWER_SERVICE),
                    mHandler, mSensorManager, this, angleThreshold);
            //用於Doze狀態發生改變時傳送廣播
            mIdleIntent = new Intent(PowerManager.ACTION_DEVICE
                      _IDLE_MODE_CHANGED);
            mIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
                    | Intent.FLAG_RECEIVER_FOREGROUND);
            //用於當輕度Doze狀態發生改變時傳送廣播
            mLightIdleIntent = new Intent(PowerManager.ACTION_LIGHT_
                    DEVICE_IDLE_MODE_CHANGED);
            mLightIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
                    | Intent.FLAG_RECEIVER_FOREGROUND);
            //註冊監聽電池狀態改變的廣播
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_BATTERY_CHANGED);
            getContext().registerReceiver(mReceiver, filter);
            //註冊監聽解除安裝應用的廣播
            filter = new IntentFilter();
            filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
            filter.addDataScheme("package");
            getContext().registerReceiver(mReceiver, filter);
            //註冊監聽網路連線改變的廣播
            filter = new IntentFilter();
            filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
            getContext().registerReceiver(mReceiver, filter);
            //註冊監聽亮滅屏的廣播
            filter = new IntentFilter();
            filter.addAction(Intent.ACTION_SCREEN_OFF);
            filter.addAction(Intent.ACTION_SCREEN_ON);
            getContext().registerReceiver(mInteractivityReceiver, filter);
            //將適用於所有情況的白名單列表通知給AMS、PMS、AlarmManagerService
            mLocalActivityManager.setDeviceIdleWhitelist(
                    mPowerSaveWhitelistAllAppIdArray);
            mLocalPowerManager.setDeviceIdleWhitelist(
                    mPowerSaveWhitelistAllAppIdArray);
            mLocalAlarmManager.setDeviceIdleUserWhitelist(
                     mPowerSaveWhitelistUserAppIdArray)
            //更新互動狀態
            updateInteractivityLocked();
        }
        //更新網路連線狀態
        updateConnectivityState(null);
    } else if (phase == PHASE_BOOT_COMPLETED) {
    }
}

首先,只有在SystemServer中啟動階段為PHASE_SYSTEM_SERVICE_READY時,才會進入到onBootPhase()方法,也就是說,DIC的onBootPhase()方法在到達這一啟動階段時才執行。在onBootPhase()方法中,獲取了一些系統服務管理類和用於System程序的本地服務,如AlarmManager、PowerManagerInternal等,用於和它們對應的系統服務進行互動;
其次獲取一個Sensor,具體獲取哪種型別的Sensor根據判斷條件而定;接下來獲取了一個AnyMotionDector物件,該物件用來檢測裝置是否處於禁止狀態;然後註冊了四種廣播的監聽,分別是:

  • 1.電源狀態發生改變時的廣播:Intent.ACTION_BATTERY_CHANGED,由BatteryService中傳送。
  • 2.解除安裝應用時的廣播:Intent.ACTION_PACKAGE_REMOVED,由PKMS傳送;
  • 3.網路連線狀態改變時的廣播:ConnectivityManager.CONNECTIVITY_ACTION
  • 4亮滅屏時的廣播:Intent.SCREEN_ON/SCREEN_OFF,由PMS相關的Notifier傳送;

最後,呼叫了一個用於更新互動狀態的updateInteractivityLocked()方法和用於更新網路狀態的updateConnectivityState()方法。先看看updateInteractivityLocked()方法:

void updateInteractivityLocked() {
    // The interactivity state from the power manager tells us whether the display is
    // in a state that we need to keep things running so they will update at a normal
    // frequency.
    //獲取裝置是否處於互動狀態
    boolean screenOn = mPowerManager.isInteractive();
    if (DEBUG) Slog.d(TAG, "updateInteractivityLocked: screenOn=" + screenOn);
    //表示當前不處於互動狀態且上次處於互動狀態
    if (!screenOn && mScreenOn) {
        mScreenOn = false;
        if (!mForceIdle) {//是否強制進入Idle
            //進入Idle模式的入口方法
            becomeInactiveIfAppropriateLocked();
        }
    } else if (screenOn) {
        mScreenOn = true;
        if (!mForceIdle) {
            //退出Idle模式
            becomeActiveLocked("screen", Process.myUid());
        }
    }
}

這個方法在Android 8.0及以前,是updateDisplayLocked(),在8.1中,修改了名字。這個方法中通過裝置當前的互動狀態,決定是否進入Doze模式或者退出Doze模式,如果當前處於不互動狀態(PMS中分析過,wakefulness=asleep或者wakefulness=doze),且上次處於互動狀態,則會呼叫becomeInteractiveIfAppropriateLocked()方法,開始準備進入Doze的工作;如果條件相反,呼叫becomeActiveLocked(),做退出Doze模式的工作。這兩個方法在下面的內容中進行分析。

分析到這裡,DIC的啟動流程就完畢了。啟動流程圖及主要工作如下所示:
這裡寫圖片描述

下篇文章中將分析Light Doze模式的實現流程。