深入研究DisplayManager.requestDisplayModes(未完)

深入研究 DisplayManager.requestDisplayModes

开启日志

DisplayManagerShellCommand
中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class DisplayManagerShellCommand extends ShellCommand {
private int getMatchContentFrameRateUserPreference() {
final Context context = mService.getContext();
final DisplayManager dm = context.getSystemService(DisplayManager.class);
getOutPrintWriter().println("Match content frame rate type: "
+ dm.getMatchContentFrameRateUserPreference());
return 0;
}
private int setMatchContentFrameRateUserPreference() {
final String matchContentFrameRatePrefText = getNextArg();
if (matchContentFrameRatePrefText == null) {
getErrPrintWriter().println("Error: no matchContentFrameRatePref specified");
return 1;
}

final int matchContentFrameRatePreference;
try {
matchContentFrameRatePreference = Integer.parseInt(matchContentFrameRatePrefText);
} catch (NumberFormatException e) {
getErrPrintWriter().println("Error: invalid format of matchContentFrameRatePreference");
return 1;
}
if (matchContentFrameRatePreference < 0) {
getErrPrintWriter().println("Error: invalid value of matchContentFrameRatePreference");
return 1;
}

final Context context = mService.getContext();
final DisplayManager dm = context.getSystemService(DisplayManager.class);

final int refreshRateSwitchingType =
toRefreshRateSwitchingType(matchContentFrameRatePreference);
dm.setRefreshRateSwitchingType(refreshRateSwitchingType);
return 0;
}
private int setDisplayModeDirectorLoggingEnabled(boolean enabled) {
mService.setDisplayModeDirectorLoggingEnabled(enabled);
return 0;
}
}

也就是说 adb shell cmd display 这 getMatchContentFrameRateUserPreferencesetMatchContentFrameRateUserPreference 这类都是调用的 DisplayManager

但部分也是直接调用 DisplayManagerService 的方法,比如 setDisplayModeDirectorLoggingEnabled

那就有个问题,它明明都持有了 DisplayManagerService 的引用,为啥还要去调用 context.getSystemService(DisplayManager.class) 去获取 DisplayManager 呢?

DisplayManager 都是往 DisplayManagerGlobal 里面调用的,DisplayManagerGlobal 再调用 IDisplayManager(IBinder),拿到的 IBinder 也是去和 DisplayManagerService 通信,饶了一大圈,为啥不直接调用,也许是我有没理解到的地方

Claude Sonnet 4.5:

1
2
3
4
5
6
7
8
9
DisplayManagerShellCommand 的混用策略是有意为之

厨师要尝味道: 直接从锅里舀一勺 (直接调用 Service)

快速、简单、不需要走流程

厨师要测试上菜流程: 按铃叫服务员,走完整流程 (通过 Manager)

确保整个流程没问题

根据 DisplayManagerShellCommand 代码 dmd-logging-enable 命令可以开启日志,也就是

1
adb shell cmd display dmd-logging-enable

调用链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──────────────────────────────┐
│ DisplayManagerShellCommand │
│ setDisplayModeDirector │
│ LoggingEnabled() │
└──────────────┬───────────────┘
│ 直接调用

┌──────────────────────────────┐
│ DisplayManagerService │
│ setDisplayModeDirector │
│ LoggingEnabled() │
└──────────────┬───────────────┘


┌──────────────────────────────┐
│ DisplayModeDirector │
│ setLoggingEnabled() │
└──────────────────────────────┘

通过命令也可以查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
➜  ~ adb shell cmd display
Display manager commands:
help
Print this help text.

show-notification NOTIFICATION_TYPE
Show notification for one of the following types: on-hotplug-error, on-link-training-failure, on-cable-dp-incapable
cancel-notifications
Cancel notifications.
set-brightness BRIGHTNESS
Sets the current brightness to BRIGHTNESS (a number between 0 and 1).
reset-brightness-configuration
Reset the brightness to its default configuration.
ab-logging-enable
Enable auto-brightness logging.
ab-logging-disable
Disable auto-brightness logging.
dwb-logging-enable
Enable display white-balance logging.
dwb-logging-disable
Disable display white-balance logging.
dmd-logging-enable
Enable display mode director logging.

