分析 Android 耗电原理后,飞书是这样做耗电治理的

动手点关注 干货不迷路 👆

飞书最近在进行耗电治理的专项优化,本篇文章将分析 Android 系统的耗电原理,分享飞书的耗电治理规划。

Android 耗电统计原理

我们先了解一下 Android 系统是如何进行耗电的统计的,最精确的方式当然是使用电流仪来进行统计,但是正常状态下手机硬件不支持,所以系统统计耗电时,使用的基本是模块功率 × 模块耗时这个公式来进行的,但不同的模块还是会有一些差别。这种统计方式没法做到非常的精确,但是也基本能反应出各应用电量的消耗大小。

模块功率

我们先来看看模块功率,每个模块的耗电功率都是不一样的,以计算方式来分,又分为下面三类:

21a93b0a2e5ae73e318a252653a8aa43.png
  1. 第一类是 Camera、FlashLight、MediaPlayer 等一般传感器或设备的模块。其工作功率基本和额定功率保持一致,所以模块电量的计算只需要统计模块的使用时长再乘以额定功率即可。

  2. 第二类是 Wifi、Mobile、BlueTooth 这类数据模块。其工作功率可以分为不同的档位,比如,当手机的 Wifi 信号比较弱的时候,Wifi 模块就必须工作在比较高的功率档位以维持数据链路,所以这类模块的电量计算有点类似于我们日常的电费计算,需要 “阶梯计费”。

  3. 第三类是屏幕,CPU 模块。CPU 模块除了每一个 CPU Core 需要像数据模块那样阶梯计算电量之外,CPU 的每一个集群(Cluster,一般一个集群包含一个或多个规格相同的 Core)也有额外的耗电,此外整个 CPU 处理器芯片也有功耗。简单计算的话,CPU 电量 = SUM(各核心功耗)+ 各集群(Cluster)功耗 + 芯片功耗 。屏幕模块的电量计算就更麻烦了,很难把屏幕功耗合理地分配给各个 App, 因此 Android 系统只是简单地计算 App 屏幕锁(WakeLock)的持有时长,按固定系数增加 App CPU 的统计时长,粗略地把屏幕功耗算进 CPU 里面。

每个模块的功耗大小位于 framework 的 power_profile.xml 文件中,由厂商自己提供,里面规定了每个模块的功耗,下面是一台一加 9 的测试机的 power_profile 文件:

9345c8de8f499c8c7bb877ca55672017.png

通过 apktook 反解出来的 power_profile 如下:

143ae04ab4342f7a8f09e6607ecef149.png

文件中每个模块的对应说明,可以在谷歌提供的文档中看到详细的说明。

https://source.android.com/devices/tech/power/values

1adfc1f5f2b2d3be2b9cd7db3b5c7701.png

模块耗时

了解了模块的功率,我们再来看看模块耗时,耗电模块在工作或者状态变更时,都会通知 batterystats 这个 service,而 BatteryStatsService 会调用 BatteryStats 对象进行耗时的统计,BatteryStats 的构造函数中会初始化各个模块的 Timer,用来进行耗时的统计,并将统计的数据存储在batterystats.bin文件中。

759b3cfd8418f9c334a98bd56a982f2e.png

我们来详细看看下面几个模块的是如何进行统计的:

  • wifi 模块

public void noteWifiOnLocked() {
        if (!mWifiOn) {
            final long elapsedRealtime = mClocks.elapsedRealtime();
            final long uptime = mClocks.uptimeMillis();
            mHistoryCur.states2 |= HistoryItem.STATE2_WIFI_ON_FLAG;
            addHistoryRecordLocked(elapsedRealtime, uptime);
            mWifiOn = true;
            mWifiOnTimer.startRunningLocked(elapsedRealtime);
            scheduleSyncExternalStatsLocked("wifi-off", ExternalStatsSync.UPDATE_WIFI);
        }
    }

    public void noteWifiOffLocked() {
        final long elapsedRealtime = mClocks.elapsedRealtime();
        final long uptime = mClocks.uptimeMillis();
        if (mWifiOn) {
            mHistoryCur.states2 &= ~HistoryItem.STATE2_WIFI_ON_FLAG;
            addHistoryRecordLocked(elapsedRealtime, uptime);
            mWifiOn = false;
            mWifiOnTimer.stopRunningLocked(elapsedRealtime);
            scheduleSyncExternalStatsLocked("wifi-on", ExternalStatsSync.UPDATE_WIFI);
        }
    }
  • Audio 模块

public void noteAudioOnLocked(int uid) {
        uid = mapUid(uid);
        final long elapsedRealtime = mClocks.elapsedRealtime();
        final long uptime = mClocks.uptimeMillis();
        if (mAudioOnNesting == 0) {
            mHistoryCur.states |= HistoryItem.STATE_AUDIO_ON_FLAG;
            if (DEBUG_HISTORY) Slog.v(TAG, "Audio on to: "
                    + Integer.toHexString(mHistoryCur.states));
            addHistoryRecordLocked(elapsedRealtime, uptime);
            mAudioOnTimer.startRunningLocked(elapsedRealtime);
        }
        mAudioOnNesting++;
        getUidStatsLocked(uid).noteAudioTurnedOnLocked(elapsedRealtime);
    }


    public void noteAudioOffLocked(int uid) {
        if (mAudioOnNesting == 0) {
            return;
        }
        uid = mapUid(uid);
        final long elapsedRealtime = mClocks.elapsedRealtime();
        final long uptime = mClocks.uptimeMillis();
        if (--mAudioOnNesting == 0) {
            mHistoryCur.states &= ~HistoryItem.STATE_AUDIO_ON_FLAG;
            if (DEBUG_HISTORY) Slog.v(TAG, "Audio off to: "
                    + Integer.toHexString(mHistoryCur.states));
            addHistoryRecordLocked(elapsedRealtime, uptime);
            mAudioOnTimer.stopRunningLocked(elapsedRealtime);
        }
        getUidStatsLocked(uid).noteAudioTurnedOffLocked(elapsedRealtime);
    }
  • Activity 状态改变

public void noteActivityResumedLocked(int uid) {
    uid = mapUid(uid);
    getUidStatsLocked(uid).noteActivityResumedLocked(mClocks.elapsedRealtime());
}

public void noteActivityPausedLocked(int uid) {
    uid = mapUid(uid);
    getUidStatsLocked(uid).noteActivityPausedLocked(mClocks.elapsedRealtime());
}


public static class Uid extends BatteryStats.Uid {

    @Override
    public void noteActivityPausedLocked(long elapsedRealtimeMs) {
        if (mForegroundActivityTimer != null) {
            mForegroundActivityTimer.stopRunningLocked(elapsedRealtimeMs);
        }
    }

    @Override
    public void noteActivityPausedLocked(long elapsedRealtimeMs) {
        if (mForegroundActivityTimer != null) {
            mForegroundActivityTimer.stopRunningLocked(elapsedRealtimeMs);
        }
    }

}

通过上面三个例子可以看到,BatteryStats 在统计模块耗时,主要通过 Timer 来进行时长的统计,如 WifiOnTimer、AudioOnTimer、ForegroundActivityTimer,并且根据是否有 UID 来决定是否要统计到 UID 对应的数据中,系统在统计应用的耗电时,就是根据 UID 下各个模块的统计数据,来进行应用的耗电计算的。

耗电计算

当我们知道了每个模块的耗时,每个模块的功耗,那么就能计算各个模块的耗电量了,耗电量的计算在 BatteryStatsHelper 这个类中,下面详细看一下 Setting 中,应用耗电详情这个功能统计耗电的实现,Setting 中的耗电统计这个应用主要是调用了 BatteryStatsHelper 中的 refreshStats()函数。

ecd508cf3392b0ae36c15f6c25eefda8.png

refreshStats 主要两个方法是 processappUsage 计算应用的耗电,记忆 processMiscUsage 计算杂项耗电,如 WIFI,通话等等。

  • 计算 app 的电量

2298af8c4886c101d5c7c5c187d88ffe.png

这里以 CameraPowerCalculator 这个简单的模块看看它是如何统计电量的:

23ad6b5ab52d789482d55c6835d58708.png

可以看到,里面只是简单的用了 totalTime * mCameraPowerOnAvg,mCameraPowerOnAvg 则是从 power_profile.xml 读取出来,其他教负责的如 CPU 模块的计算,感兴趣的可以自己看看,就不在这里说了。

  • 计算 misc 杂项的电量

a4444929f06199599cbbf4ce039a2996.png

杂项电量用来统计一些没有特定 UID 的耗电,如蓝牙,屏幕等等,计算方式也是类似的。

Android 的耗电优化策略

Doze 模式

Doze 模式也被称为低电耗模式,是针对整个系统进行一个耗电优化策略,进入 Doze 模式后会暂停所有的 Jobs,Alarm 和 Network 活动并推迟到窗口期执行,以及其他的一些限制来节约电量。

Doze 模式的进入和退出

Doze 模式分为 Deep Doze 和 Light Doze 两种模式,Doze 模式是在 Android6.0 引入的,也就是 Deep Doze 模式,Light Doze 是 Android7.0 引入的,两者进入的条件不一样,Deep Doze 的条件会更严格,下面先介绍 Deep Doze。

Deep Doze

系统处于息屏状态,并且 30 分钟不移动的情况下,就会进入到 Deep Doze 模式,Deep Doze 机制中有七种状态,分别如下:

//mState值,表示设备处于活动状态
private static final int STATE_ACTIVE = 0;
//mState值,表示设备处于不交互状态,灭屏、静止
private static final int STATE_INACTIVE = 1;
//mState值,表示设备刚结束不交互状态,等待进入IDLE状态
private static final int STATE_IDLE_PENDING = 2;
//mState值,表示设备正在感应动作
private static final int STATE_SENSING = 3;
//mState值,表示设备正在定位
private static final int STATE_LOCATING = 4;
//mState值,表示设备处于空闲状态,也即Doze模式
private static final int STATE_IDLE = 5;
//mState值,表示设备正处于Doze模式,紧接着退出Doze进入维护状态
private static final int STATE_IDLE_MAINTENANCE = 6;

