记一次解决 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.
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步