但是还有部分日志可以打开

1
2
3
4
5
6
7
8
public final class DisplayManagerService extends SystemService {
// To enable these logs, run:
// 'adb shell setprop persist.log.tag.DisplayManagerService DEBUG && adb reboot'
}
class DisplayDeviceRepository implements DisplayAdapter.Listener {
// To enable these logs, run:
// 'adb shell setprop persist.log.tag.DisplayDeviceRepository DEBUG && adb reboot'
}

我们一并打开观察日志

调用链路分析

DisplayManager.requestDisplayModes DisplayManagerGlobal.requestDisplayModes 这两个都加了
@RequiresPermission(“android.permission.RESTRICT_DISPLAY_MODES”)

这个是系统级的权限,SHELL 下都没有,所以整个调用,我都是在 root 的设备下进行的

DisplayManagerService 中的定义如下

1
2
3
4
5
6
7
@EnforcePermission(RESTRICT_DISPLAY_MODES)
@Override // Binder call
public void requestDisplayModes(IBinder token, int displayId, @Nullable int[] modeIds) {
requestDisplayModes_enforcePermission();
DisplayManagerService.this.mDisplayModeDirector.requestDisplayModes(
token, displayId, modeIds);
}

其中的 tokenDisplayManagerGlobal 中的 mToken

requestDisplayModes 调用链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
应用进程                                system_server 进程
┌──────────────────────────┐ ┌──────────────────────────┐
│ DisplayManager │ │ DisplayManagerService │
│ requestDisplayModes() │ Binder │ requestDisplayModes() │
└────────────┬─────────────┘ IPC └────────────┬─────────────┘
│ ←──────→ │
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ DisplayManagerGlobal │ │ DisplayModeDirector │
│ requestDisplayModes() │ │ requestDisplayModes() │
└────────────┬─────────────┘ └──────────────────────────┘


┌──────────────────────────┐
│ IDisplayManager │
│ (Binder Proxy) │
└──────────────────────────┘

其中 IDisplayManagerDisplayManagerService 通过 IBinder 进行通信,DisplayManagerServiceDisplayModeDirector 都是在系统框架代码里面,具体路径是 /system/framework/services.jar

直接看 DisplayModeDirector.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Delegates requestDisplayModes call to SystemRequestObserver
*/
public void requestDisplayModes(IBinder token, int displayId, int[] modeIds) {
if (mSystemRequestObserver != null) {
boolean vrrSupported;
synchronized (mLock) {
vrrSupported = isVrrSupportedLocked(displayId);
}
if (vrrSupported) {
mSystemRequestObserver.requestDisplayModes(token, displayId, modeIds);
}
}
}

这里判断了一个 vrr,目前我拿不到这个东西是 true 还是 false,false 后续的逻辑都是不会走的

我直接在 SHELL 权限下,搞到这个 DisplayManagerService,直接调用 mDisplayModeDirector.requestDisplayModes 用这种方式能不能绕过权限?也可能会受到 SELinux 的限制

mSystemRequestObserverDisplayModeDirector构造函数创建的时候创建的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DisplayModeDirector {
public DisplayModeDirector(@NonNull Context context, @NonNull Handler handler,
@NonNull Injector injector,
@NonNull DisplayManagerFlags displayManagerFlags,
@NonNull DisplayDeviceConfigProvider displayDeviceConfigProvider) {
mVotesStorage = new VotesStorage(this::notifyDesiredDisplayModeSpecsChangedLocked,
mVotesStatsReporter);
mSystemRequestObserver = new SystemRequestObserver(mVotesStorage);
}
private void notifyDesiredDisplayModeSpecsChangedLocked() {
if (mDesiredDisplayModeSpecsListener != null
&& !mHandler.hasMessages(MSG_REFRESH_RATE_RANGE_CHANGED)) {
// We need to post this to a handler to avoid calling out while holding the lock
// since we know there are things that both listen for changes as well as provide
// information. If we did call out while holding the lock, then there's no
// guaranteed lock order and we run the real of risk deadlock.
Message msg = mHandler.obtainMessage(
MSG_REFRESH_RATE_RANGE_CHANGED, mDesiredDisplayModeSpecsListener);
msg.sendToTarget();
}
}
}

