记一次解决 Flutter 官方 IDEA 插件 bug 的过程
记一次解决 Flutter 官方 IDEA 插件 bug 的过程
在2021年1月份的时候, Jetbrains IDEA 推出了年度的新版2021.1 EAP 版本, 然而随着这次更新, Google的Flutter 插件和本人维护的Flutter Storm 插件都在启动时出现了一个奇怪的报错信息, 从而导致无法运行时选择设备:
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<AnAction> actions = new ArrayList<>();
private final List<Project> 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<FlutterDevice> 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<OpenEmulatorAction> 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("<no devices>");
}
}
else if (selectedDevice == null) {
//noinspection DialogTitleCapitalization
presentation.setText("<no device selected>");
}
}
// Show the current device as selected when the combo box menu opens.
@Override
protected Condition<AnAction> getPreselectCondition() {
return action -> action == selectedDeviceAction;
}
private static class SelectDeviceAction extends AnAction {
@NotNull
private final FlutterDevice device;
SelectDeviceAction(@NotNull FlutterDevice device, @NotNull Collection<FlutterDevice> 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("<no devices>")
实际上是很简单的, 就是更新按钮下拉项的文案. 可以看到此代码是通过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<AnAction> actions = new ArrayList<>();
private final List<Project> 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<FlutterDevice> 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("<no devices>");
}
}
else if (selectedDevice == null) {
//noinspection DialogTitleCapitalization
presentation.setText("<no device selected>");
} 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<FlutterDevice> 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<OpenEmulatorAction> 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<AnAction> getPreselectCondition() {
return action -> action == selectedDeviceAction;
}
private static class SelectDeviceAction extends AnAction {
@NotNull
private final FlutterDevice device;
SelectDeviceAction(@NotNull FlutterDevice device, @NotNull Collection<FlutterDevice> 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.