记一次解决 Flutter 官方 IDEA 插件 bug 的过程
记一次解决 Flutter 官方 IDEA 插件 bug 的过程
在2021年1月份的时候, Jetbrains IDEA 推出了年度的新版2021.1 EAP 版本, 然而随着这次更新, Google的Flutter 插件和本人维护的都在启动时出现了一个奇怪的报错信息, 从而导致无法运行时选择设备:
https://github.com/flutter/flutter-intellij/issues/5223
Trying to reset custom component in a presentation
java.lang.Throwable
at com.intellij.openapi.actionSystem.Presentation.putClientProperty(Presentation.java:403)
at com.intellij.openapi.actionSystem.Presentation.copyFrom(Presentation.java:376)
at com.intellij.openapi.actionSystem.impl.ActionUpdater.lambda$reflectSubsequentChangesInOriginalPresentation$9(ActionUpdater.java:131)
at java.desktop/java.beans.PropertyChangeSupport.fire(PropertyChangeSupport.java:341)
at java.desktop/java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:333)
at java.desktop/java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:266)
at com.intellij.openapi.actionSystem.Presentation.fireObjectPropertyChange(Presentation.java:342)
at com.intellij.openapi.actionSystem.Presentation.setTextWithMnemonic(Presentation.java:190)
at com.intellij.openapi.actionSystem.Presentation.setText(Presentation.java:145)
at com.intellij.openapi.actionSystem.Presentation.setText(Presentation.java:200)
at io.flutter.actions.DeviceSelectorAction.updateActions(DeviceSelectorAction.java:164)
at io.flutter.actions.DeviceSelectorAction.lambda$update$3(DeviceSelectorAction.java:84)
at com.intellij.openapi.application.impl.ApplicationImpl.invokeAndWait(ApplicationImpl.java:433)
at io.flutter.FlutterUtils.invokeAndWait(FlutterUtils.java:97)
at io.flutter.actions.DeviceSelectorAction.update(DeviceSelectorAction.java:83)
at io.flutter.actions.DeviceSelectorAction.lambda$update$1(DeviceSelectorAction.java:73)
at io.flutter.run.daemon.DeviceService.lambda$fireChangeEvent$4(DeviceService.java:150)
at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:313)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:776)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:727)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:746)
at com.intellij.ide.IdeEventQueue.defaultDispatchEvent(IdeEventQueue.java:972)
at com.intellij.ide.IdeEventQueue._dispatchEvent(IdeEventQueue.java:838)
at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$8(IdeEventQueue.java:448)
at com.intellij.openapi.progress.impl.CoreProgressManager.computePrioritized(CoreProgressManager.java:775)
at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$9(IdeEventQueue.java:447)
at com.intellij.openapi.application.impl.ApplicationImpl.runIntendedWriteActionOnCurrentThread(ApplicationImpl.java:799)
at com.intellij.ide.IdeEventQueue.dispatchEvent(IdeEventQueue.java:501)
at com.intellij.cloudConfig.CloudConfigManager.waitDone(CloudConfigManager.java:1856)
at com.intellij.cloudConfig.CloudConfigManager.getRepositoryPlugin(CloudConfigManager.java:1841)
at com.intellij.cloudConfig.CloudConfigManager.lambda$updatePlugins$45(CloudConfigManager.java:1915)
at com.intellij.cloudConfig.CloudConfigManager.mergePlugins(CloudConfigManager.java:2082)
at com.intellij.cloudConfig.CloudConfigManager.mergePlugins(CloudConfigManager.java:2070)
at com.intellij.cloudConfig.CloudConfigManager.updatePlugins(CloudConfigManager.java:1917)
at com.intellij.cloudConfig.CloudConfigManager.safeUpdatePlugins(CloudConfigManager.java:1865)
at com.intellij.cloudConfig.CloudConfigManager.lambda$doConnection$20(CloudConfigManager.java:996)
at com.intellij.openapi.application.TransactionGuardImpl.runWithWritingAllowed(TransactionGuardImpl.java:221)
at com.intellij.openapi.application.TransactionGuardImpl.access$200(TransactionGuardImpl.java:24)
at com.intellij.openapi.application.TransactionGuardImpl$2.run(TransactionGuardImpl.java:203)
at com.intellij.openapi.application.impl.ApplicationImpl.runIntendedWriteActionOnCurrentThread(ApplicationImpl.java:799)
at com.intellij.openapi.application.impl.ApplicationImpl.lambda$invokeLater$4(ApplicationImpl.java:322)
at com.intellij.openapi.application.impl.FlushQueue.doRun(FlushQueue.java:84)
at com.intellij.openapi.application.impl.FlushQueue.runNextEvent(FlushQueue.java:133)
at com.intellij.openapi.application.impl.FlushQueue.flushNow(FlushQueue.java:46)
at com.intellij.openapi.application.impl.FlushQueue$FlushNow.run(FlushQueue.java:189)
at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:313)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:776)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:727)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:746)
at com.intellij.ide.IdeEventQueue.defaultDispatchEvent(IdeEventQueue.java:972)
at com.intellij.ide.IdeEventQueue._dispatchEvent(IdeEventQueue.java:838)
at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$8(IdeEventQueue.java:448)
at com.intellij.openapi.progress.impl.CoreProgressManager.computePrioritized(CoreProgressManager.java:775)
at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$9(IdeEventQueue.java:447)
at com.intellij.openapi.application.impl.ApplicationImpl.runIntendedWriteActionOnCurrentThread(ApplicationImpl.java:799)
at com.intellij.ide.IdeEventQueue.dispatchEvent(IdeEventQueue.java:495)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
出现问题的类的代码 io.flutter.actions.DeviceSelectorAction
/*
* Copyright 2016 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
package io.flutter.actions;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ComboBoxAction;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.SystemInfo;
import icons.FlutterIcons;
import io.flutter.FlutterBundle;
import io.flutter.FlutterUtils;
import io.flutter.run.FlutterDevice;
import io.flutter.run.daemon.DeviceService;
import io.flutter.sdk.AndroidEmulatorManager;
import io.flutter.utils.FlutterModuleUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.*;
public class DeviceSelectorAction extends ComboBoxAction implements DumbAware {
private final List actions = new ArrayList<>();
private final List knownProjects = Collections.synchronizedList(new ArrayList<>());
private SelectDeviceAction selectedDeviceAction;
DeviceSelectorAction() {
setSmallVariant(true);
}
@NotNull
@Override
protected DefaultActionGroup createPopupActionGroup(JComponent button) {
final DefaultActionGroup group = new DefaultActionGroup();
group.addAll(actions);
return group;
}
@Override
protected boolean shouldShowDisabledActions() {
return true;
}
@Override
public void update(final AnActionEvent e) {
// Suppress device actions in all but the toolbars.
final String place = e.getPlace();
if (!Objects.equals(place, ActionPlaces.NAVIGATION_BAR_TOOLBAR) && !Objects.equals(place, ActionPlaces.MAIN_TOOLBAR)) {
e.getPresentation().setVisible(false);
return;
}
// Only show device menu when the device daemon process is running.
final Project project = e.getProject();
if (!isSelectorVisible(project)) {
e.getPresentation().setVisible(false);
return;
}
super.update(e);
if (!knownProjects.contains(project)) {
knownProjects.add(project);
Disposer.register(project, () -> knownProjects.remove(project));
DeviceService.getInstance(project).addListener(() -> update(project, e.getPresentation()));
// Listen for android device changes, and rebuild the menu if necessary.
AndroidEmulatorManager.getInstance(project).addListener(() -> update(project, e.getPresentation()));
}
update(project, e.getPresentation());
}
private void update(Project project, Presentation presentation) {
FlutterUtils.invokeAndWait(() -> {
updateActions(project, presentation);
updateVisibility(project, presentation);
});
}
private static void updateVisibility(final Project project, final Presentation presentation) {
final boolean visible = isSelectorVisible(project);
presentation.setVisible(visible);
final JComponent component = (JComponent)presentation.getClientProperty("customComponent");
if (component != null) {
component.setVisible(visible);
if (component.getParent() != null) {
component.getParent().doLayout();
component.getParent().repaint();
}
}
}
private static boolean isSelectorVisible(@Nullable Project project) {
return project != null &&
DeviceService.getInstance(project).getStatus() != DeviceService.State.INACTIVE &&
FlutterModuleUtils.hasFlutterModule(project);
}
private void updateActions(@NotNull Project project, Presentation presentation) {
actions.clear();
final DeviceService deviceService = DeviceService.getInstance(project);
final FlutterDevice selectedDevice = deviceService.getSelectedDevice();
final Collection devices = deviceService.getConnectedDevices();
selectedDeviceAction = null;
for (FlutterDevice device : devices) {
final SelectDeviceAction deviceAction = new SelectDeviceAction(device, devices);
actions.add(deviceAction);
if (Objects.equals(device, selectedDevice)) {
selectedDeviceAction = deviceAction;
final Presentation template = deviceAction.getTemplatePresentation();
presentation.setIcon(template.getIcon());
presentation.setText(deviceAction.presentationName());
presentation.setEnabled(true);
}
}
// Show the 'Open iOS Simulator' action.
if (SystemInfo.isMac) {
boolean simulatorOpen = false;
for (AnAction action : actions) {
if (action instanceof SelectDeviceAction) {
final SelectDeviceAction deviceAction = (SelectDeviceAction)action;
final FlutterDevice device = deviceAction.device;
if (device.isIOS() && device.emulator()) {
simulatorOpen = true;
}
}
}
actions.add(new Separator());
actions.add(new OpenSimulatorAction(!simulatorOpen));
}
// Add Open Android emulators actions.
final List emulatorActions = OpenEmulatorAction.getEmulatorActions(project);
if (!emulatorActions.isEmpty()) {
actions.add(new Separator());
actions.addAll(emulatorActions);
}
if (devices.isEmpty()) {
final boolean isLoading = deviceService.getStatus() == DeviceService.State.LOADING;
if (isLoading) {
presentation.setText(FlutterBundle.message("devicelist.loading"));
}
else {
//noinspection DialogTitleCapitalization
presentation.setText("");
}
}
else if (selectedDevice == null) {
//noinspection DialogTitleCapitalization
presentation.setText("");
}
}
// Show the current device as selected when the combo box menu opens.
@Override
protected Condition getPreselectCondition() {
return action -> action == selectedDeviceAction;
}
private static class SelectDeviceAction extends AnAction {
@NotNull
private final FlutterDevice device;
SelectDeviceAction(@NotNull FlutterDevice device, @NotNull Collection devices) {
super(device.getUniqueName(devices), null, FlutterIcons.Phone);
this.device = device;
}
public String presentationName() {
return device.presentationName();
}
@Override
public void actionPerformed(AnActionEvent e) {
final Project project = e.getProject();
final DeviceService service = project == null ? null : DeviceService.getInstance(project);
if (service != null) {
service.setSelectedDevice(device);
}
}
}
}
首先检查了 IDEA 源代码 Presentation.java
的变更记录, 但是这个类很久都没变化了, 考虑到IDEA的复杂度, 暂时还是不太可能找到原因, 但是很明显其它的很多运行时修改文字的 Action 都是正常的, 所以换一种思路研究. 结合 IDEA 经常禁止在非EDT(Event Dispatch Thread, 事件分发线程)中做异步操作的限制, 研究了 , 再并结合报错日志, 查看164行的代码, 这行代码 presentation.setText("
实际上是很简单的, 就是更新按钮下拉项的文案. 可以看到此代码是通过Flutter插件刷新了设备列表后触发的更新Action列表的操作, 是一个异步的操作, 因此第一步怀疑是不是这一行代码导致了问题, 因此尝试注释掉了此行代码后, 果然问题消失了, 但是问题还是未解决, 因为这个文案必须是要被更新掉的, 如果把这个代码直接放到EDT里面, 是不会报错的. 需要注意的是, IDEA的Action系统中, public void update(final AnActionEvent e)
调用的频次是非常高的, 用户每次移动鼠标, 按下键盘, 切换到别的窗口或者点击界面上任何部分, 都会触发一次, 因此在此方法中的代码执行速度越快越好, 不允许长时间耗时的代码存在, 否则整个IDEA将会变得极其卡顿. 另外一个问题就是如果将变更文本的动作加入到了update
方法中之后, 异步执行的动作完成的时候就没办法直接更新文本了, 如上所属, 触发update
必须要有某些用户动作来进行, 但实际上IDEA也提供一个内部方法来触发相关的update
, 那就是调用方法:
ActivityTracker.getInstance().inc();
最终解决方案:
- move all update presentation codes out of async thread 将所有更新表示层的代码从异步线程代码中移出, 并放置到
update
方法中 - when devices thread updating finished, call 当设备列表线程更新完成后, 调用:
// Notify the IDE system to update AnAction
ActivityTracker.getInstance().inc();
将方案告诉了Google的波兰开发者 Devon Carew, 最终通过相同办法解决了这个bug.
最终修改后的代码如下:
public class DeviceSelectorAction extends ComboBoxAction implements DumbAware {
private final List actions = new ArrayList<>();
private final List knownProjects = Collections.synchronizedList(new ArrayList<>());
private SelectDeviceAction selectedDeviceAction;
public DeviceSelectorAction() {
setSmallVariant(true);
}
@NotNull
@Override
protected DefaultActionGroup createPopupActionGroup(JComponent button) {
final DefaultActionGroup group = new DefaultActionGroup();
group.addAll(actions);
return group;
}
@Override
protected boolean shouldShowDisabledActions() {
return true;
}
@Override
public void update(final AnActionEvent e) {
//// Suppress device actions in all but the toolbars.
//final String place = e.getPlace();
//if (!Objects.equals(place, ActionPlaces.NAVIGATION_BAR_TOOLBAR) && !Objects.equals(place, ActionPlaces.MAIN_TOOLBAR)) {
// e.getPresentation().setVisible(false);
// return;
//}
// Only show device menu when the device daemon process is running.
final Project project = e.getProject();
if (!isSelectorVisible(project)) {
e.getPresentation().setVisible(false);
return;
}
super.update(e);
if (!knownProjects.contains(project)) {
knownProjects.add(project);
Disposer.register(project, () -> knownProjects.remove(project));
DeviceService.getInstance(project).addListener(() -> update(project, e.getPresentation()));
// Listen for android device changes, and rebuild the menu if necessary.
AndroidEmulatorManager.getInstance(project).addListener(() -> update(project, e.getPresentation()));
update(project, e.getPresentation());
}
final DeviceService deviceService = DeviceService.getInstance(project);
final FlutterDevice selectedDevice = deviceService.getSelectedDevice();
final Collection devices = deviceService.getConnectedDevices();
Presentation presentation = e.getPresentation();
final boolean visible = isSelectorVisible(project);
presentation.setVisible(visible);
if (devices.isEmpty()) {
final boolean isLoading = deviceService.getStatus() == DeviceService.State.LOADING;
if (isLoading) {
presentation.setText(FlutterBundle.message("devicelist.loading"));
}
else {
//noinspection DialogTitleCapitalization
presentation.setText("");
}
}
else if (selectedDevice == null) {
//noinspection DialogTitleCapitalization
presentation.setText("");
} else if(selectedDeviceAction != null) {
final Presentation template = selectedDeviceAction.getTemplatePresentation();
presentation.setIcon(template.getIcon());
presentation.setText(selectedDevice.presentationName());
presentation.setEnabled(true);
}
}
private void update(Project project, Presentation presentation) {
FlutterUtils.invokeAndWait(() -> {
updateActions(project, presentation);
updateVisibility(project, presentation);
});
}
private static void updateVisibility(final Project project, final Presentation presentation) {
final boolean visible = isSelectorVisible(project);
//presentation.setVisible(visible);
final JComponent component = (JComponent)presentation.getClientProperty("customComponent");
if (component != null) {
component.setVisible(visible);
if (component.getParent() != null) {
component.getParent().doLayout();
component.getParent().repaint();
}
}
}
private static boolean isSelectorVisible(@Nullable Project project) {
return project != null &&
DeviceService.getInstance(project).getStatus() != DeviceService.State.INACTIVE &&
FlutterModuleUtils.hasFlutterModule(project);
}
private void updateActions(@NotNull Project project, Presentation presentation) {
actions.clear();
final DeviceService deviceService = DeviceService.getInstance(project);
final FlutterDevice selectedDevice = deviceService.getSelectedDevice();
final Collection devices = deviceService.getConnectedDevices();
selectedDeviceAction = null;
for (FlutterDevice device : devices) {
final SelectDeviceAction deviceAction = new SelectDeviceAction(device, devices);
actions.add(deviceAction);
if (Objects.equals(device, selectedDevice)) {
selectedDeviceAction = deviceAction;
}
}
// Show the 'Open iOS Simulator' action.
if (SystemInfo.isMac) {
boolean simulatorOpen = false;
for (AnAction action : actions) {
if (action instanceof SelectDeviceAction) {
final SelectDeviceAction deviceAction = (SelectDeviceAction)action;
final FlutterDevice device = deviceAction.device;
if (device.isIOS() && device.emulator()) {
simulatorOpen = true;
}
}
}
actions.add(new Separator());
actions.add(new OpenSimulatorAction(!simulatorOpen));
actions.add(new RefreshDeviceAction());
}
// Add Open Android emulators actions.
final List emulatorActions = OpenEmulatorAction.getEmulatorActions(project);
if (!emulatorActions.isEmpty()) {
actions.add(new Separator());
actions.addAll(emulatorActions);
}
// Notify the IDE system to update AnAction
ActivityTracker.getInstance().inc();
}
// Show the current device as selected when the combo box menu opens.
@Override
protected Condition getPreselectCondition() {
return action -> action == selectedDeviceAction;
}
private static class SelectDeviceAction extends AnAction {
@NotNull
private final FlutterDevice device;
SelectDeviceAction(@NotNull FlutterDevice device, @NotNull Collection devices) {
super(device.getUniqueName(devices), null, FlutterIcons.Phone);
this.device = device;
}
public String presentationName() {
return device.presentationName();
}
@Override
public void actionPerformed(AnActionEvent e) {
final Project project = e.getProject();
final DeviceService service = project == null ? null : DeviceService.getInstance(project);
if (service != null) {
service.setSelectedDevice(device);
}
}
}
}
最终代码在https://github.com/flutter/flutter-intellij/pull/5301中合并Build EAP and Canary #5301.