这七种状态的转换关系如下:

704e0803e9d1d50d7d383839a9a8aca8.png

根据上图,他们的关系总结如下:

  1. 当设备亮屏或者处于正常使用状态时其就为 ACTIVE 状态;

  2. ACTIVE 状态下不充电且灭屏设备就会切换到 INACTIVE 状态;

  3. INACTIVE 状态经过 30 分钟,期间检测没有打断状态的行为 Doze 就切换到 IDLE_PENDING 的状态;

  4. 然后再经过 30 分钟以及一系列的判断,状态切换到 SENSING;

  5. 在 SENSING 状态下会去检测是否有地理位置变化,没有的话就切到 LOCATION 状态;

  6. LOCATION 状态下再经过 30s 的检测时间之后就进入了 Doze 的核心状态 IDLE;

  7. 在 IDLE 模式下每隔一段时间就会进入一次 IDLE_MAINTANCE,此间用来处理之前被挂起的一些任务,这个时间段为一个小时,两个小时,四个小时,最后稳定为最长为六个小时

  8. IDLE_MAINTANCE 状态持续 5 分钟之后会重新回到 IDLE 状态;

  9. 在除 ACTIVE 以外的所有状态中,检测到打断的行为如亮屏、插入充电器,位置的改变等状态就会回到 ACTIVE,重新开始下一个轮回。

Light Doze

从上面可以看到想要进入 Doze 模式的条件是很苛刻,需要在手机息屏并且没有移动的状态下才能进入,所以 Android7.0 开始引入了 Light Doze,处于息屏状态,但仍处于移动状态可进入 Light Doze,LightDoze 有 7 个状态,分别如下:

//mLightState状态值,表示设备处于活动状态
private static final int LIGHT_STATE_ACTIVE = 0;
//mLightState状态值,表示设备处于不活动状态
private static final int LIGHT_STATE_INACTIVE = 1;
//mLightState状态值,表示设备进入空闲状态前,需要等待完成必要操作
private static final int LIGHT_STATE_PRE_IDLE = 3;
//mLightState状态值,表示设备处于空闲状态,该状态内将进行优化
private static final int LIGHT_STATE_IDLE = 4;
//mLightState状态值,表示设备处于空闲状态,要进入维护状态,先等待网络连接
private static final int LIGHT_STATE_WAITING_FOR_NETWORK = 5;
//mLightState状态值,表示设备处于维护状态
private static final int LIGHT_STATE_IDLE_MAINTENANCE = 6;

这 6 个状态的转换关系如下:

752753567d6e699de6ebb3308291cb45.png

根据上图,他们的转换关系总结如下:

  1. 当设备亮屏或者处于正常使用状态时其就为 ACTIVE 状态;

  2. ACTIVE 状态下不充电且灭屏设备就会切换到 INACTIVE 状态;

  3. INACTIVE 状态经过 3 分钟,期间检测没有打断状态的行为就切换到 PRE_IDLE 的状态;

  4. PRE_IDLE 状态经过 5 分钟,期间无打断就进入到 IDLE 状态

  5. 进入 IDLE 状态会根据是否有网络连接选择进入 WAITING_FOR_NETWORK 还是进入 MAINTENANCE 窗口期,进入窗口期的时间为:5 分钟,10 分钟,最后稳定最长为 15 分钟

  6. 进入 WAITING_FOR_NETWORK 会持续 5 分钟后重新进入到 IDLE 状态

  7. 进入 MAINTENANCE 会解除耗电策略的限制,并在 1 分钟后重新进入到 IDLE 状态

Doze 模式的优化策略

了解了 Doze 模式的进入和退出策略,我们再来看一下在 Doze 模式中,会做哪些策略来优化耗电。

Deep Doze

当系统处于 Doze 模式下,系统和白名单之外的应用将受到以下限制:

  • 无法访问网络

  • Wake Locks 被忽略

  • AlarmManager 闹铃会被推迟到下一个 maintenance window 响应

    • 使用 setAndAllowWhileIdle 或 SetExactAndAllowWhileIdle 设置闹铃的闹钟则不会受到 Doze 模式的影响

    • setAlarmClock 设置的闹铃在 Doze 模式下仍然生效,但系统会在闹铃生效前退出 Doze

  • 系统不执行 Wi-Fi/GPS 扫描;

  • 系统不允许同步适配器运行;

  • 系统不允许 JobScheduler 运行;

Deep Doze 也提供了白名单,位于白名单中的应用可以:

  • 继续使用网络并保留部分 wake lock

  • Job 和同步仍然会被推迟

  • 常规的 AlarmManager 闹铃也不会被触发

Light Doze

Light Doze 的限制没有 Deep Doze 这么严格,主要有下面几种:

  • 不允许进行网络访问

  • 不允许同步适配器运行

  • 不允许 JobScheduler 运行

Deep Doze 和 Light Doze 的总结对比如下:

Deep Doze 和 Light Doze 都需要达到一定条件后才能进入,并且进入后会定期提供窗口期来解除限制。

fdf72bd15457fab7c13ef46bceaccc57.png

它们的对比如下:

7cdb4e085b0076a7a6f5ef492809f6fe.png
Doze 模式实现原理

前面已经了解了 Doze 模式了,下面就在通过 Android 中的 Doze 机制的源码,深入了解 Doze 的实现原理。Doze 机制相关的源码都在 DeviceIdleController 这个类中。

进入 INACTIVE 状态

从 ACTIVIE 进入到 INACTIVE 的入口方法是 becomeInactiveIfAppropriateLocked 中,当充电状态发生改变,屏幕息屏等条件触发时,都会调用该方法判断是否可进入 INACTIVE 状态。

//deep doze进入INACTIVE后的延时时间,这里的COMPRESS_TIME默认为false
long inactiveTimeoutDefault = (mSmallBatteryDevice ? 15 : 30) * 60 * 1000L;
INACTIVE_TIMEOUT = mParser.getDurationMillis(KEY_INACTIVE_TIMEOUT,
                !COMPRESS_TIME ? inactiveTimeoutDefault : (inactiveTimeoutDefault / 10));

LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getDurationMillis(
        KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT,
        !COMPRESS_TIME ? 3 * 60 * 1000L : 15 * 1000L);

void becomeInactiveIfAppropriateLocked() {
    final boolean isScreenBlockingInactive =
            mScreenOn && (!mConstants.WAIT_FOR_UNLOCK || !mScreenLocked);
    //判断是否是灭屏且非充电状态
    if (!mForceIdle && (mCharging || isScreenBlockingInactive)) {
        return;
    }

    if (mDeepEnabled) {
        if (mQuickDozeActivated) {
            //1. QuickDoze是Android 10新引入的低电量的情况下,快速进入Doze的机制,会缩短进入Doze的耗时
            if (mState == STATE_QUICK_DOZE_DELAY || mState == STATE_IDLE
                    || mState == STATE_IDLE_MAINTENANCE) {
                return;
            }
            mState = STATE_QUICK_DOZE_DELAY;
            resetIdleManagementLocked();
            scheduleAlarmLocked(mConstants.QUICK_DOZE_DELAY_TIMEOUT, false);
            EventLogTags.writeDeviceIdle(mState, "no activity");
        } else if (mState == STATE_ACTIVE) {
            mState = STATE_INACTIVE;
            resetIdleManagementLocked();
            long delay = mInactiveTimeout;
            if (shouldUseIdleTimeoutFactorLocked()) {
                delay = (long) (mPreIdleFactor * delay);
            }
            //2. 执行时间为mInactiveTimeout延时的任务,这里是30分钟
            scheduleAlarmLocked(delay, false);
            EventLogTags.writeDeviceIdle(mState, "no activity");
        }
    }
    if (mLightState == LIGHT_STATE_ACTIVE && mLightEnabled) {
        mLightState = LIGHT_STATE_INACTIVE;
        resetLightIdleManagementLocked();
        //3. 执行时间为LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT延时的任务,这里是3分钟
        scheduleLightAlarmLocked(mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT);
        EventLogTags.writeDeviceIdleLight(mLightState, "no activity");
    }
}

从源码中可以看到 Deep Doze,Light Doze 的处理都在这里,并且这里还有一个 Quick Doze,它是 Android 10 引入,能在低电量情况下快速进入 Doze 的机制。

我们接着看 INACTIVE 向下一个状态的改变:

  • Deep Doze 通过scheduleAlarmLocked(delay, false)向下一个状态转变,在这个时间过程中,有开屏,充电等操作,都会导致状态转换失败

  • Light Doze 通过scheduleLightAlarmLocked(mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT)向下一个状态改变,同样在开屏和充电状态下,都会导致进入下一个状态失败

从 INACTIVE 状态开始,Light Doze 和 Deep Doze 转换的入口就不一样了,所以下面会分开讲解。

Deep Doze
1. 从 INACTIVE 进入 STATE_IDLE_PENDING

becomeInactiveIfAppropriateLocked 函数中将 mState 设置为 STATE_INACTIVE,然后调用 scheduleAlarmLocked 设置了一个 30 分钟的定时任务,它的逻辑实现如下。

void scheduleAlarmLocked(long delay, boolean idleUntil) {
    if (mMotionSensor == null) {
    //如果没有运动传感器,则返回,因为无法判断设备是否保持静止
    if (mMotionSensor == nullr) {
        return;
    }
    //设置DeepDoze的定时Alarm
    mNextAlarmTime = SystemClock.elapsedRealtime() + delay;
    if (idleUntil) {
        mAlarmManager.setIdleUntil(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                mNextAlarmTime, "DeviceIdleController.deep",
                mDeepAlarmListener, mHandler);
    } else {
        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                mNextAlarmTime, "DeviceIdleController.deep",
                mDeepAlarmListener, mHandler);
    }
}

