欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > Android 性能之 Game Mode

Android 性能之 Game Mode

2024/10/26 1:30:41 来源:https://blog.csdn.net/youthcowboy/article/details/141501911  浏览:    关键词:Android 性能之 Game Mode

目录

1. 简介

2. 识别游戏应用

3. Game Mode 功能调试

4. Game Mode 功能详细分析

4.1 GamePackageConfiguration

4.1.1 简要说明

4.1.2 初始化流程

Step 1

Step 2

Step 3

4.1.3 初始化代码

4.2 GameModeConfiguration

4.2.1 简要说明

4.2.2 应用设置 custom mode config

4.2.3 其它

4.3 设置 game mode

4.4 保存设置

4.4.1 全局设置

4.4.2 intervention(干涉/干预)

4.4.2 保存设置

4.4.3 保存当前的设置

保存 game-manager-service.xml 文件

读取 game-manager-service.xml 文件

4.4.4 保存(记录) 干涉信息

4.4.5 保存时机

4.4.5.1 关机时,保存 "当前的设置" 和 "干涉信息"

4.4.5.2 保存 "干涉信息"

4.4.5.2.1 设置 game mode 时(0延迟)

4.4.5.2.2 更新 custom mode 的 config 时(延迟10s)

4.4.5.2.3 用户启动、用户切换时(0延迟)

4.4.5.2.4 device config "game_overlay" 改变时(0延迟)

4.4.5.2.5 游戏应用安装(0延迟)、卸载(延迟10s)时

GMS$updateConfigsForUser 方法

4.4.5.3 保存 "当前的设置"

4.5 策略

4.5.1 scaling

4.5.1.1 图像缩放

4.5.1.2 分辨率缩放

4.5.1.3 应用分辨率缩放

4.5.1.4 分辨率缩放因子

4.5.1.5 分辨率缩放因子的作用效果

4.5.2 fps

4.5.3 angle

4.5.4 boost

4.5.4.1 GMS$notifyGraphicsEnvironmentSetup

4.5.4.2 GMS$setGameState

5. scaling 策略

5.1 背景知识

5.1.1 兼容模式

5.1.1.1 兼容模式 & 兼容性信息

5.1.1.2 兼容模式状态(开关)

5.1.1.3 兼容性变更

什么是兼容性变更

scaling 相关的兼容性变更项

4.5.1.4 更新应用兼容性信息的场景

应用启动时

应用的兼容模式状态改变时

5.1.2 应用程序上下文(Context)

5.1.3 应用启动(bind application)流程

思考


1. 简介

本文的目的是介绍 AOSP Game Mode 功能。

Game Mode 官方文档:https://developer.android.google.cn/games/gamemode


Game Mode 功能的核心是系统服务 GameManagerService,在本文中用 GMS 指代 GameManagerService。

在 API 26 之后,应用开发者可以通过将应用的 category 属性设置为 "game",使系统(即 GMS)能够将该应用识别为 "游戏"。

GMS 定义了 4 种 game mode,即 1|2|3|4|standard|performance|battery|custom;每种 game mode 有对应的 game mode config。

standard|performance|battery 属于系统 game mode,它们的 game mode config 由系统预置;

custom 是应用自定义的 game mode,它的 game mode config 由 "游戏" 应用设置。


game mode config 包含 4 种策略

  • scaling:修改分辨率

  • fps:修改刷新率

  • angle:开启/不开启 angle(开启 angle 时,使用 vulkan 作为图形渲染库;否则使用默认的 opengl es 作为图形渲染库)

  • loading boost:调用 powerhal api 设置 power mode 为 "Mode.GAME_LOADING",并设置持续时长

应用可以调用系统提供的 API GameManager.setGameMode 来设置/选择 game mode。(GameManager.setGameMode 远程调用 GMS 的 setGameMode 方法)

应用可以调用系统提供的 API GameManager.updateCustomGameModeConfiguration 设置 custom game mode 的 config。(GameManager.updateCustomGameModeConfiguration 远程调用 GMS 的 updateCustomGameModeConfiguration 方法)


总而言之,AOSP 这套 Game Mode 框架提供了一个机制,使设备厂商可以配置不同 game mode 下的性能/功耗策略,使应用开发者可以自由选择不同的 game mode 或者设置自定义 game mode 的性能/功耗策略。

2. 识别游戏应用

在 API 26 之后,系统通过应用的 category 属性是否为 "game" 来判断该应用是否为 "游戏"。

例如,下面的代码可用于判断应用是否为 "游戏" 。

