记一次解决 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();

最终解决方案:

  1. move all update presentation codes out of async thread 将所有更新表示层的代码从异步线程代码中移出, 并放置到update方法中
  2. 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.

posted @ 2022-05-04 20:42  beansoft  阅读(525)  评论(0编辑  收藏  举报