private final AlarmManager.OnAlarmListener mDeepAlarmListener
        = new AlarmManager.OnAlarmListener() {
    @Override
    public void onAlarm() {
        synchronized (DeviceIdleController.this) {
            ///每次Doze状态转换都会在该方法中进行
            stepIdleStateLocked("s:alarm");
        }
    }
};

Deep Doze 的 scheduleAlarmLocked 定时任务触发后,会回调 onAlarm,执行 stepIdleStateLocked 函数。

void stepIdleStateLocked(String reason) {
    final long now = SystemClock.elapsedRealtime();
    //说明1小时内有Alarm定时时间到,暂不进入IDLE状态,30min后再进入
    if ((now+mConstants.MIN_TIME_TO_ALARM) >
               mAlarmManager.getNextWakeFromIdleTime()) {
        if (mState != STATE_ACTIVE) {
            //将当前设备变为活动状态,LightDoze和DeepDoze都为Active状态
            becomeActiveLocked("alarm", Process.myUid());
            becomeInactiveIfAppropriateLocked();
        }
        return;
    }
    switch (mState) {
        case STATE_INACTIVE:
            //启动Sensor
            startMonitoringMotionLocked();
            //设置STATE_IDLE_PENDING状态时长的定时Alarm,30mins
            scheduleAlarmLocked(mConstants.IDLE_AFTER_INACTIVE_TIMEOUT,
                   false);
            mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT;//5mins
            mNextIdleDelay = mConstants.IDLE_TIMEOUT;//60mins
            //此时状态变为PENDING状态
            mState = STATE_IDLE_PENDING;
            break;
        case STATE_IDLE_PENDING:
            //此时状态变为SENSING状态
            mState = STATE_SENSING;
            //设置STATE_SENSING状态超时时长的定时Alarm,DEBUG?1:4mins
            scheduleSensingTimeoutAlarmLocked(mConstants.SENSING_TIMEOUT);
            //取消通用位置更新和GPS位置更新
            cancelLocatingLocked();
            mNotMoving = false;
            mLocated = false;
            mLastGenericLocation = null;
            mLastGpsLocation = null;
            //开始检测是否有移动
            mAnyMotionDetector.checkForAnyMotion();
            break;
        case STATE_SENSING:
            //取消用于STATE_SENSING状态超时时长的Alarm
            cancelSensingTimeoutAlarmLocked();
            //此时状态变为LOCATING
            mState = STATE_LOCATING;
            //设置STATE_LOCATING状态时长的Alarm
            scheduleAlarmLocked(mConstants.LOCATING_TIMEOUT,
                     false);//DEBUG?15:30
            //请求通用位置
            if (mLocationManager != null
                    && mLocationManager.getProvider(LocationManager.
                     NETWORK_PROVIDER) != null) {
                mLocationManager.requestLocationUpdates(mLocationRequest,
                        mGenericLocationListener, mHandler.getLooper());
                mLocating = true;
            } else {
                mHasNetworkLocation = false;
            }
            //请求GPS位置
            if (mLocationManager != null
                    && mLocationManager.getProvider(LocationManager.
                    GPS_PROVIDER) != null) {
                mHasGps = true;
                mLocationManager.requestLocationUpdates(LocationManager.
                        GPS_PROVIDER, 1000, 5,
                        mGpsLocationListener, mHandler.getLooper());
                mLocating = true;
            } else {
                mHasGps = false;
            }
            //如果true,则break,因为在Location的Listener中会进入下一个状态,
            //否则进入下一步状态
            if (mLocating) {
                break;
            }
        case STATE_LOCATING:
            //取消DeepDoze的Alarm
            cancelAlarmLocked();
            //取消位置更新
            cancelLocatingLocked();
            //Sensor停止检测
            mAnyMotionDetector.stop();
        case STATE_IDLE_MAINTENANCE:
            //设置STATE_IDLE状态时长的定时Alarm,到时后将退出IDLE状态
            scheduleAlarmLocked(mNextIdleDelay, true);
            //设置下次IDLE时间
            mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR);
            mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT);
            if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) {
                mNextIdleDelay = mConstants.IDLE_TIMEOUT;
            }
            mState = STATE_IDLE;
            //进入DeepDoze的IDLE后,覆盖LightDoze
            if (mLightState != LIGHT_STATE_OVERRIDE) {
                mLightState = LIGHT_STATE_OVERRIDE;
                //取消LightDoze的定时Alarm
                cancelLightAlarmLocked();
            }
            //申请wakelock保持CPU唤醒
            mGoingIdleWakeLock.acquire();
            //handler中处理idle状态后各个模块的限制工作
            mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON);
            break;
        case STATE_IDLE:
            mActiveIdleOpCount = 1;//表示现在有正在活动的操作
            //申请wakelock锁保持cpu唤醒
            mActiveIdleWakeLock.acquire();
            //设置STATE_IDLE_MAINTENANCE状态时长的定时Alarm,
            //到时后将退出维护状态
            scheduleAlarmLocked(mNextIdlePendingDelay, false);
            mMaintenanceStartTime = SystemClock.elapsedRealtime();
            mNextIdlePendingDelay =
                 Math.min(mConstants.MAX_IDLE_PENDING_TIMEOUT,
                    (long)(mNextIdlePendingDelay *
                    mConstants.IDLE_PENDING_FACTOR));
            if (mNextIdlePendingDelay < mConstants.IDLE_PENDING_TIMEOUT) {
                mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT;
            }
            mState = STATE_IDLE_MAINTENANCE;
            //Handler中处理退出idle状态进入维护状态后取消限制的工作
            mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
            break;
    }
}

可以看到,Deep Doze 的状态转换都是通过scheduleAlarmLocked和stepIdleStateLocked这两个函数进行的。在 case 为 STATE_INACTIVE 的逻辑中,将 mState 设置成了 STATE_IDLE_PENDING,启动 Sensor 监听,并设置了一个 30 分钟的延时任务。

2. 从 STATE_DLE_PENDING 进入 STATE_SENSING

当 30 分钟无中断,state 就从 PENDING 进入到了 SENSING 状态中。

case STATE_IDLE_PENDING:
    //此时状态变为SENSING状态
    mState = STATE_SENSING;
    //设置STATE_SENSING状态超时时长的定时Alarm,4分钟
    scheduleSensingTimeoutAlarmLocked(mConstants.SENSING_TIMEOUT);
    //取消通用位置更新和GPS位置更新
    cancelLocatingLocked();
    mNotMoving = false;
    mLocated = false;
    mLastGenericLocation = null;
    mLastGpsLocation = null;
    //开始检测是否有运动
    mAnyMotionDetector.checkForAnyMotion();
    break;

在这个状态中,会开始运动检测,并持续 4 分钟。

3. 从 STATE_SENSING 进入到 STATE_LOCATING
4. 从 STATE_LOCATING 进入到 STATE_IDLE
5. 从 STATE_IDLE_MAINTENANCE 进入到 STATE_IDLE

SENSING 的下一个状态是 STATE_LOCATING,STATE_LOCATING 和 STATE_IDLE_MAINTENANCE 的下一个状态都是 STATE_IDLE,这里一起讲。

case STATE_SENSING:
    //取消用于STATE_SENSING状态超时时长的Alarm
    cancelSensingTimeoutAlarmLocked();
    //此时状态变为LOCATING
    mState = STATE_LOCATING;
    //设置STATE_LOCATING状态时长的Alarm,
    scheduleAlarmLocked(mConstants.LOCATING_TIMEOUT, 
             false);
    //请求通用位置
    if (mLocationManager != null
            && mLocationManager.getProvider(LocationManager.
             NETWORK_PROVIDER) != null) {
        mLocationManager.requestLocationUpdates(mLocationRequest,
                mGenericLocationListener, mHandler.getLooper());
        mLocating = true;
    } else {
        mHasNetworkLocation = false;
    }
    //请求GPS位置
    if (mLocationManager != null
            && mLocationManager.getProvider(LocationManager.
            GPS_PROVIDER) != null) {
        mHasGps = true;
        mLocationManager.requestLocationUpdates(LocationManager.
                GPS_PROVIDER, 1000, 5,
                mGpsLocationListener, mHandler.getLooper());
        mLocating = true;
    } else {
        mHasGps = false;
    }
    //如果true,则break,因为在Location的Listener中会进入下一个状态,
    //否则进入下一步状态
    if (mLocating) {
        break;
    }
case STATE_LOCATING:
        //取消DeepDoze的Alarm
        cancelAlarmLocked();
        //取消位置更新
        cancelLocatingLocked();
        //Sensor停止检测
        mAnyMotionDetector.stop();
case STATE_IDLE_MAINTENANCE:
    //设置STATE_IDLE状态时长的定时Alarm,到时后将退出IDLE状态
    scheduleAlarmLocked(mNextIdleDelay, true);
    //设置下次IDLE时间
    mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR);
    mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT);
    if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) {
        mNextIdleDelay = mConstants.IDLE_TIMEOUT;
    }
    mState = STATE_IDLE;
    //进入DeepDoze的IDLE后,覆盖LightDoze
    if (mLightState != LIGHT_STATE_OVERRIDE) {
        mLightState = LIGHT_STATE_OVERRIDE;
        //取消LightDoze的定时Alarm
        cancelLightAlarmLocked();
    }
    //申请wakelock保持CPU唤醒
    mGoingIdleWakeLock.acquire();
    //handler中处理idle状态后各个模块的限制工作
    mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON);
    break;

在这个过程中检测是否有 gps 以及是否有位置移动,如果有 gps,则通过 break 跳出循环,并进行 30S 的位置移动检测;没有 gps,则进入到 case 为 STATE_IDLE_MAINTENANCE 的处理中,并将 state 设置为 STATE_IDLE。

