/** * Returns the current standby bucket of the calling app. The system determines the standby * state of the app based on app usage patterns. Standby buckets determine how much an app will * be restricted from running background tasks such as jobs and alarms. * <p>Restrictions increase progressively from {@link #STANDBY_BUCKET_ACTIVE} to * {@link #STANDBY_BUCKET_RARE}, with {@link #STANDBY_BUCKET_ACTIVE} being the least * restrictive. The battery level of the device might also affect the restrictions. * <p>Apps in buckets ≤ {@link #STANDBY_BUCKET_ACTIVE} have no standby restrictions imposed. * Apps in buckets > {@link #STANDBY_BUCKET_FREQUENT} may have network access restricted when * running in the background. * <p>The standby state of an app can change at any time either due to a user interaction or a * system interaction or some algorithm determining that the app can be restricted for a period * of time before the user has a need for it. * <p>You can also query the recent history of standby bucket changes by calling * {@link #queryEventsForSelf(long, long)} and searching for * {@link UsageEvents.Event#STANDBY_BUCKET_CHANGED}. * * @return the current standby bucket of the calling app. One of STANDBY_BUCKET_* constants. */ public@StandbyBucketsintgetAppStandbyBucket(){ try { return mService.getAppStandbyBucket(mContext.getOpPackageName(), mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { } return STANDBY_BUCKET_ACTIVE; }
set-standby-bucket [--user <USER_ID>] <PACKAGE> active|working_set|frequent|rare Puts an app in the standby bucket. get-standby-bucket [--user <USER_ID>] <PACKAGE> Returns the standby bucket of an app.
set-inactive [--user <USER_ID>] <PACKAGE> true|false Sets the inactive state of an app. get-inactive [--user <USER_ID>] <PACKAGE> Returns the inactive state of an app.
/** * Reports an event to the UsageStatsManager. * * @param component The component for which this event occurred. * @param userId The user id to which the component belongs to. * @param eventType The event that occurred. Valid values can be found at * {@link UsageEvents} */ publicabstractvoidreportEvent(ComponentName component, @UserIdInt int userId, int eventType);
/** * Reports an event to the UsageStatsManager. * * @param packageName The package for which this event occurred. * @param userId The user id to which the component belongs to. * @param eventType The event that occurred. Valid values can be found at * {@link UsageEvents} */ publicabstractvoidreportEvent(String packageName, @UserIdInt int userId, int eventType);
/** * This local service implementation is primarily used by ActivityManagerService. * ActivityManagerService will call these methods holding the 'am' lock, which means we * shouldn't be doing any IO work or other long running tasks in these methods. */ privatefinalclassLocalServiceextendsUsageStatsManagerInternal{
@Override publicvoidreportEvent(ComponentName component, int userId, int eventType){ if (component == null) { Slog.w(TAG, "Event reported without a component name"); return; }
@Override publicvoidreportEvent(String packageName, int userId, int eventType){ if (packageName == null) { Slog.w(TAG, "Event reported without a package name"); return; }
UsageEvents.Event event = new UsageEvents.Event(); event.mPackage = packageName;
// This will later be converted to system time. event.mTimeStamp = SystemClock.elapsedRealtime();
<!-- Set this to true to enable the platform's auto-power-save modes like doze and app standby. These are not enabled by default because they require a standard cloud-to-device messaging service for apps to interact correctly with the modes (such as to be able to deliver an instant message to the device even when it is dozing). This should be enabled if you have such services and expect apps to correctly use them when installed on your device. Otherwise, keep this disabled so that applications can still use their own mechanisms. --> <boolname="config_enableAutoPowerModes">false</bool>
case MSG_CHECK_PACKAGE_IDLE_STATE: checkAndUpdateStandbyState((String) msg.obj, msg.arg1, msg.arg2, mInjector.elapsedRealtime());
调用了checkAndUpdateStandbyState方法:
/** Check if we need to update the standby state of a specific app. */ privatevoidcheckAndUpdateStandbyState(String packageName, @UserIdInt int userId, int uid, long elapsedRealtime){ // 包是由于一些原因在白名单,那么isAppSpecial会返回true, 包将不会进入standby状态 finalboolean isSpecial = isAppSpecial(packageName, UserHandle.getAppId(uid), userId);
// If the bucket was forced by the user/developer, leave it alone. // A usage event will be the only way to bring it out of this forced state // 如果的bucket的设置原因是被用户或者开发者,强制设置的,将不会改变它的组别 if (oldMainReason == REASON_MAIN_FORCED) { return; } finalint oldBucket = app.currentBucket; // 动态调整buncket, 最高等级只能设置到 10 , 也就是STANDBY_BUCKET_ACTIVE int newBucket = Math.max(oldBucket, STANDBY_BUCKET_ACTIVE); // Undo EXEMPTED boolean predictionLate = predictionTimedOut(app, elapsedRealtime); // Compute age-based bucket if (oldMainReason == REASON_MAIN_DEFAULT || oldMainReason == REASON_MAIN_USAGE || oldMainReason == REASON_MAIN_TIMEOUT || predictionLate) {
/** * Evaluates next bucket based on time since last used and the bucketing thresholds. * @param packageName the app * @param userId the user * @param elapsedRealtime as the name suggests, current elapsed time * @return the bucket for the app, based on time since last used */ @GuardedBy("mAppIdleLock") @StandbyBucketsintgetBucketForLocked(String packageName, int userId, long elapsedRealtime){ int bucketIndex = mAppIdleHistory.getThresholdIndex(packageName, userId, elapsedRealtime, mAppStandbyScreenThresholds, mAppStandbyElapsedThresholds); return THRESHOLD_BUCKETS[bucketIndex]; }
/** * Returns the index in the arrays of screenTimeThresholds and elapsedTimeThresholds * that corresponds to how long since the app was used. * @param packageName * @param userId * @param elapsedRealtime current time * @param screenTimeThresholds Array of screen times, in ascending order, first one is 0 * @param elapsedTimeThresholds Array of elapsed time, in ascending order, first one is 0 * @return The index whose values the app's used time exceeds (in both arrays) */ intgetThresholdIndex(String packageName, int userId, long elapsedRealtime, long[] screenTimeThresholds, long[] elapsedTimeThresholds){ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, elapsedRealtime, false); // If we don't have any state for the app, assume never used // 对于从来没有使用过的app , 就设置成最低级别的bucket, STANDBY_BUCKET_RARE if (appUsageHistory == null) return screenTimeThresholds.length - 1; // getScreenOnTime(elapsedRealtime) 获取设备的总亮屏时间(有记录在案的时间) // appUsageHistory.lastUsedScreenTime app最后一次亮屏时间点,基于ScreenOn basetime // screenOnDelta 计算出来就是app最后一次亮屏使用,到现在,已经有多久的亮屏时间 // getElapsedTime(elapsedRealtime) 获取是被从bron开始现在的时间 // appUsageHistory.lastUsedElapsedTime 基于ElapsedTime该package最后一次使用的时间点 // elapsedDelta 计算出来就是app最后一次使用到现在的时间点 long screenOnDelta = getScreenOnTime(elapsedRealtime) - appUsageHistory.lastUsedScreenTime; long elapsedDelta = getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedElapsedTime;
if (DEBUG) Slog.d(TAG, packageName + " lastUsedScreen=" + appUsageHistory.lastUsedScreenTime + " lastUsedElapsed=" + appUsageHistory.lastUsedElapsedTime); if (DEBUG) Slog.d(TAG, packageName + " screenOn=" + screenOnDelta + ", elapsed=" + elapsedDelta); for (int i = screenTimeThresholds.length - 1; i >= 0; i--) { if (screenOnDelta >= screenTimeThresholds[i] && elapsedDelta >= elapsedTimeThresholds[i]) { return i; } } return0; // 对应STANDBY_BUCKET_ACTIVE }
从代码可以看出,应用待机分组和app 是否使用android P 的 SDK 开发没有关系, 所有app, 只要是安装到android P的设备上都会受到系统的限制.
3 应用待机模式
在android M 版本上添加了Doze 和 app standby模式, 长时间没有在前台使用, app 的行为也会受到限制,详情可见Optimize for Doze and App Standby. 该功能就是将app设置为是否idle状态来进行限制, 处于idle状态的app 的网络,JobScheduler等都会限制住. 在android P中,由于添加了应用待机分组功能,app的行为被限制的更加精细化.
在P上的AppIdleHistory.java中的setIdle方法设置为
/* Returns the new standby bucket the app is assigned to */ publicintsetIdle(String packageName, int userId, boolean idle, long elapsedRealtime){ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, elapsedRealtime, true); if (idle) { appUsageHistory.currentBucket = STANDBY_BUCKET_RARE; appUsageHistory.bucketingReason = REASON_MAIN_FORCED; } else { appUsageHistory.currentBucket = STANDBY_BUCKET_ACTIVE; // This is to pretend that the app was just used, don't freeze the state anymore. appUsageHistory.bucketingReason = REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION; } return appUsageHistory.currentBucket; }
对比之前的android 版本的代码:
publicvoidsetIdle(String packageName, int userId, long elapsedRealtime){ ArrayMap<String, PackageHistory> userHistory = getUserHistory(userId); PackageHistory packageHistory = getPackageHistory(userHistory, packageName, elapsedRealtime);
/** Inform listeners if the bucket has changed since it was last reported to listeners */ privatevoidmaybeInformListeners(String packageName, int userId, long elapsedRealtime, int bucket, int reason, boolean userStartedInteracting){ synchronized (mAppIdleLock) { if (mAppIdleHistory.shouldInformListeners(packageName, userId, elapsedRealtime, bucket)) { final StandbyUpdateRecord r = StandbyUpdateRecord.obtain(packageName, userId, bucket, reason, userStartedInteracting); if (DEBUG) Slog.d(TAG, "Standby bucket for " + packageName + "=" + bucket); mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, r)); } } }
MSG_INFORM_LISTENERS 异步消息进过调用,informListeners
case MSG_INFORM_LISTENERS: StandbyUpdateRecord r = (StandbyUpdateRecord) msg.obj; informListeners(r.packageName, r.userId, r.bucket, r.reason, r.isUserInteraction); r.recycle(); break;
进入informListeners:
voidinformListeners(String packageName, int userId, int bucket, int reason, boolean userInteraction){ // app所处的buncket的优先级在RARE以及RARE之下,标记为idle finalboolean idle = bucket >= STANDBY_BUCKET_RARE; synchronized (mPackageAccessListeners) { for (AppIdleStateChangeListener listener : mPackageAccessListeners) { // onAppIdleStateChanged 用于通知监听者 listener.onAppIdleStateChanged(packageName, userId, idle, bucket, reason); // 用户与package交互才导致的更改bucket, 则userInteraction为true, if (userInteraction) { listener.onUserInteractionStarted(packageName, userId); } } } }
/** Callback to inform listeners that the idle state has changed to a new bucket. */ publicabstractvoidonAppIdleStateChanged(String packageName, @UserIdInt int userId, boolean idle, int bucket, int reason);
/** * Callback to inform listeners that the parole state has changed. This means apps are * allowed to do work even if they're idle or in a low bucket. */ publicabstractvoidonParoleStateChanged(boolean isParoleOn);
/** * Optional callback to inform the listener that the app has transitioned into * an active state due to user interaction. */ publicvoidonUserInteractionStarted(String packageName, @UserIdInt int userId){ // No-op by default } }
/** * Tracking of app assignments to standby buckets */ finalclassAppStandbyTrackerextendsUsageStatsManagerInternal.AppIdleStateChangeListener{
publicvoidonAppIdleStateChanged(final String packageName, final @UserIdInt int userId, boolean idle, int bucket, int reason){ mHandler.removeMessages(AlarmHandler.APP_STANDBY_BUCKET_CHANGED); mHandler.obtainMessage(AlarmHandler.APP_STANDBY_BUCKET_CHANGED, userId, -1, packageName) .sendToTarget(); }
/** * Tracking of app assignments to standby buckets */ finalclassStandbyTrackerextendsAppIdleStateChangeListener{
// AppIdleStateChangeListener interface for live updates
@Override publicvoidonAppIdleStateChanged(final String packageName, final @UserIdInt int userId, boolean idle, int bucket, int reason){ finalint uid = mLocalPM.getPackageUid(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); if (uid < 0) { if (DEBUG_STANDBY) { Slog.i(TAG, "App idle state change for unknown app " + packageName + "/" + userId); } return; }
finalint bucketIndex = standbyBucketToBucketIndex(bucket); // update job bookkeeping out of band BackgroundThread.getHandler().post(() -> { if (DEBUG_STANDBY) { Slog.i(TAG, "Moving uid " + uid + " to bucketIndex " + bucketIndex); } synchronized (mLock) { mJobs.forEachJobForSourceUid(uid, job -> { // double-check uid vs package name to disambiguate shared uids if (packageName.equals(job.getSourcePackageName())) { job.setStandbyBucket(bucketIndex); // 在JobStatus.java中修改job的app所在的bucket } }); onControllerStateChanged(); //重点分析 } }); } @Override publicvoidonParoleStateChanged(boolean isParoleOn){ if (DEBUG_STANDBY) { Slog.i(TAG, "Global parole state now " + (isParoleOn ? "ON" : "OFF")); } mInParole = isParoleOn; }
@Override publicvoidonUserInteractionStarted(String packageName, int userId){ finalint uid = mLocalPM.getPackageUid(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); if (uid < 0) { // Quietly ignore; the case is already logged elsewhere return; }
long sinceLast = mUsageStats.getTimeSinceLastJobRun(packageName, userId); if (sinceLast > 2 * DateUtils.DAY_IN_MILLIS) { // Too long ago, not worth logging sinceLast = 0L; } final DeferredJobCounter counter = new DeferredJobCounter(); synchronized (mLock) { mJobs.forEachJobForSourceUid(uid, counter); } if (counter.numDeferred() > 0 || sinceLast > 0) { BatteryStatsInternal mBatteryStatsInternal = LocalServices.getService (BatteryStatsInternal.class); mBatteryStatsInternal.noteJobsDeferred(uid, counter.numDeferred(), sinceLast); } } }
在JobSchedulerService 创建的时候就添加Listener:
// Set up the app standby bucketing tracker mStandbyTracker = new StandbyTracker(); mUsageStats = LocalServices.getService(UsageStatsManagerInternal.class); mUsageStats.addAppIdleStateChangeListener(mStandbyTracker);
onControllerStateChanged()发送了异步消息MSG_CHECK_JOB :
case MSG_CHECK_JOB: if (mReportedActive) { // if jobs are currently being run, queue all ready jobs for execution. // job 正在执行执行,让其2执行完 queueReadyJobsForExecutionLocked(); } else { // Check the list of jobs and run some of them if we feel inclined. maybeQueueReadyJobsForExecutionLocked(); } break;
/** * Criteria for moving a job into the pending queue: * - It's ready. * - It's not pending. * - It's not already running on a JSC. * - The user that requested the job is running. * - The job's standby bucket has come due to be runnable. * - The component is enabled and runnable. */ privatebooleanisReadyToBeExecutedLocked(JobStatus job){ ..... // If the app is in a non-active standby bucket, make sure we've waited // an appropriate amount of time since the last invocation. During device- // wide parole, standby bucketing is ignored. // // Jobs in 'active' apps are not subject to standby, nor are jobs that are // specifically marked as exempt. if (DEBUG_STANDBY) { Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + " parole=" + mInParole + " active=" + job.uidActive + " exempt=" + job.getJob().isExemptedFromAppStandby()); } if (!mInParole && !job.uidActive && !job.getJob().isExemptedFromAppStandby()) { finalint bucket = job.getStandbyBucket(); if (DEBUG_STANDBY) { Slog.v(TAG, " bucket=" + bucket + " heartbeat=" + mHeartbeat + " next=" + mNextBucketHeartbeat[bucket]); } if (mHeartbeat < mNextBucketHeartbeat[bucket]) { // Only skip this job if the app is still waiting for the end of its nominal // bucket interval. Once it's waited that long, we let it go ahead and clear. // The final (NEVER) bucket is special; we never age those apps' jobs into // runnability. finallong appLastRan = heartbeatWhenJobsLastRun(job); if (bucket >= mConstants.STANDBY_BEATS.length || (mHeartbeat > appLastRan && mHeartbeat < appLastRan + mConstants.STANDBY_BEATS[bucket])) { // TODO: log/trace that we're deferring the job due to bucketing if we hit this if (job.getWhenStandbyDeferred() == 0) { if (DEBUG_STANDBY) { Slog.v(TAG, "Bucket deferral: " + mHeartbeat + " < " + (appLastRan + mConstants.STANDBY_BEATS[bucket]) + " for " + job); } job.setWhenStandbyDeferred(sElapsedRealtimeClock.millis()); } returnfalse; } else { if (DEBUG_STANDBY) { Slog.v(TAG, "Bucket deferred job aged into runnability at " + mHeartbeat + " : " + job); } } } ...... }
/** * Mapping: standby bucket -> number of heartbeats between each sweep of that * bucket's jobs. * * Bucket assignments as recorded in the JobStatus objects are normalized to be * indices into this array, rather than the raw constants used * by AppIdleHistory. */ finalint[] STANDBY_BEATS = { 0, DEFAULT_STANDBY_WORKING_BEATS, DEFAULT_STANDBY_FREQUENT_BEATS, DEFAULT_STANDBY_RARE_BEATS };