VotesStorage 持有 DisplayModeDirector.notifyDesiredDisplayModeSpecsChangedLocked

进入 SystemRequestObserver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class SystemRequestObserver {

void requestDisplayModes(IBinder token, int displayId, @Nullable int[] modeIds) {
if (modeIds == null) {
removeSystemRequestedVote(token, displayId);
} else {
addSystemRequestedVote(token, displayId, modeIds);
}
}

private void addSystemRequestedVote(IBinder token, int displayId, @NonNull int[] modeIds) {
try {
boolean needLinkToDeath = false;
List<Integer> modeIdsList = new ArrayList<>();
for (int mode: modeIds) {
modeIdsList.add(mode);
}
synchronized (mLock) {
SparseArray<List<Integer>> modesByDisplay = mDisplaysRestrictions.get(token);
if (modesByDisplay == null) {
needLinkToDeath = true;
modesByDisplay = new SparseArray<>();
mDisplaysRestrictions.put(token, modesByDisplay);
}

modesByDisplay.put(displayId, modeIdsList);
updateStorageLocked(displayId);
}
if (needLinkToDeath) {
Slog.d(TAG, "binder linking to death: " + token);
token.linkToDeath(mDeathRecipient, 0);
}
} catch (RemoteException re) {
Slog.d(TAG, "linking to death failed: " + token, re);
removeSystemRequestedVotes(token);
}
}

@GuardedBy("mLock")
private void updateStorageLocked(int displayId) {
List<Integer> modeIds = new ArrayList<>();
boolean[] modesFound = new boolean[1];

mDisplaysRestrictions.forEach((key, value) -> {
List<Integer> modesForDisplay = value.get(displayId);
if (modesForDisplay != null) {
if (!modesFound[0]) {
modeIds.addAll(modesForDisplay);
modesFound[0] = true;
} else {
modeIds.retainAll(modesForDisplay);
}
}
});

mVotesStorage.updateVote(displayId, Vote.PRIORITY_SYSTEM_REQUESTED_MODES,
modesFound[0] ? Vote.forSupportedModes(modeIds) : null);
}
}

总结链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──────────────────────┐
│ DisplayModeDirector │
│ requestDisplayModes()│
└─────────┬────────────┘


┌──────────────────────┐
│ SystemRequestObserver│
│addSystemRequestedVote│
└─────────┬────────────┘


┌──────────────────────┐
│ SystemRequestObserver│
│ updateStorageLocked()│
└─────────┬────────────┘


┌──────────────────────┐
│ VotesStorage │
│ updateVote() │
└──────────────────────┘

这里 VotesStorage. 等只是表达这个函数在哪个类里面,不是它的静态方法

DisplayModeDirector 的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DisplayModeDirector {
public DisplayModeDirector(@NonNull Context context, @NonNull Handler handler,
@NonNull Injector injector,
@NonNull DisplayManagerFlags displayManagerFlags,
@NonNull DisplayDeviceConfigProvider displayDeviceConfigProvider) {
mVotesStorage = new VotesStorage(this::notifyDesiredDisplayModeSpecsChangedLocked,
mVotesStatsReporter);
}
}
class VotesStorage {
VotesStorage(@NonNull Listener listener, @Nullable VotesStatsReporter votesStatsReporter) {
mListener = listener;
mVotesStatsReporter = votesStatsReporter;
}
}