进入到 STATE_IDLE 后,会申请 wakelock,同时调用 MSG_REPORT_IDLE_ON 的 handler 任务来进行耗电策略的限制,这里和 light doze 的 idle 状态处理都是同一个入口,所以 MSG_REPORT_IDLE_ON 在下面 light doze 中在详细讲。

同时,我们可以看到,进入 STATE_IDLE 后,会设置一个时间为:

IDLE_TIMEOUT = mParser.getDurationMillis(KEY_IDLE_TIMEOUT,
        !COMPRESS_TIME ? 60 * 60 * 1000L : 6 * 60 * 1000L);

mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR);
mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT);
if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) {
    mNextIdleDelay = mConstants.IDLE_TIMEOUT;
}

的延时任务,IDLE_FACTOR 为 2,mNextIdleDelay 初始值为 60 分钟,MAX_IDLE_TIMEOUT 为 6 个小时,所以这个时间为1 个小时、2 个小时、4 个小时,最后稳定为 6 个小时。

6. 从 STATE_IDLE 进入到 STATE_IDLE_MAINTENANCE
case STATE_IDLE:
        mActiveIdleOpCount = 1;//表示现在有正在活动的操作
        //申请wakelock锁保持cpu唤醒
        mActiveIdleWakeLock.acquire();
        //设置STATE_IDLE_MAINTENANCE状态时长的定时Alarm,
        //到时后将退出维护状态
        scheduleAlarmLocked(mNextIdlePendingDelay, false);
        mMaintenanceStartTime = SystemClock.elapsedRealtime();
        mNextIdlePendingDelay =
             Math.min(mConstants.MAX_IDLE_PENDING_TIMEOUT,
                (long)(mNextIdlePendingDelay *
                mConstants.IDLE_PENDING_FACTOR));
        if (mNextIdlePendingDelay < mConstants.IDLE_PENDING_TIMEOUT) {
            mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT;
        }
        mState = STATE_IDLE_MAINTENANCE;
        //Handler中处理退出idle状态进入维护状态后取消限制的工作
        mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
        break;

进入 MAINTENANCE 状态后,会在 MSG_REPORT_IDLE_OFF 的 handler 中取消各种限制,并位置 mNextIdlePendingDelay 时间段。

mNextIdlePendingDelay =
         Math.min(mConstants.MAX_IDLE_PENDING_TIMEOUT,
            (long)(mNextIdlePendingDelay *
            mConstants.IDLE_PENDING_FACTOR));
if (mNextIdlePendingDelay < mConstants.IDLE_PENDING_TIMEOUT) {
    mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT;
}

IDLE_PENDING_TIMEOUT 为 5 分钟。

Light Doze
1. 从 INACTIVE 进入 LIGHT_STATE_PRE_IDLE

scheduleLightAlarmLocked到达时间后,会触发下面的回调:

void scheduleLightAlarmLocked(long delay) {
    mNextLightAlarmTime = SystemClock.elapsedRealtime() + delay;
    //到达时间后,回调mLightAlarmListener.onAlarm()
    mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
            mNextLightAlarmTime, "DeviceIdleController.light",
            mLightAlarmListener, mHandler);
}

private final AlarmManager.OnAlarmListener mLightAlarmListener
        = new AlarmManager.OnAlarmListener() {
    @Override
    public void onAlarm() {
        synchronized (DeviceIdleController.this) {
            //每次LightDoze的状态改变,都会调用该方法进行处理
            stepLightIdleStateLocked("s:alarm");
        }
    }
};

Light Doze 的状态改变也都是在stepLightIdleStateLocked函数中处理:

void stepLightIdleStateLocked(String reason) {
    //如果mLigthSate为LIGHT_STATE_OVERRIDE,说明DeepDoze处于Idle状态,由
    // DeepDoze将LightDoze覆盖了,因此不需要进行LightDoze了
    if (mLightState == LIGHT_STATE_OVERRIDE) {
        return;
    }
    switch (mLightState) {
        case LIGHT_STATE_INACTIVE:
            //当前最小预算时间
            mCurIdleBudget =
              mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;//1min
            //表示LightDoze 进入空闲(Idle)状态的时间
            mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT;//5mins
            //LightDoze进入维护状态(maintenance)的开始时间
            mMaintenanceStartTime = 0;
            if (!isOpsInactiveLocked()) {
                //将状态置为LIGHT_STATE_PRE_IDLE状态
                mLightState = LIGHT_STATE_PRE_IDLE;
                //设置一个3分钟的定时器
                scheduleLightAlarmLocked(mConstants.LIGHT_PRE_
                  IDLE_TIMEOUT);
                break;
            }
        case LIGHT_STATE_PRE_IDLE:
        case LIGHT_STATE_IDLE_MAINTENANCE:
            if (mMaintenanceStartTime != 0) {
            //维护状态的时长
                long duration = SystemClock.elapsedRealtime() -
                 mMaintenanceStartTime;
                if (duration <
                 mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
                    mCurIdleBudget += (mConstants.LIGHT_IDLE_MAINTENANCE
                       _MIN_BUDGET-duration);
                } else {
                    mCurIdleBudget -= (duration-mConstants.LIGHT_IDLE_
                      MAINTENANCE_MIN_BUDGET);
                }
            }
            mMaintenanceStartTime = 0;//重置维护开始时间
            //设置一个定时器,到达时间后用来处理LightDoze处于IDLE状态的操作
            scheduleLightAlarmLocked(mNextLightIdleDelay);
           //计算下次进入Idle状态的
            mNextLightIdleDelay =
            Math.min(mConstants.LIGHT_MAX_IDLE_TIMEOUT,
                    (long)(mNextLightIdleDelay *
                mConstants.LIGHT_IDLE_FACTOR));
            if (mNextLightIdleDelay < mConstants.LIGHT_IDLE_TIMEOUT) {
                mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT;
            }
            //将LightDoze模式置为IDLE状态,开始进行一些限制
            mLightState = LIGHT_STATE_IDLE;
            addEvent(EVENT_LIGHT_IDLE);
            //申请一个wakelock锁,保持CPU唤醒
            mGoingIdleWakeLock.acquire();
            //处理LightDoze进入Idle状态后的操作
            mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON_LIGHT);
            break;
        case LIGHT_STATE_IDLE:
        case LIGHT_STATE_WAITING_FOR_NETWORK:
            if (mNetworkConnected || mLightState ==
            LIGHT_STATE_WAITING_FOR_NETWORK) {
                //如果网络有链接或者当前LightDoze模式为等待网络状态,则进行维护,
                // 并将LightDoze模式退出IDLE状态,进入维护状态
                mActiveIdleOpCount = 1;
                mActiveIdleWakeLock.acquire();
                mMaintenanceStartTime = SystemClock.elapsedRealtime();
            // 保证10<=mCurIdleBudget<=30mins ,mCurIdleBudget是维护状态的时间
                if (mCurIdleBudget <
                mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
                    mCurIdleBudget =
                     mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;
                } else if (mCurIdleBudget >
                mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET) {
                    mCurIdleBudget =
                    mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET;
                }
                //设置一个定时器,到达时间后用来处理LightDoze处于维护状态的操作
                scheduleLightAlarmLocked(mCurIdleBudget);
                mLightState = LIGHT_STATE_IDLE_MAINTENANCE;//进入维护状态
                addEvent(EVENT_LIGHT_MAINTENANCE);
                //处理LightDoze进入Maintenance状态后的操作
                mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
            } else {
                //将LightDoze模式置为LIGHT_STATE_WAITING_FOR_NETWORK,
            //在进入维护状态前需要获取网络
                //设置一个定时器,到达时间后用来处理LightDoze处于
            //WAITING_FOR_NETWORK状态的操作
                scheduleLightAlarmLocked(mNextLightIdleDelay);//600000,5mins
                mLightState = LIGHT_STATE_WAITING_FOR_NETWORK;
                EventLogTags.writeDeviceIdleLight(mLightState, reason);
            }
            break;
    }
}

从代码中可以看到,case 为 LIGHT_STATE_INACTIVE 的处理逻辑中,做了这几件事:

  1. 将当前状态设置为 LIGHT_STATE_PRE_IDLE;

  2. 并发送一个 3 分钟的闹钟,准备进入下一个状态。

后续状态也全部是通过scheduleLightAlarmLocked来设置定时任务,然后在stepLightIdleStateLocked函数中处理状态的转换和对应状态的逻辑。

2. 从 LIGHT_STATE_PRE_IDLE 进入 LIGHT_STATE_IDLE
3. 从 LIGHT_STATE_IDLE_MAINTENANCE 进入 LIGHT_STATE_IDLE

LIGHT_STATE_PRE_IDLE 和 LIGHT_STATE_IDLE_MAINTENANCE 的下一个状态都是 LIGHT_STATE_IDLE,所以他们的处理也在同一个入口。

LIGHT_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_IDLE_TIMEOUT,
        !COMPRESS_TIME ? 5 * 60 * 1000L : 15 * 1000L);

LIGHT_MAX_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_MAX_IDLE_TIMEOUT,
                !COMPRESS_TIME ? 15 * 60 * 1000L : 60 * 1000L);

