Issue
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:
- Call the static
FXMLLoader#load(<various resource args>)
method. - Initialize an FXMLLoader (with the resource location), and then call
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.
Solution
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 matchingloadImpl()
overload (static if theload()
call was static and vice-versa) with the provided arguments. All existingloadImpl()
overloads also ask for the class which called them, which the method attempts to provide with ajava.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 givingnull
for the missing one (except in the case of a missingcharset
, which is given a default value).
The more arguments you give toload()
, 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 theFXMLLoader
class is created at the final staticloadImpl()
, which is used to continue onto the non-static calls.Once reaching the non-static
loadImpl()
calls, things begin to get interesting. If using theload(void)
overload, anInputStream
is created based on arguments set when theFXMLLoader
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 theload(InputStream inputStream)
overload; this is the fastest method I know of to get from the initialload()
call to XML processing), we finally exit theloadImpl()
hierarchy, and move to XML processing.Two things happen here:
- a
ControllerAccessor
instance is given thecallingClass
argument passed up theloadImpl()
hierarchy. I can't exactly explain how this class works, but it contains twoMap
's;controllerFields
andcontrollerMethods
, used in the initialization of controllers. clearImports()
is called, clearingpackages
(aList
) andclasses
(aMap
), 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 theInputStream
if using that particular overload).- a
Next, the XML parser itself is loaded. First, a new instance of a
XMLInputFactory
is created. This is then used to create aXmlStreamReader
from the providedInputStream
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 ofxmlStreamReader.hasNext()
.
During each cycle, a switch statement is entered, routing execution to differentprocess<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 likeprocessImports()
, which callsimportPackage()
orimportClass()
, in turn populating thepackages
andclasses
caches. Those caches are accessed bygetType()
, 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 callinggetControllerFields()
orgetControllerMethods()
, which access the aforementionedcontrollerFields
andcontrollerMethods
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 previouswhile
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 bycontrollerAccessor.getControllerMethods()
and "it" is called using theMethodHelper
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;
- The
ControllerAccessor
has its "calling class" variable nulled, and itscontrollerFields
andcontrollerMethods
caches cleared. - The
XmlStreamReader
instance is nulled. - The root node is returned, and thus the function exits.
- The
Thanks to @jewelsea for links to other answers and for recommending I look at the source.
Answered By - the4naves
Answer Checked By - Cary Denson (JavaFixing Admin)