再结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class VotesStorage {
/** updates vote storage */
void updateVote(int displayId, @Vote.Priority int priority, @Nullable Vote vote) {
if (mLoggingEnabled) {
Slog.i(TAG, "updateVoteLocked(displayId=" + displayId
+ ", priority=" + Vote.priorityToString(priority)
+ ", vote=" + vote + ")");
}
if (priority < Vote.MIN_PRIORITY || priority > Vote.MAX_PRIORITY) {
Slog.w(TAG, "Received a vote with an invalid priority, ignoring:"
+ " priority=" + Vote.priorityToString(priority)
+ ", vote=" + vote);
return;
}
boolean changed = false;
SparseArray<Vote> votes;
synchronized (mStorageLock) {
if (mVotesByDisplay.contains(displayId)) {
votes = mVotesByDisplay.get(displayId);
} else {
votes = new SparseArray<>();
mVotesByDisplay.put(displayId, votes);
}
var currentVote = votes.get(priority);
if (vote != null && !vote.equals(currentVote)) {
votes.put(priority, vote);
changed = true;
} else if (vote == null && currentVote != null) {
votes.remove(priority);
changed = true;
}
}
if (mLoggingEnabled) {
Slog.i(TAG, "Updated votes for display=" + displayId + " votes=" + votes);
}
if (changed) {
if (mVotesStatsReporter != null) {
mVotesStatsReporter.reportVoteChanged(displayId, priority, vote);
}
mListener.onChanged();
}
}
}

关键行 mListener.onChanged();

mListener 的值也就是 DisplayModeDirector.notifyDesiredDisplayModeSpecsChangedLocked

看到这里,很难不感叹,为啥 java 就是没有回调函数,类似于其他语言的闭包,所有的回调,都需要依赖接口

最终 mVotesStorage.updateVote 调用到的是 DisplayModeDirector.notifyDesiredDisplayModeSpecsChangedLocked

整理链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌──────────────────────────────────────────────┐
│ SystemRequestObserver │
│ requestDisplayModes() │
└────────────────────────┬─────────────────────┘


┌──────────────────────────────────────────────┐
│ SystemRequestObserver │
│ addSystemRequestedVote() │
└────────────────────────┬─────────────────────┘


┌──────────────────────────────────────────────┐
│ SystemRequestObserver │
│ updateStorageLocked() │
└────────────────────────┬─────────────────────┘


┌──────────────────────────────────────────────┐
│ VotesStorage │
│ updateVote() │
└────────────────────────┬─────────────────────┘


┌──────────────────────────────────────────────┐
│ DisplayModeDirector │
│ notifyDesiredDisplayModeSpecsChangedLocked() │
└──────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Step 1: 接收请求
SystemRequestObserver.requestDisplayModes(token, displayId, modeIds)

└─→ 判断是添加还是移除投票

Step 2: 添加投票
addSystemRequestedVote()
│ 保存请求信息到 mDisplaysRestrictions
│ Map<IBinder, SparseArray<List<Integer>>>
└─→ linkToDeath() 监听调用进程

Step 3: 更新存储
updateStorageLocked()
│ 遍历所有 token 的请求
│ 对同一 displayId 的 modeIds 求交集
└─→ 得到最终支持的 modes

Step 4: 投票机制
VotesStorage.updateVote()
│ 优先级: PRIORITY_SYSTEM_REQUESTED_MODES
│ 比较新旧投票是否变化
└─→ changed = true 时触发回调

Step 5: 异步通知
mListener.onChanged()
│ 实际调用 DisplayModeDirector.notify...Locked()
│ 投递 MSG_REFRESH_RATE_RANGE_CHANGED 到 Handler
└─→ 避免在持有锁时回调外部代码(防止死锁)

DisplayModeDirector

1
2
3
4
5
6
7
8
9
10
11
12
private void notifyDesiredDisplayModeSpecsChangedLocked() {
if (mDesiredDisplayModeSpecsListener != null
&& !mHandler.hasMessages(MSG_REFRESH_RATE_RANGE_CHANGED)) {
// We need to post this to a handler to avoid calling out while holding the lock
// since we know there are things that both listen for changes as well as provide
// information. If we did call out while holding the lock, then there's no
// guaranteed lock order and we run the real of risk deadlock.
Message msg = mHandler.obtainMessage(
MSG_REFRESH_RATE_RANGE_CHANGED, mDesiredDisplayModeSpecsListener);
msg.sendToTarget();
}
}