void stepLightIdleStateLocked(String reason) {
    //如果mLigthSate为LIGHT_STATE_OVERRIDE,说明DeepDoze处于Idle状态,由
    // DeepDoze将LightDoze覆盖了,因此不需要进行LightDoze了
    if (mLightState == LIGHT_STATE_OVERRIDE) {
        return;
    }
    switch (mLightState) {
        ……
        case LIGHT_STATE_PRE_IDLE:
        case LIGHT_STATE_IDLE_MAINTENANCE:
            if (mMaintenanceStartTime != 0) {
            //维护状态的时长
                long duration = SystemClock.elapsedRealtime() -
                 mMaintenanceStartTime;
                if (duration <
                 mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
                    mCurIdleBudget += (mConstants.LIGHT_IDLE_MAINTENANCE
                       _MIN_BUDGET-duration);
                } else {
                    mCurIdleBudget -= (duration-mConstants.LIGHT_IDLE_
                      MAINTENANCE_MIN_BUDGET);
                }
            }
            mMaintenanceStartTime = 0;//重置维护开始时间
            //设置一个定时器,到达时间后用来处理LightDoze处于IDLE状态的操作
            scheduleLightAlarmLocked(mNextLightIdleDelay);
           //计算下次进入Idle状态的
            mNextLightIdleDelay =
            Math.min(mConstants.LIGHT_MAX_IDLE_TIMEOUT,
                    (long)(mNextLightIdleDelay *
                mConstants.LIGHT_IDLE_FACTOR));
            if (mNextLightIdleDelay < mConstants.LIGHT_IDLE_TIMEOUT) {
                mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT;
            }
            //将LightDoze模式置为IDLE状态,开始进行一些限制
            mLightState = LIGHT_STATE_IDLE;
            addEvent(EVENT_LIGHT_IDLE);
            //申请一个wakelock锁,保持CPU唤醒
            mGoingIdleWakeLock.acquire();
            //处理LightDoze进入Idle状态后的操作
            mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON_LIGHT);
            break;
        ……
    }
}

这里会将 state 设置成 LIGHT_STATE_IDLE,并设置一个 mNextLightIdleDelay 的计时任务,以便进入下一个状态,mNextLightIdleDelay 的初始值是 5 分钟。

这里我们可以看到 LIGHT_STATE_PRE_IDLE 和 LIGHT_STATE_IDLE_MAINTENANCE 是同一个 case 处理逻辑,这两个状态的下一个状态都是 LIGHT_STATE_IDLE。

如果上一个状态是 LIGHT_STATE_IDLE_MAINTENANCE,则 mNextLightIdleDelay = Math.min(mConstants.LIGHT_MAX_IDLE_TIMEOUT,(long)(mNextLightIdleDelay * mConstants.LIGHT_IDLE_FACTOR)),LIGHT_MAX_IDLE_TIMEOUT 为 15 分钟,LIGHT_IDLE_FACTOR 为 2

所以 light doze 的 IDLE 时间为5 分钟、10 分钟,最后稳定为 15 分钟。

当 state 的状态转换成 IDLE 后,这里会申请 wakelock 锁,让 cpu 唤醒,然后通过MSG_REPORT_IDLE_ON_LIGHT 的 Handler 任务进行逻辑处理,然后再释放 wakelock 锁,让 cpu 休眠。

剩下的几种状态函数转换都在上面的函数中有注释,就不详细讲解了。

Doze 限制逻辑

我们接着看 MSG_REPORT_IDLE_ON_LIGHT 中做了哪些事情:

case MSG_REPORT_IDLE_ON:
case MSG_REPORT_IDLE_ON_LIGHT:: {
    final boolean deepChanged;
    final boolean lightChanged;
    if (msg.what == MSG_REPORT_IDLE_ON) {
        //通知PMS设置Deep Doze模式处于IDLE状态
        deepChanged = mLocalPowerManager.setDeviceIdleMode(true);
        //通知PMS为Light Doze模式不处于IDLE状态
        lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false);
    } else {
        //通知PMS设置Deep Doze模式不处于IDLE状态
        deepChanged = mLocalPowerManager.setDeviceIdleMode(false);
        //通知PMS为Light Doze模式处于IDLE状态
        lightChanged = mLocalPowerManager.setLightDeviceIdleMode(true);
    }
    try {
        //通知NetworkPolicyManager进入IDLE状态,进行网络访问的限制
        mNetworkPolicyManager.setDeviceIdleMode(true);
        //通知BatteryStatsService统计Light Doze或者Deep Doze进入IDLE状态
        mBatteryStats.noteDeviceIdleMode(msg.what == MSG_REPORT_IDLE_ON
                ? BatteryStats.DEVICE_IDLE_MODE_DEEP
                : BatteryStats.DEVICE_IDLE_MODE_LIGHT, null, Process.myUid());
    } catch (RemoteException e) {
    }
    //发送DeepDoze模式改变的广播
    if (deepChanged) {
        getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL);
    }
    //发送Light模式改变的广播
    if (lightChanged) {
        getContext().sendBroadcastAsUser(mLightIdleIntent, UserHandle.ALL);
    }
    //释放wakelock
    mGoingIdleWakeLock.release();
} break;

可以看到,Deep Doze 和 Light Doze 在进入 IDLE 状态后的逻辑处理在同一个地方。这里根据模式的不同,通知 PowerServiceManager,NetworkPolicyManager,BatteryStats 等进行不同的优化策略。这里主要做的事情有这几件:

  1. 调用 mLocalPowerManager.setDeviceIdleMode 设置是否是 Deep Doze 的 Idle 状态,如果为 Idle,这一步会将应用设置成忽略 WakeLock 的状态

  2. 调用 mLocalPowerManager.setLightDeviceIdleMode 设置是否是 Light Doze 的 Idle 状态

  3. 调用 mNetworkPolicyManager.setDeviceIdleMode(true),通过添加防火墙规则,来进行网络访问限制

  4. 调用 BatteryStats.noteDeviceIdleMode 进行状态变更及耗时统计

  5. 调用 sendBroadcastAsUser 发送广播,进入 Deep Doze 或者 Light Doze 的 Idle 状态

  6. 释放 WakeLock

Doze 限制逻辑取消

Light Doze 和 Deep Doze 进入 MAINTENCANCE 后都会取消各种限制,取消的逻辑在 MSG_REPORT_IDLE_OFF 的 handler 任务中处理。

case MSG_REPORT_IDLE_OFF: {
    // mActiveIdleWakeLock is held at this point
    EventLogTags.writeDeviceIdleOffStart("unknown");
    final boolean deepChanged = mLocalPowerManager.setDeviceIdleMode(false);
    final boolean lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false);
    try {
        mNetworkPolicyManager.setDeviceIdleMode(false);
        mBatteryStats.noteDeviceIdleMode(BatteryStats.DEVICE_IDLE_MODE_OFF,
                null, Process.myUid());
    } catch (RemoteException e) {
    }
    if (deepChanged) {
        incActiveIdleOps();
        getContext().sendOrderedBroadcastAsUser(mIdleIntent, UserHandle.ALL,
                null, mIdleStartedDoneReceiver, null, 0, null, null);
    }
    if (lightChanged) {
        incActiveIdleOps();
        getContext().sendOrderedBroadcastAsUser(mLightIdleIntent, UserHandle.ALL,
                null, mIdleStartedDoneReceiver, null, 0, null, null);
    }
    decActiveIdleOps();
} break;

Standby 模式

Doze 模式是针对整个系统的耗电优化模式,而 Standby 模式,即应用群组待机模式是针对单个应用的耗电优化模式,它是 Android7.0 引入的,当应用处于闲置状态时,系统会根据应用应用最近使用的时间和频率,设置成对应的群组,不同的群组下,jobs,alarm 和 network 的使用限制程度不一样。

Standby 模式的进入和退出

当用户有一段时间未触摸应用时,系统便会判断进入 Standby 模式,以下条件下不适用或者会退出 Standby 模式:

  1. 用户主动启动该 App;

  2. 该 App 当前有一个前台进程(或包含一个活动的前台服务,或被另一个 activity 或前台 service 使用);

  3. 在锁定屏幕或通知栏中看到的通知;

  4. 系统应用;

  5. 充电状态;

Standby 模式优化策略

应用在进入 Standby 后,会根据该应用所属的状态,对 Jobs,Alarms 和 Network 进行相应的限制,应用的状态分为五个等级:

  1. Activie:如果用户当前正在使用应用,应用将被归到“atcitive”状态中

  2. WORKING_SER:如果应用经常运行(12 至 24 小时内使用过),但当前未处于活跃状态,它将被归到“工作集”群组中。例如,用户在大部分时间都启动的某个社交媒体应用可能就属于“工作集”群组。如果应用被间接使用,它们也会被升级到“工作集”群组中 。

  3. FREQUENT:如果应用会定期使用,但不是每天都必须使用(亮屏时间差超过 1 小时、使用时间差超过 24 小时),它将被归到“常用”群组中。例如,用户在健身房运行的某个锻炼跟踪应用可能就属于“常用”群组。

  4. RARE:如果应用不经常使用(亮屏时间差超过 2 小时、使用时间差超过 48 小时),那么它属于“极少使用”群组。例如,用户仅在入住酒店期间运行的酒店应用就可能属于“极少使用”群组。如果应用处于“极少使用”群组,系统将对它运行作业、触发警报和接收高优先级 FCM 消息的能力施加严格限制。系统还会限制应用连接到网络的能力。

  5. NEVER:安装但是从未运行过的应用会被归到“从未使用”群组中。系统会对这些应用施加极强的限制。

下面是对这个五个等级的应用的限制情况:

https://developer.android.com/topic/performance/power/power-details

ee897068fe3a7ba3fb242d5185ea59c8.png
Standby 模式实现原理

Standby 模式的逻辑实现在 AppStandbyController 对象中,该对象提供了 reportEvent,来让外部进行 app 行为变化的通知,如 ams,NotificationManagerService 等都会调用 reportEvent 来告知 app 有行为变化并更新 Bucket

