加载文档时,FXMLLoader 究竟做了什么?

What exactly does FXMLLoader do when a document is loaded?

假设我想加载一个 FXML 文档以在我的应用程序中的某个地方使用。据我所知,有两种方法可以做到这一点:

  1. 调用静态 FXMLLoader#load(<various resource args>) 方法。
  2. 初始化 FXMLLoader(带有资源位置),然后对该实例调用 load()

我的问题是在这里“加载”FXML 文档到底做了什么。

最初,我假设静态方法会在每次调用时执行一个完整的解析“循环”,并且创建一个实例将允许多次加载以利用某种预处理表示,但非静态方法的文档load() 方法只是说明;

“从 FXML 文档加载对象层次结构。将从中加载文档的位置...”,听起来好像每次调用都会加载文档。

我正在使用 JavaFX 17。

在花了相当多的时间研究源代码之后,我觉得我可以很好地概述 FXML 在幕后如何加载功能。话虽这么说,我不能保证我没有错过任何东西。我已经仔细查看了很多我认为很重要的代码,但大部分还不是全部,我可能只是没有注意到某些东西。

这个答案应该对 JavaFX 17 有效。

作为 TLDR 回答我的问题的主要问题:据我所知,load() 调用中没有缓存信息,无论您使用静态还是非-静态版本。也就是说,非静态调用 仍然会给你带来轻微的性能提升,其中最快的是 load(InputStream inputStream) 重载,它(除了跳过一些参数处理之外) 将阻止加载程序在每次调用时打开一个新的 InputStream

我构建了一个调用图 (CallGraph Viewer),显示了 FXML 加载代码的重要部分,以使其更易于理解。 这很容易成为我答案中最有可能包含不准确之处的部分。为了生成此图,我只是将 FXMLLoader 代码复制到 Eclipse 中,并为我认为重要的代码部分生成了连接。不幸的是,该插件并不总是正确地解析包含缺失导入的代码,需要我为几个 classes 编写定义,但我留下了大部分。此外,最初的结果是难以理解的,需要进行相当多的手动清理,其中很大一部分是根据我认为某些东西听起来有用与否来完成的。

如果您不熟悉 eclipse 的图标,可以找到文档 here(确保缩放图像,或在新选项卡中打开它,否则我怀疑您能看到很多)。
是的,有三个 processEndElement() 方法具有相同的签名,它们是 Element 的子 class 中的重写方法。 如果您想知道我将所有手动清理时间花在了什么上,请尽量不要担心单个方法,更多的是整体结构。

这是我对这个混乱的分解,逐步重现调用 load() 时发生的事情:

  1. 应用程序调用 public load() 方法之一。这只是使用提供的参数调用匹配的 loadImpl() 重载(如果 load() 调用是静态的,则为静态的,反之亦然)。所有现有的 loadImpl() 重载还要求调用它们的 class,该方法试图通过 java.lang.StackWalker 提供。没有进行额外的处理。

  2. 通过 public 接口后,执行将通过 loadImpl() 调用的层次结构进行路由。每个重载只是调用一个比自身多一个参数的重载,传递自己的参数并为缺少的参数提供 null(缺少 charset 的情况除外,它被赋予默认值) .
    您给 load() 的参数越多,您在层次结构中的起始位置就越远,非静态版本在静态版本之后开始。如果调用其中一个静态重载,则会在最终静态 loadImpl() 处创建 FXMLLoader class 的实例,用于继续进行非静态调用。

  3. 一旦达到非静态 loadImpl() 调用,事情就开始变得有趣起来。如果使用 load(void) 重载,则会根据初始化 FXMLLoader 实例时设置的参数创建 InputStream,并像以前一样提供给层次结构中的下一个阶段。在最终(非静态)loadImpl()(可以使用 load(InputStream inputStream) 重载立即调用;这是我所知道的从初始 load() 调用到 XML 处理),我们最终退出 loadImpl() 层级,并移动到 XML 处理。

  4. 这里发生了两件事:

    1. 一个 ControllerAccessor 实例被赋予了 callingClass 参数传递到 loadImpl() 层次结构。我无法准确解释这个 class 是如何工作的,但它包含两个 MapcontrollerFieldscontrollerMethods,用于控制器的初始化。
    2. clearImports() 被调用,清除 packages (a List) 和 classes (a Map),两者都用于进一步的 XML 处理中。

    这里的四个变量(除了控制器变量,我对它们有点怀疑)充当后端 XML 处理周期的重要缓存数据。但是,所有加载之间都会被清除(没有逻辑控制它们的执行,如果加载成功,缓存数据将不会存活),因此使用 FXMLLoader 实例不会提高性能 由于 数据缓存(它仍然值得使用一个,但是,因为非静态调用跳过了大部分 loadImpl() 层次结构,如果使用那个特定的,你甚至可以重用 InputStream过载)。

  5. 接下来,加载 XML 解析器本身。首先,创建一个 XMLInputFactory 的新实例。然后使用它从提供的 InputStream 创建一个 XmlStreamReader

    最后,我们现在开始实际处理加载的XML。

  6. 主要的XML处理循环其实解释起来比较简单;
    首先,代码进入 while 循环,检查 xmlStreamReader.hasNext() 的值。

    在每个循环中,都会输入一个 switch 语句,根据 XML reader 遇到的情况将执行路由到不同的 process<X>() 方法。这些方法处理传入事件,并使用更多“后端”方法的分类来执行常见操作(调用图的 'backend XML processing' 部分只是实际代码的一小部分).这些包括像 processImports() 这样的方法,它调用 importPackage()importClass(),然后填充 packagesclasses 缓存。这些缓存由 getType() 访问,这是许多其他处理方法使用的后端方法。

    另外,我认为部分控制器在这个阶段被“分配”了;例如,processEndElements() 最终会调用 getControllerFields()getControllerMethods(),它们会访问上述 controllerFieldscontrollerMethods 缓存,但有时也会 modify 他们。话虽这么说,调用图有点太深了,我现在不容易理解,而且这些方法也是稍后调用的,所以我不能确定。

  7. 经过XML处理后,一个控制器(controllers?看下面评论)被初始化。您可以在 James_D 的回答 中阅读一些关于控制器初始化的内容,但我没有太多要说的,因为这是部分我对理解最没有信心。

    话虽如此,值得注意的是这段代码不在之前的 while 循环中;只调用一个初始化方法。要么看起来像一个调用实际上是多个调用(这绝对是可能的;调用的初始化“方法”由 controllerAccessor.getControllerMethods() 返回,而“它”是使用 MethodHelper JavaFX class 调用的),或者这里只初始化一个控制器(假设是根节点的控制器),其他的在解析时初始化。我倾向于第一种可能性,但这纯粹是基于直觉。

  8. 最后(如果您现在还在阅读,请认为我印象深刻),我们进入清理。这个阶段超级简单;

    1. ControllerAccessor 的“调用 class”变量已清空,其 controllerFieldscontrollerMethods 缓存已清除。
    2. XmlStreamReader 实例已取消。
    3. 返回根节点,函数退出。

感谢@jewelsea 提供其他答案的链接并推荐我查看源代码。