交给 Handler 处理,并且传递的事件类型就是刷新率变化,也就是默认了 Mode 改变,就是刷新率变化

Handler 处理比较简单,直接回调传过来的 DesiredDisplayModeSpecsListener,但是自己持有回调的情况下,为啥还要交给 Handler 去处理嘞

学习了一下

Claude Sonnet 4.5 Start

根据代码中的注释,这个问题有明确的答案:

1
2
3
4
5
6
7
8
9
10
11
12
private void notifyDesiredDisplayModeSpecsChangedLocked() {
if (mDesiredDisplayModeSpecsListener != null
&& !mHandler.hasMessages(MSG_REFRESH_RATE_RANGE_CHANGED)) {
// We need to post this to a handler to avoid calling out while holding the lock
// since we know there are things that both listen for changes as well as provide
// information. If we did call out while holding the lock, then there's no
// guaranteed lock order and we run the real of risk deadlock.
Message msg = mHandler.obtainMessage(
MSG_REFRESH_RATE_RANGE_CHANGED, mDesiredDisplayModeSpecsListener);
msg.sendToTarget();
}
}

原因解析

主要目的:避免死锁

  1. 持有锁的问题: 当前方法名 notifyDesiredDisplayModeSpecsChangedLocked() 表明调用时持有锁 (mLock)

  2. 回调风险: 如果直接在持有锁的情况下调用监听器回调:

    • 监听器可能会调用其他方法
    • 这些方法可能需要获取其他锁
    • 无法保证锁的获取顺序一致
    • 容易造成死锁
  3. Handler 的作用:

    • 将回调投递到 Handler 的消息队列
    • 在 Handler 线程处理时,当前持有的锁已经释放
    • 避免了在持有锁时调用外部代码的风险

类比说明

这是一个经典的异步解耦模式:

1
持有锁 -> 需要通知外部 -> 不能直接调用(会死锁) -> 投递消息 -> 释放锁 -> Handler处理消息

类似于你在一个房间里拿着钥匙,需要叫外面的人进来,但如果你拿着钥匙直接开门让人进来,可能会撞到门。更安全的做法是先把钥匙放下,再开门让人进来。

Claude Sonnet 4.5 End

比喻手法不错

再来看 DisplayModeDirector.DisplayModeDirectorHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private final class DisplayModeDirectorHandler extends Handler {
DisplayModeDirectorHandler(Looper looper) {
super(looper, null, true /*async*/);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REFRESH_RATE_RANGE_CHANGED:
DesiredDisplayModeSpecsListener desiredDisplayModeSpecsListener =
(DesiredDisplayModeSpecsListener) msg.obj;
desiredDisplayModeSpecsListener.onDesiredDisplayModeSpecsChanged();
break;
}
}
}

那这个 DesiredDisplayModeSpecsListener 又是哪里设置的呢?在函数构造的时候并没有初始化,而是通过
DisplayModeDirector

1
2
3
4
5
6
public void setDesiredDisplayModeSpecsListener(
@Nullable DesiredDisplayModeSpecsListener desiredDisplayModeSpecsListener) {
synchronized (mLock) {
mDesiredDisplayModeSpecsListener = desiredDisplayModeSpecsListener;
}
}