更新 Bucket
void reportEvent(UsageEvents.Event event, long elapsedRealtime, int userId) {
    if (!mAppIdleEnabled) return;
    synchronized (mAppIdleLock) {
        // TODO: Ideally this should call isAppIdleFiltered() to avoid calling back
        // about apps that are on some kind of whitelist anyway.
        final boolean previouslyIdle = mAppIdleHistory.isIdle(
                event.mPackage, userId, elapsedRealtime);
        // Inform listeners if necessary
        if ((event.mEventType == UsageEvents.Event.ACTIVITY_RESUMED
                || event.mEventType == UsageEvents.Event.ACTIVITY_PAUSED
                || event.mEventType == UsageEvents.Event.SYSTEM_INTERACTION
                || event.mEventType == UsageEvents.Event.USER_INTERACTION
                || event.mEventType == UsageEvents.Event.NOTIFICATION_SEEN
                || event.mEventType == UsageEvents.Event.SLICE_PINNED
                || event.mEventType == UsageEvents.Event.SLICE_PINNED_PRIV
                || event.mEventType == UsageEvents.Event.FOREGROUND_SERVICE_START)) {

            final AppUsageHistory appHistory = mAppIdleHistory.getAppUsageHistory(
                    event.mPackage, userId, elapsedRealtime);
            final int prevBucket = appHistory.currentBucket;
            final int prevBucketReason = appHistory.bucketingReason;
            final long nextCheckTime;
            final int subReason = usageEventToSubReason(event.mEventType);
            final int reason = REASON_MAIN_USAGE | subReason;

            //根据使用行为更新bucket
            if (event.mEventType == UsageEvents.Event.NOTIFICATION_SEEN
                    || event.mEventType == UsageEvents.Event.SLICE_PINNED) {
                mAppIdleHistory.reportUsage(appHistory, event.mPackage,
                        STANDBY_BUCKET_WORKING_SET, subReason,
                        0, elapsedRealtime + mNotificationSeenTimeoutMillis);
                nextCheckTime = mNotificationSeenTimeoutMillis;
            } else if (event.mEventType == UsageEvents.Event.SYSTEM_INTERACTION) {
                mAppIdleHistory.reportUsage(appHistory, event.mPackage,
                        STANDBY_BUCKET_ACTIVE, subReason,
                        0, elapsedRealtime + mSystemInteractionTimeoutMillis);
                nextCheckTime = mSystemInteractionTimeoutMillis;
            } else if (event.mEventType == UsageEvents.Event.FOREGROUND_SERVICE_START) {
                // Only elevate bucket if this is the first usage of the app
                if (prevBucket != STANDBY_BUCKET_NEVER) return;
                mAppIdleHistory.reportUsage(appHistory, event.mPackage,
                        STANDBY_BUCKET_ACTIVE, subReason,
                        0, elapsedRealtime + mInitialForegroundServiceStartTimeoutMillis);
                nextCheckTime = mInitialForegroundServiceStartTimeoutMillis;
            } else {
                mAppIdleHistory.reportUsage(appHistory, event.mPackage,
                        STANDBY_BUCKET_ACTIVE, subReason,
                        elapsedRealtime, elapsedRealtime + mStrongUsageTimeoutMillis);
                nextCheckTime = mStrongUsageTimeoutMillis;
            }
            //设置延时消息,根据使用时间更新bucket
            mHandler.sendMessageDelayed(mHandler.obtainMessage
                    (MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, event.mPackage),
                    nextCheckTime);
            final boolean userStartedInteracting =
                    appHistory.currentBucket == STANDBY_BUCKET_ACTIVE &&
                    prevBucket != appHistory.currentBucket &&
                    (prevBucketReason & REASON_MAIN_MASK) != REASON_MAIN_USAGE;
            maybeInformListeners(event.mPackage, userId, elapsedRealtime,
                    appHistory.currentBucket, reason, userStartedInteracting);

            if (previouslyIdle) {
                notifyBatteryStats(event.mPackage, userId, false);
            }
        }
    }
}

reportEvent 会根据 mEventType 进行一次 Bucket 更新,并根据 mEventType 设置一次延时任务,这个延时任务中会再次根据应用的使用行为再次更新 Bucket。其中 Notification 类型的消息的延迟时间为 12 小时,SYSTEM_INTERACTION 为 10 分钟,其他的 mStrongUsageTimeoutMillis 为 1 小时。

MSG_CHECK_PACKAGE_IDLE_STATE 的 handler 消息主要根据使用时长更新 Bucket。

static final int[] THRESHOLD_BUCKETS = {
        STANDBY_BUCKET_ACTIVE,
        STANDBY_BUCKET_WORKING_SET,
        STANDBY_BUCKET_FREQUENT,
        STANDBY_BUCKET_RARE
};

static final long[] SCREEN_TIME_THRESHOLDS = {
        0,
        0,
        COMPRESS_TIME ? 120 * 1000 : 1 * ONE_HOUR,
        COMPRESS_TIME ? 240 * 1000 : 2 * ONE_HOUR
};

static final long[] ELAPSED_TIME_THRESHOLDS = {
        0,
        COMPRESS_TIME ?  1 * ONE_MINUTE : 12 * ONE_HOUR,
        COMPRESS_TIME ?  4 * ONE_MINUTE : 24 * ONE_HOUR,
        COMPRESS_TIME ? 16 * ONE_MINUTE : 48 * ONE_HOUR
};

long[] mAppStandbyScreenThresholds = SCREEN_TIME_THRESHOLDS;
long[] mAppStandbyElapsedThresholds = ELAPSED_TIME_THRESHOLDS;

@StandbyBuckets int getBucketForLocked(String packageName, int userId,
        long elapsedRealtime) {
    int bucketIndex = mAppIdleHistory.getThresholdIndex(packageName, userId,
            elapsedRealtime, mAppStandbyScreenThresholds, mAppStandbyElapsedThresholds);
    return THRESHOLD_BUCKETS[bucketIndex];
}

AppIdleHistory.java

int getThresholdIndex(String packageName, int userId, long elapsedRealtime,
        long[] screenTimeThresholds, long[] elapsedTimeThresholds) {
    ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
    AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
            elapsedRealtime, false);
    if (appUsageHistory == null) return screenTimeThresholds.length - 1;

    //app最后一次亮屏使用到现在,已经有多久的亮屏时间
    long screenOnDelta = getScreenOnTime(elapsedRealtime) - appUsageHistory.lastUsedScreenTime;
    //app最后一次使用到现在的时间点
    long elapsedDelta = getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedElapsedTime;
    for (int i = screenTimeThresholds.length - 1; i >= 0; i--) {
        if (screenOnDelta >= screenTimeThresholds[i]
            && elapsedDelta >= elapsedTimeThresholds[i]) {
            return i;
        }
    }
    return 0;
}

App 耗电分析

Battery Historian

Android 官方提供了 Battery Historian 来进行电量使用的分析,Battery Historian 图表会显示一段时间内与电源相关的事件。

b0fa4a6e2a099fd74cdebbbcd995c214.png

从上面的图也可以看到,进入到 Doze 后,BLE scanning,GPS 等就无行为了,并且 cpu,wakelock 等活动的频率也变低了。

我们还能通过 Battery Historian 获取应用的:

  • 在设备上的估计耗电量

  • 网络信息

  • 唤醒锁定次数

  • 服务

  • 进程信息

5c5a2a43b27bba6ad4a102cb0276b703.png

官方文档已经讲的非常详细,就不在这儿细说了:

https://developer.android.com/topic/performance/power/setup-battery-historian?hl=zh-cn

Slardar

Slardar 电量相关的统计指标项包括:

  • app 处于前台时,提供电流作为耗电指标

  • 通过采集 app 的 cpu、流量和 gps 等的使用,来计算出一个加权和作为耗电指标

  • 电池温度,作为衡量耗电的辅助参考

归因项有:

  • 高 CPU 可以通过 cpu 菜单查看高耗 CPU 的堆栈

  • gps(location),alarm 和 wakelock 使用在超过指定持有时间和频次后,会上报当时的采集堆栈

虽然 Slardar 有上报很多功耗相关指标,但是目前还只能作为整体功耗的参考,并且很多指标波动起伏大,没法对更细化的治理提供帮助。

飞书耗电治理

治理目标

  1. 消除主流手机的高功耗提醒

  2. 建立健全的功耗监控及防劣化体系

治理方案

在前面我们已经知道耗电=模块功率 × 模块耗时,所以治理本质就是在不影响性能和功能的情况下,减少飞书中所使用到的模块的耗时,并且我们了解了系统进行耗电优化的策略,在飞书的耗电治理中,也可以同样的参考对应的策略。

治理方案主要分为监控的完善和耗电的治理。

功耗治理

为了能体系化地进行功耗治理,这里分为了针对耗电模块进行治理和针对状态进行执行两大类。

分模块治理

模块的耗电治理主要体现在下面几个方面:

1.CPU

  • 死循环函数,高频函数,高耗时函数,无效函数等不必要的 cpu 消耗或消耗较多的函数治理

  • cpu 使用率较高的场景及业务治理

2.GPU 和 Display

  • 过度绘制,过多的动画,不可见区域的动画等浪费 GPU 的场景治理

  • 主动降低屏幕亮度,使用深色 UI 等方案降低屏幕电量消耗

3.网络

  • 不影响业务和性能前提下,降低网络访问频率

  • Doze 状态时减少无效的网络请求

4.GPS

  • 对使用 GPS 的场景,如小程序等,合理的降低精度,减少请求频率

5.Audio、Camera、Video 等项

除了分模块治理,还针对状态进行治理,主要状态有这几种:

分状态治理

1.前台状态

  • 渲染场景优化

  • 音视频等场景优化

  • ……

2.后台状态

  • task 任务降频或者丢弃

  • 网络访问降频,适配 Doze 模式

  • 减少 cpu 消耗较多的函数执行

  • 减少 gps 等高功耗场景

完善功耗分析和监控体系

为了能更好地进行治理,完善的功耗分析和监控体系是不可避免的,不然就会出现无的放矢的状态。在这一块主要建设的点有:

1. 完善的 CPU 消耗监控

  • 前后台高 cpu 消耗场景监控,高 cpu 消耗线程监控(slardar 已有)

  • 高频 task,高耗时 task,后台 task 监控(已有)

  • 消耗较高,耗时较高的函数监控

