一:概念
App Standby是一种电池管理技术,根据应用最近使用时间和使用频率,来进行对应用使用jobs,alarm,network的优化,达到省电的目的。
二:群组介绍
从这个表上可以看出根据应用使用情况,分成了5个群组。
2.1 ACTIVE
如果用户正在使用应用,那么这个应用将被划分到ACTIVE群组中。例如:
·应用已启动一个activity
·应用正在运行前台服务
·应用的同步适配器与某个前台应用使用的 content provider 关联
·用户在应用中点击了某个通知
2.2 WORKING_SET
如果应用经常运行,但当前未处于活跃状态,它将被归到“工作集”群组中。 例如,用户在大部分时间都启动的某个社交媒体应用可能就属于“工作集”群组。 如果应用被间接使用,它们也会被升级到“工作集”群组中 。
2.3 FREQUENT
如果应用会定期使用,但不是每天都必须使用,它将被归到“常用”群组中。 例如,用户在健身房运行的某个锻炼跟踪应用可能就属于“常用”群组。
2.4 RARE
如果应用不经常使用,那么它属于“极少使用”群组。 例如,用户仅在入住酒店期间运行的酒店应用就可能属于“极少使用”群组。
2.5 RESTRICTED
安装但是从未运行过的应用会被归到“从未使用”群组中。 系统会对这些应用施加极强的限制。
三:系统如何根据应用使用情况调整应用所属群组
3.1 应用状态改变触发群组调整
下面以Activity启动为例,解析应用状态改变是如何触发群组调整的
3.1.1 ActivityTaskSupervisor.realStartActivityLocked
调用Task的minimalResumeActivityLocked函数来进行触发群组调整的流程。Activity启动流程这里就不赘述了,后面会出专门的文章来解析。
boolean realStartActivityLocked(ActivityRecord r, WindowProcessController proc,
boolean andResume, boolean checkConfig) throws RemoteException {
...
if (andResume && readyToResume()) {
// As part of the process of launching, ActivityThread also performs
// a resume.
rootTask.minimalResumeActivityLocked(r);
}
...
}
3.1.2 Task.minimalResumeActivityLocked
调用ActivityRecord的setState函数来继续触发流程,注意这里传入的state是RESUMED。
void minimalResumeActivityLocked(ActivityRecord r) {
r.setState(RESUMED, "minimalResumeActivityLocked");
...
}
3.1.3 ActivityRecord.setState
调用ActivityTaskManagerService的updateActivityUsageStats函数来继续触发流程。这里传入的event是ACTIVITY_RESUMED
void setState(State state, String reason) {
...
switch (state) {
case RESUMED:
mAtmService.updateBatteryStats(this, true);
mAtmService.updateActivityUsageStats(this, Event.ACTIVITY_RESUMED);
...
}
...
}
3.1.4 ActivityTaskManagerService.updateActivityUsageStats
调用ActivityManagerService的updateActivityUsageStats函数来继续触发流程。
void updateActivityUsageStats(ActivityRecord activity, int event) {
...
final Message m = PooledLambda.obtainMessage(
ActivityManagerInternal::updateActivityUsageStats, mAmInternal,
activity.mActivityComponent, activity.mUserId, event, activity.token, taskRoot,
new ActivityId(taskId, activity.shareableActivityToken));
mH.sendMessage(m);
}
3.1.5 ActivityManagerService.updateActivityUsageStats
调用UsageStatsService的reportEvent函数来继续触发流程。UsageStatsService的reportEvent函数是触发群组调整的统一接口,无论是Activity状态改变还是Service状态改变等,最终都是通过这个函数来通知系统进行群组调整的。
public void updateActivityUsageStats(ComponentName activity, int userId, int event,
IBinder appToken, ComponentName taskRoot, ActivityId activityId) {
if (mUsageStatsService != null) {
mUsageStatsService.reportEvent(activity, userId, event, appToken.hashCode(), taskRoot);
if (event == Event.ACTIVITY_RESUMED) {
// Report component usage as an activity is an app component
mUsageStatsService.reportEvent(
activity.getPackageName(), userId, Event.APP_COMPONENT_USED);
}
}
...
}
3.1.6 UsageStatsService.LocalService.reportEvent
调用UsageStatsService的reportEventOrAddToQueue来继续触发流程
private final class LocalService extends UsageStatsManagerInternal {
@Override
public void reportEvent(ComponentName component, int userId, int eventType,
int instanceId, ComponentName taskRoot) {
...
Event event = new Event(eventType, SystemClock.elapsedRealtime());
event.mPackage = component.getPackageName();
event.mClass = component.getClassName();
event.mInstanceId = instanceId;
...
reportEventOrAddToQueue(userId, event);
}
}
3.1.7 UsageStatsService.reportEventOrAddToQueue
发送MSG_REPORT_EVENT的Message继续触发流程
private void reportEventOrAddToQueue(int userId, Event event) {
if (mUserUnlockedStates.contains(userId)) {
mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
return;
}
synchronized (mLock) {
LinkedList<Event> events = mReportedEvents.get(userId);
if (events == null) {
events = new LinkedList<>();
mReportedEvents.put(userId, events);
}
events.add(event);
if (events.size() == 1) {
// 把event时间保存到磁盘上进行持久化存储
// Every time a file is persisted to disk, mReportedEvents is cleared for this user
// so trigger a flush to disk every time the first event has been added.
mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL);
}
}
}
3.1.8 UsageStatsService.reportEvent
Handler的handleMessage接收到MSG_REPORT_EVENT Message后,会调用UsageStatsService的reportEvent函数继续触发流程,这里会触发所有实现了UsageEventListener类的onUsageEvent回调函数。
void reportEvent(Event event, int userId) {
...
synchronized (mUsageEventListeners) {
final int size = mUsageEventListeners.size();
for (int i = 0; i < size; ++i) {
mUsageEventListeners.valueAt(i).onUsageEvent(userId, event);
}
}
}
3.1.9 AppStandbyController.onUsageEvent
AppStandbyController是更新群组的核心类,它实现了UsageStatsManagerInternal.UsageEventListener,触发了onUsageEvent函数的执行,这函数里会判断event类型,符合要求的event会继续调用reportEventLocked来进行群组更新
public void onUsageEvent(int userId, @NonNull UsageEvents.Event event) {
if (!mAppIdleEnabled) return;
final int eventType = event.getEventType();
if ((eventType == UsageEvents.Event.ACTIVITY_RESUMED
|| eventType == UsageEvents.Event.ACTIVITY_PAUSED
|| eventType == UsageEvents.Event.SYSTEM_INTERACTION
|| eventType == UsageEvents.Event.USER_INTERACTION
|| eventType == UsageEvents.Event.NOTIFICATION_SEEN
|| eventType == UsageEvents.Event.SLICE_PINNED
|| eventType == UsageEvents.Event.SLICE_PINNED_PRIV
|| eventType == UsageEvents.Event.FOREGROUND_SERVICE_START)) {
final String pkg = event.getPackageName();
final List<UserHandle> linkedProfiles = getCrossProfileTargets(pkg, userId);
synchronized (mAppIdleLock) {
final long elapsedRealtime = mInjector.elapsedRealtime();
reportEventLocked(pkg, eventType, elapsedRealtime, userId);
...
}
}
}
3.1.10 AppStandbyController.reportEventLocked
根据不同的event,给AppIdleHistory传入不同的Bucket值,来更新Bucket。当新的Bucket和上一次Bucket值不同时,发送一个延时消息来进行后续Bucket更新,并调用maybeInformListeners来通知其他模块(例如Alarm、Job、Network等)该进程Bucket发生变化了,需要根据新的Bucket调整策略。
private void reportEventLocked(String pkg, int eventType, long elapsedRealtime, int userId) {
...
//Bucket相关信息都是保存在AppUsageHistory里的,这里是获取上一次的Bucket信息
final int prevBucket = appHistory.currentBucket;
final int prevBucketReason = appHistory.bucketingReason;
...
} else {//根据不同的event,走不同的逻辑,这里传入的event事件是ACTIVITY_RESUMED,不满足其他条件,走最终的else判断
//通过调用AppIdleHistory的reportUsage更新Bucket相关信息
mAppIdleHistory.reportUsage(appHistory, pkg, userId,
STANDBY_BUCKET_ACTIVE, subReason,
elapsedRealtime, elapsedRealtime + mStrongUsageTimeoutMillis);
nextCheckDelay = mStrongUsageTimeoutMillis;
}
//Bucket信息有更新
if (appHistory.currentBucket != prevBucket) {
//发送一个延时消息来进行后续的Bucket更新
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkg),
nextCheckDelay);
final boolean userStartedInteracting =
appHistory.currentBucket == STANDBY_BUCKET_ACTIVE
&& (prevBucketReason & REASON_MAIN_MASK) != REASON_MAIN_USAGE;
//通知其他模块Bucket有更新
maybeInformListeners(pkg, userId, elapsedRealtime,
appHistory.currentBucket, reason, userStartedInteracting);
}
...
}
3.1.11 AppIdleHistory.reportUsage
先判断应用Bucket是不是STANDBY_BUCKET_RESTRICTED,如果是受限的,则只允许用户操作改变应用所属群组。再判断新的Bucket值是不是小于等于上一次Bucket,如果是,则更新Bucket相关信息到appUsageHistory。至此,群组更新就完成了
public AppUsageHistory reportUsage(String packageName, int userId, int newBucket,
int usageReason, long nowElapsedRealtimeMs, long expiryElapsedRealtimeMs) {
ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
//获取进程对应的AppUsageHistory对象
AppUsageHistory history = getPackageHistory(userHistory, packageName,
nowElapsedRealtimeMs, true);
return reportUsage(history, packageName, userId, newBucket, usageReason,
nowElapsedRealtimeMs, expiryElapsedRealtimeMs);
}
AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName, int userId,
int newBucket, int usageReason,
long nowElapsedRealtimeMs, long expiryElapsedRealtimeMs) {
...
//如果应用的Bucket之前是STANDBY_BUCKET_RESTRICTED状态,那么只允许用户改变该状态
if (appUsageHistory.currentBucket == STANDBY_BUCKET_RESTRICTED && !isUserUsage
&& (appUsageHistory.bucketingReason & REASON_MAIN_MASK) != REASON_MAIN_TIMEOUT) {
// Only user usage should bring an app out of the RESTRICTED bucket, unless the app
// just timed out into RESTRICTED.
newBucket = STANDBY_BUCKET_RESTRICTED;
bucketingReason = appUsageHistory.bucketingReason;
} else {//设置到期时间
// Set the expiry time if applicable
if (expiryElapsedRealtimeMs > nowElapsedRealtimeMs) {
// Convert to elapsed timebase
final long expiryTimeMs = getElapsedTime(expiryElapsedRealtimeMs);
if (appUsageHistory.bucketExpiryTimesMs == null) {
appUsageHistory.bucketExpiryTimesMs = new SparseLongArray();
}
final long currentExpiryTimeMs = appUsageHistory.bucketExpiryTimesMs.get(newBucket);
appUsageHistory.bucketExpiryTimesMs.put(newBucket,
Math.max(expiryTimeMs, currentExpiryTimeMs));
removeElapsedExpiryTimes(appUsageHistory, getElapsedTime(nowElapsedRealtimeMs));
}
}
...
//如果新的Bucket值小于等于上一次Bucket,则更新Bucket相关信息到appUsageHistory
if (appUsageHistory.currentBucket >= newBucket) {
if (appUsageHistory.currentBucket > newBucket) {
appUsageHistory.currentBucket = newBucket;
logAppStandbyBucketChanged(packageName, userId, newBucket, bucketingReason);
}
appUsageHistory.bucketingReason = bucketingReason;
}
...
}
3.2 系统轮询检测更新群组
由第二章我们知道,应用群组分为5个,那么一个应用所在的群组是如何更新的呢?且往下看看。
PS:这里要注意几个时间点。
1. 轮询检测的时间间隔:3小时,mCheckIdleIntervalMillis变量保存。
long mCheckIdleIntervalMillis = Math.min(DEFAULT_ELAPSED_TIME_THRESHOLDS[1] / 4,
ConstantsObserver.DEFAULT_CHECK_IDLE_INTERVAL_MS);
2. 屏幕使用时间阈值:mAppStandbyScreenThresholds变量保存。
static final long[] DEFAULT_SCREEN_TIME_THRESHOLDS = {
0,
0,
COMPRESS_TIME ? 2 * ONE_MINUTE : 1 * ONE_HOUR,
COMPRESS_TIME ? 4 * ONE_MINUTE : 2 * ONE_HOUR,
COMPRESS_TIME ? 8 * ONE_MINUTE : 6 * ONE_HOUR
};
3. 使用时间阈值:mAppStandbyElapsedThresholds变量保存。
static final long[] DEFAULT_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,
COMPRESS_TIME ? 32 * ONE_MINUTE : 8 * ONE_DAY
};
3.2.1 SystemServer.startCoreServices
Android开机时,会在SystemServer启动系统所有核心服务,其中就有UsageStatsService,UsageStatsService服务是Android系统中的一个服务,用于跟踪应用程序的使用情况和统计数据。应用群组的更新,也是通过这个服务来进行的。
private void startCoreServices(@NonNull TimingsTraceAndSlog t) {
...
// Tracks application usage stats.
t.traceBegin("StartUsageService");
mSystemServiceManager.startService(UsageStatsService.class);
mActivityManagerService.setUsageStatsManager(
LocalServices.getService(UsageStatsManagerInternal.class));
t.traceEnd();
...
}
3.2.2 UsageStatsService.onStart
UsageStatsService启动,会监听用户开始和移除的广播,当监听到新用户启动时,会调用AppStandby的postCheckIdleStates函数来检查应用行为。
PS:这里的用户是指多用户(设置-系统-多用户)
public void onStart() {
...
//监听用户行为变化
IntentFilter filter = new IntentFilter(Intent.ACTION_USER_REMOVED);
filter.addAction(Intent.ACTION_USER_STARTED);
getContext().registerReceiverAsUser(new UserActionsReceiver(), UserHandle.ALL, filter,
null, mHandler);
...
}
private class UserActionsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
...
} else if (Intent.ACTION_USER_STARTED.equals(action)) {//用户启动
if (userId >= 0) {
mAppStandby.postCheckIdleStates(userId);
}
}
}
}
3.2.3 AppStandbyController.postCheckIdleStates
把userId和当前时间存入mPendingIdleStateChecks数组中,然后发送MSG_CHECK_IDLE_STATES message来开启循环检查应用状态之旅。
public void postCheckIdleStates(int userId) {//这里传入的userId是>=0的,所以会走else
if (userId == UserHandle.USER_ALL) {
postOneTimeCheckIdleStates();
} else {
synchronized (mPendingIdleStateChecks) {
mPendingIdleStateChecks.put(userId, mInjector.elapsedRealtime());
}
//发送MSG_CHECK_IDLE_STATES message
mHandler.obtainMessage(MSG_CHECK_IDLE_STATES).sendToTarget();
}
}
3.2.4 AppStandbyController.AppStandbyHandler.handleMessage
上一步已经往mPendingIdleStateChecks存入了一个值,所以for循环会走进去且只走一遍。checkIdleStates函数继续检查更新Bucket的流程。mCheckIdleIntervalMillis是轮询的周期,这里是3个小时,expirationTime是下次轮询的时间,当这轮检查完成之后,会把expirationTime赋值给earliestCheck用来发送延时消息,触发下一次检查更新。
这里就是轮询检查更新的精髓所在,下面再来看看检查更新是如何操作的。
class AppStandbyHandler extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
...
case MSG_CHECK_IDLE_STATES:
removeMessages(MSG_CHECK_IDLE_STATES);
long earliestCheck = Long.MAX_VALUE;
final long nowElapsed = mInjector.elapsedRealtime();
synchronized (mPendingIdleStateChecks) {
for (int i = mPendingIdleStateChecks.size() - 1; i >= 0; --i) {
long expirationTime = mPendingIdleStateChecks.valueAt(i);
if (expirationTime <= nowElapsed) {
final int userId = mPendingIdleStateChecks.keyAt(i);
//checkIdleStates函数继续检查更新Bucket的流程
if (checkIdleStates(userId) && mAppIdleEnabled) {
//mCheckIdleIntervalMillis是检查周期的阈值
expirationTime = nowElapsed + mCheckIdleIntervalMillis;
mPendingIdleStateChecks.put(userId, expirationTime);
} else {
mPendingIdleStateChecks.removeAt(i);
continue;
}
}
//下次检查的时间,就是把expirationTime赋值给earliestCheck
earliestCheck = Math.min(earliestCheck, expirationTime);
}
}
//发送一个延时message,触发下一次检查
if (earliestCheck != Long.MAX_VALUE) {
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_CHECK_IDLE_STATES),
earliestCheck - nowElapsed);
}
break;
}
}
};
long mCheckIdleIntervalMillis = Math.min(DEFAULT_ELAPSED_TIME_THRESHOLDS[1] / 4,
ConstantsObserver.DEFAULT_CHECK_IDLE_INTERVAL_MS);
static final long[] DEFAULT_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,
COMPRESS_TIME ? 32 * ONE_MINUTE : 8 * ONE_DAY
}
public static final long DEFAULT_CHECK_IDLE_INTERVAL_MS =
COMPRESS_TIME ? ONE_MINUTE : 4 * ONE_HOUR;
3.2.5 AppStandbyController.checkIdleStates
先判断要检查的user是否是正在运行的,如果不是则不检查。获取该用户下所有已安装的应用,遍历所有应用做检查、更新Bucket
boolean checkIdleStates(int checkUserId) {
...
final int[] runningUserIds;
try {
//获取系统所有的在运行的用户id
runningUserIds = mInjector.getRunningUserIds();
//如果要检查的userid处于非运行状态,直接return false,不做检查
if (checkUserId != UserHandle.USER_ALL
&& !ArrayUtils.contains(runningUserIds, checkUserId)) {
return false;
}
}
for (int i = 0; i < runningUserIds.length; i++) {
final int userId = runningUserIds[i];
...
//获取该用户下所有已安装应用
List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(
PackageManager.MATCH_DISABLED_COMPONENTS,
userId);
final int packageCount = packages.size();
//遍历所有应用
for (int p = 0; p < packageCount; p++) {
final PackageInfo pi = packages.get(p);
final String packageName = pi.packageName;
//检查、更新Bucket
checkAndUpdateStandbyState(packageName, userId, pi.applicationInfo.uid,
elapsedRealtime);
}
}
...
}
3.2.6 AppStandbyController.checkAndUpdateStandbyState
对系统应用、doze白名单应用、有widget正在绑定、使用精准alarm等类型应用,设置一个允许被设置的最小Bucket,即对这些应用做保护。使用getBucketForLocked,根据距离上次使用屏幕时间和距离上次使用时间等,获取对应的Bucket,并更新Bucket、reason等信息。
private void checkAndUpdateStandbyState(String packageName, @UserIdInt int userId,
int uid, long elapsedRealtime) {
...
//获取应用允许的最小的Bucket值
final int minBucket = getAppMinBucket(packageName,
UserHandle.getAppId(uid),
userId);
...
//如果是非限制应用
if (minBucket <= STANDBY_BUCKET_ACTIVE) {
synchronized (mAppIdleLock) {
previouslyIdle = mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime);
//更新应用Bucket为minBucket
mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,
minBucket, REASON_MAIN_DEFAULT);
stillIdle = mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime);
}
//更新Listener
maybeInformListeners(packageName, userId, elapsedRealtime,
minBucket, REASON_MAIN_DEFAULT, false);
} else {
synchronized (mAppIdleLock) {
previouslyIdle = mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime);
final AppIdleHistory.AppUsageHistory app =
mAppIdleHistory.getAppUsageHistory(packageName,
userId, elapsedRealtime);
int reason = app.bucketingReason;
final int oldMainReason = reason & REASON_MAIN_MASK;
//如果应用上次Bucket更新的原因是用户调整,则不做处理
if (oldMainReason == REASON_MAIN_FORCED_BY_USER) {
return;
}
//如果应用上次Bucket是NEVER,则不做处理
final int oldBucket = app.currentBucket;
if (oldBucket == STANDBY_BUCKET_NEVER) {
// None of this should bring an app out of the NEVER bucket.
return;
}
int newBucket = Math.max(oldBucket, STANDBY_BUCKET_ACTIVE); // Undo EXEMPTED
//如果超过12小时没有对应用做预测,则返回true
boolean predictionLate = predictionTimedOut(app, elapsedRealtime);
// Compute age-based bucket
if (oldMainReason == REASON_MAIN_DEFAULT
|| oldMainReason == REASON_MAIN_USAGE
|| oldMainReason == REASON_MAIN_TIMEOUT
|| predictionLate) {
//如果没有预测超时且上次预测的Bucket在ACIVE和RARE之间,把上次预测的Bucket更新到最新的Bucket并更新reason
if (!predictionLate && app.lastPredictedBucket >= STANDBY_BUCKET_ACTIVE
&& app.lastPredictedBucket <= STANDBY_BUCKET_RARE) {
newBucket = app.lastPredictedBucket;
reason = REASON_MAIN_PREDICTED | REASON_SUB_PREDICTED_RESTORED;
} else {
//不更新已重置应用的Bucket状态
if (!(oldMainReason == REASON_MAIN_DEFAULT
&& (app.bucketingReason & REASON_SUB_MASK)
== REASON_SUB_DEFAULT_APP_RESTORED)) {
//根据距离上次使用时间以及距离上次使用屏幕时间,获取对应的Bucket
newBucket = getBucketForLocked(packageName, userId, elapsedRealtime);
reason = REASON_MAIN_TIMEOUT;
}
}
}
// Check if the app is within one of the expiry times for forced bucket elevation
final long elapsedTimeAdjusted = mAppIdleHistory.getElapsedTime(elapsedRealtime);
//获取有效到期时间内的最小的Bucket(只有当usageHistory.bucketExpiryTimesMs保存的Bucket大于newBucket时,才返回非STANDBY_BUCKET_UNKNOWN)
final int bucketWithValidExpiryTime = getMinBucketWithValidExpiryTime(app,
newBucket, elapsedTimeAdjusted);
//如果usageHistory.bucketExpiryTimesMs保存的Bucket大于newBucket
if (bucketWithValidExpiryTime != STANDBY_BUCKET_UNKNOWN) {
newBucket = bucketWithValidExpiryTime;
if (newBucket == STANDBY_BUCKET_ACTIVE || app.currentBucket == newBucket) {
reason = app.bucketingReason;
} else {
reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_ACTIVE_TIMEOUT;
}
}
//判断是否需要更新为受限制的Bucket
if (app.lastUsedByUserElapsedTime >= 0
&& app.lastRestrictAttemptElapsedTime > app.lastUsedByUserElapsedTime
&& elapsedTimeAdjusted - app.lastUsedByUserElapsedTime
>= mInjector.getAutoRestrictedBucketDelayMs()) {
newBucket = STANDBY_BUCKET_RESTRICTED;
reason = app.lastRestrictReason;
}
//如果新的Bucket大于该应用允许的最小Bucket,则把最小Bucket赋值给新的Bucket
if (newBucket > minBucket) {
newBucket = minBucket;
}
//Bucket有更新或者超过12小时没有对应用做预测
if (oldBucket != newBucket || predictionLate) {
mAppIdleHistory.setAppStandbyBucket(packageName, userId,
elapsedRealtime, newBucket, reason);
stillIdle = mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime);
maybeInformListeners(packageName, userId, elapsedRealtime,
newBucket, reason, false);
} else {
stillIdle = previouslyIdle;
}
}
}
...
}
3.2.7 AppStandbyController.getAppMinBucket
通过getAppMinBucket可以获取系统应用、doze白名单应用、有widget正在绑定、使用精准alarm等类型应用允许被设置的最小Bucket
private int getAppMinBucket(String packageName, int appId, int userId) {
if (packageName == null) return STANDBY_BUCKET_NEVER;
// If not enabled at all, of course nobody is ever idle.
if (!mAppIdleEnabled) {
return STANDBY_BUCKET_EXEMPTED;
}
if (appId < Process.FIRST_APPLICATION_UID) {
// System uids never go idle.
return STANDBY_BUCKET_EXEMPTED;
}
if (packageName.equals("android")) {
// Nor does the framework (which should be redundant with the above, but for MR1 we will
// retain this for safety).
return STANDBY_BUCKET_EXEMPTED;
}
if (mSystemServicesReady) {
// We allow all whitelisted apps, including those that don't want to be whitelisted
// for idle mode, because app idle (aka app standby) is really not as big an issue
// for controlling who participates vs. doze mode.
if (mInjector.isNonIdleWhitelisted(packageName)) {
return STANDBY_BUCKET_EXEMPTED;
}
if (isActiveDeviceAdmin(packageName, userId)) {
return STANDBY_BUCKET_EXEMPTED;
}
if (isAdminProtectedPackages(packageName, userId)) {
return STANDBY_BUCKET_EXEMPTED;
}
if (isActiveNetworkScorer(packageName)) {
return STANDBY_BUCKET_EXEMPTED;
}
final int uid = UserHandle.getUid(userId, appId);
synchronized (mSystemExemptionAppOpMode) {
if (mSystemExemptionAppOpMode.indexOfKey(uid) >= 0) {
if (mSystemExemptionAppOpMode.get(uid)
== AppOpsManager.MODE_ALLOWED) {
return STANDBY_BUCKET_EXEMPTED;
}
} else {
int mode = mAppOpsManager.checkOpNoThrow(
AppOpsManager.OP_SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS, uid,
packageName);
mSystemExemptionAppOpMode.put(uid, mode);
if (mode == AppOpsManager.MODE_ALLOWED) {
return STANDBY_BUCKET_EXEMPTED;
}
}
}
if (mAppWidgetManager != null
&& mInjector.isBoundWidgetPackage(mAppWidgetManager, packageName, userId)) {
return STANDBY_BUCKET_ACTIVE;
}
if (isDeviceProvisioningPackage(packageName)) {
return STANDBY_BUCKET_EXEMPTED;
}
if (mInjector.isWellbeingPackage(packageName)) {
return STANDBY_BUCKET_WORKING_SET;
}
if (mInjector.shouldGetExactAlarmBucketElevation(packageName,
UserHandle.getUid(userId, appId))) {
return STANDBY_BUCKET_WORKING_SET;
}
}
// Check this last, as it can be the most expensive check
if (isCarrierApp(packageName)) {
return STANDBY_BUCKET_EXEMPTED;
}
if (isHeadlessSystemApp(packageName)) {
return STANDBY_BUCKET_ACTIVE;
}
if (mPackageManager.checkPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION,
packageName) == PERMISSION_GRANTED) {
return STANDBY_BUCKET_FREQUENT;
}
return STANDBY_BUCKET_NEVER;
}
private boolean isHeadlessSystemApp(String packageName) {
synchronized (mHeadlessSystemApps) {
return mHeadlessSystemApps.contains(packageName);
}
}
3.2.8 AppStandbyController.getBucketForLocked
把DEFAULT_SCREEN_TIME_THRESHOLDS数组和DEFAULT_ELAPSED_TIME_THRESHOLDS传给AppIdleHistory的getThresholdIndex函数,返回bucketIndex,再获取THRESHOLD_BUCKETS数据该index对应的Bucket值
private int getBucketForLocked(String packageName, int userId,
long elapsedRealtime) {
int bucketIndex = mAppIdleHistory.getThresholdIndex(packageName, userId,
elapsedRealtime, mAppStandbyScreenThresholds, mAppStandbyElapsedThresholds);
return bucketIndex >= 0 ? THRESHOLD_BUCKETS[bucketIndex] : STANDBY_BUCKET_NEVER;
}
static final long[] DEFAULT_SCREEN_TIME_THRESHOLDS = {
0,
0,
COMPRESS_TIME ? 2 * ONE_MINUTE : 1 * ONE_HOUR,
COMPRESS_TIME ? 4 * ONE_MINUTE : 2 * ONE_HOUR,
COMPRESS_TIME ? 8 * ONE_MINUTE : 6 * ONE_HOUR
};
static final long[] DEFAULT_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,
COMPRESS_TIME ? 32 * ONE_MINUTE : 8 * ONE_DAY
};
private static final int[] THRESHOLD_BUCKETS = {
STANDBY_BUCKET_ACTIVE,
STANDBY_BUCKET_WORKING_SET,
STANDBY_BUCKET_FREQUENT,
STANDBY_BUCKET_RARE,
STANDBY_BUCKET_RESTRICTED
};
3.2.9 AppIdleHistory.getThresholdIndex
通过距离上次使用屏幕的时间和距离上次使用的时间,通过遍历传进来的两个数组,返回符合条件的数组下标
int getThresholdIndex(String packageName, int userId, long elapsedRealtime,
long[] screenTimeThresholds, long[] elapsedTimeThresholds) {
...
//获取距离上次使用屏幕的时间
long screenOnDelta = getScreenOnTime(elapsedRealtime) - appUsageHistory.lastUsedScreenTime;
//距离上次使用的时间
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;
}
四:应用Bucket更新后,系统如何得知并及时调整对该应用的策略
从3.1.10和3.2.5可以看到,在Bucket更新后,都会调用maybeInformListeners,这个函数会先通过AppIdleHistory的shouldInformListeners判断当前需要通知的Bucket值和上次通知的Bucket是否一样,如果不一样会发送MSG_INFORM_LISTENERS的message,调用informListeners。在informListeners里会使所有继承了AppIdleStateChangeListener的类,收到onAppIdleStateChanged函数回调。
遍历系统所有继承AppIdleStateChangeListener,发现只有AlarmManagerService、QuotaController和NetworkPolicyManagerService这三个类的内部类继承了,下面分别从这三个类来看下系统是如何对不同Bucket做限制的。
4.1 AlarmManagerService
下面来看下系统是如何针对不同Bucket来限制Alarm的。
PS:这里要注意几个时间点。
1. Alarm窗口时间,用APP_STANDBY_WINDOW保存。
public long APP_STANDBY_WINDOW = DEFAULT_APP_STANDBY_WINDOW;
private static final long DEFAULT_APP_STANDBY_WINDOW = 60 * 60 * 1000; // 1 hr
2. Alarm窗口期内,允许唤醒的次数,用APP_STANDBY_QUOTAS数组保存。
private final int[] DEFAULT_APP_STANDBY_QUOTAS = {
720, // Active
10, // Working
2, // Frequent
1, // Rare
0 // Never
};
4.1.1 AlarmManagerService.AppStandbyTracker.onAppIdleStateChanged
AlarmManagerService的内部类AppStandbyTracker继承了AppIdleStateChangeListener,通过第三章我们可以知道,当应用Bucket改变时,会调用AppIdleStateChangeListener的onAppIdleStateChanged函数,也就是AppStandbyTracker的onAppIdleStateChanged函数会执行,onAppIdleStateChanged发送了APP_STANDBY_BUCKET_CHANGED message,当AlarmHandler接收到该message后,会调用reorderAlarmsBasedOnStandbyBuckets,根据应用当前Bucket重新计算Alarm的发送时间。
private final class AppStandbyTracker extends AppIdleStateChangeListener {
@Override
public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId,
boolean idle, int bucket, int reason) {
mHandler.obtainMessage(AlarmHandler.APP_STANDBY_BUCKET_CHANGED, userId, -1, packageName)
.sendToTarget();
}
}
class AlarmHandler extends Handler {
...
public void handleMessage(Message msg) {
switch (msg.what) {
case APP_STANDBY_BUCKET_CHANGED:
synchronized (mLock) {
final ArraySet<UserPackage> filterPackages = new ArraySet<>();
filterPackages.add(UserPackage.of(msg.arg1, (String) msg.obj));
//根据应用当前Bucket重新计算Alarm发送时间
if (reorderAlarmsBasedOnStandbyBuckets(filterPackages)) {
//派发下一次即将到来的唤醒Alarm
rescheduleKernelAlarmsLocked();
//更新下一个Alarm的时间
updateNextAlarmClockLocked();
}
}
break;
...
}
}
}
4.1.2 AlarmManagerService.reorderAlarmsBasedOnStandbyBuckets
调用adjustDeliveryTimeBasedOnBucketLocked进行Alarm发送时间的调整
boolean reorderAlarmsBasedOnStandbyBuckets(ArraySet<UserPackage> targetPackages) {
final long start = mStatLogger.getTime();
final boolean changed = mAlarmStore.updateAlarmDeliveries(a -> {
...
return adjustDeliveryTimeBasedOnBucketLocked(a);
});
...
return changed;
}
4.1.3 AlarmManagerService.adjustDeliveryTimeBasedOnBucketLocked
根据Bucket获取允许的最大唤醒次数,和应用唤醒次数总和做比较,限定一小时之内允许的最大唤醒次数
private boolean adjustDeliveryTimeBasedOnBucketLocked(Alarm alarm) {
final long nowElapsed = mInjector.getElapsedRealtimeMillis();
//如果是AppStandby豁免应用或者当前设备正在充电,不延时Alarm发送时间
if (mConstants.USE_TARE_POLICY == EconomyManager.ENABLED_MODE_ON
|| isExemptFromAppStandby(alarm) || mAppStandbyParole) {
return alarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, nowElapsed);
}
final String sourcePackage = alarm.sourcePackage;
final int sourceUserId = UserHandle.getUserId(alarm.creatorUid);
//获取应用当前Bucket
final int standbyBucket = mUsageStatsManagerInternal.getAppStandbyBucket(
sourcePackage, sourceUserId, nowElapsed);
//获取应用唤醒次数总和
final int wakeupsInWindow = mAppWakeupHistory.getTotalWakeupsInWindow(sourcePackage,
sourceUserId);
//如果是限制类型的Bucket
if (standbyBucket == UsageStatsManager.STANDBY_BUCKET_RESTRICTED) {
// Special case because it's 1/day instead of 1/hour.
// AppWakeupHistory doesn't delete old wakeup times until a new one is logged, so we
// should always have the last wakeup available.
if (wakeupsInWindow > 0) {
//获取上一次唤醒的时间
final long lastWakeupTime = mAppWakeupHistory.getNthLastWakeupForPackage(
sourcePackage, sourceUserId, mConstants.APP_STANDBY_RESTRICTED_QUOTA);
//判断距离上一次唤醒的时间是否小于一天
if ((nowElapsed - lastWakeupTime) < mConstants.APP_STANDBY_RESTRICTED_WINDOW) {
//如果小于一天,则把唤醒时间延长,一天内最多只允许一次唤醒
return alarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX,
lastWakeupTime + mConstants.APP_STANDBY_RESTRICTED_WINDOW);
}
}
} else {
//获取当前Bucket允许的最大唤醒次数
final int quotaForBucket = getQuotaForBucketLocked(standbyBucket);
//如果唤醒次数总和超过当前Bucket允许的最大唤醒次数
if (wakeupsInWindow >= quotaForBucket) {
final long minElapsed;
//如果这个应用有临时配额,不做延时
if (mTemporaryQuotaReserve.hasQuota(sourcePackage, sourceUserId, nowElapsed)) {
alarm.mUsingReserveQuota = true;
return alarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, nowElapsed);
}
//如果当前Bucket允许的最大唤醒次数小于等于0
if (quotaForBucket <= 0) {
//延时365天
minElapsed = nowElapsed + INDEFINITE_DELAY;
} else {
final long t = mAppWakeupHistory.getNthLastWakeupForPackage(
sourcePackage, sourceUserId, quotaForBucket);
//设置一小时之内允许唤醒的最大次数为quotaForBucket
minElapsed = t + mConstants.APP_STANDBY_WINDOW;
}
//更新Alarm唤醒时间
return alarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, minElapsed);
}
}
// wakeupsInWindow are less than the permitted quota, hence no deferring is needed.
alarm.mUsingReserveQuota = false;
return alarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, nowElapsed);
}
4.2 QuotaController
下面来看下系统是如何针对不同Bucket来限制job的。
PS:这里要注意几个时间点。
1. job窗口时间,用mBucketPeriodsMs数组保存。
private final long[] mBucketPeriodsMs = new long[]{
QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS,//10min
QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS,//2h
QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS,//8h
QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS,//24h
0, // NEVER
QcConstants.DEFAULT_WINDOW_SIZE_RESTRICTED_MS,//24h
QcConstants.DEFAULT_WINDOW_SIZE_EXEMPTED_MS//10min
};
2. job窗口期内,会话允许运行的时间,用mAllowedTimePerPeriodMs数组保存。
private final long[] mAllowedTimePerPeriodMs = new long[]{
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS,//10min
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_WORKING_MS,//10min
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS,//10min
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_RARE_MS,//10min
0, // NEVER
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS,//10min
QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS//10min
};
4.2.1 QuotaController.StandbyTracker.onAppIdleStateChanged
在AppSchedulingModuleThread这个子线程中调用updateStandbyBucket函数做检查更新的操作
final class StandbyTracker extends AppIdleStateChangeListener {
@Override
public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId,
boolean idle, int bucket, int reason) {
// Update job bookkeeping out of band.
AppSchedulingModuleThread.getHandler().post(() -> {
final int bucketIndex = JobSchedulerService.standbyBucketToBucketIndex(bucket);
updateStandbyBucket(userId, packageName, bucketIndex);
});
}
...
}
4.2.2 QuotaController.updateStandbyBucket
继续调用maybeUpdateConstraintForPkgLocked函数做检查更新的操作
void updateStandbyBucket(
final int userId, final @NonNull String packageName, final int bucketIndex) {
List<JobStatus> restrictedChanges = new ArrayList<>();
synchronized (mLock) {
...
for (int i = jobs.size() - 1; i >= 0; i--) {
JobStatus js = jobs.valueAt(i);
if ((bucketIndex == RESTRICTED_INDEX || js.getStandbyBucket() == RESTRICTED_INDEX)
&& bucketIndex != js.getStandbyBucket()) {
restrictedChanges.add(js);
}
js.setStandbyBucket(bucketIndex);
}
//重新分发job
Timer timer = mPkgTimers.get(userId, packageName);
if (timer != null && timer.isActive()) {
timer.rescheduleCutoff();
}
timer = mEJPkgTimers.get(userId, packageName);
if (timer != null && timer.isActive()) {
timer.rescheduleCutoff();
}
//调用maybeUpdateConstraintForPkgLocked来更新job
mStateChangedListener.onControllerStateChanged(
maybeUpdateConstraintForPkgLocked(
sElapsedRealtimeClock.millis(), userId, packageName));
}
if (restrictedChanges.size() > 0) {
mStateChangedListener.onRestrictedBucketChanged(restrictedChanges);
}
}
4.2.3 QuotaController.Timer.rescheduleCutoff
获取距离下次job工作的时间,然后延时这个时间发送MSG_REACHED_QUOTA信息
private void scheduleCutoff() {
synchronized (mLock) {
if (!isActive()) {
return;
}
Message msg = mHandler.obtainMessage(
mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_EJ_QUOTA, mPkg);
//获取距离下次job工作的时间
final long timeRemainingMs = mRegularJobTimer
? getTimeUntilQuotaConsumedLocked(mPkg.userId, mPkg.packageName)
: getTimeUntilEJQuotaConsumedLocked(mPkg.userId, mPkg.packageName);
//延时发送MSG_REACHED_QUOTA信息
mHandler.sendMessageDelayed(msg, timeRemainingMs);
}
}
4.2.4 QuotaController.getTimeUntilQuotaConsumedLocked
获取执行统计信息,根据Bucket更新窗口时间、窗口时间内允许运行的时间等。再计算配额完全用完所需的时间
long getTimeUntilQuotaConsumedLocked(final int userId, @NonNull final String packageName) {
...
List<TimedEvent> events = mTimingEvents.get(userId, packageName);
//获取执行统计信息
final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
if (events == null || events.size() == 0) {
//如果窗口时间和允许的周期时间相同,说明是Active,返回4h
if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) {
return mMaxExecutionTimeMs;
}
//返回允许的周期时间
return mAllowedTimePerPeriodMs[standbyBucket];
}
//窗口开始时间
final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
//最大开始时间
final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;//MAX_PERIOD_MS=24h
//窗口期内允许运行的时间
final long allowedTimePerPeriodMs = mAllowedTimePerPeriodMs[standbyBucket];
//窗口期内剩余允许运行的时间
final long allowedTimeRemainingMs = allowedTimePerPeriodMs - stats.executionTimeInWindowMs;
//窗口期内最大执行剩余时间
final long maxExecutionTimeRemainingMs =
mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs;
//如果是Active,则传入最大开始时间和窗口期内最大执行剩余时间进行配额完全用完所需时间的计算
if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) {
return calculateTimeUntilQuotaConsumedLocked(
events, startMaxElapsed, maxExecutionTimeRemainingMs, false);
}
//计算配额完全用完所需时间
return Math.min(
calculateTimeUntilQuotaConsumedLocked(
events, startMaxElapsed, maxExecutionTimeRemainingMs, false),
calculateTimeUntilQuotaConsumedLocked(
events, startWindowElapsed, allowedTimeRemainingMs, true));
}
4.2.5 QuotaController.getExecutionStatsLocked
通过传入的Bucket获取窗口时间、一个窗口时间内允许运行job的时间、允许同时运行job的次数、允许同时具有的最大活动会话数,更新ExecutionStats对象。
ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
//注意这里传入的最后一个参数是true
return getExecutionStatsLocked(userId, packageName, standbyBucket, true);
}
private ExecutionStats getExecutionStatsLocked(final int userId,
@NonNull final String packageName, final int standbyBucket,
final boolean refreshStatsIfOld) {
//如果是never,new一个新对象返回
if (standbyBucket == NEVER_INDEX) {
return new ExecutionStats();
}
ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
ExecutionStats stats = appStats[standbyBucket];
//refreshStatsIfOld是传入的最后一个参数,是true
if (refreshStatsIfOld) {
//注释分别列出来ACTIVE WORKING_SET FREQUENT RARE RESTRICTED几类Bucket对应的值
//获取该Bucket在一个窗口时间内允许运行job的时间
final long bucketAllowedTimeMs = mAllowedTimePerPeriodMs[standbyBucket];//10min
//获取该Bucket对应的窗口时间
final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];//10min 2h 8h 24h 24h
//获取该Bucket允许同时运行job的次数
final int jobCountLimit = mMaxBucketJobCounts[standbyBucket];//75 120 200 48 10
//指示特定作业可以同时具有的最大活动会话数。每个作业都可以有一个或多个会话
final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket];//75 10 8 3 1
Timer timer = mPkgTimers.get(userId, packageName);
//如果有正在运行的定时器或者上面四个参数有更新
if ((timer != null && timer.isActive())
|| stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis()
|| stats.allowedTimePerPeriodMs != bucketAllowedTimeMs
|| stats.windowSizeMs != bucketWindowSizeMs
|| stats.jobCountLimit != jobCountLimit
|| stats.sessionCountLimit != sessionCountLimit) {
// The stats are no longer valid.
stats.allowedTimePerPeriodMs = bucketAllowedTimeMs;
stats.windowSizeMs = bucketWindowSizeMs;
stats.jobCountLimit = jobCountLimit;
stats.sessionCountLimit = sessionCountLimit;
//更新执行统计信息
updateExecutionStatsLocked(userId, packageName, stats);
}
}
return stats;
}
4.2.6 QuotaController.calculateTimeUntilQuotaConsumedLocked
根据传入的allowQuotaBumps决定是否允许配额增加,允许,在事件结束时间超过超过配额增加窗口开始时间,则增加1分钟,最多可增加8分钟
private long calculateTimeUntilQuotaConsumedLocked(@NonNull List<TimedEvent> sessions,
final long windowStartElapsed, long deadSpaceMs, boolean allowQuotaBumps) {
long timeUntilQuotaConsumedMs = 0;
long start = windowStartElapsed;
int numQuotaBumps = 0;
//获取配额增加窗口开始时间
final long quotaBumpWindowStartElapsed =
sElapsedRealtimeClock.millis() - mQuotaBumpWindowSizeMs;
final int numSessions = sessions.size();
//允许配额增加
if (allowQuotaBumps) {
for (int i = numSessions - 1; i >= 0; --i) {
TimedEvent event = sessions.get(i);
//增加的配额事件
if (event instanceof QuotaBump) {
//如果事件结束时间超过超过配额增加窗口开始时间,则增加1分钟,最多可增加8分钟
if (event.getEndTimeElapsed() >= quotaBumpWindowStartElapsed
&& numQuotaBumps++ < mQuotaBumpLimit) {
deadSpaceMs += mQuotaBumpAdditionalDurationMs;
} else {
break;
}
}
}
}
//遍历所有会话
for (int i = 0; i < numSessions; ++i) {
TimedEvent event = sessions.get(i);
//如果是配额增加事件,不做处理,继续下一次循环
if (event instanceof QuotaBump) {
continue;
}
TimingSession session = (TimingSession) event;
//会话结束时间小于窗口开始时间,说明在窗口之外,继续下一次循环
if (session.endTimeElapsed < windowStartElapsed) {
// Outside of window. Ignore.
continue;
} else if (session.startTimeElapsed <= windowStartElapsed) {
//重叠会话。可以在窗口中按会话部分延长时间
timeUntilQuotaConsumedMs += session.endTimeElapsed - windowStartElapsed;
start = session.endTimeElapsed;
} else {
//完全在窗口内。允许增加会话时间一直到时间超过窗口期内剩余允许运行时间
long diff = session.startTimeElapsed - start;
if (diff > deadSpaceMs) {
break;
}
timeUntilQuotaConsumedMs += diff
+ (session.endTimeElapsed - session.startTimeElapsed);
deadSpaceMs -= diff;
start = session.endTimeElapsed;
}
}
// Will be non-zero if the loop didn't look at any sessions.
timeUntilQuotaConsumedMs += deadSpaceMs;
if (timeUntilQuotaConsumedMs > mMaxExecutionTimeMs) {
Slog.wtf(TAG, "Calculated quota consumed time too high: " + timeUntilQuotaConsumedMs);
}
return timeUntilQuotaConsumedMs;
}
4.2.7 QuotaController.QcHandler.handleMessage
在走完上述流程后,会成功获取到距离下次job工作的时间,并把该值返回到4.2.3中,会延长该时间发送MSG_REACHED_QUOTA message,在收到message后,如果该作业剩余时间小于50ms,则会启动进程去停止job,如果该作业剩余时间大于50ms,会再次计算距离下次job工作的时间,延时发送MSG_REACHED_QUOTA message。
这里就成了一个循环,不断地计算距离下次job工作的时间,延时唤醒。
public void handleMessage(Message msg) {
synchronized (mLock) {
switch (msg.what) {
case MSG_REACHED_QUOTA: {
UserPackage pkg = (UserPackage) msg.obj;
//该作业剩余运行时间
long timeRemainingMs = getRemainingExecutionTimeLocked(pkg.userId,
pkg.packageName);
if (timeRemainingMs <= 50) {
// Less than 50 milliseconds left. Start process of shutting down jobs.
mStateChangedListener.onControllerStateChanged(
maybeUpdateConstraintForPkgLocked(
sElapsedRealtimeClock.millis(),
pkg.userId, pkg.packageName));
} else {
Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg);
//获取距离下次job工作的时间
timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId,
pkg.packageName);
sendMessageDelayed(rescheduleMsg, timeRemainingMs);
}
break;
}
}
}
}
4.3 NetworkPolicyManagerService
下面来看下系统是如何针对不同Bucket来限制network的。
PS:网络限制就比较简单粗暴了,判断app的当前Bucket是否大于STANDBY_BUCKET_RARE,如果是则认为app处于idle状态,直接限制网络使用
4.3.1 NetworkPolicyManagerService.NetPolicyAppIdleStateChangeListener.onAppIdleStateChanged
在接收到onAppIdleStateChanged的回调后,会调用updateRuleForAppIdleUL函数来更新进程的网络策略
public void onAppIdleStateChanged(String packageName, int userId, boolean idle, int bucket,
int reason) {
try {
final int uid = mContext.getPackageManager().getPackageUidAsUser(packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
synchronized (mUidRulesFirstLock) {
mLogger.appIdleStateChanged(uid, idle);
//更新进程的网络策略
updateRuleForAppIdleUL(uid, PROCESS_STATE_UNKNOWN);
//根据设备的电源限制情况来更新网络策略规则
updateRulesForPowerRestrictionsUL(uid);
}
} catch (NameNotFoundException nnfe) {
}
}
4.3.2 NetworkPolicyManagerService.updateRuleForAppIdleUL
会先判断应用需不需要使用网络(有没有申请并赋予Network权限),如果不需要则不用更新规则,直接return。再根据白名单列表及应用是否处于idle状态,更新应用的网络规则
void updateRuleForAppIdleUL(int uid, int uidProcessState) {
//如果是不需要使用网络的应用,直接return,不用更新
if (!isUidValidForDenylistRulesUL(uid)) return;
try {
int appId = UserHandle.getAppId(uid);
if (!mPowerSaveTempWhitelistAppIds.get(appId) && isUidIdle(uid, uidProcessState)
&& !isUidForegroundOnRestrictPowerUL(uid)) {
setUidFirewallRuleUL(FIREWALL_CHAIN_STANDBY, uid, FIREWALL_RULE_DENY);
} else {
setUidFirewallRuleUL(FIREWALL_CHAIN_STANDBY, uid, FIREWALL_RULE_DEFAULT);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_NETWORK);
}
}
4.3.3 NetworkPolicyManagerService.isUidIdle
通过uid获取对应的包名列表,遍历列表调用mUsageStats的isAppIdle函数来进行idle状态的判断
private boolean isUidIdle(int uid, int uidProcessState) {
synchronized (mUidRulesFirstLock) {
if (uidProcessState != PROCESS_STATE_UNKNOWN && isProcStateConsideredInteraction(
uidProcessState)) {
return false;
}
if (mAppIdleTempWhitelistAppIds.get(uid)) {
// UID is temporarily allowlisted.
return false;
}
}
final String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
final int userId = UserHandle.getUserId(uid);
if (packages != null) {
for (String packageName : packages) {
if (!mUsageStats.isAppIdle(packageName, uid, userId)) {
return false;
}
}
}
return true;
}
4.3.4 UsageStatsService.isAppIdle
调用AppStandbyController的isAppIdleFiltered函数继续idle的判断流程
public boolean isAppIdle(String packageName, int uidForAppId, int userId) {
return mAppStandby.isAppIdleFiltered(packageName, uidForAppId,
userId, SystemClock.elapsedRealtime());
}
4.3.5 AppStandbyController.isAppIdleFiltered
根据isAppIdleUnfiltered返回的结果以及app的minBucket是否允许被设置成STANDBY_BUCKET_RARE或者限制等级更高的Bucket来判断app是否idle
public boolean isAppIdleFiltered(String packageName, int appId, int userId,
long elapsedRealtime) {
if (!mAppIdleEnabled || mIsCharging) {
return false;
}
return isAppIdleUnfiltered(packageName, userId, elapsedRealtime)
&& getAppMinBucket(packageName, appId, userId) >= AppIdleHistory.IDLE_BUCKET_CUTOFF;
}
4.3.6 AppStandbyController.isAppIdleUnfiltered
调用AppIdleHistory的isIdle来继续idle的判断流程
private boolean isAppIdleUnfiltered(String packageName, int userId, long elapsedRealtime) {
synchronized (mAppIdleLock) {
return mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime);
}
}
4.3.7 AppIdleHistory.isIdle
根据app的当前Bucket是否大于STANDBY_BUCKET_RARE判断app是否处于idle状态
public boolean isIdle(String packageName, int userId, long elapsedRealtime) {
ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
AppUsageHistory appUsageHistory =
getPackageHistory(userHistory, packageName, elapsedRealtime, true);
//IDLE_BUCKET_CUTOFF=STANDBY_BUCKET_RARE
return appUsageHistory.currentBucket >= IDLE_BUCKET_CUTOFF;
}
五:总结
1. 在设置-开发者选项里有一个待机应用菜单,在这里可以看到每个应用当前的Bucket值,可允许被设置的Bucket范围
2. 一些adb调试方法
2.1 adb shell am get-standby-bucket
可列出所有应用的当前Bucket
2.2 adb shell am get-standby-bucket [packagename]
可查出当前应用的当前Bucket
2.3 adb shell am set-standby-bucket packagename active|working_set|frequent|rare
可更改当前应用的Bucket,这里要注意,设置的Bucket不能超过minBucket,否则会设置无效
2.4 adb shell am set-standby-bucket package1 bucket1 package2 bucket2...
可一次更改多个应用的Bucket
3. 通过dumpsys查看相关值
3.1 adb shell dumpsys usagestats
mAppIdleEnabled=true mIsCharging=true
mScreenThresholds=[0, 0, 3600000, 7200000, 21600000]
mElapsedThresholds=[0, 43200000, 86400000, 172800000, 691200000]
mAppIdleEnabled - app idle 是否enable
mScreenThresholds - 屏幕使用阈值
mElapsedThresholds - 距离上一次使用阈值
3.2 adb shell dumpsys alarm
app_standby_window=+1h0m0s0ms
standby_quota_active=720
standby_quota_working=10
standby_quota_frequent=2
standby_quota_rare=1
standby_quota_never=0
standby_quota_restricted=1