public static boolean packageIsGame(Context context, String packageName) {try {ApplicationInfo info = context.getPackageManager().getApplicationInfo(packageName, 0);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {return info.category == ApplicationInfo.CATEGORY_GAME;} else {// We are suppressing deprecation since there are no other options in this API Level//noinspection deprecationreturn (info.flags & ApplicationInfo.FLAG_IS_GAME) == ApplicationInfo.FLAG_IS_GAME;}} catch (PackageManager.NameNotFoundException e) {Log.e("Util", "Package info not found for name: " + packageName, e);// Or throw an exception if you wantreturn false;}
}

需要注意,这种识别 "游戏" 的方式有一定局限性,识别是否准确依赖于应用对 category 属性的设置是否正确。

3. Game Mode 功能调试

可以通过 adb shell cmd game 命令来调试 Game Mode 功能。

命令

255|yudi:/ # cmd game
Game manager (game) commands:helpPrint this help text.downscaleDeprecated. Please use `custom` command.list-configs <PACKAGE_NAME>Lists the current intervention configs of an app.list-modes <PACKAGE_NAME>Lists the current selected and available game modes of an app.mode [--user <USER_ID>] [1|2|3|4|standard|performance|battery|custom] <PACKAGE_NAME>Set app to run in the specified game mode, if supported.--user <USER_ID>: apply for the given user,the current user is used when unspecified.set [intervention configs] <PACKAGE_NAME>Set app to run at custom mode using provided intervention configsIntervention configs consists of:--downscale [0.3|0.35|0.4|0.45|0.5|0.55|0.6|0.65|0.7|0.75|0.8|0.85|0.9|disable]: Set app to run at thespecified scaling ratio.--fps [30|45|60|90|120|disable]: Set app to run at the specified fps,if supported.reset [--mode [2|3|performance|battery] --user <USER_ID>] <PACKAGE_NAME>Resets the game mode of the app to device configuration.This should only be used to reset any override to non custom game modeapplied using the deprecated `set` command--mode [2|3|performance|battery]: apply for the given mode,resets all modes when unspecified.--user <USER_ID>: apply for the given user,the current user is used when unspecified.

代码

#frameworks/base/services/core/java/com/android/server/app/GameManagerShellCommand.java355      @Override
356      public void onHelp() {
357          PrintWriter pw = getOutPrintWriter();
358          pw.println("Game manager (game) commands:");
359          pw.println("  help");
360          pw.println("      Print this help text.");
361          pw.println("  downscale");
362          pw.println("      Deprecated. Please use `custom` command.");
363          pw.println("  list-configs <PACKAGE_NAME>");
364          pw.println("      Lists the current intervention configs of an app.");
365          pw.println("  list-modes <PACKAGE_NAME>");
366          pw.println("      Lists the current selected and available game modes of an app.");
367          pw.println("  mode [--user <USER_ID>] [1|2|3|4|standard|performance|battery|custom] "
368                  + "<PACKAGE_NAME>");
369          pw.println("      Set app to run in the specified game mode, if supported.");
370          pw.println("      --user <USER_ID>: apply for the given user,");
371          pw.println("                        the current user is used when unspecified.");
372          pw.println("  set [intervention configs] <PACKAGE_NAME>");
373          pw.println("      Set app to run at custom mode using provided intervention configs");
374          pw.println("      Intervention configs consists of:");
375          pw.println("      --downscale [0.3|0.35|0.4|0.45|0.5|0.55|0.6|0.65");
376          pw.println("                  |0.7|0.75|0.8|0.85|0.9|disable]: Set app to run at the");
377          pw.println("                                                   specified scaling ratio.");
378          pw.println("      --fps [30|45|60|90|120|disable]: Set app to run at the specified fps,");
379          pw.println("                                       if supported.");
380          pw.println("  reset [--mode [2|3|performance|battery] --user <USER_ID>] <PACKAGE_NAME>");
381          pw.println("      Resets the game mode of the app to device configuration.");
382          pw.println("      This should only be used to reset any override to non custom game mode");
383          pw.println("      applied using the deprecated `set` command");
384          pw.println("      --mode [2|3|performance|battery]: apply for the given mode,");
385          pw.println("                                        resets all modes when unspecified.");
386          pw.println("      --user <USER_ID>: apply for the given user,");
387          pw.println("                        the current user is used when unspecified.");
388      }
389  }

4. Game Mode 功能详细分析

4.1 GamePackageConfiguration

4.1.1 简要说明

GamePackageConfiguration 是 GMS 的内部类。

每个游戏对应一个 GamePackageConfiguration 对象:

  • 系统启动时,GMS 为每个预置的游戏应用创建一个 GamePackageConfiguration 对象;

  • 游戏应用安装时,GMS 为该应用创建一个 GamePackageConfiguration 对象。

GamePackageConfiguration 包含两类信息:

  • 游戏应用支持的 game mode 通过成员变量 mPerfModeOverridden、mBatteryModeOverridden、mAllowDownscale、mAllowAngle、mAllowFpsOverride 维护

  • 游戏应用每个 game mode 对应的 game config 通过一个或多个 GameModeConfiguration 维护

4.1.2 初始化流程

GamePackageConfiguration 构造时,

Step 1

先调用 parseInterventionFromXml 方法解析游戏应用配置的 game mode config 文件(如果应用有配置的话)。

game mode config 文件名没有要求,

但是它的 root 标签必须是 "game-mode-config",

它的 attr 标签应该是以下标签中的一个或多个

"supportsBatteryGameMode"

"supportsPerformanceGameMode"

"allowGameAngleDriver"

"allowGameDownscaling"

"allowGameFpsOverride"

将 mPerfModeOverridden、mBatteryModeOverridden、mAllowDownscale、mAllowAngle、mAllowFpsOverride 分别设置为以上标签的值。

如果对应的标签未配置,mPerfModeOverridden 和 mBatteryModeOverridden 缺省为 false,mAllowDownscale、mAllowAngle 和mAllowFpsOverride 缺省为 true。

game mode config 文件格式可以参考 cts/tests/tests/gamemanager/GameTestApp/res/xml/game_mode_config.xml

19 <game-mode-config
20     xmlns:android="http://schemas.android.com/apk/res/android"
21     android:supportsBatteryGameMode="true"
22     android:supportsPerformanceGameMode="true"
23     android:allowGameAngleDriver="true"
24     android:allowGameDownscaling="true"
25 />

game mode config 文件支持的 attr 标签定义在:

frameworks/base/core/res/res/values/attrs.xml

9179     <!-- Use <code>game-mode-config</code> as the root tag of the XML resource that
9180          describes a GameModeConfig.
9181          Described here are the attributes that can be included in that tag. -->
9182     <declare-styleable name="GameModeConfig">
9183         <!-- Set true to opt in BATTERY mode. -->
9184         <attr name="supportsBatteryGameMode" format="boolean" />
9185         <!-- Set true to opt in PERFORMANCE mode. -->
9186         <attr name="supportsPerformanceGameMode" format="boolean" />
9187         <!-- Set true to enable ANGLE. -->
9188         <attr name="allowGameAngleDriver" format="boolean" />
9189         <!-- Set true to allow resolution downscaling intervention. -->
9190         <attr name="allowGameDownscaling" format="boolean" />
9191         <!-- Set true to allow FPS override intervention. -->
9192         <attr name="allowGameFpsOverride" format="boolean" />
9193     </declare-styleable>

应用的 game mode config 配置文件是通过应用 manifest 中的 meta-data 标签设置的,可以参考

cts/tests/tests/gamemanager/GameTestApp/AndroidManifest.xml

        <meta-data android:name="android.game_mode_config"android:resource="@xml/game_mode_config" /
Step 2

如果应用没有配置 game mode config 文件,解析应用 manifest 中的 meta-data 标签,

将 mPerfModeOverridden、mBatteryModeOverridden、mAllowDownscale、mAllowAngle 分别设置为以下 meta-data 标签的值:

"com.android.app.gamemode.performance.enabled"

"com.android.app.gamemode.battery.enabled"

"com.android.graphics.intervention.wm.allowDownscale"

"com.android.graphics.intervention.angle.allowAngle"

如果对应的 meta-data 标签未配置,mPerfModeOverridden 和 mBatteryModeOverridden 缺省为 false,mAllowDownscale 和 mAllowAngle 缺省为 true。

应用在 manifest 中配置对应的 meta-data 标签可以参考:

            <meta-dataandroid:name="com.android.app.gamemode.performance.enabled"android:value="true|false" />
Step 3

然后解析 device config "game_overlay" 得到 game config,并保存到 GameModeConfiguration 对象中

device config "game_overlay" 以 ":" 符号分隔,每段字串表示一份 game config,对应一个 GameModeConfiguration 对象。

4.1.3 初始化代码

GamePackageConfiguration 构造方法。

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.javaGamePackageConfiguration(PackageManager packageManager, String packageName, int userId) {mPackageName = packageName;try {final ApplicationInfo ai = packageManager.getApplicationInfoAsUser(packageName,PackageManager.GET_META_DATA, userId);// (1) 解析应用配置的 game mode config 文件if (!parseInterventionFromXml(packageManager, ai, packageName)&& ai.metaData != null) {// (2) 解析应用 manifest 中的 meta-data 标签mPerfModeOverridden = ai.metaData.getBoolean(METADATA_PERFORMANCE_MODE_ENABLE);mBatteryModeOverridden = ai.metaData.getBoolean(METADATA_BATTERY_MODE_ENABLE);mAllowDownscale = ai.metaData.getBoolean(METADATA_WM_ALLOW_DOWNSCALE, true);mAllowAngle = ai.metaData.getBoolean(METADATA_ANGLE_ALLOW_ANGLE, true);}} catch (NameNotFoundException e) {// Not all packages are installed, hence ignore those that are not installed yet.Slog.v(TAG, "Failed to get package metadata");}// (3) 解析 device config "game_overlay" 得到 game mode config, // 并保存到 GameModeConfiguration 对象final String configString = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_GAME_OVERLAY, packageName);if (configString != null) {final String[] gameModeConfigStrings = configString.split(":");for (String gameModeConfigString : gameModeConfigStrings) {try {final KeyValueListParser parser = new KeyValueListParser(',');parser.setString(gameModeConfigString);// (4)addModeConfig(new GameModeConfiguration(parser));} catch (IllegalArgumentException e) {Slog.e(TAG, "Invalid config string");}}}}

parseInterventionFromXml 方法。

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.java//静态成员变量/*** Metadata that allows a game to specify all intervention information with an XML file in* the application field.*/// 应用 manifest 文件中 meta-data 元素的键值。// meta-data 元素引用一个 xml 资源文件(xml 资源文件应放在应用的 res/xml 目录下)。public static final String METADATA_GAME_MODE_CONFIG = "android.game_mode_config";// xml 资源文件根结点的 TAG 名称。private static final String GAME_MODE_CONFIG_NODE_NAME = "game-mode-config";//parseInterventionFromXml方法private boolean parseInterventionFromXml(PackageManager packageManager, ApplicationInfo ai,String packageName) {boolean xmlFound = false;// 加载应用 "game-mode-config" meta-data 引用的 xml 资源文件try (XmlResourceParser parser = ai.loadXmlMetaData(packageManager,METADATA_GAME_MODE_CONFIG)) {if (parser == null) {Slog.v(TAG, "No " + METADATA_GAME_MODE_CONFIG+ " meta-data found for package " + mPackageName);} else {xmlFound = true;final Resources resources = packageManager.getResourcesForApplication(packageName);final AttributeSet attributeSet = Xml.asAttributeSet(parser);int type;while ((type = parser.next()) != XmlPullParser.END_DOCUMENT&& type != XmlPullParser.START_TAG) {// Do nothing}// 检查 xml 资源文件的根节点 TAG 是否为 "game-mode-config"boolean isStartingTagGameModeConfig =GAME_MODE_CONFIG_NODE_NAME.equals(parser.getName());if (!isStartingTagGameModeConfig) {Slog.w(TAG, "Meta-data does not start with "+ GAME_MODE_CONFIG_NODE_NAME+ " tag");} else {final TypedArray array = resources.obtainAttributes(attributeSet,com.android.internal.R.styleable.GameModeConfig);// 获取 android:supportsPerformanceGameMode 属性值,设置到 mPerfModeOverridden mPerfModeOverridden = array.getBoolean(GameModeConfig_supportsPerformanceGameMode, false);// 获取 android:supportsBatteryGameMode 属性值,设置到 mBatteryModeOverridden mBatteryModeOverridden = array.getBoolean(GameModeConfig_supportsBatteryGameMode,false);// 获取 android:allowGameDownscaling 属性值,设置到 mAllowDownscalemAllowDownscale = array.getBoolean(GameModeConfig_allowGameDownscaling,true);// 获取 android:allowGameAngleDriver 属性值,设置到 mAllowAnglemAllowAngle = array.getBoolean(GameModeConfig_allowGameAngleDriver, true);// 获取 android:allowGameFpsOverride 属性值,设置到 mAllowFpsOverridemAllowFpsOverride = array.getBoolean(GameModeConfig_allowGameFpsOverride,true);array.recycle();}}} catch (NameNotFoundException | XmlPullParserException | IOException ex) {// set flag back to default values when parsing failsmPerfModeOverridden = false;mBatteryModeOverridden = false;mAllowDownscale = true;mAllowAngle = true;mAllowFpsOverride = true;Slog.e(TAG, "Error while parsing XML meta-data for "+ METADATA_GAME_MODE_CONFIG);}return xmlFound;}

meta-data 说明

Android 应用可以在 AndroidManifest.xml 文件中定义 meta-data。

meta-data 可以引用一个 XML 资源文件,也可以设置一个值。

<application ... >// 引用 xml 资源文件<meta-data android:name="xxx" android:resource="@xml/test.xml" /> // 设置值<meta-dataandroid:name="yyy"android:value="true|false" />
</application>

使用 meta-data 引用 XML 资源文件的具体步骤如下:

  1. 创建 XML 文件:在 res/xml 目录下创建一个 XML 文件(如 game_mode_config.xml)。

    1. 如果 res/xml 目录不存在,您需要手动创建该目录。

  2. 定义 meta-data:在 AndroidManifest.xml 文件中的 <application> 标签或某个 <activity> 标签下,添加一个 meta-data 元素来引用这个 XML 文件。

  3. 使用 XML 文件:在代码中,ai.loadXmlMetaData(packageManager, "xxx") 将会查找并加载这个 XML 文件。

4.2 GameModeConfiguration

4.2.1 简要说明

每个游戏对应一个 GamePackageConfiguration 对象,

每个 GamePackageConfiguration 又包含 4 个 GameModeConfiguration 对象,即每一个 game mode 都对应一个 GameModeConfiguration 对象(一共 4 种 game mode,即 1|2|3|4|standard|performance|battery|custom)。

上文说到 GamePackageConfiguration 初始化时,解析 device config "game_overlay" 得到 game config,并保存到 GameModeConfiguration 对象中。这是属于系统的设置,而应用程序可以设置自己的 custom game mode 的 config。

4.2.2 应用设置 custom mode config

设置 custom game mode config 的方法是 GMS.updateCustomGameModeConfiguration。

应用可以通过 GameManager 调用 GMS.updateCustomGameModeConfiguration。

GMS.updateCustomGameModeConfiguration 的主要工作流程:

  1. 更新应用的 custom mode config;

  2. 发送两个延时(10s)消息 WRITE_SETTINGS、WRITE_GAME_MODE_INTERVENTION_LIST_FILE,在 handler 中保存设置~~

  3. 如果当前目标游戏处于 custom mode,则调用 GMS.updateInterventions 方法应用新的 custom mode config 策略。

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.java/*** Updates the config for the game's {@link GameManager#GAME_MODE_CUSTOM} mode.** @throws SecurityException        if caller doesn't have*                                  {@link android.Manifest.permission#MANAGE_GAME_MODE}*                                  permission.* @throws IllegalArgumentException if the user ID provided doesn't exist.*/@Override@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)public void updateCustomGameModeConfiguration(String packageName,GameModeConfiguration gameModeConfig, int userId)throws SecurityException, IllegalArgumentException {checkPermission(Manifest.permission.MANAGE_GAME_MODE);if (!isPackageGame(packageName, userId)) {Slog.d(TAG, "No-op for attempt to update custom game mode for non-game app: "+ packageName);return;}synchronized (mLock) {if (!mSettings.containsKey(userId)) {throw new IllegalArgumentException("User " + userId + " wasn't started");}}// TODO(b/243448953): add validation on gameModeConfig provided// Adding game mode config override of the given package nameGamePackageConfiguration configOverride;synchronized (mLock) {if (!mSettings.containsKey(userId)) {return;}final GameManagerSettings settings = mSettings.get(userId);// look for the existing GamePackageConfiguration overrideconfigOverride = settings.getConfigOverride(packageName);if (configOverride == null) {configOverride = new GamePackageConfiguration(packageName);settings.setConfigOverride(packageName, configOverride);}}// (1) 更新应用的 custom mode configGamePackageConfiguration.GameModeConfiguration internalConfig =configOverride.getOrAddDefaultGameModeConfiguration(GameManager.GAME_MODE_CUSTOM);final float scalingValueFrom = internalConfig.getScaling();final int fpsValueFrom = internalConfig.getFps();internalConfig.updateFromPublicGameModeConfig(gameModeConfig);// (2) 设置 delay msg,在 handler 线程中保存设置sendUserMessage(userId, WRITE_SETTINGS, EVENT_UPDATE_CUSTOM_GAME_MODE_CONFIG,WRITE_DELAY_MILLIS);sendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE,EVENT_UPDATE_CUSTOM_GAME_MODE_CONFIG, WRITE_DELAY_MILLIS /*delayMillis*/);// (3) 如果应用当前就处于 custom mode,则应用新的 custom mode configfinal int gameMode = getGameMode(packageName, userId);if (gameMode == GameManager.GAME_MODE_CUSTOM) {updateInterventions(packageName, gameMode, userId);}+ internalConfig.getScaling() + " under user " + userId);int gameUid = -1;try {gameUid = mPackageManager.getPackageUidAsUser(packageName, userId);} catch (NameNotFoundException ex) {Slog.d(TAG, "Cannot find the UID for package " + packageName + " under user " + userId);}FrameworkStatsLog.write(FrameworkStatsLog.GAME_MODE_CONFIGURATION_CHANGED, gameUid,Binder.getCallingUid(), gameModeToStatsdGameMode(GameManager.GAME_MODE_CUSTOM),scalingValueFrom, gameModeConfig.getScalingFactor(),fpsValueFrom, gameModeConfig.getFpsOverride());}

重点看一下更新应用 custom mode config 的方法,即 GameModeConfiguration$updateFromPublicGameModeConfig。

注意到,该方法只会更新 mScaling 和 mFps 的值。

也就是说,应用只能设置 custom mode 的 scaling 和 fps 策略,不能设置 angle 和 loading boost 策略。

        /*** GameModeConfiguration contains all the values for all the interventions associated with* a game mode.*/public class GameModeConfiguration {...void updateFromPublicGameModeConfig(android.app.GameModeConfiguration config) {mScaling = config.getScalingFactor();mFps = String.valueOf(config.getFpsOverride());}/*** @hide*/public String toString() {return "[Game Mode:" + mGameMode + ",Scaling:" + mScaling + ",Use Angle:"+ mUseAngle + ",Fps:" + mFps + ",Loading Boost Duration:"+ mLoadingBoostDuration + "]";}}

4.2.3 其它

注意到,GMS 还有两个方法会修改应用的 game mode config。

分别是 GMS$setGameModeConfigOverride 方法和 GMS$updateResolutionScalingFactor 方法

1. GMS$setGameModeConfigOverride 方法的作用是

  1. 修改目标应用、目标 game mode 的 fps、scaling 策略

  2. 设置目标应用的 game mode 为目标 game mode

调用 GMS$setGameModeConfigOverride 方法的应用需要配置权限 Manifest.permission.MANAGE_GAME_MODE。

    /*** Set the Game Mode Configuration override.* Update the config if exists, create one if not.*/@VisibleForTesting@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)public void setGameModeConfigOverride(String packageName, @UserIdInt int userId,@GameMode int gameMode, String fpsStr, String scaling) throws SecurityException {checkPermission(Manifest.permission.MANAGE_GAME_MODE);
...// Adding game mode config override of the given package nameGamePackageConfiguration configOverride;synchronized (mLock) {if (!mSettings.containsKey(userId)) {return;}final GameManagerSettings settings = mSettings.get(userId);// look for the existing GamePackageConfiguration overrideconfigOverride = settings.getConfigOverride(packageName);if (configOverride == null) {configOverride = new GamePackageConfiguration(packageName);settings.setConfigOverride(packageName, configOverride);}}// modify GameModeConfiguration intervention settings// 获取到目标应用、目标 game mode 的 GameModeConfiguration 对象GamePackageConfiguration.GameModeConfiguration modeConfigOverride =configOverride.getOrAddDefaultGameModeConfiguration(gameMode);// 修改 fps 策略(fps 值)if (fpsStr != null) {modeConfigOverride.setFpsStr(fpsStr);} else {modeConfigOverride.setFpsStr(GamePackageConfiguration.GameModeConfiguration.DEFAULT_FPS);}// 修改 scaling 策略(scaling 值)if (scaling != null) {modeConfigOverride.setScaling(Float.parseFloat(scaling));}Slog.i(TAG, "Package Name: " + packageName+ " FPS: " + String.valueOf(modeConfigOverride.getFps())+ " Scaling: " + modeConfigOverride.getScaling());// 设置 game modesetGameMode(packageName, gameMode, userId);}

2. GMS$updateResolutionScalingFactor 方法的作用是调用 GMS$setGameModeConfigOverride

  1. 修改目标应用、目标 game mode 的 scaling 策略

  2. 设置目标应用的 game mode 为目标 game mode

    /*** Updates the resolution scaling factor for the package's target game mode and activates it.** @param scalingFactor enable scaling override over any other compat scaling if positive,*                      or disable the override otherwise* @throws SecurityException        if caller doesn't have*                                  {@link android.Manifest.permission#MANAGE_GAME_MODE}*                                  permission.* @throws IllegalArgumentException if the user ID provided doesn't exist.*/@Override@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)public void updateResolutionScalingFactor(String packageName, int gameMode, float scalingFactor,int userId) throws SecurityException, IllegalArgumentException {checkPermission(Manifest.permission.MANAGE_GAME_MODE);synchronized (mLock) {if (!mSettings.containsKey(userId)) {throw new IllegalArgumentException("User " + userId + " wasn't started");}}setGameModeConfigOverride(packageName, userId, gameMode, null /*fpsStr*/,Float.toString(scalingFactor));}

不过,这两个方法应用都无法调用(至少目前是如此)。

GMS$setGameModeConfigOverride 方法是 Android 13 加入的,不是 binder 方法;

GMS$updateResolutionScalingFactor 方法是 Android 14 加入的,是 binder 方法。

GMS$setGameModeConfigOverride 只在 adb shell cmd game ... 时调用,由于不是 binder 方法,应用无法直接调用;

GMS$updateResolutionScalingFactor 虽然是 binder 方法,但是没有封装到 GameManager 中,应用也无法调用(未来可能会放到 GameManager 中给应用调用)。

4.3 设置 game mode

game mode 有 4 种:[1|2|3|4|standard|performance|battery|custom]

设置 game mode 的方法是 GMS.setGameMode 。

应用可以通过 GameManager 调用 GMS.setGameMode 。

GMS.setGameMode 的主要工作流程:

  1. 设置目标应用的 game mode,设置到全局的 GameManagerSettings 对象中

  2. 调用 GMS.updateInterventions 方法应用目标 game mode;

  3. 如果有客户端注册了监听 (监听应用 game mode 变化),则通知客户端

  4. 发送延时(10s)消息 WRITE_SETTINGS 和 0 延时消息 WRITE_GAME_MODE_INTERVENTION_LIST_FILE,在 handler 中保存设置~~

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.java/*** Sets the Game Mode for the package name.* Verifies that the calling process has {@link android.Manifest.permission#MANAGE_GAME_MODE}.*/@Override@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)public void setGameMode(String packageName, @GameMode int gameMode, int userId)throws SecurityException {checkPermission(Manifest.permission.MANAGE_GAME_MODE);if (gameMode == GameManager.GAME_MODE_UNSUPPORTED) {Slog.d(TAG, "No-op for attempt to set UNSUPPORTED mode for app: " + packageName);return;} else if (!isPackageGame(packageName, userId)) {Slog.d(TAG, "No-op for attempt to set game mode for non-game app: " + packageName);return;}int fromGameMode;synchronized (mLock) {userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),Binder.getCallingUid(), userId, false, true, "setGameMode","com.android.server.app.GameManagerService");if (!mSettings.containsKey(userId)) {Slog.d(TAG, "Failed to set game mode for package " + packageName+ " as user " + userId + " is not started");return;}GameManagerSettings userSettings = mSettings.get(userId);fromGameMode = userSettings.getGameModeLocked(packageName);// (1) 设置目标应用的 game modeuserSettings.setGameModeLocked(packageName, gameMode);}// (2) 应用目标 game modeupdateInterventions(packageName, gameMode, userId);synchronized (mGameModeListenerLock) {// (3) 如果有客户端注册了监听 (监听应用 game mode 变化),则通知客户端for (IGameModeListener listener : mGameModeListeners.keySet()) {Binder.allowBlocking(listener.asBinder());try {listener.onGameModeChanged(packageName, fromGameMode, gameMode, userId);} catch (RemoteException ex) {Slog.w(TAG, "Cannot notify game mode change for listener added by "+ mGameModeListeners.get(listener));}}}// (4) 设置 delay msg,在 handler 中保存设置sendUserMessage(userId, WRITE_SETTINGS, EVENT_SET_GAME_MODE, WRITE_DELAY_MILLIS);sendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE,EVENT_SET_GAME_MODE, 0 /*delayMillis*/);int gameUid = -1;try {gameUid = mPackageManager.getPackageUidAsUser(packageName, userId);} catch (NameNotFoundException ex) {Slog.d(TAG, "Cannot find the UID for package " + packageName + " under user " + userId);}FrameworkStatsLog.write(FrameworkStatsLog.GAME_MODE_CHANGED, gameUid,Binder.getCallingUid(), gameModeToStatsdGameMode(fromGameMode),gameModeToStatsdGameMode(gameMode));}

4.4 保存设置

4.4.1 全局设置

GameManagerSettings 保存 GMS 的全局设置,每个 userid 有一个 GameManagerSettings 对象。

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.javaprivate final ArrayMap<Integer, GameManagerSettings> mSettings = new ArrayMap<>();

GameManagerSettings 包含成员 mGameModes 和 mConfigOverrides。

mGameModes 存放的是 <游戏应用包名、当前的 game mode> 键值对。应用程序设置 game mode 时修改 mGameModes。

mConfigOverrides 存放的是 <游戏应用包名、GamePackageConfiguration> 键值对。mConfigOverrides 与 GMS 的 mConfigs 同步。

#frameworks/base/services/core/java/com/android/server/app/GameManagerSettings.java// PackageName -> GameModeprivate final ArrayMap<String, Integer> mGameModes = new ArrayMap<>();// PackageName -> GamePackageConfigurationprivate final ArrayMap<String, GamePackageConfiguration> mConfigOverrides = new ArrayMap<>();

4.4.2 intervention(干涉/干预)

intervention 是指应用 game mode 策略,即通过应用 game mode 策略来干涉应用的行为。

执行 intervention 的方法是 GMS$updateInterventions。

实际上,GMS$updateInterventions 只应用 fps 和 angle 策略。

scaling 策略是在游戏应用 restart 时自动读取并应用的。

boost 策略则是由游戏应用发起(设置 loading 状态)并由 GMS 应用的。

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.javaprivate void updateInterventions(String packageName,@GameMode int gameMode, @UserIdInt int userId) {final GamePackageConfiguration packageConfig = getConfig(packageName, userId);if (gameMode == GameManager.GAME_MODE_STANDARD|| gameMode == GameManager.GAME_MODE_UNSUPPORTED || packageConfig == null|| packageConfig.willGamePerformOptimizations(gameMode)|| packageConfig.getGameModeConfiguration(gameMode) == null) {resetFps(packageName, userId);// resolution scaling does not need to be reset as it's now read dynamically on game// restart, see #getResolutionScalingFactor and CompatModePackages#getCompatScale.// TODO: reset Angle intervention here once implementedif (packageConfig == null) {Slog.v(TAG, "Package configuration not found for " + packageName);return;}} else {// 应用 fps 策略updateFps(packageConfig, packageName, gameMode, userId);}// 应用 angle 策略(updateUseAngle 目前是空函数)updateUseAngle(packageName, gameMode);}

4.4.2 保存设置

保存设置分为两种情况

  1. 保存当前的设置,即将所有游戏应用当前的 game mode 及其所有的 game mode config 写到 data/system_de/<userid>/system/game-manager-service.xml 文件中。

  2. 保存(记录) 干涉信息,即将应用 game mode 策略的行为记录到 /system/data/game_mode_intervention.list 文件中。

4.4.3 保存当前的设置

保存 game-manager-service.xml 文件

保存当前的设置,即将所有游戏应用当前的 game mode 及其所有的 game mode config 写到 data/system_de/<userid>/system/game-manager-service.xml 文件中。

所有游戏应用的包名是通过 GameManagerSettings 成员 mConfigOverrides 的 key 集合得到到,

每个游戏应用当前的 game mode 是从 GameManagerSettings 成员 mGameModes 中获取的。

保存当前设置的方法是 GameManagerSettings$writePersistentDataLocked。

#frameworks/base/services/core/java/com/android/server/app/GameManagerSettings.java/*** Writes all current game service settings into disk.* This operation must be synced with an external lock.*/void writePersistentDataLocked() {FileOutputStream fstr = null;try {fstr = mSettingsFile.startWrite();final TypedXmlSerializer serializer = Xml.resolveSerializer(fstr);serializer.startDocument(null, true);serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);serializer.startTag(null, TAG_PACKAGES);   // 根标签 <packages>// 遍历所有游戏包名(即 mConfigOverrides 的 key 集合)final ArraySet<String> packageNames = new ArraySet<>(mGameModes.keySet());packageNames.addAll(mConfigOverrides.keySet());for (String packageName : packageNames) {serializer.startTag(null, TAG_PACKAGE);  // 应用节点标签 <package>serializer.attribute(null, ATTR_NAME, packageName);  // 节点属性 name="xxx"if (mGameModes.containsKey(packageName)) {// 节点属性 gameMode="xxx"// xxx 为应用当前的 game mode,从 mConfigOverrides 中获取serializer.attributeInt(null, ATTR_GAME_MODE, mGameModes.get(packageName));}// 标签 <gameModeConfig> ... </gameModeConfig>writeGameModeConfigTags(serializer, mConfigOverrides.get(packageName));serializer.endTag(null, TAG_PACKAGE);    // 应用节点标签 <package> 结束}serializer.endTag(null, TAG_PACKAGES);  // 根标签 <packages> 结束serializer.endDocument();mSettingsFile.finishWrite(fstr);FileUtils.setPermissions(mSettingsFile.toString(),FileUtils.S_IRUSR | FileUtils.S_IWUSR| FileUtils.S_IRGRP | FileUtils.S_IWGRP,-1, -1);return;} catch (java.io.IOException e) {mSettingsFile.failWrite(fstr);Slog.wtf(TAG, "Unable to write game manager service settings, "+ "current changes will be lost at reboot", e);}}

data/system_de/<userid>/system/game-manager-service.xml 文件的内容格式如下:

内容包含每个游戏应用当前的 game mode,以及各 game mode 的 config。

    // The XML file follows the below format:// <?xml>// <packages>//     <package name="游戏包名" gameMode="应用的 game mode 值">//       <gameModeConfig gameMode="game mode 值" fps="fps值" scaling="scaling值" useAngle="true|false" loadingBoost="boost时长">//       </gameModeConfig>//       ...//     </package>//     ...// </packages>
读取 game-manager-service.xml 文件

系统启动或用户切换时,GMS 创建对应用户的全局设置即 GameManagerSettings 对象~~~

将 data/system_de/<userid>/ 作为 File dataDir 传递给 GameManagerSettings。

GameManagerSettings 读取 data/system_de/<userid>/system/game-manager-service.xml 文件,初始化成员 mGameModes 和 mConfigOverrides 。

(注意 system、system_de、system_ce 的区别,system_de 和 system_ce 下的文件加密过,system_de 下的文件在开机 dm-verity 后才可用。)

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.java/*** SystemService lifecycle for GameService.** @hide*/public static class Lifecycle extends SystemService {private GameManagerService mService;...@Overridepublic void onUserStarting(@NonNull TargetUser user) {Slog.d(TAG, "Starting user " + user.getUserIdentifier());// (1)mService.onUserStarting(user,Environment.getDataSystemDeDirectory(user.getUserIdentifier()));}
...void onUserStarting(@NonNull TargetUser user, File settingDataDir) {final int userId = user.getUserIdentifier();synchronized (mLock) {if (!mSettings.containsKey(userId)) {// (2)GameManagerSettings userSettings = new GameManagerSettings(settingDataDir);mSettings.put(userId, userSettings);// (3)userSettings.readPersistentDataLocked();}}sendUserMessage(userId, POPULATE_GAME_MODE_SETTINGS, EVENT_ON_USER_STARTING,0 /*delayMillis*/);if (mGameServiceController != null) {mGameServiceController.notifyUserStarted(user);}}

GameManagerSettings 构造方法中,

打开 data/system_de/<userid>/system/game-manager-service.xml 文件,赋值给成员变量 mSettingsFile;

首次启动用户时,会在 data/system_de/<userid>/system/ 下创建 game-manager-service.xml 文件。

#frameworks/base/services/core/java/com/android/server/app/GameManagerSettings.javaprivate static final String GAME_SERVICE_FILE_NAME = "game-manager-service.xml";GameManagerSettings(File dataDir) {mSystemDir = new File(dataDir, "system");mSystemDir.mkdirs();FileUtils.setPermissions(mSystemDir.toString(),FileUtils.S_IRWXU | FileUtils.S_IRWXG| FileUtils.S_IROTH | FileUtils.S_IXOTH,-1, -1);mSettingsFile = new AtomicFile(new File(mSystemDir, GAME_SERVICE_FILE_NAME));}

GameManagerSettings$readPersistentDataLocked 方法读取 data/system_de/<userid>/system/game-manager-service.xml 文件,初始化 GameManagerSettings 成员 mGameModes 和 mConfigOverrides。

4.4.4 保存(记录) 干涉信息

保存(记录) 干涉信息,即将应用 game mode 策略的行为写到 /system/data/game_mode_intervention.list 文件中。

保存干涉信息的方法是 GameManagerSettings$writeGameModeInterventionsToFile。

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.java/*Write the interventions and mode of each game to file /system/data/game_mode_intervention.listEach line will contain the information of each game, separated by tab.The format of the output is:<package name> <UID> <current mode> <game mode 1> <interventions> <game mode 2> <interventions>For example:com.android.app1   1425    1   2   angle=0,scaling=1.0,fps=60  3   angle=1,scaling=0.5,fps=30*/private void writeGameModeInterventionsToFile(@UserIdInt int userId) {FileOutputStream fileOutputStream = null;BufferedWriter bufferedWriter;try {// game_mode_intervention.list 文件输出流fileOutputStream = mGameModeInterventionListFile.startWrite();bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream,Charset.defaultCharset()));final StringBuilder sb = new StringBuilder();// game app 列表final List<String> installedGamesList = getInstalledGamePackageNamesByAllUsers(userId);for (final String packageName : installedGamesList) {// app 的 game configGamePackageConfiguration packageConfig = getConfig(packageName, userId);if (packageConfig == null) {continue;}sb.append(packageName);sb.append("\t");sb.append(mPackageManager.getPackageUidAsUser(packageName, userId));sb.append("\t");// 获取 app 的 game modesb.append(getGameMode(packageName, userId));sb.append("\t");// app game config 支持的所有 modesfinal int[] modes = packageConfig.getAvailableGameModes();for (int mode : modes) {// app game config 的某个 mode configfinal GamePackageConfiguration.GameModeConfiguration gameModeConfiguration =packageConfig.getGameModeConfiguration(mode);if (gameModeConfiguration == null) {continue;}sb.append(mode);sb.append("\t");final int useAngle = gameModeConfiguration.getUseAngle() ? 1 : 0;sb.append(TextUtils.formatSimple("angle=%d", useAngle));sb.append(",");final float scaling = gameModeConfiguration.getScaling();sb.append("scaling=");sb.append(scaling);sb.append(",");final int fps = gameModeConfiguration.getFps();sb.append(TextUtils.formatSimple("fps=%d", fps));sb.append("\t");}sb.append("\n");}bufferedWriter.append(sb);bufferedWriter.flush();FileUtils.sync(fileOutputStream);mGameModeInterventionListFile.finishWrite(fileOutputStream);} catch (Exception e) {mGameModeInterventionListFile.failWrite(fileOutputStream);Slog.wtf(TAG, "Failed to write game_mode_intervention.list, exception " + e);}return;}

/system/data/game_mode_intervention.list 文件的内容格式为:

     <package name> <UID> <current mode> <game mode 1> <interventions> <game mode 2> <interventions>

文件中每一行表示一个游戏应用。包含当前的 game mode,以及各 game mode 的 config 等信息。

/system/data/game_mode_intervention.list 文件不需要加载/解析,因此没有保存为 xml 格式。

如下:

     com.android.app1   1425    1   2   angle=0,scaling=1.0,fps=60  3   angle=1,scaling=0.5,fps=30...

4.4.5 保存时机

4.4.5.1 关机时,保存 "当前的设置" 和 "干涉信息"

GMS 监听关机广播。

在收到关机广播时,发送两个 0 延迟的消息 WRITE_SETTINGS、WRITE_GAME_MODE_INTERVENTION_LIST_FILE,在 handler 中处理消息,执行保存设置的动作。

handler 接收到 WRITE_SETTINGS 消息时,调用 GameManagerSettings.writePersistentDataLocked 方法将 "当前设置" 保存到文件 data/system_de/<userid>/system/game-manager-service.xml;

handler 接收到 WRITE_GAME_MODE_INTERVENTION_LIST_FILE 消息时,调用 GMS.writeGameModeInterventionsToFile 方法将 "干涉信息" 保存到文件 /system/data/game_mode_intervention.list。

在执行保存设置的动作时,会将 handler 线程的调度优先级提高到 Process.THREAD_PRIORITY_DEFAULT,执行完后再恢复为 Process.THREAD_PRIORITY_BACKGROUND。

1. 接收关机广播,设置 msg

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.java/*** Notified when boot is completed.*/@VisibleForTestingvoid onBootCompleted() {Slog.d(TAG, "onBootCompleted");if (mGameServiceController != null) {mGameServiceController.onBootComplete();}mContext.registerReceiver(new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {// 接收到关机广播if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) {synchronized (mLock) {// Note that the max wait time of broadcast is 10s (see// {@ShutdownThread#MAX_BROADCAST_TIMEMAX_BROADCAST_TIME}) currently so// this can be optional only if we have message delay plus processing// time significant smaller to prevent data loss.for (Map.Entry<Integer, GameManagerSettings> entry : mSettings.entrySet()) {final int userId = entry.getKey();// 保存当前设置,0 延迟sendUserMessage(userId, WRITE_SETTINGS,EVENT_RECEIVE_SHUTDOWN_INDENT, 0 /*delayMillis*/);// 保存干涉信息,0 延迟sendUserMessage(userId,WRITE_GAME_MODE_INTERVENTION_LIST_FILE,EVENT_RECEIVE_SHUTDOWN_INDENT,0 /*delayMillis*/);}}}}}, new IntentFilter(Intent.ACTION_SHUTDOWN));}

2. msg 处理

Handler 线程在执行存储时,提高线程优先级到 "default",执行完后降低线程优先级到 "background"。

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.javaclass SettingsHandler extends Handler {SettingsHandler(Looper looper) {super(looper);}@Overridepublic void handleMessage(Message msg) {doHandleMessage(msg);}void doHandleMessage(Message msg) {switch (msg.what) {// 保存当前设置case WRITE_SETTINGS: {final int userId = (int) msg.obj;if (userId < 0) {Slog.wtf(TAG, "Attempt to write settings for invalid user: " + userId);synchronized (mLock) {removeEqualMessages(WRITE_SETTINGS, msg.obj);}break;}// 调高 handler 线程优先级Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);synchronized (mLock) {removeEqualMessages(WRITE_SETTINGS, msg.obj);if (mSettings.containsKey(userId)) {GameManagerSettings userSettings = mSettings.get(userId);userSettings.writePersistentDataLocked();}}// 降低 handler 线程优先级Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);break;}... ...// 保存干涉信息case WRITE_GAME_MODE_INTERVENTION_LIST_FILE: {final int userId = (int) msg.obj;if (userId < 0) {Slog.wtf(TAG, "Attempt to write setting for invalid user: " + userId);synchronized (mLock) {removeEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, msg.obj);}break;}// 调高 handler 线程优先级Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);removeEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, msg.obj);writeGameModeInterventionsToFile(userId);// 降低 handler 线程优先级Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);break;}}}}
4.4.5.2 保存 "干涉信息"

除了关机外,只要发生了 "干涉" (即伴随着 GMS$updateInterventions 方法的调用),都会执行保存干涉信息的动作。

发生 "干涉" 的场景又可以细分为以下几种情况。

4.4.5.2.1 设置 game mode 时(0延迟)

设置 game mode 时,重新应用 game mode 策略。

    public void setGameMode(String packageName, @GameMode int gameMode, int userId)throws SecurityException {
...updateInterventions(packageName, gameMode, userId);
...sendUserMessage(userId, WRITE_SETTINGS, EVENT_SET_GAME_MODE, WRITE_DELAY_MILLIS);// 记录干涉信息,0 延迟sendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE,EVENT_SET_GAME_MODE, 0 /*delayMillis*/);
...
4.4.5.2.2 更新 custom mode 的 config 时(延迟10s)

更新 custom mode 的 config 时,如果当前的 game mode 就是 custom mode,要重新应用 game mode 策略。

    /*** Updates the config for the game's {@link GameManager#GAME_MODE_CUSTOM} mode.** @throws SecurityException        if caller doesn't have*                                  {@link android.Manifest.permission#MANAGE_GAME_MODE}*                                  permission.* @throws IllegalArgumentException if the user ID provided doesn't exist.*/@Override@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)public void updateCustomGameModeConfiguration(String packageName,GameModeConfiguration gameModeConfig, int userId)throws SecurityException, IllegalArgumentException {
...sendUserMessage(userId, WRITE_SETTINGS, EVENT_UPDATE_CUSTOM_GAME_MODE_CONFIG,WRITE_DELAY_MILLIS);// 记录干涉信息,延迟 10ssendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE,EVENT_UPDATE_CUSTOM_GAME_MODE_CONFIG, WRITE_DELAY_MILLIS /*delayMillis*/);final int gameMode = getGameMode(packageName, userId);if (gameMode == GameManager.GAME_MODE_CUSTOM) {updateInterventions(packageName, gameMode, userId);}Slog.i(TAG, "Updated custom game mode config for package: " + packageName+ " with FPS=" + internalConfig.getFps() + ";Scaling="+ internalConfig.getScaling() + " under user " + userId);
...
4.4.5.2.3 用户启动、用户切换时(0延迟)

用户启动、用户切换时,调用 GMS$updateConfigsForUser 方法更新所有游戏应用的 GamePackageConfiguration。

参见 GMS$updateConfigsForUser

// 用户启动void onUserStarting(@NonNull TargetUser user, File settingDataDir) {final int userId = user.getUserIdentifier();synchronized (mLock) {if (!mSettings.containsKey(userId)) {GameManagerSettings userSettings = new GameManagerSettings(settingDataDir);mSettings.put(userId, userSettings);userSettings.readPersistentDataLocked();}}// (1)sendUserMessage(userId, POPULATE_GAME_MODE_SETTINGS, EVENT_ON_USER_STARTING,0 /*delayMillis*/);if (mGameServiceController != null) {mGameServiceController.notifyUserStarted(user);}}// 用户切换void onUserSwitching(TargetUser from, TargetUser to) {final int toUserId = to.getUserIdentifier();// we want to re-populate the setting when switching user as the device config may have// changed, which will only update for the previous user, see// DeviceConfigListener#onPropertiesChanged.// (1)sendUserMessage(toUserId, POPULATE_GAME_MODE_SETTINGS, EVENT_ON_USER_SWITCHING,0 /*delayMillis*/);if (mGameServiceController != null) {mGameServiceController.notifyNewForegroundUser(to);}}
    class SettingsHandler extends Handler {SettingsHandler(Looper looper) {super(looper);}@Overridepublic void handleMessage(Message msg) {doHandleMessage(msg);}void doHandleMessage(Message msg) {switch (msg.what) {...case POPULATE_GAME_MODE_SETTINGS: {removeEqualMessages(POPULATE_GAME_MODE_SETTINGS, msg.obj);final int userId = (int) msg.obj;final String[] packageNames = getInstalledGamePackageNames(userId);// (2)updateConfigsForUser(userId, false /*checkGamePackage*/, packageNames);break;}...
4.4.5.2.4 device config "game_overlay" 改变时(0延迟)

device config "game_overlay" 改变时,调用 GMS$updateConfigsForUser 方法更新目标(game mode config 发生变化的)游戏应用的 GamePackageConfiguration。

参见 GMS$updateConfigsForUser

    private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {DeviceConfigListener() {super();DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_GAME_OVERLAY,mContext.getMainExecutor(), this);}@Overridepublic void onPropertiesChanged(Properties properties) {final String[] packageNames = properties.getKeyset().toArray(new String[0]);// (1)updateConfigsForUser(ActivityManager.getCurrentUser(), true /*checkGamePackage*/,packageNames);}@Overridepublic void finalize() {DeviceConfig.removeOnPropertiesChangedListener(this);}}
4.4.5.2.5 游戏应用安装(0延迟)、卸载(延迟10s)时

游戏应用安装时,调用 GMS$updateConfigsForUser 方法更新目标应用的 GamePackageConfiguration。

参见 GMS$updateConfigsForUser

游戏应用卸载时,删除目标应用的 GamePackageConfiguration,从全局设置中删除目标应用的设置。并且保存当前设置、记录干涉行为。

    private void registerPackageReceiver() {final IntentFilter packageFilter = new IntentFilter();packageFilter.addAction(ACTION_PACKAGE_ADDED);packageFilter.addAction(ACTION_PACKAGE_REMOVED);packageFilter.addDataScheme("package");final BroadcastReceiver packageReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(@NonNull final Context context, @NonNull final Intent intent) {final Uri data = intent.getData();try {final int userId = getSendingUserId();if (userId != ActivityManager.getCurrentUser()) {return;}final String packageName = data.getSchemeSpecificPart();try {final ApplicationInfo applicationInfo = mPackageManager.getApplicationInfoAsUser(packageName, PackageManager.MATCH_ALL, userId);// 过滤非游戏应用if (applicationInfo.category != ApplicationInfo.CATEGORY_GAME) {return;}} catch (NameNotFoundException e) {// Ignore the exception.}switch (intent.getAction()) {case ACTION_PACKAGE_ADDED:// (1)updateConfigsForUser(userId, true /*checkGamePackage*/, packageName);break;case ACTION_PACKAGE_REMOVED:if (!intent.getBooleanExtra(EXTRA_REPLACING, false)) {// 删除目标应用的 GamePackageConfigurationsynchronized (mDeviceConfigLock) {mConfigs.remove(packageName);}synchronized (mLock) {// 从全局设置中删除目标应用的设置if (mSettings.containsKey(userId)) {mSettings.get(userId).removeGame(packageName);}// 保存当前设置、记录干涉行为,10s 延迟sendUserMessage(userId, WRITE_SETTINGS,Intent.ACTION_PACKAGE_REMOVED, WRITE_DELAY_MILLIS);sendUserMessage(userId,WRITE_GAME_MODE_INTERVENTION_LIST_FILE,Intent.ACTION_PACKAGE_REMOVED, WRITE_DELAY_MILLIS);}}break;default:// do nothingbreak;}...
GMS$updateConfigsForUser 方法

用户启动/切换时、device config "game_overlay" 的值变化时、游戏应用安装时,都会调用 GMS$updateConfigsForUser 方法。

GMS$updateConfigsForUser 方法的作用是:

  1. 更新目标应用的 GamePackageConfiguration

  2. 如果目标应用当前的 game mode 不再支持,则设置 game mode 为 standard

  3. 如果目标应用当前的 game mode 依然支持,则执行干涉

    /*** @hide*/@VisibleForTestingvoid updateConfigsForUser(@UserIdInt int userId, boolean checkGamePackage,String... packageNames) {// 过滤掉不是 "游戏" 的应用包名// 例如,之前是 "游戏" 的应用,升级后不再是 "游戏"~~ 这里就要过滤掉if (checkGamePackage) {packageNames = Arrays.stream(packageNames).filter(p -> isPackageGame(p, userId)).toArray(String[]::new);}try {synchronized (mDeviceConfigLock) {for (final String packageName : packageNames) {// 重新构造(初始化)游戏应用的 GamePackageConfiguration 对象final GamePackageConfiguration config =new GamePackageConfiguration(mPackageManager, packageName, userId);// 如果新的 GamePackageConfiguration 对象无效(包含的 GameModeConfiguration 数量为 0),则将该游戏应用从 mConfigs 中删除。// 否则,将新的 GamePackageConfiguration 对象更新(或添加)为 mConfigs 中游戏应用的 value。 if (config.isActive()) {if (DEBUG) {Slog.i(TAG, "Adding config: " + config.toString());}mConfigs.put(packageName, config);} else {if (DEBUG) {Slog.w(TAG, "Inactive package config for "+ config.getPackageName() + ":" + config.toString());}mConfigs.remove(packageName);}}}synchronized (mLock) {if (!mSettings.containsKey(userId)) {return;}}for (final String packageName : packageNames) {// 游戏应用当前的 game modeint gameMode = getGameMode(packageName, userId);// Make sure the user settings and package configs don't conflict.// I.e. the user setting is set to a mode that no longer available due to// config/manifest changes.// Most of the time we won't have to change anything.GamePackageConfiguration config = null;synchronized (mDeviceConfigLock) {config = mConfigs.get(packageName);}// getNewGameMode 的返回值是入参 gameMode 或者 GameManager.GAME_MODE_STANDARD 即 standard mode。// 当游戏应用的 GamePackageConfiguration 不包含目标 game mode(即入参 gameMode) 的 config 时,返回 GameManager.GAME_MODE_STANDARD。final int newGameMode = getNewGameMode(gameMode, config);// 如果游戏应用不再支持当前的 game mode,会将游戏应用的 game mode 设置为 standard。// 否则只是执行干涉 (重新应用 gmae mode 策略)。if (newGameMode != gameMode) {setGameMode(packageName, newGameMode, userId);} else {// Make sure we handle the case when the interventions are changed while// the game mode remains the same. We call only updateInterventions() here.updateInterventions(packageName, gameMode, userId);}}// 记录干涉行为,0 延迟sendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE,"UPDATE_CONFIGS_FOR_USERS", 0 /*delayMillis*/);} catch (Exception e) {Slog.e(TAG, "Failed to update configs for user " + userId + ": " + e);}}
4.4.5.3 保存 "当前的设置"

除了关机外,还有几种保存 "当前设置" 的场景。

  1. 设置 game mode 时(延迟10s)

  2. 更新 custom mode 的 config 时(延迟10s)

  3. 游戏应用卸载时(延迟10s)

这几个场景都与 "保存干涉信息" 的场景重叠,这里不再展开。

4.5 策略

game mode 有 4 种策略

  • scaling:修改分辨率

  • fps:修改刷新率

  • angle:使用/不使用 angle

  • loading boost:调用 powerhal 设置 power mode 为 "Mode.GAME_LOADING",并设置持续时长

4.5.1 scaling

scaling 这里指应用的分辨率缩放。

应用的分辨率缩放和 "图像缩放" 是不一样的。

4.5.1.1 图像缩放

我们常说的图像缩放功能一般也是称为 scaling,是图像处理中的一种常用功能,目的是将输入图像从一种分辨率转换到另一种分辨率输出。在实时系统中,还要满足视频实时输入实时输出的要求。我们将输入图像称为原图,输出图像称为目标图。

图像缩放存在方向性:分为水平方向缩放和垂直方向缩放。

  • 在某个方向上,目标图的分辨率比原图的分辨率大,在这个方向上为图像放大。

  • 在某个方向上,目标图的分辨率比原图的分辨率小,在这个方向上为图像缩小。

目标图的任意一个像素点都可以映射到原图某个特定的像素点。缩放的过程就是利用原图相邻像素点就可计算出这个特定的像素点。举一个一维简单的例子:从4个像素放大到7个像素,位置映射关系如下。

"缩放倍数" 和 "缩放因子" 的概念区别:

  • 缩放倍数sr(scaling ratio) = 目标图/原图 = 7/4 = 1.75(倍)

  • 缩放因子sf(scaling factor) = 1/sr = 原图/目标图 = 4/7 = 0.5714

二维图像缩放就是在水平和垂直两个方向分别做映射。假设(x,y)为目标图像的像素坐标,(x’,y’)为原图像的像素坐标,ver_sf, hor_sf分别为垂直和水平方向缩放因子,那么由目标图像像素点在原图像中映射的位置计算公式如下。

x’ = x * hor_sf

y’ = y * ver_sf

如果计算出的原图像的像素坐标(x’,y’)不是整数位置,就需要通过相邻的像素点用插值方法来计算。插值定义:通过已知的离散数据求未知数据的过程或方法。图像缩放从数学上来说就是插值。因此实现图像缩放的关键步骤:一是计算目标图像像素在原图像中映射位置;二是插值算法和系数。常见的插值算法有最近邻插值,双线性插值,双立方插值。对于插值算法的介绍,网上有很多,这里就不做介绍。

图像缩放的结果

  • 图像放大时,固定窗口内能显示的图像内容变少

  • 图像缩小时,固定窗口内能显示的图像内容变多

4.5.1.2 分辨率缩放

分辨率缩放,是指在应用窗口大小不变的情况下,窗口的分辨率缩放。

分辨率缩放的结果

  • 分辨率放大时,固定窗口内能显示的图像内容变多

  • 分辨率缩小时,固定窗口内能显示的图像内容变少

分辨率缩放时,如果图像不需要适应窗口,图像缩放因子其实是 1,即图像大小是不变的(以像素为单位)。

4.5.1.3 应用分辨率缩放

应用分辨率缩放,指在屏幕、系统分辨率实际不变的情况下,让应用窗口可以显示更多内容!

如下图所示,屏幕、系统实际分辨率不变,要想使应用分辨率放大一倍,显示元素的大小需要缩小一倍!

暂时无法在飞书文档外展示此内容

4.5.1.4 分辨率缩放因子

在 android 系统中,分辨率缩放因子保存在应用的 CompatibilityInfo(兼容性信息)对象中(CompatibilityInfo$applicationScale)。

在构造 CompatibilityInfo 对象(即 CompatModePackages$compatibilityInfoForPackageLocked 方法)时,

会根据应用的 game mode 策略以及应用的兼容性配置,设置 CompatibilityInfo$applicationScale 变量的值。

参见兼容模式更新应用兼容性信息的场景

考虑 game mode 的 scaling 策略,如果目标游戏应用配置的 scaling 值是 2,那么它对应的 CompatibilityInfo$applicationScale 值就是 1/2(0.5)。

#frameworks/base/services/core/java/com/android/server/wm/CompatModePackages.java// 构造并返回 CompatibilityInfo 对象public CompatibilityInfo compatibilityInfoForPackageLocked(ApplicationInfo ai) {final boolean forceCompat = getPackageCompatModeEnabledLocked(ai);// 获取目标应用的 scaling 值final float compatScale = getCompatScale(ai.packageName, ai.uid);final Configuration config = mService.getGlobalConfiguration();return new CompatibilityInfo(ai, config.screenLayout, config.smallestScreenWidthDp,forceCompat, compatScale);}float getCompatScale(String packageName, int uid) {final UserHandle userHandle = UserHandle.getUserHandleForUid(uid);// 如果目标应用 game mode 的 scaling 策略值有效(>0),// 则按照 game mode 的 scaling 策略设置 scaling 值。//// 注意,game mode 的 scaling 策略值表示的是分辨率放大、缩小的倍率,// 而 getCompatScale 方法返回的 scaling 值实际上是分辨率缩放因子,是倍率的倒数。// 例如,game mode scaling 策略值为 2 时,getCompatScale 返回 0.5,表示分辨率要放大 2 倍。if (mGameManager == null) {mGameManager = LocalServices.getService(GameManagerInternal.class);}if (mGameManager != null) {final int userId = userHandle.getIdentifier();final float scalingFactor = mGameManager.getResolutionScalingFactor(packageName,userId);if (scalingFactor > 0) {return 1f / scalingFactor;}}// 否则,如果兼容性行为 DOWNSCALED 或 DOWNSCALED_INVERSE 使能。// 则按照 DOWNSCALED 或 DOWNSCALED_INVERSE 的配置设置 scaling 值。// DOWNSCALED_INVERSE 使能时,返回的 scaling 值 < 1(分辨率放大);// 否则,DOWNSCALED 使能时,返回的 scaling 值 > 1(分辨率缩小)。final boolean isDownscaledEnabled = CompatChanges.isChangeEnabled(DOWNSCALED, packageName, userHandle);final boolean isDownscaledInverseEnabled = CompatChanges.isChangeEnabled(DOWNSCALED_INVERSE, packageName, userHandle);if (isDownscaledEnabled || isDownscaledInverseEnabled) {final float scalingFactor = getScalingFactor(packageName, userHandle);if (scalingFactor != 1f) {// For Upscaling the returned factor must be scalingFactor// For Downscaling the returned factor must be 1f / scalingFactorreturn isDownscaledInverseEnabled ? scalingFactor : 1f / scalingFactor;}}if (mService.mHasLeanbackFeature) {final Configuration config = mService.getGlobalConfiguration();final float density = config.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT;final int smallestScreenWidthPx = (int) (config.smallestScreenWidthDp * density + .5f);if (smallestScreenWidthPx > 1080 && !CompatChanges.isChangeEnabled(DO_NOT_DOWNSCALE_TO_1080P_ON_TV, packageName, userHandle)) {return smallestScreenWidthPx / 1080f;}}return 1f;}// 根据应用兼容性行为 DOWNSCALE_XXX 的配置,返回 scaling factor(0~1)。
// 兼容性行为的优先级:DOWNSCALE_90 > DOWNSCALE_85 > ...
// 例如,当 DOWNSCALE_90 使能时返回 0.9;
//      当 DOWNSCALE_90 失能,DOWNSCALE_85 使能时返回 0.85。private static float getScalingFactor(String packageName, UserHandle userHandle) {if (CompatChanges.isChangeEnabled(DOWNSCALE_90, packageName, userHandle)) {return 0.9f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_85, packageName, userHandle)) {return 0.85f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_80, packageName, userHandle)) {return 0.8f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_75, packageName, userHandle)) {return 0.75f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_70, packageName, userHandle)) {return 0.7f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_65, packageName, userHandle)) {return 0.65f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_60, packageName, userHandle)) {return 0.6f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_55, packageName, userHandle)) {return 0.55f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_50, packageName, userHandle)) {return 0.5f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_45, packageName, userHandle)) {return 0.45f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_40, packageName, userHandle)) {return 0.4f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_35, packageName, userHandle)) {return 0.35f;}if (CompatChanges.isChangeEnabled(DOWNSCALE_30, packageName, userHandle)) {return 0.3f;}return 1f;}#frameworks/base/services/core/java/com/android/server/app/GameManagerService.java// 返回 game mode 的 scaling 策略值private final class LocalService extends GameManagerInternal {@Overridepublic float getResolutionScalingFactor(String packageName, int userId) {// 游戏(应用)当前的 game modefinal int gameMode = getGameModeFromSettingsUnchecked(packageName, userId);// (1)return getResolutionScalingFactorInternal(packageName, gameMode, userId);}}/*** Gets the resolution scaling factor for the package's target game mode.** @return scaling factor for the game mode if exists or negative value otherwise.* @throws SecurityException        if caller doesn't have*                                  {@link android.Manifest.permission#MANAGE_GAME_MODE}*                                  permission.* @throws IllegalArgumentException if the user ID provided doesn't exist.*/@Override@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)public float getResolutionScalingFactor(String packageName, int gameMode, int userId)throws SecurityException, IllegalArgumentException {checkPermission(Manifest.permission.MANAGE_GAME_MODE);synchronized (mLock) {if (!mSettings.containsKey(userId)) {throw new IllegalArgumentException("User " + userId + " wasn't started");}}// (2)return getResolutionScalingFactorInternal(packageName, gameMode, userId);}float getResolutionScalingFactorInternal(String packageName, int gameMode, int userId) {final GamePackageConfiguration packageConfig = getConfig(packageName, userId);if (packageConfig == null) {// 应用不是游戏时,返回 -1return GamePackageConfiguration.GameModeConfiguration.DEFAULT_SCALING;}final GamePackageConfiguration.GameModeConfiguration modeConfig =packageConfig.getGameModeConfiguration(gameMode);if (modeConfig != null) {// (3)return modeConfig.getScaling();}// 应用当前的 game mode 无效时,返回 -1return GamePackageConfiguration.GameModeConfiguration.DEFAULT_SCALING;}

4.5.1.5 分辨率缩放因子的作用效果

分辨率缩放因子的作用效果是修正应用UI元素的大小(单位像素),以匹配期望的应用分辨率缩放效果。

先以 NumberPicker 控件为例,说明分辨率缩放因子的作用。

NumberPicker 是 "数值选择器" 组件。

AccessibilityNodeProviderImpl 是 NumberPicker 组件的无障碍功能 (Accessibility) 实现的一部分,它通过一个自定义的 AccessibilityNodeProvider 来为虚拟视图(如按钮或输入框)提供无障碍支持。AccessibilityNodeProviderImpl 类是 AccessibilityNodeProvider 的一个实现,用于描述 NumberPicker 中虚拟视图的无障碍节点。

无障碍节点(AccessibilityNodeInfo)是帮助屏幕阅读器和其他辅助技术获取应用界面中的控件信息的。NumberPicker 组件通过该类为虚拟视图提供描述信息,帮助辅助工具正确识别和操作这些控件。

(屏幕阅读器 Screen Reader 是一种辅助技术软件,专门为视力障碍者设计,用于将计算机或移动设备屏幕上的文本和控件内容通过语音合成 Text-to-Speech, TTS 或盲文输出设备读出,帮助用户理解屏幕上的内容并与之交互。)

NumberPicker$AccessibilityNodeProviderImpl 的 createAccessibilityNodeInfoForNumberPicker 方法:

  1. 通过上下文获取分辨率缩放因子

  2. 根据分辨率缩放因子,对控制的 Rect 进行缩放

NumberPicker$AccessibilityNodeProviderImpl 的 createAccessibilityNodeInfoForNumberPicker 方法中调用的 getContext 方法是外部类 NumberPicker 的 getContext 方法。

NumberPicker 是 View 的派生类(NumberPicker <- LinearLayout <- ViewGroup <- View),

因此,NumberPicker 的 getContext 方法返回的实际上是 View$mContext。

View 对象创建时,会传入 Context 对象设置 View$mContext。传入的 Context 对象一般是 ContextWrapper 的派生类对象(Activity等~~)。

#frameworks/base/core/java/android/widget/NumberPicker.java/*** Class for managing virtual view tree rooted at this picker.*/class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider {...private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top,int right, int bottom) {AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();...// 通过 WindowContext(ContextWrapper) //      -> ContextImpl//      -> Resources //      -> ResourcesImpl//      -> CompatibilityInfo // 获取分辨率缩放因子final float applicationScale =getContext().getResources().getCompatibilityInfo().applicationScale;// 调用 Rect.scale 方法对 Rect 进行缩放~Rect boundsInParent = mTempRect;boundsInParent.set(left, top, right, bottom);boundsInParent.scale(applicationScale);info.setBoundsInParent(boundsInParent);info.setVisibleToUser(isVisibleToUser());Rect boundsInScreen = boundsInParent;int[] locationOnScreen = mTempArray;getLocationOnScreen(locationOnScreen);boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);boundsInScreen.scale(applicationScale);info.setBoundsInScreen(boundsInScreen);...

NumberPicker 控件中显示当前数值的 UI 元素是一个 EditText。

EditText 是 TextView 的派生类。

#frameworks/base/core/java/android/widget/NumberPicker.java/*** The text for showing the current value.*/@UnsupportedAppUsageprivate final EditText mInputText;public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {...mInputText = findViewById(R.id.numberpicker_input);...

下面是 TextView 代码。

将分辨率缩放因子设置到 TextPaint。

#frameworks/base/core/java/android/widget/TextView.java@SuppressWarnings("deprecation")public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);// TextView is important by default, unless app developer overrode attribute.if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);}if (getImportantForContentCapture() == IMPORTANT_FOR_CONTENT_CAPTURE_AUTO) {setImportantForContentCapture(IMPORTANT_FOR_CONTENT_CAPTURE_YES);}setTextInternal("");final Resources res = getResources();final CompatibilityInfo compat = res.getCompatibilityInfo();mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);mTextPaint.density = res.getDisplayMetrics().density;// 将分辨率缩放因子设置到 TextPaintmTextPaint.setCompatibilityScaling(compat.applicationScale);mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mHighlightPaint.setCompatibilityScaling(compat.applicationScale);...

TextPaint 是 Paint 的派生类。

将分辨率缩放因子设置到 Paint$mCompatScaling !

#frameworks/base/graphics/java/android/graphics/Paint.java/** @hide */@UnsupportedAppUsagepublic void setCompatibilityScaling(float factor) {if (factor == 1.0) {mHasCompatScaling = false;mCompatScaling = mInvCompatScaling = 1.0f;} else {mHasCompatScaling = true;mCompatScaling = factor;mInvCompatScaling = 1.0f/factor;}}

在计算文本(Text)的显示长度时,会乘以分辨率缩放因子即 Paint$mCompatScaling 的值!

#frameworks/base/graphics/java/android/graphics/Paint.java/*** Return the width of the text.** @param text  The text to measure. Cannot be null.* @param index The index of the first character to start measuring* @param count THe number of characters to measure, beginning with start* @return      The width of the text*/public float measureText(char[] text, int index, int count) {if (text == null) {throw new IllegalArgumentException("text cannot be null");}if ((index | count) < 0 || index + count > text.length) {throw new ArrayIndexOutOfBoundsException();}if (text.length == 0 || count == 0) {return 0f;}if (!mHasCompatScaling) {return (float) Math.ceil(nGetTextAdvances(mNativePaint, text,index, count, index, count, mBidiFlags, null, 0));}final float oldSize = getTextSize();setTextSize(oldSize * mCompatScaling);final float w = nGetTextAdvances(mNativePaint, text, index, count, index, count,mBidiFlags, null, 0);setTextSize(oldSize);return (float) Math.ceil(w*mInvCompatScaling);}/*** Return the width of the text.** @param text  The text to measure. Cannot be null.* @param start The index of the first character to start measuring* @param end   1 beyond the index of the last character to measure* @return      The width of the text*/public float measureText(String text, int start, int end) {if (text == null) {throw new IllegalArgumentException("text cannot be null");}if ((start | end | (end - start) | (text.length() - end)) < 0) {throw new IndexOutOfBoundsException();}if (text.length() == 0 || start == end) {return 0f;}if (!mHasCompatScaling) {return (float) Math.ceil(nGetTextAdvances(mNativePaint, text,start, end, start, end, mBidiFlags, null, 0));}final float oldSize = getTextSize();setTextSize(oldSize * mCompatScaling);final float w = nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags,null, 0);setTextSize(oldSize);return (float) Math.ceil(w * mInvCompatScaling);}

4.5.2 fps

应用 fps 策略的场景:设置 game mode、修改 game config 时,调用 GMS$updateInterventions 方法。

GMS$updateInterventions 方法中

  1. 调用 resetFps 方法应用当前 game mode 的 fps 策略

  2. 调用 updateUseAngle 方法应用当前 game mode 的 angle 策略

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.javaprivate void updateInterventions(String packageName,@GameMode int gameMode, @UserIdInt int userId) {final GamePackageConfiguration packageConfig = getConfig(packageName, userId);if (gameMode == GameManager.GAME_MODE_STANDARD|| gameMode == GameManager.GAME_MODE_UNSUPPORTED || packageConfig == null|| packageConfig.willGamePerformOptimizations(gameMode)|| packageConfig.getGameModeConfiguration(gameMode) == null) {resetFps(packageName, userId);// resolution scaling does not need to be reset as it's now read dynamically on game// restart, see #getResolutionScalingFactor and CompatModePackages#getCompatScale.// TODO: reset Angle intervention here once implementedif (packageConfig == null) {Slog.v(TAG, "Package configuration not found for " + packageName);return;}} else {updateFps(packageConfig, packageName, gameMode, userId);}updateUseAngle(packageName, gameMode);

GMS$updateFps 方法。

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.javaprivate void updateFps(GamePackageConfiguration packageConfig, String packageName,@GameMode int gameMode, @UserIdInt int userId) {final GamePackageConfiguration.GameModeConfiguration modeConfig =packageConfig.getGameModeConfiguration(gameMode);if (modeConfig == null) {Slog.d(TAG, "Game mode " + gameMode + " not found for " + packageName);return;}try {final float fps = modeConfig.getFps();final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);setOverrideFrameRate(uid, fps);} catch (PackageManager.NameNotFoundException e) {return;}}

GMS$setOverrideFrameRate 方法。

调用 jni native 方法 nativeSetOverrideFrameRate。

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.java@VisibleForTestingvoid setOverrideFrameRate(int uid, float frameRate) {nativeSetOverrideFrameRate(uid, frameRate);}

nativeSetOverrideFrameRate 方法。

#frameworks/base/services/core/jni/com_android_server_app_GameManagerService.cppstatic void android_server_app_GameManagerService_nativeSetOverrideFrameRate(JNIEnv* env,jclass clazz, jint uid,jfloat frameRate) {// (1)SurfaceComposerClient::setOverrideFrameRate(uid, frameRate);
}static const JNINativeMethod gMethods[] = {{"nativeSetOverrideFrameRate", "(IF)V",(void*)android_server_app_GameManagerService_nativeSetOverrideFrameRate},
};int register_android_server_app_GameManagerService(JNIEnv* env) {return jniRegisterNativeMethods(env, "com/android/server/app/GameManagerService", gMethods,NELEM(gMethods));
}

SurfaceComposerClient::setOverrideFrameRate 调用 binder 服务端即 SurfaceFlinger 的 SurfaceComposerAIDL::setOverrideFrameRate 方法。

SurfaceComposerClient(android::SurfaceComposerClient)是对 android::gui::ISurfaceComposerClient(android::gui::BpSurfaceComposerClient)的封装。

SurfaceFlinger 的 Client 类是 android::gui::BnSurfaceComposerClient 的派生类,作为 binder 服务端。

#frameworks/native/services/surfaceflinger/SurfaceFlinger.cppbinder::Status SurfaceComposerAIDL::setOverrideFrameRate(int32_t uid, float frameRate) {status_t status;const int c_uid = IPCThreadState::self()->getCallingUid();if (c_uid == AID_ROOT || c_uid == AID_SYSTEM) {// (1)status = mFlinger->setOverrideFrameRate(uid, frameRate);} else {ALOGE("setOverrideFrameRate() permission denied for uid: %d", c_uid);status = PERMISSION_DENIED;}return binderStatusFromStatusT(status);
}status_t SurfaceFlinger::setOverrideFrameRate(uid_t uid, float frameRate) {PhysicalDisplayId displayId = [&]() {Mutex::Autolock lock(mStateLock);return getDefaultDisplayDeviceLocked()->getPhysicalId();}();// (2)mScheduler->setGameModeRefreshRateForUid(FrameRateOverride{static_cast<uid_t>(uid), frameRate});// (3)mScheduler->onFrameRateOverridesChanged(mAppConnectionHandle, displayId);return NO_ERROR;
}

Scheduler::setGameModeRefreshRateForUid 方法。

设置 override frame rate。

#frameworks/native/services/surfaceflinger/Scheduler/Scheduler.cppvoid Scheduler::setGameModeRefreshRateForUid(FrameRateOverride frameRateOverride) {if (frameRateOverride.frameRateHz > 0.f && frameRateOverride.frameRateHz < 1.f) {return;}mFrameRateOverrideMappings.setGameModeRefreshRateForUid(frameRateOverride);
}

Scheduler::onFrameRateOverridesChanged 方法。

回调客户端(system_server) override frame rate。

这部分逻辑比较复杂,后面会用一个章节专门分析。

#frameworks/native/services/surfaceflinger/Scheduler/Scheduler.cppvoid Scheduler::onFrameRateOverridesChanged(ConnectionHandle handle, PhysicalDisplayId displayId) {// 判断是否支持 override frame rateconst bool supportsFrameRateOverrideByContent =pacesetterSelectorPtr()->supportsAppFrameRateOverrideByContent();// override frame ratesstd::vector<FrameRateOverride> overrides =mFrameRateOverrideMappings.getAllFrameRateOverrides(supportsFrameRateOverrideByContent);android::EventThread* thread;{std::lock_guard lock(mConnectionsLock);RETURN_IF_INVALID_HANDLE(handle);thread = mConnections[handle].thread.get();}// 回调客户端(system_server)thread->onFrameRateOverridesChanged(displayId, std::move(overrides));
}

4.5.3 angle

应用 angle 策略的场景:设置 game mode、修改 game config 时,调用 updateInterventions 方法。

updateInterventions 方法中调用 updateUseAngle,同上。

updateUseAngle 方法目前的实现为空。

//frameworks/base/services/core/java/com/android/server/app/GameManagerService.java@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)private void updateUseAngle(String packageName, @GameMode int gameMode) {// TODO (b/188475576): Nothing to do yet. Remove if it's still empty when we're ready to// ship.}

4.5.4 boost

应用 boost 策略有两种场景。

一是在游戏应用启动时,应用框架代码会调用 GMS$notifyGraphicsEnvironmentSetup 方法应用 boost 策略;

二是游戏应用可以主动调用 GMS$setGameState 方法应用 boost 策略(只有当游戏应用处于 performance mode 时才有效)。

4.5.4.1 GMS$notifyGraphicsEnvironmentSetup

应用启动(bind application)时,调用 GraphicsEnvironment$setup 方法设置图形环境。

#frameworks/base/core/java/android/app/ActivityThread.java@UnsupportedAppUsageprivate void handleBindApplication(AppBindData data) {...if (!Process.isIsolated()) {final int oldMask = StrictMode.allowThreadDiskWritesMask();try {setupGraphicsSupport(appContext);} finally {StrictMode.setThreadPolicyMask(oldMask);}} else {HardwareRenderer.setIsolatedProcess(true);}...private void setupGraphicsSupport(Context context) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "setupGraphicsSupport");...// mCoreSettings is only updated from the main thread, while this function is only called// from main thread as well, so no need to lock here.GraphicsEnvironment.getInstance().setup(context, mCoreSettings);Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);}

GraphicsEnvironment$setup 方法的最后,调用 GameManager$notifyGraphicsEnvironmentSetup 方法通知 GMS 图形环境设置完成。

#frameworks/base/core/java/android/os/GraphicsEnvironment.java/*** Set up GraphicsEnvironment*/public void setup(Context context, Bundle coreSettings) {...Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "notifyGraphicsEnvironmentSetup");if (mGameManager != null&& appInfoWithMetaData.category == ApplicationInfo.CATEGORY_GAME) {mGameManager.notifyGraphicsEnvironmentSetup();}Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);}

GMS.notifyGraphicsEnvironmentSetup 方法执行以下动作:

  1. 执行 mPowerManagerInternal.setPowerMode(Mode.GAME_LOADING, true); 即调用 powerhal 接口设置 power mode 为 "Mode.GAME_LOADING";

  2. 发送延时消息 CANCEL_GAME_LOADING_MODE,loadingBoostDuration 秒后取消 "Mode.GAME_LOADING"。

注意,loading boost 调用的是 powerhal 服务的 setMode 接口,而不是 setBoost 接口;原生 powerhal 服务只是一个 demo,底层需要 vendor 厂商实现。

#frameworks/base/services/core/java/com/android/server/app/GameManagerService.java/*** If loading boost is enabled, invoke it.*/@Override@RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)@GameMode public void notifyGraphicsEnvironmentSetup(String packageName, int userId)throws SecurityException {userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),Binder.getCallingUid(), userId, false, true, "notifyGraphicsEnvironmentSetup","com.android.server.app.GameManagerService");if (!isValidPackageName(packageName, userId)) {Slog.d(TAG, "No-op for attempt to notify graphics env setup for different package"+ "than caller with uid: " + Binder.getCallingUid());return;}// 目标应用当前的 game modefinal int gameMode = getGameMode(packageName, userId);if (gameMode == GameManager.GAME_MODE_UNSUPPORTED) {Slog.d(TAG, "No-op for attempt to notify graphics env setup for non-game app: "+ packageName);return;}// 从目标应用的 game mode config 中获取 boost durationint loadingBoostDuration = getLoadingBoostDuration(packageName, userId);if (loadingBoostDuration != -1) {if (loadingBoostDuration == 0 || loadingBoostDuration > LOADING_BOOST_MAX_DURATION) {loadingBoostDuration = LOADING_BOOST_MAX_DURATION;}if (mHandler.hasMessages(CANCEL_GAME_LOADING_MODE)) {// The loading mode has already been set and is waiting to be unset. It is not// required to set the mode again and we should replace the queued cancel// instruction.mHandler.removeMessages(CANCEL_GAME_LOADING_MODE);} else {// 调用 powerhal 接口,设置 power mode 为 GAME_LOADINGmPowerManagerInternal.setPowerMode(Mode.GAME_LOADING, true);}// 设置 cancel msg,延迟时间 boost durationmHandler.sendMessageDelayed(mHandler.obtainMessage(CANCEL_GAME_LOADING_MODE), loadingBoostDuration);}}

handler 收到消息 CANCEL_GAME_LOADING_MODE 时,调用 powerhal 接口取消 "Mode.GAME_LOADING"。

    class SettingsHandler extends Handler {SettingsHandler(Looper looper) {super(looper);}@Overridepublic void handleMessage(Message msg) {doHandleMessage(msg);}void doHandleMessage(Message msg) {switch (msg.what) {...case CANCEL_GAME_LOADING_MODE: {mPowerManagerInternal.setPowerMode(Mode.GAME_LOADING, false);break;}...
4.5.4.2 GMS$setGameState

应用程序可以通过 GameManager 主动调用 GMS$setGameState 方法。

GMS$setGameState 方法设置立即消息 SET_GAME_STATE,将 GameState 对象发送给 handler。

    /*** Called by games to communicate the current state to the platform.** @param packageName The client package name.* @param gameState   An object set to the current state.* @param userId      The user associated with this state.*/public void setGameState(String packageName, @NonNull GameState gameState,@UserIdInt int userId) {if (!isPackageGame(packageName, userId)) {Slog.d(TAG, "No-op for attempt to set game state for non-game app: " + packageName);// Restrict to games only.return;}final Message msg = mHandler.obtainMessage(SET_GAME_STATE);final Bundle data = new Bundle();data.putString(PACKAGE_NAME_MSG_KEY, packageName);data.putInt(USER_ID_MSG_KEY, userId);msg.setData(data);msg.obj = gameState;mHandler.sendMessage(msg);}

handler 收到 SET_GAME_STATE 消息时,解析 GameState。

只有当 GameState 是 loading 状态,且应用当前处于 performance mode 时,才会执行 boost。

                case SET_GAME_STATE: {final GameState gameState = (GameState) msg.obj;// loading 状态final boolean isLoading = gameState.isLoading();final Bundle data = msg.getData();final String packageName = data.getString(PACKAGE_NAME_MSG_KEY);final int userId = data.getInt(USER_ID_MSG_KEY);// Restrict to games only. Requires performance mode to be enabled.// 只有 performance mode 才执行 boostfinal boolean boostEnabled =getGameMode(packageName, userId) == GameManager.GAME_MODE_PERFORMANCE;int uid;try {uid = mPackageManager.getPackageUidAsUser(packageName, userId);} catch (NameNotFoundException e) {Slog.v(TAG, "Failed to get package metadata");uid = -1;}FrameworkStatsLog.write(FrameworkStatsLog.GAME_STATE_CHANGED, packageName, uid,boostEnabled, gameStateModeToStatsdGameState(gameState.getMode()),isLoading, gameState.getLabel(), gameState.getQuality());if (boostEnabled) {if (mPowerManagerInternal == null) {Slog.d(TAG, "Error setting loading mode for package " + packageName+ " and userId " + userId);break;}if (mHandler.hasMessages(CANCEL_GAME_LOADING_MODE)) {mHandler.removeMessages(CANCEL_GAME_LOADING_MODE);}// 执行 boostmPowerManagerInternal.setPowerMode(Mode.GAME_LOADING, isLoading);if (isLoading) {int loadingBoostDuration = getLoadingBoostDuration(packageName, userId);loadingBoostDuration = loadingBoostDuration > 0 ? loadingBoostDuration: LOADING_BOOST_MAX_DURATION;mHandler.sendMessageDelayed(mHandler.obtainMessage(CANCEL_GAME_LOADING_MODE),loadingBoostDuration);}}break;}

5. scaling 策略

5.1 背景知识

5.1.1 兼容模式

5.1.1.1 兼容模式 & 兼容性信息

在 Android 中,Compat mode(兼容模式)指的是一种使较旧或不兼容的应用程序能够在较新的设备或 Android 版本上正确运行的模式。当某个应用未设计为支持当前屏幕尺寸、分辨率或其他系统功能时,Android 可以通过“兼容模式”调整应用的行为,以确保其正常工作,通常是通过缩放应用的界面或改变其运行方式来实现。

以 CompatModePackages$computeCompatModeLocked 方法为例。

CompatModePackages$computeCompatModeLocked 方法根据应用的兼容性信息,计算应用是否要开启兼容模式。

#frameworks/base/services/core/java/com/android/server/wm/CompatModePackages.javapublic int computeCompatModeLocked(ApplicationInfo ai) {final CompatibilityInfo info = compatibilityInfoForPackageLocked(ai);if (info.alwaysSupportsScreen()) {return ActivityManager.COMPAT_MODE_NEVER;}if (info.neverSupportsScreen()) {return ActivityManager.COMPAT_MODE_ALWAYS;}return getPackageCompatModeEnabledLocked(ai) ? ActivityManager.COMPAT_MODE_ENABLED: ActivityManager.COMPAT_MODE_DISABLED;}
  1. compatibilityInfoForPackageLocked(ai):获取应用(ApplicationInfo ai)的兼容性信息。这决定了应用是否支持或不支持当前的屏幕配置。

  2. info.alwaysSupportsScreen():检查应用是否始终支持当前的屏幕配置。如果返回 true,则将兼容模式设置为 COMPAT_MODE_NEVER,表示应用不需要进行兼容性调整。

  3. info.neverSupportsScreen():检查应用是否始终不支持当前的屏幕配置。如果返回 true,则将兼容模式设置为 COMPAT_MODE_ALWAYS,表示应用需要进行兼容性调整。

  4. getPackageCompatModeEnabledLocked(ai):检查应用的兼容模式状态(开关)。根据开关状态,返回 COMPAT_MODE_ENABLED 或 COMPAT_MODE_DISABLED。

兼容模式调整应用在具有不同屏幕尺寸或系统配置的设备上的运行方式,确保较旧或不兼容的应用能够顺利运行。该模式既可以由 Android 系统自动决定,也可能由用户或开发者手动设置。

5.1.1.2 兼容模式状态(开关)

应用是否开启兼容模式,可以在 packages-compat.xml 文件中配置。

应用兼容模式可以有 3 个状态,0、1(COMPAT_FLAG_DONT_ASK)、2(COMPAT_FLAG_ENABLED);也可以是 3(COMPAT_FLAG_DONT_ASK|COMPAT_FLAG_ENABLED)~

2 表示应用兼容模式打开;

0 表示应用兼容模式关闭;

1 比较特殊,表示由用户(系统管理员程序或者 google 应用?)决定是否打开应用兼容模式。

#frameworks/base/services/core/java/com/android/server/wm/CompatModePackages.java// mPackages Map 保存应用的兼容模式状态。// 系统启动时,解析 packages-compat.xml 文件(如果有的话),// <compat-packages>//    <pkg name="xxx" mode="xxx">//    ...// 解析得到应用的兼容模式状态(如果有的话),并保存到 mPackages Map。// 一般来说,兼容模式状态的值应该是 0(兼容模式开关关闭) 或者 2(兼容模式开关开启)// 或者 1(询问系统管理员程序是否开启兼容模式)private final HashMap<String, Integer> mPackages = new HashMap<>();// 生成并返回目标应用的 CompatibilityInfo 对象。// 此时,会先获取应用的兼容模式开关,// 兼容模式开关(开启或关闭)也会包含在 CompatibilityInfo 对象中。public CompatibilityInfo compatibilityInfoForPackageLocked(ApplicationInfo ai) {final boolean forceCompat = getPackageCompatModeEnabledLocked(ai);final float compatScale = getCompatScale(ai.packageName, ai.uid);final Configuration config = mService.getGlobalConfiguration();return new CompatibilityInfo(ai, config.screenLayout, config.smallestScreenWidthDp,forceCompat, compatScale);}// 获取目标应用的兼容模式是否开启,// 根据目标应用的兼容模式状态(可以是 0、1、2 或它们的 "或" 集 3)来判断。// COMPAT_FLAG_ENABLED 的值是 2(1<<1),// 也就是说,只有当目标应用的兼容模式状态为 2或(2|1) 时,兼容模式才开启。private boolean getPackageCompatModeEnabledLocked(ApplicationInfo ai) {return (getPackageFlags(ai.packageName) & COMPAT_FLAG_ENABLED) != 0;}// 获取目标应用的兼容模式状态。// 如果 mPackages Map 不包含目标应用,则返回 0private int getPackageFlags(String packageName) {Integer flags = mPackages.get(packageName);return flags != null ? flags : 0;}

状态 1 即 COMPAT_FLAG_DONT_ASK(1<<0)。

即如果应用兼容模式的状态设置为 1(或者包含1),则不再询问用户选择是否开启应用兼容模式;否则如果应用兼容模式的状态不包含1,则会询问用户选择是否开启应用兼容模式。

    // Compatibility state: no longer ask user to select the mode.public static final int COMPAT_FLAG_DONT_ASK = 1<<0;

AMS 提供了 setPackageAskCompatMode 和 getPackageAskCompatMode 接口。

setPackageAskCompatMode 接口设置状态 1~

应用调用 setPackageAskCompatMode 接口需要申请 android.Manifest.permission.SET_SCREEN_COMPATIBILITY 权限,

该权限的安全等级为 "signature",即应用需要平台签名。

由此推测 setPackageAskCompatMode 接口应该是给系统管理员程序调用的!

getPackageAskCompatMode 接口返回 true/false,返回 true 表示系统询问用户选择是否开启应用兼容模式,返回 false 表示不再询问用户。

应用调用 getPackageAskCompatMode 接口不需要申请权限~

#frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java@Overridepublic boolean getPackageAskScreenCompat(String packageName) {enforceNotIsolatedCaller("getPackageAskScreenCompat");synchronized (mGlobalLock) {return mCompatModePackages.getPackageAskCompatModeLocked(packageName);}}@Overridepublic void setPackageAskScreenCompat(String packageName, boolean ask) {mAmInternal.enforceCallingPermission(android.Manifest.permission.SET_SCREEN_COMPATIBILITY,"setPackageAskScreenCompat");synchronized (mGlobalLock) {mCompatModePackages.setPackageAskCompatModeLocked(packageName, ask);}}#frameworks/base/services/core/java/com/android/server/wm/CompatModePackages.javapublic boolean getPackageAskCompatModeLocked(String packageName) {return (getPackageFlags(packageName)&COMPAT_FLAG_DONT_ASK) == 0;}public void setPackageAskCompatModeLocked(String packageName, boolean ask) {setPackageFlagLocked(packageName, COMPAT_FLAG_DONT_ASK, ask);}

setPackageAskCompatMode 和 getPackageAskCompatMode 接口在 AOSP 中没有被使用,正如推测,它们应该给系统管理员程序使用。

假设 packages-compat.xml 中配置了目标应用的兼容模式状态为 0,默认目标应用的兼容模式关闭。系统管理员程序调用 getPackageAskScreenCompat 将返回 true,表明被询问是否要设置兼容模式,此时系统管理员程序可以调用 setPackageScreenCompatMode 方法设置兼容模式开启(设置状态2)或者设置兼容模式关闭(设置状态0),也可以调用 setPackageAskCompatMode 设置不再询问(设置状态1)。

假设 packages-compat.xml 中配置了目标应用的兼容模式状态为 2,默认目标应用的兼容模式开启。系统管理员程序调用 getPackageAskScreenCompat 将返回 true,表明被询问是否要设置兼容模式,此时系统管理员程序可以调用 setPackageScreenCompatMode 方法设置兼容模式开启(设置状态2)或者设置兼容模式关闭(设置状态0),也可以调用 setPackageAskCompatMode 设置不再询问(设置状态1)。

假设 packages-compat.xml 中配置了目标应用的兼容模式状态为 1,默认目标应用的兼容模式关闭。系统管理员程序调用 getPackageAskScreenCompat 将返回 false,表明不询问是否要设置兼容模式,此时系统管理员程序可以不做处理。

可以看到,应用兼容模式是否开启最终还是由 COMPAT_FLAG_ENABLED 标志决定的。COMPAT_FLAG_DONT_ASK 只是服务于系统管理员程序控制应用兼容模式开、关的逻辑。

5.1.1.3 兼容性变更
什么是兼容性变更

在 AOSP 中,AppCompat 机制(或称应用兼容性机制)用于处理系统更新或设备兼容性变化时,确保应用程序能够继续在不同版本的 Android 系统上运行,避免由于系统变化导致应用程序出现异常行为或崩溃。为此,Android 引入了 "兼容性规则" 来动态调整应用行为。

通过这种机制,系统可以在不修改应用程序的情况下,在不同的系统版本或设备上调整应用的行为,以确保兼容性。系统利用兼容性规则来动态管理应用程序的功能和权限,以防止应用程序因为系统升级或设备特性变化而出现问题。

系统用兼容性变更ID(Change ID)表示兼容性行为,Change ID 一般在 java 代码中定义为

    @ChangeId@Disabled 或 @Enabled 或 @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.XXX)public static final long <Change ID变量> = <Change ID值>;

@ChangeId 表示该变量是一个兼容性变更ID。

@Disabled 表示该 Change ID 默认失能

@Enabled 表示该 Change ID 默认使能

@EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.XXX) 表示应用的 sdk 版本高于 XXX 时,该 Change ID 默认使能

在系统编译时,会根据 java 注解生成兼容性配置文件(包含应用兼容性变更ID及其使能条件)。

etc/compatconfig/xxx.xml

system_ext/etc/compatconfig/xxx.xml

apex/<apex module>/etc/compatconfig/xxx.xml

设备制造商也可以自定义兼容性配置文件,放到 product 下。

/product/etc/appcompat/compat_framework_overrides.xml

开发者选项中,可以针对应用程序修改兼容性配置,修改的配置会更新到 /data 下的配置文件中。

/data/misc/appcompat/compat_framework_overrides.xml

系统启动时,会从上面这些配置文件中初始化应用兼容性变更配置。

官方文档:兼容性框架变更 (Android 15) | Android Developers (google.cn)

scaling 相关的兼容性变更项
#frameworks/base/services/core/java/com/android/server/wm/CompatModePackages.javapublic final class CompatModePackages {// DOWNSCALED_INVERSE是 "所有应用分辨率" 的 "反向缩小" 变更的控制开关。// 启动此变更,将允许 DOWNSCALE_90 ~ DOWNSCALE_30 这些变更比例(缩放因子)。// 如果某个应用包启用了 DOWNSCALED_INVERSE,则该应用将被强制缩放到启用的最低缩放比例,// 例如,如果同时启用了 1/0.8 和 1/0.7(*100%),则应用将被强制调整到 1/0.8。// 当 DOWNSCALED_INVERSE 和 DOWNSCALED 同时启用时,DOWNSCALED_INVERSE 优先生效。@ChangeId@Disabled@Overridablepublic static final long DOWNSCALED_INVERSE = 273564678L; // This is a Bug ID.// DOWNSCALED是 "所有应用分辨率" 的 "缩小" 变更的控制开关。// 启动此变更,将允许 DOWNSCALE_90 ~ DOWNSCALE_30 这些变更比例(缩放因子)。// 如果某个应用包启用了 DOWNSCALED,则该应用将被强制调整到启用的最高缩放比例,// 例如,如果同时启用了 80% 和 70%,则应用将被强制调整到 80%。@ChangeId@Disabled@Overridablepublic static final long DOWNSCALED = 168419799L;@ChangeId@Disabled@Overridablepublic static final long DOWNSCALE_90 = 182811243L;@ChangeId@Disabled@Overridablepublic static final long DOWNSCALE_85 = 189969734L;@ChangeId@Disabled@Overridablepublic static final long DOWNSCALE_80 = 176926753L;...@ChangeId@Disabled@Overridablepublic static final long DOWNSCALE_30 = 189970040L;
4.5.1.4 更新应用兼容性信息的场景

主要有两个场景更新应用兼容性信息。

一是应用启动时。

二是应用的兼容模式改变时(一般不会改变,除非用 adb shell am 命令手动设置)。

兼容性信息保存在应用进程的 LoadedApk 对象中。

应用启动时

应用启动时(bind application 阶段)system_server 构造 CompatibilityInfo 对象,并传递给应用进程。

应用进程 ActivityThread 构造 LoadedApk 对象、Application 对象。

CompatibilityInfo 对象被保存到 ActivityThread(ActivityThread$mCompatibilityInfo) 和 LoadedApk(LoadedApk$mDisplayAdjustments$mCompatInfo)中。

参见应用启动流程.

应用的兼容模式状态改变时

CompatModePackages$setPackageScreenCompatModeLocked 方法设置应用的兼容模式状态。

下面是具体流程。

#frameworks/base/services/core/java/com/android/server/wm/CompatModePackages.javapublic void setPackageScreenCompatModeLocked(String packageName, int mode) {ApplicationInfo ai = null;try {ai = AppGlobals.getPackageManager().getApplicationInfo(packageName, 0, 0);} catch (RemoteException e) {}if (ai == null) {Slog.w(TAG, "setPackageScreenCompatMode failed: unknown package " + packageName);return;}setPackageScreenCompatModeLocked(ai, mode);}void setPackageScreenCompatModeLocked(ApplicationInfo ai, int mode) {final String packageName = ai.packageName;...// 构造目标应用的兼容性信息 (新 CompatibilityInfo 对象)CompatibilityInfo ci = compatibilityInfoForPackageLocked(ai);if (ci.alwaysSupportsScreen()) {Slog.w(TAG, "Ignoring compat mode change of " + packageName+ "; compatibility never needed");newFlags = 0;}if (ci.neverSupportsScreen()) {Slog.w(TAG, "Ignoring compat mode change of " + packageName+ "; compatibility always needed");newFlags = 0;}if (newFlags != curFlags) {if (newFlags != 0) {mPackages.put(packageName, newFlags);} else {mPackages.remove(packageName);}// Need to get compatibility info in new state.ci = compatibilityInfoForPackageLocked(ai);scheduleWrite();final Task rootTask = mService.getTopDisplayFocusedRootTask();ActivityRecord starting = rootTask.restartPackage(packageName);// Tell all processes that loaded this package about the change.SparseArray<WindowProcessController> pidMap = mService.mProcessMap.getPidMap();// 遍历所有应用进程for (int i = pidMap.size() - 1; i >= 0; i--) {final WindowProcessController app = pidMap.valueAt(i);// 只选择目标应用的进程if (!app.containsPackage(packageName)) {continue;}try {if (app.hasThread()) {ProtoLog.v(WM_DEBUG_CONFIGURATION, "Sending to proc %s "+ "new compat %s", app.mName, ci);// (用新构造的 CompatibilityInfo 对象)更新应用进程的兼容性信息app.getThread().updatePackageCompatibilityInfo(packageName, ci);}...#frameworks/base/core/java/android/app/ActivityThread.javaprivate class ApplicationThread extends IApplicationThread.Stub {...public void updatePackageCompatibilityInfo(String pkg, CompatibilityInfo info) {UpdateCompatibilityData ucd = new UpdateCompatibilityData();ucd.pkg = pkg;ucd.info = info;// 更新进程级别的的 scaling 值 (即更新 CompatibilityInfo 的 static 成员变量 sOverrideInvertedScale,java 的 static 成员作用域是类级别的)updateCompatOverrideScale(info);// 在 handler 中处理兼容性信息sendMessage(H.UPDATE_PACKAGE_COMPATIBILITY_INFO, ucd);}...private void updateCompatOverrideScale(CompatibilityInfo info) {// 更新 CompatibilityInfo 的 static 成员变量 sOverrideInvertedScaleCompatibilityInfo.setOverrideInvertedScale(info.hasOverrideScaling() ? info.applicationInvertedScale : 1f);}...class H extends Handler {...case UPDATE_PACKAGE_COMPATIBILITY_INFO:handleUpdatePackageCompatibilityInfo((UpdateCompatibilityData)msg.obj);break;private void handleUpdatePackageCompatibilityInfo(UpdateCompatibilityData data) {mCompatibilityInfo = data.info;// 将新的应用兼容性信息(CompatibilityInfo)设置到应用的 LoadedApk 中。// 即 LoadedApk$mDisplayAdjustments$mCompatInfo 引用新的 CompatibilityInfo 对象。// LoadedApk 对象保存的是 apk 的本地状态。LoadedApk apk = peekPackageInfo(data.pkg, false);if (apk != null) {apk.setCompatibilityInfo(data.info);}apk = peekPackageInfo(data.pkg, true);if (apk != null) {apk.setCompatibilityInfo(data.info);}// 应用应用程序的 configuration 变化 (应用兼容性信息变化)mConfigurationController.handleConfigurationChanged(data.info);}#frameworks/base/core/java/android/app/ConfigurationController.java/*** Update the configuration to latest.* @param compat The new compatibility information.*/void handleConfigurationChanged(@NonNull CompatibilityInfo compat) {// 应用 configuration 变化handleConfigurationChanged(mConfiguration, compat);WindowManagerGlobal.getInstance().reportNewConfiguration(mConfiguration);}

ActivityManager 封装了设置应用兼容模式的接口。

但是注意到该接口的 @hide 注解,说明这个接口应该是留给系统级别的代码调用,而非第三方应用。通常用于系统应用、设备厂商的定制功能或系统调试工具,用于管理应用的屏幕兼容模式。

搜索 AOSP 代码,没有发现调用此接口的系统代码。

#frameworks/base/core/java/android/app/ActivityManager.java/** @hide */public void setPackageScreenCompatMode(String packageName, int mode) {try {getTaskService().setPackageScreenCompatMode(packageName, mode);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}#AMS@Overridepublic void setPackageScreenCompatMode(String packageName, int mode) {mActivityTaskManager.setPackageScreenCompatMode(packageName, mode);}#frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java@Overridepublic void setPackageScreenCompatMode(String packageName, int mode) {mAmInternal.enforceCallingPermission(android.Manifest.permission.SET_SCREEN_COMPATIBILITY,"setPackageScreenCompatMode");synchronized (mGlobalLock) {mCompatModePackages.setPackageScreenCompatModeLocked(packageName, mode);}}

不过在 ActivityManagerShellCommand 中,有代码直接调用 AMS 的 setPackageScreenCompatMode 方法设置应用的兼容模式。

即,可以通过命令

adb shell am screen-compat on/off <packagename> <packagename> ...

来手动设置应用的兼容模式~

#frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.javaint runScreenCompat(PrintWriter pw) throws RemoteException {String mode = getNextArgRequired();boolean enabled;if ("on".equals(mode)) {enabled = true;} else if ("off".equals(mode)) {enabled = false;} else {getErrPrintWriter().println("Error: enabled mode must be 'on' or 'off' at " + mode);return -1;}String packageName = getNextArgRequired();do {try {// mInterface 是 AMS 对象mInterface.setPackageScreenCompatMode(packageName, enabled? ActivityManager.COMPAT_MODE_ENABLED: ActivityManager.COMPAT_MODE_DISABLED);} catch (RemoteException e) {}packageName = getNextArg();} while (packageName != null);return 0;}

应用进程在更新兼容性信息时,会更新静态成员变量 CompatibilityInfo$sOverrideInvertedScale 的值,即进程级别的 scaling(java 的 static 成员变量作用域是类级别的)。

#frameworks/base/core/java/android/content/res/CompatibilityInfo.java/** The process level override inverted scale. See {@link #HAS_OVERRIDE_SCALING}. */private static float sOverrideInvertedScale = 1f;/** @see #sOverrideInvertedScale */public static void setOverrideInvertedScale(float invertedRatio) {sOverrideInvertedScale = invertedRatio;}

5.1.2 应用程序上下文(Context)

应用程序上下文(Context)是指应用程序运行时的环境信息。

在 Android 中,应用程序上下文用 Context 对象表示。

Context 是一个抽象类。实现类主要是 ContextImpl,ContextImpl 通常会被封装到 ContextWrapper 中。

Application、Activity、Service、WindowContext 都是 ContextWrapper 的派生类;

其中 WindowContext 表示窗口上下文,包含窗口的信息。

ContextWrapper 和 ContextImpl 的关系。


在 Android 中,ContextImpl 和 ContextWrapper 都是 Context 的实现类,而 Context 是 Android 提供的一个抽象类,用于应用程序与 Android 系统进行交互,比如访问资源、启动活动、广播等。

ContextImpl

ContextImpl 的成员变量 mOuterContext 代表的是外部的 Context,它通常是一个指向更高层次 Context 的引用,比如 Activity、Service、或者 Application。这些更高层次的类通常包含更多的功能和行为,并扩展了 Context 的基本功能。

mOuterContext 用于关联 ContextImpl 的内部实现与其“外部” Context。比如,在 Activity 中,它的 Context 实际上是 Activity 本身,但很多与系统交互的具体实现是通过 ContextImpl 来完成的。mOuterContext 就是指向外部 Activity 的引用,确保 Activity 可以将 ContextImpl 的功能与它的外部功能结合起来。

举个例子

  1. 在 Activity 中:mOuterContext 通常指向的是 Activity 自己。

  2. 在 Service 中:mOuterContext 通常指向的是 Service。

这样做的好处是,虽然 ContextImpl 执行了很多具体操作,但它仍然能够通过 mOuterContext 引用回外部的 Context 实例,从而确保与外部 Context 的联动。

在 Android 框架中,Activity、Service、Application 等类实际上是 ContextWrapper 的派生类,它们都有一个内部的 ContextImpl 实例(ContextWrapper$mBase)来处理具体的系统交互和工作。

ContextImpl 用于执行所有与 Android 系统资源和服务相关的任务(如访问资源、启动活动、服务、广播接收器等)。然而,Activity、Service、Application 等类通常通过 ContextImpl$mOuterContext(即外部 Context)保持对它们自己实例的引用,以便在完成某些操作后能够返回到这些更高级的类,从而保持外部类的控制和行为一致性。

在 ContextImpl 完成系统交互后,仍然可以通过 mOuterContext 回到外部的 Context(即 Activity、Service 等)层次,确保调用链能够返回到正确的对象,继续执行外部类的逻辑。

这种架构确保了 Android 中的 Context 使用既灵活又模块化,ContextImpl 处理系统交互,而 Activity、Service、Application 等保持自己的业务逻辑和行为定义。

ContextWrapper

ContextWrapper 是一个用于包装另一个 Context 实例的类。ContextWrapper 主要用于提供一种将 Context 功能和行为委托给另一个基础 Context 实例的机制。

mBase 成员变量的含义:

  • Context mBase:mBase 是 ContextWrapper 的一个成员变量,表示该 ContextWrapper 所包装的基础 Context 实例。

ContextWrapper 的作用:

  • ContextWrapper 的主要作用是将所有方法调用委托到其包装的 mBase 上。这使得 ContextWrapper 可以用来增强或扩展现有的 Context 功能,而不需要重新实现所有的 Context 方法。

attachBaseContext 方法:

  • attachBaseContext(Context base):这个方法用于设置 mBase 的值。调用该方法时,ContextWrapper 将会将所有的 Context 操作委托给这个基础 Context 实例。

  • 检查 mBase 是否已被设置:方法中包含一个检查,确保 mBase 只被设置一次。如果 mBase 已经被设置,则抛出 IllegalStateException 异常。这防止了重复设置基础 Context,确保 ContextWrapper 只会有一个基础 Context 实例。

使用场景:

  • 创建自定义 Context:ContextWrapper 可以用于创建自定义 Context 类,添加特定的行为或逻辑,同时委托所有的 Context 功能给 mBase。

  • 处理上下文:在某些情况下,例如在 Activity 的 attachBaseContext 方法中,它会被调用来传递和设置新的基础上下文。这允许 Activity 对基础上下文进行扩展,同时保持对原始上下文的访问。

下面是一个使用 ContextWrapper 的例子

public class CustomContextWrapper extends ContextWrapper {public CustomContextWrapper(Context base) {super(base);}@Overridepublic Resources getResources() {// 自定义资源获取逻辑return super.getResources();}
}

在这个示例中,CustomContextWrapper 会将 getResources() 方法的调用委托给 mBase,但你也可以在此方法中添加自定义逻辑。mBase 在这里是通过 ContextWrapper 的构造函数传递的,并被存储在 mBase 中。

总之,mBase 在 ContextWrapper 中表示了所有的实际 Context 操作的目标,它允许 ContextWrapper 类在不重新实现所有 Context 方法的情况下,扩展或修改 Context 的行为。

实际上,Application、Activity、Service 都是 ContextWrapper

通过 ContextWrapper$mBase 和 ContextImpl$mOuterContext 将 ContextWrapper 和 ContextImpl 关联起来。


创建 Context 有几种方式。

  • ContextImpl$createApplicationContext 方法。

返回的是 ContextImpl 实例。

这个方法在 RemoteViews 中使用。

RemoteViews 是跨进程 UI 操作的组件,典型场景是通知和小部件。

  • ContextImpl$createAppContext 方法。

返回的是 ContextImpl 实例。

这个方法在应用启动时、Service 创建时(Service$createServiceBaseContext)使用(都在 ActivityThread.java)。

  • ContextImpl$createActivityContext 方法。

返回的是 ContextImpl 实例。

这个方法在 launch Activity时(ActivityThread.java)使用。

#frameworks/base/core/java/android/app/ContextImpl.java// 返回 ContextImpl 实例@Overridepublic Context createApplicationContext(ApplicationInfo application, int flags)throws NameNotFoundException {LoadedApk pi = mMainThread.getPackageInfo(application, mResources.getCompatibilityInfo(),flags | CONTEXT_REGISTER_PACKAGE);if (pi != null) {// 创建 Context 实例(ContextImpl 是 Context 的实现类)ContextImpl c = new ContextImpl(this, mMainThread, pi, ContextParams.EMPTY,mAttributionSource.getAttributionTag(),mAttributionSource.getNext(),null, mToken, new UserHandle(UserHandle.getUserId(application.uid)),flags, null, null);final int displayId = getDisplayId();final Integer overrideDisplayId = mForceDisplayOverrideInResources? displayId : null;// 创建 Resources,设置到 Contextc.setResources(createResources(mToken, pi, null, overrideDisplayId, null,getDisplayAdjustments(displayId).getCompatibilityInfo(), null));if (c.mResources != null) {return c;}}throw new PackageManager.NameNotFoundException("Application package " + application.packageName + " not found");}// 返回 ContextImpl 实例@UnsupportedAppUsagestatic ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {return createAppContext(mainThread, packageInfo, null);}static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,String opPackageName) {if (packageInfo == null) throw new IllegalArgumentException("packageInfo");ContextImpl context = new ContextImpl(null, mainThread, packageInfo,ContextParams.EMPTY, null, null, null, null, null, 0, null, opPackageName);context.setResources(packageInfo.getResources());context.mContextType = isSystemOrSystemUI(context) ? CONTEXT_TYPE_SYSTEM_OR_SYSTEM_UI: CONTEXT_TYPE_NON_UI;return context;}// 返回 ContextImpl 实例@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)static ContextImpl createActivityContext(ActivityThread mainThread,LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,Configuration overrideConfiguration) {if (packageInfo == null) throw new IllegalArgumentException("packageInfo");...ContextImpl context = new ContextImpl(null, mainThread, packageInfo, ContextParams.EMPTY,attributionTag, null, activityInfo.splitName, activityToken, null, 0, classLoader,null);context.mContextType = CONTEXT_TYPE_ACTIVITY;context.mIsConfigurationBasedContext = true;...// 返回 WindowContext        @NonNull@Overridepublic WindowContext createWindowContext(@WindowType int type,@Nullable Bundle options) {if (getDisplay() == null) {throw new UnsupportedOperationException("Please call this API with context associated"+ " with a display instance, such as Activity or context created via"+ " Context#createDisplayContext(Display), or try to invoke"+ " Context#createWindowContext(Display, int, Bundle)");}return createWindowContextInternal(getDisplay(), type, options);}@NonNull@Overridepublic WindowContext createWindowContext(@NonNull Display display, @WindowType int type,@Nullable Bundle options) {if (display == null) {throw new IllegalArgumentException("Display must not be null");}return createWindowContextInternal(display, type, options);}/*** The internal implementation of {@link Context#createWindowContext(int, Bundle)} and* {@link Context#createWindowContext(Display, int, Bundle)}.** @param display The {@link Display} instance to be associated with.** @see Context#createWindowContext(Display, int, Bundle)* @see Context#createWindowContext(int, Bundle)*/private WindowContext createWindowContextInternal(@NonNull Display display,@WindowType int type, @Nullable Bundle options) {// Step 1. Create a WindowTokenClient to associate with the WindowContext's Resources//         instance and it will be later used to receive configuration updates from the//         server side.final WindowTokenClient windowTokenClient = new WindowTokenClient();// Step 2. Create the base context of the window context, it will also create a Resources//         associated with the WindowTokenClient and set the token to the base context.final ContextImpl windowContextBase = createWindowContextBase(windowTokenClient,display.getDisplayId());// Step 3. Create a WindowContext instance and set it as the outer context of the base//         context to make the service obtained by #getSystemService(String) able to query//         the WindowContext's WindowManager instead of the default one.final WindowContext windowContext = new WindowContext(windowContextBase, type, options);windowContextBase.setOuterContext(windowContext);// Step 4. Attach the WindowContext to the WindowTokenClient. In this way, when there's a//         configuration update from the server side, the update will then apply to//         WindowContext's resources.windowTokenClient.attachContext(windowContext);// Step 5. Associate the WindowContext's token to a DisplayArea.windowContext.attachToDisplayArea();return windowContext;}
 #frameworks/base/core/java/android/app/Service.java/*** Creates the base {@link Context} of this {@link Service}.* Users may override this API to create customized base context.** @see android.window.WindowProviderService WindowProviderService class for example* @see ContextWrapper#attachBaseContext(Context)** @hide*/public Context createServiceBaseContext(ActivityThread mainThread, LoadedApk packageInfo) {return ContextImpl.createAppContext(mainThread, packageInfo);}

Resources 的创建方式。

ContextImpl$createResources 方法返回 Resources 实例,会将 Compatinfo 设置到 Resources。

也就是说,Compatinfo 也会影响资源的查找,这里不展开讲。

我们这里只要知道 "分辨率缩放因子" 不会影响资源查找(它不影响 density)。

#frameworks/base/core/java/android/app/ContextImpl.java// 创建 Resourcesprivate static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,@Nullable Integer overrideDisplayId, Configuration overrideConfig,CompatibilityInfo compatInfo, List<ResourcesLoader> resourcesLoader) {final String[] splitResDirs;final ClassLoader classLoader;try {splitResDirs = pi.getSplitPaths(splitName);classLoader = pi.getSplitClassLoader(splitName);} catch (NameNotFoundException e) {throw new RuntimeException(e);}// 使用 ResourcesManager 创建 Resources。// ResourcesManager 是单例。return ResourcesManager.getInstance().getResources(activityToken,pi.getResDir(),splitResDirs,pi.getOverlayDirs(),pi.getOverlayPaths(),pi.getApplicationInfo().sharedLibraryFiles,overrideDisplayId,overrideConfig,compatInfo,classLoader,resourcesLoader);}
#frameworks/base/core/java/android/content/res/Resources.java/*** Return the compatibility mode information for the application.* The returned object should be treated as read-only.** @return compatibility info.* @hide*/@UnsupportedAppUsagepublic CompatibilityInfo getCompatibilityInfo() {return mResourcesImpl.getCompatibilityInfo();}/*** This is just for testing.* @hide*/@VisibleForTesting@UnsupportedAppUsagepublic void setCompatibilityInfo(CompatibilityInfo ci) {if (ci != null) {mResourcesImpl.updateConfiguration(null, null, ci);}}

5.1.3 应用启动(bind application)流程

顺着 bind application 的流程,搞清楚 system_server 构造的 CompatibilityInfo 对象是如何传递到应用进程的。

(system_server)AMS$attachApplicationLocked 方法中,

  1. 构造兼容性信息即 CompatibilityInfo 对象,

  2. bind application,将兼容性信息传递给应用进程。

#frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java@GuardedBy("this")private void attachApplicationLocked(@NonNull IApplicationThread thread,int pid, int callingUid, long startSeq) {// Find the application record that is being attached...  either via// the pid if we are running in multiple processes, or just pull the// next app record if we are emulating process with anonymous threads.ProcessRecord app;...            ProtoLog.v(WM_DEBUG_CONFIGURATION, "Binding proc %s with config %s",processName, app.getWindowProcessController().getConfiguration());ApplicationInfo appInfo = instr != null ? instr.mTargetInfo : app.info;// 构造目标应用的兼容性信息(新 CompatibilityInfo 对象),// 并设置到 ProcessRecord 中。app.setCompat(compatibilityInfoForPackage(appInfo));...checkTime(startTime, "attachApplicationLocked: immediately before bindApplication");bindApplicationTimeMillis = SystemClock.uptimeMillis();mAtmInternal.preBindApplication(app.getWindowProcessController());final ActiveInstrumentation instr2 = app.getActiveInstrumentation();if (mPlatformCompat != null) {mPlatformCompat.resetReporting(app.info);}final ProviderInfoList providerList = ProviderInfoList.fromList(providers);// 执行 bind applicatoin。// 将 CompatibilityInfo 对象(app.getCompat())作为参数传递给应用进程 if (app.getIsolatedEntryPoint() != null) {// This is an isolated process which should just call an entry point instead of// being bound to an application.thread.runIsolatedEntryPoint(app.getIsolatedEntryPoint(), app.getIsolatedEntryPointArgs());} else if (instr2 != null) {thread.bindApplication(processName, appInfo,app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,providerList,instr2.mClass,profilerInfo, instr2.mArguments,instr2.mWatcher,instr2.mUiAutomationConnection, testMode,mBinderTransactionTrackingEnabled, enableTrackAllocation,isRestrictedBackupMode || !normalMode, app.isPersistent(),new Configuration(app.getWindowProcessController().getConfiguration()),app.getCompat(), getCommonServicesLocked(app.isolated),mCoreSettingsObserver.getCoreSettingsLocked(),buildSerial, autofillOptions, contentCaptureOptions,app.getDisabledCompatChanges(), serializedSystemFontMap,app.getStartElapsedTime(), app.getStartUptime());} else {thread.bindApplication(processName, appInfo,app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,providerList, null, profilerInfo, null, null, null, testMode,mBinderTransactionTrackingEnabled, enableTrackAllocation,isRestrictedBackupMode || !normalMode, app.isPersistent(),new Configuration(app.getWindowProcessController().getConfiguration()),app.getCompat(), getCommonServicesLocked(app.isolated),mCoreSettingsObserver.getCoreSettingsLocked(),buildSerial, autofillOptions, contentCaptureOptions,app.getDisabledCompatChanges(), serializedSystemFontMap,app.getStartElapsedTime(), app.getStartUptime());}...

(应用进程)bind application 流程

  1. 应用进程取出应用兼容性信息(data.compatInfo),将兼容性信息保存到 mConfigurationController.setCompatConfiguration 和本地成员变量 mCompatibilityInfo。

  2. 使用兼容性信息构造 LoadedApk 对象并设置给 AppBindData 的成员 info。

  3. 使用 LoadedApk 对象构造应用程序上下文即 ContextImpl 对象,这个 ContextImpl 对象用于 Application 的早期阶段,提供初步的 Context。这是因为在 Application 对象尚未完全初始化之前,需要为其提供一个基础的 Context。

  4. 创建和初始化 Instrumentaion 对象。

  5. 创建 Application 实例,设置到本地成员变量 mInitialApplication。使用 LoadedApk$makeApplicationInner 方法创建 Application 实例。

  6. 调用 Instrumentation 的 onCreate 方法。

  7. 通过 Instrumentation 调用 Application 的 onCreate 方法。

#frameworks/base/core/java/android/app/ActivityThread.java@UnsupportedAppUsageprivate void handleBindApplication(AppBindData data) {...mBoundApplication = data;mConfigurationController.setConfiguration(data.config);mConfigurationController.setCompatConfiguration(data.config);// 将 system_server 传递过来的 Configuration 保存到变量 mConfigurationmConfiguration = mConfigurationController.getConfiguration();// 将 system_server 传递过来的 CompatibilityInfo 保存到变量 mCompatibilityInfomCompatibilityInfo = data.compatInfo;...synchronized (mResourcesManager) {/** Update the system configuration since its preloaded and might not* reflect configuration changes. The configuration object passed* in AppBindData can be safely assumed to be up to date*/// 将新的 CompatibilityInfo 应用到 ResourcesManager。mResourcesManager.applyConfigurationToResources(data.config, data.compatInfo);mCurDefaultDisplayDpi = data.config.densityDpi;// This calls mResourcesManager so keep it within the synchronized block.mConfigurationController.applyCompatConfiguration();}// 以新的 CompatibilityInfo 对象为参数,构造一个 LoadedApk 对象。// 设置到 data.info (AppBindData 的成员 LoadedApk info)。final boolean isSdkSandbox = data.sdkSandboxClientAppPackage != null;data.info = getPackageInfo(data.appInfo, mCompatibilityInfo, null /* baseLoader */,false /* securityViolation */, true /* includeCode */,false /* registerPackage */, isSdkSandbox);...final IActivityManager mgr = ActivityManager.getService();// 以 LoadedApk 对象为参数,构造一个应用程序上下文 (ContextImpl 是 Context 的实现类)final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);mConfigurationController.updateLocaleListFromAppContext(appContext);...// Continue loading instrumentation.// 创建(初始化) Instrumentation 对象if (ii != null) {initInstrumentation(ii, data, appContext);} else {mInstrumentation = new Instrumentation();mInstrumentation.basicInit(this);}...// Allow disk access during application and provider setup. This could// block processing ordered broadcasts, but later processing would// probably end up doing the same disk access.// 创建 Application 实例,设置到本地成员变量 mInitialApplicationApplication app;...try {// If the app is being launched for full backup or restore, bring it up in// a restricted environment with the base application class.// 创建 Application 实例app = data.info.makeApplicationInner(data.restrictedBackupMode, null);...// 设置本地成员变量 mInitialApplicationmInitialApplication = app;...// Do this after providers, since instrumentation tests generally start their// test thread at this point, and we don't want that racing.try {// 调用 Instrumentation 的 onCreate 方法mInstrumentation.onCreate(data.instrumentationArgs);}catch (Exception e) {throw new RuntimeException("Exception thrown in onCreate() of "+ data.instrumentationName + ": " + e.toString(), e);}try {// 通过 Instrumentation 调用 Application 的 onCreate 方法。mInstrumentation.callApplicationOnCreate(app);} catch (Exception e) {if (!mInstrumentation.onException(app, e)) {throw new RuntimeException("Unable to create application " + app.getClass().getName()+ ": " + e.toString(), e);}}} finally {// If the app targets < O-MR1, or doesn't change the thread policy// during startup, clobber the policy to maintain behavior of b/36951662if (data.appInfo.targetSdkVersion < Build.VERSION_CODES.O_MR1|| StrictMode.getThreadPolicy().equals(writesAllowedPolicy)) {StrictMode.setThreadPolicy(savedPolicy);}}...

首先来看 ActivityThread$handleBindApplication 方法中下面这段代码,这段代码用于加载和初始化应用程序的 Instrumentation 对象。

// Continue loading instrumentation.
if (ii != null) {initInstrumentation(ii, data, appContext);
} else {mInstrumentation = new Instrumentation();mInstrumentation.basicInit(this);
}

Instrumentation 是 Android 中用于监控和控制应用程序的一个类。它允许开发者通过编程手段模拟用户交互、调用生命周期回调、甚至监视和修改应用的行为。主要用于自动化测试中。

这段代码的主要作用是确保 Instrumentation 对象的正确初始化:

  • 如果有自定义 Instrumentation,即 ii != null,ii 是 InstrumentationInfo 对象。则通过 ii 初始化它。

  • 如果没有自定义 Instrumentation,则创建一个默认的 Instrumentation 对象,并进行基本初始化。

一般,

在 "生产环境" 和 "普通应用启动" 中没有自定义 Instrumentation。

在 "测试环境" 或 "特殊用途" 下(如自动化测试或调试工具中)会有自定义 Instrumentation。

Instrumentation 是测试和调试过程中重要的机制,它允许开发者通过编程方式对应用进行更深入的控制和操作。

在非测试环境下,Instrumentation 对象的初始化(Instrumentation$basicInit)只是将 ActivityThread 对象设置到 Instrumentation 的 mThread 成员。

#frameworks/base/core/java/android/app/Instrumentation.java/*** Only sets the ActivityThread up, keeps everything else null because app is not being* instrumented.*/final void basicInit(ActivityThread thread) {mThread = thread;}

然后我们再来看 Application 对象的创建。

    private Application makeApplicationInner(boolean forceDefaultAppClass,Instrumentation instrumentation, boolean allowDuplicateInstances) {// Application 对象已经实例化了,直接返回 mApplicationif (mApplication != null) {return mApplication;}Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "makeApplication");// 从 cache 即 sApplications 中查找 Application 对象并返回。// 一般情况下,allowDuplicateInstances 为 false,sApplications 为空。synchronized (sApplications) {final Application cached = sApplications.get(mPackageName);if (cached != null) {// Looks like this is always happening for the system server, because// the LoadedApk created in systemMain() -> attach() isn't cached properly?if (!"android".equals(mPackageName)) {Slog.wtfStack(TAG, "App instance already created for package=" + mPackageName+ " instance=" + cached);}if (!allowDuplicateInstances) {mApplication = cached;return cached;}// Some apps intentionally call makeApplication() to create a new Application// instance... Sigh...}}Application app = null;final String myProcessName = Process.myProcessName();String appClass = mApplicationInfo.getCustomApplicationClassNameForProcess(myProcessName);if (forceDefaultAppClass || (appClass == null)) {appClass = "android.app.Application";}try {final java.lang.ClassLoader cl = getClassLoader();if (!mPackageName.equals("android")) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,"initializeJavaContextClassLoader");initializeJavaContextClassLoader();Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);}// Rewrite the R 'constants' for all library apks.SparseArray<String> packageIdentifiers = getAssets().getAssignedPackageIdentifiers(false, false);for (int i = 0, n = packageIdentifiers.size(); i < n; i++) {final int id = packageIdentifiers.keyAt(i);if (id == 0x01 || id == 0x7f) {continue;}rewriteRValues(cl, packageIdentifiers.valueAt(i), id);}// 创建一个应用程序上下文 ContextImpl appContextContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);// The network security config needs to be aware of multiple// applications in the same process to handle discrepanciesNetworkSecurityConfigProvider.handleNewApplication(appContext);// 通过 Instrumentation 使用 appContext 创建 Applicaton 实例app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext);// 将 Application 实例设置为 appContext 的 outer ContextappContext.setOuterContext(app);} catch (Exception e) {if (!mActivityThread.mInstrumentation.onException(app, e)) {Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);throw new RuntimeException("Unable to instantiate application " + appClass+ " package " + mPackageName + ": " + e.toString(), e);}}mActivityThread.mAllApplications.add(app);// 设置 mApplicationmApplication = app;// 如果 allowDuplicateInstances 为 true,将 Application 实例缓存到 sApplications 中。// 一般 allowDuplicateInstances 为 false。if (!allowDuplicateInstances) {synchronized (sApplications) {sApplications.put(mPackageName, app);}}if (instrumentation != null) {try {instrumentation.callApplicationOnCreate(app);} catch (Exception e) {if (!instrumentation.onException(app, e)) {Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);throw new RuntimeException("Unable to create application " + app.getClass().getName()+ ": " + e.toString(), e);}}}Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);return app;}

Application 实例是通过 Instrumentation 创建的。

Instrumentation$newApplication 方法的作用是创建一个 Application 实例。Application 实例的类通常是由应用自己定义的。开发者可以在应用的 AndroidManifest.xml 文件中指定自定义的 Application 类。如果没有指定,系统会使用默认的 Application 类。

#frameworks/base/core/java/android/app/Instrumentation.java/*** Perform instantiation of the process's {@link Application} object.  The* default implementation provides the normal system behavior.** @param cl The ClassLoader with which to instantiate the object.* @param className The name of the class implementing the Application*                  object.* @param context The context to initialize the application with** @return The newly instantiated Application object.*/public Application newApplication(ClassLoader cl, String className, Context context)throws InstantiationException, IllegalAccessException,ClassNotFoundException {Application app = getFactory(context.getPackageName()).instantiateApplication(cl, className);app.attach(context);return app;}

在应用的 AndroidManifest.xml 文件中,可以通过 <application> 标签的 android:name 属性来指定自定义的 Application 类。具体格式如下:

<applicationandroid:name=".MyCustomApplication" <!-- 这里指定自定义的 Application 类 -->android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:theme="@style/AppTheme"><!-- 其他组件声明,比如 activity、service 等 -->
</application>

其中,android:name 的值是自定义 Application 类的全路径名称。如果类在根包中,也可以使用以点开头的简写方式(例如,.MyCustomApplication),表示该类位于应用的主包中。

下面是一个简单的 MyCustomApplication 类实现的示例。这个类继承自 android.app.Application,并可以在应用的整个生命周期中执行一些初始化逻辑,如配置全局状态、初始化库等。

package com.example.myapp;import android.app.Application;
import android.util.Log;public class MyCustomApplication extends Application {private static final String TAG = "MyCustomApplication";@Overridepublic void onCreate() {super.onCreate();// 在应用启动时初始化全局逻辑Log.d(TAG, "Application is starting");// 比如初始化一些全局变量或库initGlobalState();}private void initGlobalState() {// 在这里初始化全局状态或库,如网络库、日志库等Log.d(TAG, "Global state initialized");}@Overridepublic void onTerminate() {super.onTerminate();Log.d(TAG, "Application is terminating");// 应用终止时的清理工作(注意:onTerminate 在正常流程中不常被调用)}
}

注意,Instrumentation$newApplication 方法中,会调用 Application$attach 方法将之前LoadedApk$makeApplicationInner 方法中创建的 ContextImpl 对象设置给 Application。

Application(以及 Activity、Service 等) 也是 Context 的实现类,不过并不是 ContextImpl 的派生类,而是 ContextWrapper 的派生类。

Application$attach 方法中,

  • 调用 ContextWrapper$attachBaseContext 将 ContextImpl 对象设置到 ContextWrapper$mBase。

  • 通过 ContextImpl 对象获取 LoadedApk,设置到 Application$mLoadedApk。

#frameworks/base/core/java/android/app/Application.java/*** @hide*/@UnsupportedAppUsage/* package */ final void attach(Context context) {attachBaseContext(context);mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;}

LoadedApk$makeApplicationInner 方法创建 Application 实例后,调用 ContextImpl$setOuterContext 方法将 Application 实例设置到 appContext 的 outer Context,即 ContextImpl$mOuterContext(Context)。

最后看一下 Instrumentation 调用 Application 的 onCreate 方法。

默认的 Application 的 onCreate 是个空函数,应用程序可以自定义 Application 类实现 onCreate 方法~~

#frameworks/base/core/java/android/app/Instrumentation.java/*** Perform calling of the application's {@link Application#onCreate}* method.  The default implementation simply calls through to that method.** <p>Note: This method will be called immediately after {@link #onCreate(Bundle)}.* Often instrumentation tests start their test thread in onCreate(); you* need to be careful of races between these.  (Well between it and* everything else, but let's start here.)** @param app The application being created.*/public void callApplicationOnCreate(Application app) {app.onCreate();}#frameworks/base/core/java/android/app/Application.java/*** Called when the application is starting, before any activity, service,* or receiver objects (excluding content providers) have been created.** <p>Implementations should be as quick as possible (for example using* lazy initialization of state) since the time spent in this function* directly impacts the performance of starting the first activity,* service, or receiver in a process.</p>** <p>If you override this method, be sure to call {@code super.onCreate()}.</p>** <p class="note">Be aware that direct boot may also affect callback order on* Android {@link android.os.Build.VERSION_CODES#N} and later devices.* Until the user unlocks the device, only direct boot aware components are* allowed to run. You should consider that all direct boot unaware* components, including such {@link android.content.ContentProvider}, are* disabled until user unlock happens, especially when component callback* order matters.</p>*/@CallSuperpublic void onCreate() {}

总结一下 bind application 流程:

system_server 将构造的 CompatibilityInfo 对象传递给应用。

应用进程 ActivityThread 构造 LoadedApk 对象、Application 对象。

CompatibilityInfo 对象被保存到应用进程的 ActivityThread(ActivityThread$mCompatibilityInfo) 和 LoadedApk(LoadedApk$mDisplayAdjustments$mCompatInfo)中。

思考

思考一个问题:目前(未来)主流游戏集成 Game Mode 的比例有多少?

"集成 Game Mode" 是指游戏应用程序设置了 "game" category,并且实现了自定义的 game mode,或者在一个或多个场景下设置 game mode。

如果游戏应用程序集成 Game Mode 是趋势的话,那么作为 vendor 或者设备厂商,可能有必要对 AOSP Game Mode 做一些定制或加强,以便更好地发挥设备性能。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com