2. GPU 和 Display 消耗监控

  • 动画场景,过度绘制检测,View 层级检测,屏幕电量消耗监控等

3. 网络

  • Rust,OkHttp 及其他网络请求场景,频率,消耗监控

  • 后台网络访问监控

4. GPS

  • GPS 使用场景,时长,电量消耗监控

5. Audio、Camera、Video

  • 使用场景,时长,电量消耗监控

6. 整体和场景的电量消耗

  • 飞书整体的电量消耗和不同场景的电量消耗,用来度量版本功耗的质量

加入我们

我们是飞书 Android 客户端品质团队。当前团队处于组建初期,各个方向都有非常多的岗位开放,团队氛围不卷,上升空间大,可 base 北京、杭州和深圳。欢迎对性能优化和稳定性建设有兴趣的同学加入,大家一起把 lark(字节内最复杂的一款产品)打造成全球性能最好的办公套件类产品(微软 office 套件,谷歌 G-suite)。

招聘链接:

  • Lark Android 技术专家岗位

https://people.bytedance.net/hire/referral/position?job_post_id=6830690441337440520

  • Lark Android 工程师岗位

https://people.bytedance.net/hire/referral/position?job_post_id=6811324588367268110

  • Lark Android 开发实习生

https://people.bytedance.net/hire/referral/position?job_post_id=6916040393788377358

092a8b1f927d89959d711cfc55f219a4.png  点击“阅读原文”了解岗位详情!

相关推荐

2024 抖音欢笑中国年(五):Wasm、WebGL 在互动技术中的创新应用

前言随着 Web 前端技术的不断发展,越来越多的新兴技术方案被引入到 Web 开发中,其中 Wasm 和 WebGL 作为前端领域的两大利器,为开发者带来了更多的可能性。本文将结合2024 年抖音欢笑中国年的部分项目,重点介绍如何利用 Wasm 和 WebGL 对目前流行的一些前端互动技术(比如 Lottie、渲染引擎、动画图片等)进行创新和实践,利用 Wasm 和 WebGL 等新技术方案的特性...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(四):渲染技术实践与探索

作者:陈瑞、欧阳浩铸、王武俊、倪梵云前言抖音在2024年春节期间推出了欢笑中国年系列活动,为用户带来了全新的体验和乐趣。而SAR Creator则为该项目研发工作提供了重要的技术支持。SAR Creator是一款基于 Typescript 的高性能、轻量化的互动解决方案,目前支持了浏览器和跨端框架平台,服务于字节内部的各种互动业务。这些绚烂多彩的互动场景当然也离不开实时渲染技术的支持,因此本文将专...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(三):编辑器技巧与实践

前言本次春节活动中,我们大部分场景使用内部的 SAR Creator互动方案来实现。SAR Creator 是一款基于 TypeScript 的高性能、轻量化的互动解决方案,目前支持了Web和字节内部跨端框架平台,服务于字节内部的各种互动业务,包括但不限于抖音春节、抖音直播礼物、抖音UG活动等。SAR Creator 编辑器支持了图形化界面,提供了各类完善的系统(光照、动画、脚本等)供用户快速便捷...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(二):AnnieX互动容器创新玩法解析

本文基于24年抖音春节活动业务背景,介绍了字节跨端容器AnnieX在游戏互动套件上的探索,致力于提升容器在游戏互动场景的优化能力。业务背景AnnieX作为字节一方游戏统一容器,服务字节内部电商、直播、UG等跨端场景业务。在字节一方游戏互动场景,有大量的一方游戏业务对容器有特定的流量、端能力和游戏优化的诉求。因此我们不断深入互动游戏业务特点,为字节游戏提供完善游戏端能力和流量运营能力,同时提供游戏互...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(一):招财神龙互动技术揭秘

字节跳动旗下的抖音等 App 在 2024 年春节期间推出了欢笑中国年系列活动,在实现增长业务目标的同时,为用户带来了全新的体验和乐趣。「招财神龙」是其中的一个重要玩法。前言本次春节活动,使用到了字节内的主要前端、跨端、互动技术产品。主要涉及:跨端框架提供了首屏直出的方案使其具有较短的首屏时间,能够大大提升业务加载成功率。跨端框架也提供了 Canvas 作为 SAR Creator 等渲染引擎的...

字节跳动技术团队官方博客 1万+

Monorepo 解决方案 — 基于 Bazel 的 Xcode 性能优化实践

背景介绍书接上回《Monorepo 解决方案 — Bazel 在头条 iOS 的实践》,在头条工程切换至 Bazel 构建系统后,为了支持用户使用 Xcode 开发的习惯,我们使用了开源项目 Tulsi 作为生成工具,用于将 Bazel 工程转换为 Xcode 工程。但是在使用的过程中,我们发现了一些问题,其中影响较大的是,Xcode 工程卡顿:对于头条这种大型项目来说,Xcode 卡顿一直是本地...

字节跳动技术团队官方博客 1万+

抖音 ANR 自动归因平台建设实践

背景介绍本文在 2024 年初最新一期『抖音客户端基础技术大揭秘』技术沙龙活动中已做过专题分享,本次将内容重新整理文章进行分享。公众号后台回复技术沙龙可查看沙龙回放及 PPT~抖音作为一个超大型的应用,我们在 ANR 问题治理上面临着很大的挑战。首先对于存量问题的优化,由于缺少有效的归因手段,一些长期的疑难问题一直难以突破解决,例如长期位于 Top 1 的 nativePollOnce 问题。同时...

字节跳动技术团队官方博客 2万+

CVPR 2024 | CAMixerSR 动态注意力分配的超分辨率加速框架

背景随着相关技术和应用的发展,比如超高清屏幕、虚拟现实(VR)等沉浸式体验的增加,用户对超高分辨率图像和视频的需求变得越来越强烈。在这些场景中,图像的质量和清晰度对于提供最佳的用户体验至关重要。超高分辨率不仅能提供更清晰、更真实的视觉效果,还能在一定程度上增强人们的互动和沉浸感,在一些VR场景中我们需要8K甚至16K的才可以满足需求。然而要生成或者处理这些超高分辨率的内容,对算力的要求也是与日增长...

字节跳动技术团队官方博客 1万+

CVPR 2024 | Modular Blind Video Quality Assessment:模块化无参视频质量评估

无参视频质量评估 (Blind Video Quality Assessment,BVQA) 在评估和改善各种视频平台并服务用户的观看体验方面发挥着关键作用。当前基于深度学习的模型主要以下采样/局部块采样的形式分析视频内容,而忽视了实际空域分辨率和时域帧率对视频质量的影响,随着高分辨率和高帧率视频投稿逐渐普及,特别是跨分辨率/帧率视频转码档位画质评估场景中,这种影响变得更加不可忽视。在本文中,我们...

字节跳动技术团队官方博客 1万+

2024 AI & 前端:回首展望,光芒未至,破晓之前!

前言回望 2023 年,ChatGPT 的突然爆火,让 AI 无疑成为最为值得注目的新兴领域之一,我们也一起见证了生成式 AI 的寒武纪大爆发。这一年来,国内外的生成式 AI 、大模型和相关产品以令人眼花缭乱的速度更新迭代,新的创业浪潮风起云涌。在这 AI 浪潮下,也让我们有了新的开发思考,探索着在各个环节中“前端 & AI”的应用场景。勇于探索的前端开发者们已经开始挥舞着 AI 的“魔法...

字节跳动技术团队官方博客 1万+

Kotlin 云端差分缓存技术

本文由字节跳动 Buildinfra 团队出品。在我们的工程上线 Monorepo 全源码后,Kotlin 编译成了整个编译中最耗时的步骤,全源码过程中大量的 BuildCache Miss 导致我们的编译数据落后原来多仓二进制时代很多,且业界没有相关的解决方案。本篇文章我们来具体阐述下 BuildInfra 团队自研的解决方案 - Kotlin 云端差分方案的原理和技术实现。一、Monorepo...

字节跳动技术团队官方博客 1万+

字节跳动基础架构SRE-Copilot获得2023 CCF国际AIOps挑战赛冠军

近日,2023 CCF国际AIOps挑战赛决赛暨“大模型时代的AIOps”研讨会在北京成功举办,活动吸引了来自互联网、运营商、科研院所、高校、软硬件厂商等领域多名专家学者参与,为智能运维的前沿学术研究、落地生产实践打开了新思路。决赛中,从初赛两百多支队伍中脱颖而出的十支入围队伍分别展示了各自的方案,并进行了现场答辩,评审专家从选题方向、创新性、实用性、完整度和实验复现结果等多角度进行了综合评定,最...

字节跳动技术团队官方博客 2万+

字节跳动百万级Metrics Agent性能优化的探索与实践

背景metricserver2 (以下简称Agent)是与字节内场时序数据库 ByteTSD 配套使用的用户指标打点 Agent,用于在物理机粒度收集用户的指标打点数据,在字节内几乎所有的服务节点上均有部署集成,装机量达到百万以上。此外Agent需要负责打点数据的解析、聚合、压缩、协议转换和发送,属于CPU和Mem密集的服务。两者结合,使得Agent在监控全链路服务成本中占比达到70%以上,对Ag...

字节跳动技术团队官方博客 1万+

西瓜视频RenderThread引起的闪退问题攻坚历程

背景影响西瓜之前存在过一类RenderThread闪退,从堆栈上看,全部都是系统so调用,给人的第一印象像是一个系统bug,无从下手。闪退集中在Android 5~6上,表现为打开直播间立即闪退。该问题在2022年占据Native Crash Top5,2023年更是上升到到Top1。因此有必要投入时间和精力再重新审视一下这个问题。在历经多周的源码分析和排查后,逐步明确了问题根因并修复,最终取得了...

字节跳动技术团队官方博客 2万+

字节电商双11 大促容量保障是如何做的?

