手机也打盹儿之Doze模式源码分析

科技的仿生学无处不在,给予我们启发。为了延长电池是使用寿命,google从蛇的冬眠中得到体会,那就是在某种情况下也让手机进入类冬眠的情况,从而引入了今天的主题,Doze模式,Doze中文是打盹儿,打盹当然比活动节约能量了。

手机打盹儿的时候会怎样呢?####

Doze.png

按照google的官方说法,Walklocks,网络访问,jobshedule,闹钟,GPS/WiFi扫描都会停止。这些停止后,将会节省30%的电量。

手机什么时候才会开始打盹呢?####

Doze时序图.png

上图是谷歌的Doze时序示意图,可以看出让手机打盹要满足三个条件

1.屏幕熄灭
2 .不插电
3.静止不动

这个是不是很仿生学呢?屏幕熄灭->闭上双眼,不插电->不吃东西,静止不动->安静地做个睡美人。生物不也是要满足这些条件才能打盹吗?妙,是在妙!

打盹总得呼吸吧?上图中的maintenance window就是给你呼吸的!!呼吸的时候Walklocks,网络访问,jobshedule,闹钟,GPS/WiFi扫描这些都会恢复,来吧重重的吸一口新鲜空气吧!随着时间的推移,呼吸的间隔会越变越大,而每次呼吸的时间也会变长,当然,伙计,不会无限长!!最后都会归于一个定值。下面分析源码就知道了,biu!

没源码,说个球儿####

下面以一台手机静静地放在桌面上,随着时间的推移,进入doze模式的过程来分析源码。
源码路径:/frameworks/base/services/core/java/com/android/server/DeviceIdleController.java
系统中用一个全局整形变量来表示当前doze的状态

private int mState;

状态值的可能取值有以下,一开始的状态是STATE_ACTIVE。会依次经过1,2,3,4,状态后进入5状态,即STATE_IDLE

    private static final int STATE_ACTIVE = 0;
    private static final int STATE_INACTIVE = 1;
    private static final int STATE_IDLE_PENDING = 2;
    private static final int STATE_SENSING = 3;
    private static final int STATE_LOCATING = 4;
    private static final int STATE_IDLE = 5;
    private static final int STATE_IDLE_MAINTENANCE = 6;

首先屏幕熄灭,回调熄屏处理函数

    private final DisplayManager.DisplayListener mDisplayListener
            = new DisplayManager.DisplayListener() {
        @Override public void onDisplayAdded(int displayId) {
        }

        @Override public void onDisplayRemoved(int displayId) {
        }

        @Override public void onDisplayChanged(int displayId) {
            if (displayId == Display.DEFAULT_DISPLAY) {
                synchronized (DeviceIdleController.this) {
                    updateDisplayLocked();  //屏幕状态改变
                }
            }
        }
    };

进入updateDisplayLocked

    void updateDisplayLocked() {
                ...
                becomeInactiveIfAppropriateLocked(); //看是否可以进入Inactive状态
                ....
        }
    }

然后我们拔出usb,不充电,会回调充电处理函数

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override public void onReceive(Context context, Intent intent) {
            if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
                int plugged = intent.getIntExtra("plugged", 0);
                updateChargingLocked(plugged != 0); //充电状态改变
            } else if (ACTION_STEP_IDLE_STATE.equals(intent.getAction())) {
                synchronized (DeviceIdleController.this) {
                    stepIdleStateLocked();
                }
            }
        }
    };

进入updateChargingLocked

    void updateChargingLocked(boolean charging) {
         ....
         becomeInactiveIfAppropriateLocked();//看是否可以进入Inactive状态
         .....
    }

最后不插电和熄灭屏幕后都会进入becomeInactiveIfAppropriateLocked,状态mState变成STATE_INACTIVE,并且开启了一个定时器

    void becomeInactiveIfAppropriateLocked() {
        if (DEBUG) Slog.d(TAG, "becomeInactiveIfAppropriateLocked()");
        //不插电和屏幕熄灭的条件都满足了
        if (((!mScreenOn && !mCharging) || mForceIdle) && mEnabled && mState == STATE_ACTIVE) {
            .....
            mState = STATE_INACTIVE;
            scheduleAlarmLocked(mInactiveTimeout, false);   
            ......
        }
    }

    定时时长为常量30分钟
    INACTIVE_TIMEOUT = mParser.getLong(KEY_INACTIVE_TIMEOUT,
                        !COMPRESS_TIME ? 30 * 60 * 1000L : 3 * 60 * 1000L);

手机静静地躺在桌面上30分钟后,定时器时间到达后,pendingintent会被发出,广播接收器进行处理

 Intent intent = new Intent(ACTION_STEP_IDLE_STATE)
                     .setPackage("android")
                     .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
 mAlarmIntent = PendingIntent.getBroadcast(getContext(), 0, intent, 0);

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override public void onReceive(Context context, Intent intent) {
            if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
                int plugged = intent.getIntExtra("plugged", 0);
                updateChargingLocked(plugged != 0);
            } else if (ACTION_STEP_IDLE_STATE.equals(intent.getAction())) {
                synchronized (DeviceIdleController.this) {
                    stepIdleStateLocked();   //接收到广播
                }
            }
        }
    };

