Let's say I want to load an FXML document to be used somewhere in my application. As far as I'm aware, there are two ways of doing this:
FXMLLoader#load(<various resource args>)
method.load()
on that instance.My question is what exactly "loading" an FXML document does here.
Initially, I assumed the static method would do an entire parse "cycle" on every call, and that creating an instance would allow multiple loads to take advantage of some kind of preprocessed representation, but documentation for the non-static load()
method just states;
"Loads an object hierarchy from an FXML document. The location from which the document will be loaded...", which sounds like the document is loaded on every call.
I'm using JavaFX 17.
After spending a fair bit of time with the source, I feel I can give a pretty good overview of how FXML loading functions behind the scenes. That being said, I can't guarantee that there isn't anything I didn't miss. I've thoroughly looked over quite a bit of code I thought to be important, but most isn't all, and I may have simply not noticed something.
This answer should be valid for JavaFX 17.
As a TLDR answering the main concern of my question: As far as I can tell, no information is cached across load()
calls, regardless of whether you use the static or non-static versions. That being said, the non-static calls will still give you a slight performance gain, the fastest of which is the load(InputStream inputStream)
overload, which (in addition to skipping some argument processing) will prevent the loader from opening a new InputStream
on every call.
I've built a call graph (CallGraph Viewer) showing important parts of the FXML loading code in order to make it a bit more digestible.
This is easily the most likely part of my answer to contain inaccuracies. To generate this graph, I simply copied the FXMLLoader
code into eclipse and generated connections for parts of the code I deemed important. Unfortunately, the plugin doesn't always correctly parse code containing missing imports, requiring me to write in definitions for a couple of classes, but I left most alone. Additionally, the initial result was incomprehensible and needed a fair bit of manual cleanup, a large portion of which was done simply based on whether I thought something sounded useful or not.
If you are unfamiliar with eclipse's icons, documentation can be found here (make sure to zoom the image, or open it in a new tab, or I doubt you will be able to see much).
Yes, there are three processEndElement()
methods with the same signature, they are overridden methods in subclasses of Element
.
If you're wondering what I spent all that manual cleanup time on, try not to worry about the individual methods, more the overall structure.
Here's my breakdown of this mess as a step by step recreation of what happens when load()
is called:
The application calls one of the public load()
methods. This simply calls a matching loadImpl()
overload (static if the load()
call was static and vice-versa) with the provided arguments. All existing loadImpl()
overloads also ask for the class which called them, which the method attempts to provide with a java.lang.StackWalker
. No additional processing is done.
After passing the public interface, execution is routed through a hierarchy of loadImpl()
calls. Each overload just calls an overload with one more argument than itself, passing on its own arguments and giving null
for the missing one (except in the case of a missing charset
, which is given a default value).
The more arguments you give to load()
, the farther you start in the hierarchy, with non-static versions beginning after the static ones. If you call one of the static overloads, an instance of the FXMLLoader
class is created at the final static loadImpl()
, which is used to continue onto the non-static calls.
Once reaching the non-static loadImpl()
calls, things begin to get interesting. If using the load(void)
overload, an InputStream
is created based on arguments set when the FXMLLoader
instance was initialized, and is given to the next stage in the hierarchy as before. At the final (non-static) loadImpl()
(which can be called immediately using the load(InputStream inputStream)
overload; this is the fastest method I know of to get from the initial load()
call to XML processing), we finally exit the loadImpl()
hierarchy, and move to XML processing.
Two things happen here:
ControllerAccessor
instance is given the callingClass
argument passed up the loadImpl()
hierarchy. I can't exactly explain how this class works, but it contains two Map
's; controllerFields
and controllerMethods
, used in the initialization of controllers.clearImports()
is called, clearing packages
(a List
) and classes
(a Map
), both used in further XML processing.The four variables here (except for maybe the controller ones, I'm a little iffy on them) act as important cache data for the backend XML processing cycle. However, all are cleared between loads (there is no logic controlling their execution, if the load succeeded, the cache data will not have survived), so using an FXMLLoader instance will not improve performance due to data caching (it's still worth using one, however, as the non-static calls skip much of the loadImpl()
hierarchy, and you can even reuse the InputStream
if using that particular overload).
Next, the XML parser itself is loaded. First, a new instance of a XMLInputFactory
is created. This is then used to create a XmlStreamReader
from the provided InputStream
Finally, we now begin actually processing the loaded XML.
The main XML processing loop is actually relatively simple to explain;
First, the code enters a while loop, checking the value of xmlStreamReader.hasNext()
.
During each cycle, a switch statement is entered, routing execution to different process<X>()
methods depending on what the XML reader encounters. Those methods process the incoming events, and use an assortment of more "backend" methods to carry out common operations (The 'backend XML processing' section of the call graph is only a small portion of the actual code). These include methods like processImports()
, which calls importPackage()
or importClass()
, in turn populating the packages
and classes
caches. Those caches are accessed by getType()
, a backend method used by many other processing methods.
Additionally, I think that some part of controllers is "assigned" during this stage; processEndElements()
, for example, ends up calling getControllerFields()
or getControllerMethods()
, which access the aforementioned controllerFields
and controllerMethods
caches, but also sometimes modify them. That being said, the call graph gets a bit too deep for me to easily understand at this point, and those methods are also called later, so I can't be sure.
After XML processing, a controller (controllers? see comment below) is initialized. You can read about controller initialization a bit in James_D's answer here, but I don't have much to say about it, as this is the section I am least confident in understanding.
That being said, it is interesting to note that this code is out of the previous while
loop; only one initialization method is called. Either what seems like one call is actually multiple (which is definitely possible; the initialization "method" called is returned by controllerAccessor.getControllerMethods()
and "it" is called using the MethodHelper
JavaFX class), or only one controller is initialized here (assumedly the controller for the root node) and the others are initialized during parsing. I'd lean towards the first possibility here, but that's based purely on intuition.
Finally (and if you're still reading by now, consider me impressed), we enter cleanup. This stage is super simple;
ControllerAccessor
has its "calling class" variable nulled, and its controllerFields
and controllerMethods
caches cleared.XmlStreamReader
instance is nulled.Thanks to @jewelsea for links to other answers and for recommending I look at the source.