前言Rhino 简介Rhino是字节自研全链路容量评估产品,致力于构建完整的全链路容量评估解决方案(覆盖:容量预估->资源准备->数据准备->容量验证->监控->分析->决策->处理反馈);围绕容量在稳定性、成本、效率 三方面提供业务全方位基础支撑。Rhino 目前已经成为字节各业务容量评估主流解决方案,并且历年来在业务大型活动稳定性保障中(抖音春节项目、...

字节跳动技术团队官方博客 10万+

使用火山引擎 APMPlus 解决抖音Top 1 Java 崩溃的通用优化方案

背景近3个月,抖音 Android 版面临一个多次触发线上报警的崩溃问题,全量版本和灰度版本的异常数据激增,该问题不仅容易触发报警,更成为了 Java Top 1 崩溃问题,带来巨大困扰,急需攻坚解决。本文展现了具体的分析过程、优化思路和解决方案,同时提供了已集成该方案的实用工具。初步分析多维特征我们以某发版期间数据为例进行分析:机型方面:比较分散,有聚集部分samsung sm-s9180 占比...

字节跳动技术团队官方博客 2万+

用 Addon 增强 Node.js 和 Electron 应用的原生能力

前言Node.js Addon 是 Node.js 中为 JavaScript 环境提供 C/C++ 交互能力的机制。其形态十分类似 Java 的 JNI,都是通过提供一套 C/C++ SDK,用于在 C/C++ 中创建函数方法、进行数据转换,以便 JavaScript / Java 等语言进行调用。这样编写的代码通常叫做 Bindings。此外还有基于 C ABI Calling Convent...

字节跳动技术团队官方博客 2万+

火山引擎 ByteHouse 的增强型数据导入技术实践

作为企业数字化建设的必备要素,易用的数据引擎能帮助企业提升数据使用效率,更好提升数据应用价值,夯实数字化建设基础。数据导入是衡量OLAP引擎性能及易用性的重要标准之一,高效的数据导入能力能够加速数据实时处理和分析的效率。作为一款OLAP引擎,火山引擎云原生数据仓库ByteHouse源于开源ClickHouse,在字节跳动多年打磨下,提供更丰富的能力和更强性能,能为用户带来极速分析体验,支撑实时数据...

字节跳动技术团队官方博客 1万+

打造企业级智能问答系统的秘密:如何使用云数据库 PostgreSQL 版实现向量检索

本文就如何利用火山引擎云数据库 PostgreSQL 版和大语言模型技术(Large Language Model,简称 LLM),实现企业级智能交互式问答系统进行介绍。通过本文,你将会了解交互式问答系统的原理,学习 PostgreSQL 的向量化存储和检索技术,以及大语言模型交互技术等。背景在大数据的浪潮下,众多企业建立了自己的知识库,以便于信息检索和知识查询。然而,随着知识库内容的膨胀,传统的...

字节跳动技术团队官方博客 2万+

抖音大型直播的画质优化实践

面临挑战随着抖音内容生态的不断丰富,越来越多的大型赛事在抖音平台进行直播,世界杯/春晚/亚运会等各项赛事节目引来大量用户观看。卡塔尔世界杯期间,抖音提供的稳定高质直播画面为观众带来了完美的观赛体验,决赛的 PCU 高达 3700W+。不同赛事节目涉及链路众多,且不同赛事之间存在差异,如何保障各链路的画质稳定并进一步提升画质,是一个巨大的挑战。如何应对挑战?画质优化链路大型赛事直播涉及链路较长,不同...

字节跳动技术团队官方博客 2万+

抖音直播新一代BVC编码器正式亮相

面临挑战在直播行业发展如火如荼的今天,用户对视频体验的要求也水涨船高。视频基础体验的关键要素包括清晰度、流畅度、低延迟等,而这些要素的“第一性原理”,就是视频本身的编码效率,也就是压缩率。视频编码是整个技术体系的基座,编码效率的显著提升,能够在同等码率下极大提高画质,从而改善用户体验。视频编码效率的重要性不言而喻,但进一步地提升也并非易事,尤其在直播场景中,对编码速度、延迟、码率控制等方面都有很高...

字节跳动技术团队官方博客 7356

Go Metrics SDK Tag 校验性能优化实践

背景Metrics SDK 是与字节内场时序数据库 ByteTSD 配套的用户指标打点 SDK,在字节内数十万服务中集成,应用广泛,因此 SDK 的性能优化是个重要和持续性的话题。本文主要以 Go Metrics SDK 为例,讲述对打点 API 的 hot-path 优化的实践。用户在使用 SDK API 进行打点时,需要传入指标对应的 Tag:tags:=[]m.T{{Name:"foo...

字节跳动技术团队官方博客 1万+

云上智能驾驶三维重建最佳实践

智能驾驶技术的不断发展,正在改变着我们的出行方式和交通系统。作为其中的一个关键技术,三维重建在智能驾驶系统中起着重要的作用。除去车端本身的感知、重建算法,自动驾驶技术的落地与发展需要庞大的云端重建能力支撑,火山引擎多媒体实验室通过行业领先的自研三维重建技术,结合强大的云平台资源与能力,助力相关技术在云端大规模重建、自动标注、真实感仿真等场景的落地与应用。本文重点介绍火山引擎多媒体实验室三维重建技术...

字节跳动技术团队官方博客 6305

火山引擎实时、低延时拥塞控制算法的优化实践

摘要火山引擎智能拥塞控制算法 VICC(Volcano Intelligent Congestion Control)是一种自适应的拥塞控制算法,旨在解决全球不同网络环境下,不同音视频应用对带宽利用率和延时的差异化要求。它结合了传统拥塞控制算法(如 GCC 和 BBR)的优点,并且能够根据不同的网络条件、业务偏好和码率特征进行自适应调整,包括自适应拥塞响应速度、自适应带宽探测幅度、自适应丢包检测策...

字节跳动技术团队官方博客 6735

veImageX 演进之路:Web 图片加载提速50%

背景说明火山引擎veImageX演进之路主要介绍了veImageX在字节内部从2012年随着字节成长过程中逐步演进的过程,演进中包括V1、V2、V3版本并最终面向行业输出;整个演进过程中包括服务端、客户端、网络库、业务场景与优化等多个角度介绍在图像处理压缩、省成本与体验优化的经验与方案;本篇文章重点介绍在web端演进和提供的能力,图片是 Web 站点中的重要元素,图片体积、格式、分辨率以及渲染方式...

字节跳动技术团队官方博客 6589

自研多模态追踪算法 PICO 为「手柄小型化」找到新思路

作者:张韬、林泽一 、闻超 、赵洋研发背景作为头戴的追踪配件,VR手柄可以通过HMD(头戴显示设备)的inside-out光学追踪定位原理,计算出手柄的空间运动轨迹,同时结合6轴传感器实现6DoF空间定位。与此同时,结合手柄控制器的物理按键、马达反馈、摇杆等,用户还能获得逼真、细腻的触觉反馈,进一步增强虚拟现实人机交互的能力以及沉浸感,这也是目前无手柄方案所难以实现的。目前主流VR手柄的追踪技术方...

字节跳动技术团队官方博客 7818

如何利用播放器节省20%点播成本

点播成本节省的点其实涉及诸多部分,例如:CDN、转码、存储等,而利用播放器降本却是很多客户比较陌生的部分。火山引擎基于内部支撑抖音集团相关业务的实践,播放器恰恰是成本优化中最重要和最为依赖的部分。火山引擎的视频团队做了份数据统计,在一个很经典的视频业务中,我们在2022年至2023年大约1年半的时间里,针对这个业务进行了33次成本优化点,其中13次是播放器主导的优化,其余的有12次也是需要播放器强...

字节跳动技术团队官方博客 6176

火山引擎 ByteHouse:ClickHouse 如何保证海量数据一致性

背景ClickHouse是一个开源的OLAP引擎,不仅被全球开发者广泛使用,在字节各个应用场景中也可以看到它的身影。基于高性能、分布式特点,ClickHouse可以满足大规模数据的分析和查询需求,因此字节研发团队以开源ClickHouse为基础,推出火山引擎云原生数据仓库ByteHouse。在日常工作中,研发人员经常会遇到业务链路过长,导致流程稳定性和数据一致性难保障的问题,这在分布式、跨服务的场...

字节跳动技术团队官方博客 6900

抖音集团都在用的画质评估工具,确定不试试吗?

导读本文从抖音集团内部画质评估体系的建设历程着笔,主要分享了画质评测对于业务的重要性、主要应用场景和内部产品的一些典型实践案例。通过分享业务视角遇到的一些问题和我们的解决思路,希望能抛砖引玉,为遇到类似困扰的伙伴们提供有价值的参考。画质评估体系建设历程为何评测画质如此重要?我们通过线上业务大量实验发现,图片画质优劣对点击率、 停留时长等消费类指标有正相关影响,间接影响用户收益指标。因此,建设一套行...

字节跳动技术团队官方博客 7940

VLDB 2023 | CDSBen: 字节跳动 veDB 数据库存储系统性能测试模型

背景随着业务爆炸式增长与云原生技术的日渐成熟,大量云原生分布式数据库产品如雨后春笋般涌现,其中一部分主打 OLTP 场景的分布式数据库强调的是从计算-存储分离架构获得弹性收益;对于业界各种计算-存储分离架构的数据库而言,怎么用真实的端到端数据库 workload 去 benchmark 其底层存储系统一直存在以下难题:对于数据库专用存储系统,不存在如 fio 一样的“事实标准” benchmark...

字节跳动技术团队官方博客 5896
上一篇: 抖音 Android 性能优化系列:Java OOM 优化之 NativeBitmap 方案
下一篇: ByteDoc 3.0:MongoDB 云原生实践
 字节跳动技术团队
字节跳动技术团队 企业官方账号 企业官方账号
博客等级 码龄7年 8153粉丝 316原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值