跳转到内容

IntelliJ 平台中的 Disposer

本文是一个案例研究,旨在简化 IntelliJ 平台,并介绍 IntelliJ 平台中的 Disposer。

事实上,SDK 文档已经对这一点进行了更详细的描述:Disposer 和 Disposable,在开始之前,最好先看一下这里。

我在这里模拟了一个场景。有一个 ToolWindow 布局如下,这里我们使用 UI Inspector 来分析布局:

一个带有 BorderLayout 的 rootPanel,顶部(North)是一个 Toolbar,中心(Center)是一个 JPanel。点击 ➕ 将添加一个随机代码片段到 Center 的 JPanel。

场景如上所示,是一个非常简单的 ToolWindow,功能也非常简单。

这是代码片段的关键部分:

MyToolWindowFactory.java
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory;
import com.obiscr.template.dispose.MainPanel;
import org.jetbrains.annotations.NotNull;
public class MyToolWindowFactory implements ToolWindowFactory {
@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
ContentFactory contentFactory = ContentFactory.getInstance();
MainPanel panel = new MainPanel(project);
Content content = contentFactory.createContent(panel.getComponent(), "Dispose", false);
toolWindow.getContentManager().addContent(content);
}
}

and

MainPanel.java
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.EditorSettings;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
import com.intellij.openapi.project.Project;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.ui.components.panels.VerticalLayout;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
public class MainPanel {
private final JPanel rootPanel;
private final JPanel centerPanel;
private final Project myProject;
public MainPanel(Project project) {
myProject = project;
rootPanel = new JPanel(new BorderLayout());
centerPanel = new JPanel(new VerticalLayout(JBUI.scale(10)));
centerPanel.setBorder(JBUI.Borders.empty(10));
DefaultActionGroup actionGroup = new DefaultActionGroup();
AnAction addAction = new AnAction(() -> "Add Item", AllIcons.General.Add) {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
String text = "// Code snippet at " + LocalDateTime.now().
format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + "\n";
text += HelloWorld.getRandomHelloWorldCode();
Editor editor = createEditor(myProject, text);
centerPanel.add(editor.getComponent());
centerPanel.revalidate();
centerPanel.repaint();
}
};
actionGroup.add(addAction);
ActionToolbarImpl actionToolbar = new ActionToolbarImpl("MyPanelToolbar", actionGroup, true);
actionToolbar.setTargetComponent(rootPanel);
actionToolbar.setBorder(JBUI.Borders.customLine(UIUtil.getBoundsColor(), 1,0,1,0));
rootPanel.add(actionToolbar, BorderLayout.NORTH);
rootPanel.add(centerPanel, BorderLayout.CENTER);
}
public JPanel getComponent() {
return rootPanel;
}
private Editor createEditor(Project project, String text) {
EditorFactory editorFactory = EditorFactory.getInstance();
EditorEx editor = (EditorEx) editorFactory.createViewer(
editorFactory.createDocument(text),
project);
// Others settings
// ...
return editor;
}
}

这里我们只是创建了 Editor 对象,但当程序退出时,我们没有清理它占用的内存。

当关闭 IDE 时,会显示相关异常消息(当然,这些信息也会显示在 idea.log 中):

根据错误日志,我们可以知道这是因为创建的 Editor 对象在应用程序关闭时没有释放,存在内存溢出的风险。

好的,现在我们已经知道问题所在,接下来我们解决这个问题,解决方案也很简单,只需要关闭 IDE,释放这个对象即可。

首先,让 MainPanel 实现 Disposable 接口并重写 dispose() 方法。然后,在创建 editor 后,将 Disposer 注册到父 this 中,表示 当这个对象被释放时,Editor 将被销毁

public class MainPanel implements Disposable {
@Override
public void dispose() {
centerPanel.removeAll();
rootPanel.removeAll();
}
private Editor createEditor(Project project, String text) {
EditorEx editor = ...;
Disposer.register(this, () -> EditorFactory.getInstance().releaseEditor(editor));
return editor;
}
}

此外,由于在这种情况下组件都基于 Content,我们还将 MainPanel 注册为 Content 的 Disposer。

public class MyToolWindowFactory implements ToolWindowFactory {
@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
MainPanel panel = new MainPanel(project);
Content content = ...;
content.setDisposer(panel);
...
}
}

ContentImpl.java 的源代码如下,你可以看到当 content 被销毁时,它会调用 Disposer.dispose(myDisposer); 来销毁设置的 myDisposer,也就是上面的 MainPanel。

@Override
public void dispose() {
if (myShouldDisposeContent && myComponent instanceof Disposable) {
Disposer.dispose((Disposable)myComponent);
}
if (myDisposer != null) {
Disposer.dispose(myDisposer);
myDisposer = null;
}
myFocusRequest = null;
clearUserData();
}

那么现在整个过程如下:

Terminal window
content.dispose() --(call)--> panel.dispose() --(call)--> EditorFactory.getInstance().releaseEditor(editor);

所以,当 content 被关闭时,Editor 对象也会被销毁。