DisplayManagerServicesystemReady 方法中设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public final class DisplayManagerService extends SystemService {

public void systemReady(boolean safeMode) {
// ***
mDisplayModeDirector.setDesiredDisplayModeSpecsListener(
new DesiredDisplayModeSpecsObserver());
// ***

}

class DesiredDisplayModeSpecsObserver
implements DisplayModeDirector.DesiredDisplayModeSpecsListener {

private final Consumer<LogicalDisplay> mSpecsChangedConsumer = display -> {
int displayId = display.getDisplayIdLocked();
DisplayModeDirector.DesiredDisplayModeSpecs desiredDisplayModeSpecs =
mDisplayModeDirector.getDesiredDisplayModeSpecs(displayId);
DisplayModeDirector.DesiredDisplayModeSpecs existingDesiredDisplayModeSpecs =
display.getDesiredDisplayModeSpecsLocked();
if (DEBUG) {
Slog.i(TAG,
"Comparing display specs: " + desiredDisplayModeSpecs
+ ", existing: " + existingDesiredDisplayModeSpecs);
}
if (!desiredDisplayModeSpecs.equals(existingDesiredDisplayModeSpecs)) {
display.setDesiredDisplayModeSpecsLocked(desiredDisplayModeSpecs);
mChanged = true;
}
};

@GuardedBy("mSyncRoot")
private boolean mChanged = false;

public void onDesiredDisplayModeSpecsChanged() {
synchronized (mSyncRoot) {
mChanged = false;
mLogicalDisplayMapper.forEachLocked(mSpecsChangedConsumer,
/* includeDisabled= */ false);
if (mChanged) {
scheduleTraversalLocked(false);
mChanged = false;
}
}
}
}
}

好好好,又回到 DisplayManagerService
再总结链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
应用进程                                    system_server 进程
┌──────────────────────────┐ ┌──────────────────────────┐
│ DisplayManager │ │ DisplayManagerService │
│ requestDisplayModes() │ Binder │ requestDisplayModes() │
└────────────┬─────────────┘ IPC └────────────┬─────────────┘
│ ←──────────→ │
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ DisplayManagerGlobal │ │ DisplayModeDirector │
│ requestDisplayModes() │ │ requestDisplayModes() │
└────────────┬─────────────┘ └────────────┬─────────────┘
│ │
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ IDisplayManager │ │ SystemRequestObserver │
│ (Binder Proxy) │ │ requestDisplayModes() │
└──────────────────────────┘ └────────────┬─────────────┘


┌──────────────────────────┐
│ VotesStorage │
│ updateVote() │
└────────────┬─────────────┘
│ 回调

┌──────────────────────────┐
│ DisplayModeDirector │
│ notifyDesiredDisplayMode │
│ SpecsChangedLocked() │
└────────────┬─────────────┘
│ Handler 异步

┌───────────────────────────────────────┐
│ DisplayManagerService │
│ DesiredDisplayModeSpecsObserver. │
│ onDesiredDisplayModeSpecsChanged() │
└───────────────────────────────────────┘

不支持闭包的痛苦,虽然面相接口编程是好事,让整体架构可以变得清晰,但四面八方都是接口,这时候带来的收益就不好说了

关键调用

1
display.setDesiredDisplayModeSpecsLocked(desiredDisplayModeSpecs);
1
2
3
4
5
6
final class LogicalDisplay {
public void setDesiredDisplayModeSpecsLocked(
DisplayModeDirector.DesiredDisplayModeSpecs specs) {
mDesiredDisplayModeSpecs = specs;
}
}

好像,饶了这么长一圈,最后给一个变量赋值,当然,其中应该有很多额外的逻辑,有每个类分别处理

并没有任何和 native code 交互的代码,例如调用 SurfaceControl 等

经过一些猜测,我怀疑这个特么就是给默认显示器用的,告知当前应该用多少刷新率渲染

和应用用 SurfaceView.setFrameRate,好像是一样?

死马当死马医

我还是尝试调用一下,看下会出什么日志

Cannot get MTK settings - java.lang.ClassNotFoundException: com.mediatek.provider.MtkSettingsExt$System

一加两个设备都报错

tag:DisplayManagerService | tag:DisplayDeviceRepository | tag:DisplayModeDirector | tag:SystemRequestObserver

作者

梦魇兽

发布于

2025-11-04

更新于

2025-12-18

许可协议

评论