进入stepIdleStateLocked,该函数是状态转换处理的主要函数

    void stepIdleStateLocked() {
        if (DEBUG) Slog.d(TAG, "stepIdleStateLocked: mState=" + mState);
        EventLogTags.writeDeviceIdleStep();

        final long now = SystemClock.elapsedRealtime();
        if ((now+mConstants.MIN_TIME_TO_ALARM) > mAlarmManager.getNextWakeFromIdleTime()) {
            // Whoops, there is an upcoming alarm.  We don't actually want to go idle.
            if (mState != STATE_ACTIVE) {
                becomeActiveLocked("alarm", Process.myUid());
            }
            return;
        }

        switch (mState) {
            case STATE_INACTIVE:
                // We have now been inactive long enough, it is time to start looking
                // for significant motion and sleep some more while doing so.
                startMonitoringSignificantMotion(); //观察是否有小动作
                scheduleAlarmLocked(mConstants.IDLE_AFTER_INACTIVE_TIMEOUT, false); //设置观察小动作要观察多久
                mState = STATE_IDLE_PENDING; //状态更新为STATE_IDLE_PENDING
                break;
            case STATE_IDLE_PENDING: //小动作观察结束,很厉害,一直都没有小动作,会进入这里
                mState = STATE_SENSING;//状态更新为STATE_SENSING
                scheduleSensingAlarmLocked(mConstants.SENSING_TIMEOUT);//设置传感器感应时长
                mAnyMotionDetector.checkForAnyMotion(); //传感器感应手机有没有动
                break;
            case STATE_SENSING: //传感器也没发现手机动,就来最后一发,看GPS有没有动
                mState = STATE_LOCATING;//状态更新为STATE_LOCATING
                scheduleSensingAlarmLocked(mConstants.LOCATING_TIMEOUT);//设置GPS观察时长
                mLocationManager.requestLocationUpdates(mLocationRequest, mGenericLocationListener,
                        mHandler.getLooper());//GPS开始感应
                break;
            case STATE_LOCATING:  //GPS也发现没动
                cancelSensingAlarmLocked();
                cancelLocatingLocked();
                mAnyMotionDetector.stop();  //这里没有break,直接进入下一个case
            case STATE_IDLE_MAINTENANCE:
                scheduleAlarmLocked(mNextIdleDelay, true);//设置打盹多久后进行呼吸
                mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR);//更新下次打盹多久后进行呼吸
                if (DEBUG) Slog.d(TAG, "Setting mNextIdleDelay = " + mNextIdleDelay);
                mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT);
                mState = STATE_IDLE; //噢耶 终于进入了STATE_IDLE
                mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON);
                break;
            case STATE_IDLE: //打盹完了,呼吸一下就是这里了
                scheduleAlarmLocked(mNextIdlePendingDelay, false);
                mState = STATE_IDLE_MAINTENANCE; //状态更新为STATE_IDLE_MAINTENANCE
                mNextIdlePendingDelay = Math.min(mConstants.MAX_IDLE_PENDING_TIMEOUT,
                        (long)(mNextIdlePendingDelay * mConstants.IDLE_PENDING_FACTOR));
               //更新下次呼吸的时间
                mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
                break;
        }
    }

Math.min(mConstants.MAX_IDLE_PENDING_TIMEOUT,
(long)(mNextIdlePendingDelay * mConstants.IDLE_PENDING_FACTOR));
这一句看到了吗?取最小值,这里就是保证了idle和窗口的时间不会变成无限大。
为了让各位有个感官的体验,上面的一些时间我直接列出来吧

熄屏不插电进入INACTIVE时间上面说了30分钟

观察小动作的时间30分钟
IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getLong(KEY_IDLE_AFTER_INACTIVE_TIMEOUT,
                        !COMPRESS_TIME ? 30 * 60 * 1000L : 3 * 60 * 1000L);

观察传感器的时间4分钟
SENSING_TIMEOUT = mParser.getLong(KEY_SENSING_TIMEOUT,
                        !DEBUG ? 4 * 60 * 1000L : 60 * 1000L);
 
观察GPS的时间30秒
LOCATING_TIMEOUT = mParser.getLong(KEY_LOCATING_TIMEOUT,
                        !DEBUG ? 30 * 1000L : 15 * 1000L);

所以进入idle的总时间为30分钟+30分钟+4分钟+30s=1小时4分钟30秒,哈哈哈哈!!

下面给张状态转换图看看,没到达idle状态前,基本上有什么风吹草动都会变回ACTIVE状态。而变成IDLE状态后,只能插电或者点亮屏幕才离开IDLE状态。就像人入睡前,很容易被吵醒,而深度入眠后,估计只有闹钟能闹醒你了!!

状态转换图

上面说了这么多,跟我应用开发有什么关系?####

其实,没多大关系,看下源码不行噻。
不过作为一种新的机制,最好测试下你的应用在这几种状态下是否能够正常运行,起码不能挂掉啊。
google提供了adb的指令来强制变换状态,这样你就不用干等着它状态变化了。

adb shell dumpsys battery unplug       //相当于不插电
adb shell dumpsys device idle step     //让状态转换
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容