• 记一次解决 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.

  • 相关阅读:
    信号发生器的设计(期末课程设计)
    小程序异步获取openid解决方案
    微信小程序生成二维码之传参(接收的参数乱码该咋解决)
    小程序tabBar,点击进入tabBar刷新tabBar页面
    微信小程序函数执行顺序问题
    RESTful规范与常用状态码
    dedeCMS二次开发api简单接口代码
    DEDECMS怎么获取当前栏目及所有子栏目的文章数量
    DedeCMSV57数据库结构文档(数据字典)
    【JQuery插件】把网页或某div或table表格内容转为图片并下载
  • 原文地址:https://www.cnblogs.com/beansoft/p/16220407.html
Copyright © 2020-2023  润新知