场景加载太慢

我正在构建一个JavaFX应用程序,我想知道是否有关于如何尽快在当前Stage加载新Scene的建议(最佳实践)。

目前我正在做的是(或多或少)这个:

 Parent root = (Parent)myFXLoader.load(); currentStage.setScene(new Scene (root); 

以上工作对于简单的Scene工作得很好而且足够快但在加载更复杂的场景时会初始化TableViewCombobox等等, Scene之间的过渡需要很多秒才很烦人。

我在Controllerinitialize(URL url, ResourceBundle rb)方法中做的所有初始化。
在那里我将项目添加到Choice/Combo框,初始化TableView等,但正如我所说,它需要太多时间。
难道我做错了什么? 我应该在其他地方初始化吗?

谢谢。

编辑:
任何有兴趣帮助解决这个问题,或者甚至为他们的项目获取想法的人,我都已经在google.com上传了我项目的一部分(Netbeans项目)。
你可以使用SVN查看它。 这是链接:
http://tabularasafx.googlecode.com/svn/trunk/
userName:tabularasafx-read-only
无需密码
运行项目后的说明:
第一个屏幕是登录屏幕,只需单击确定
第二个屏幕是“homePage”,在那里你可以看到一个treeView菜单并导航到4个不同的屏幕
我的问题是classes-> create page的加载时间。 看看它,如果你发现任何东西,请告诉我

编辑:
我对@jewelsea提出了3项修改。
我使用HashMap来保存每个屏幕的所有控制器
2.我只更新场景的一部分而不是整个场景
3.我使用了JavaFX2的答案- 在向网格面板动态添加自定义(fxml)面板以帮助控制器加载速度时, 性能非常差 ,如答案中所述。

现在一切都快得多!!!!
随意使用该项目作为指导
此外,我更新程序以浏览3个屏幕以便更好地理解
请注意我的代码很乱

一些背景

我看了看你的Dimitris项目。

我为“类创建”页面计算了您的负载创建时间(OS X 10.9,2012 Macbook Air上的Java 8 b129)。 我花了一秒多钟。

为了简化测试,我删除了使用并发服务加载新FXML的部分,并在请求时直接在JavaFX应用程序线程上加载FXML – 使用这种方式更容易。

对不起,这里有很长的答案。 这样的事情通常不适合StackOverflow,它们在教程或博客forms中最好,但我很好奇发生了什么,所以我想我需要一些时间来研究它并写下它起来。

不要为每个加载的FXML创建一个新场景

每次加载FXML时都设置一个新场景(使用新的大小)。 无论出于何种原因,这是一项相当昂贵的操作,您无需这样做。 你已经在你的舞台上有一个场景,只需重复使用它。 所以替换下面的代码:

 stage.setScene(new Scene(service.getValue().getRoot(), service.getValue().getX(), service.getValue().getY())); 

有:

 stage.getScene().setRoot(service.getValue().getRoot()); 

这将在加载时间上节省超过半秒,因此现在class-> create在第一次运行时大约需要400毫秒。

此更改是轻松获得性能的一个示例。

它还提供了更好的用户体验,因为在我的机器上,当您更换场景时舞台闪烁灰色,但是当您只是替换现有场景的场景根时,没有灰色闪光。

因为JVM使用Java的即时编译器运行,所以后续显示classes-> create的请求变得更快,因此在打开场景两到三次之后需要大约250ms(或四分之一秒)。

FXMLLoader很慢

在剩余的250ms加载中,在初始化代码中花费大约2ms,JavaFX渲染控件花费另外2ms,FXMLLoader花费其他246ms加载FXML并实例化节点进入场景。

使用UI代码的想法是,您希望将转换的目标时间缩短到<16到30毫秒。 这将使用户快速顺利地过渡。

将您的UI代码与网络和数据库代码分开

网络和数据库调用最好是从JavaFX应用程序线程完成的,因此您可以使用JavaFX并发工具来包装这些任务。 但我建议分开顾虑。 使用并发服务来获取数据,但是一旦获得数据,使用Platform.runLater或Task返回值来传输JavaFX应用程序线程的数据并在JavaFX应用程序线程上运行填充(因为该填充任务将是反正很快)。

这样,您就可以将系统中的multithreading划分为不同的逻辑组件 – 网络在其自己的线程上运行,UI操作在不同的线程上运行。 它使事情更容易推理和设计。 可以想象它有点像Web编程,其中ajax调用同时向UI提取数据,然后提供调用以将数据处理到UI中的回调。

这样做的另一个原因是许多网络库无论如何都带有自己的线程实现,因此您只需使用它而不是生成自己的线程。

如何使FXML加载更快

您不应该真正需要multithreading代码来加载FXML文件。 FXML的初始化function运行得非常快(只需几毫秒)。 FXMLLoader需要250ms。 我没有详细介绍它,看看为什么会这样。 但Sebastian对JavaFX2的回答有一些迹象 – 在动态地将定制(fxml)面板添加到gridpane时性能非常差 。 我认为主要的性能问题是FXMLLoader在很大程度上依赖于reflection。

因此,在FXMLLoader缓慢出现问题的情况下,最佳解决方案是使用FXMLLoader的一些替代方案,该方案性能更好,不依赖于reflection。 我相信JavaFX团队正在研究FXMLLoader的二进制等价物(例如,FXML文件在构建阶段被预先解析为二进制Java类文件,可以快速加载到JVM中)。 但JavaFX团队尚未发布该工作(如果存在)。 Tom Schindl已经完成了类似的工作,它将FXML预编译为Java源代码 ,然后可以将其编译为Java类,因此您的应用程序再次使用编译的类,这应该是很好的和快速的。

因此,使FXML加载更快的解决方案目前正在开发中,但在生产系统上并不是非常稳定和可用。 所以你需要其他方法来处理这个问题。

使您的表单更简单

这对我来说似乎是一个警察,但IMO你的“创建类”场景的设计有点复杂。 您可能需要考虑使用多阶段向导替换它。 这样的向导通常会加载更快,因为您只需要在每个向导屏幕上加载一些项目。 但更重要的一点是,这样的向导可能更容易使用,并为您的用户提供更好的设计。

仅替换场景中需要的部分

您正在加载FXML文件,为每个新页面创建整个应用程序UI。 但是您不需要这样做,因为顶级菜单,状态栏和导航侧栏等内容不会因为用户加载新表单而改变 – 只显示“创建类”表单的中央部分正在发生变化。 因此,只需加载正在更改的场景部分的节点,而不是整个场景内容。

此外,这将通过在每个阶段替换整个UI来帮助解决您的应用程序将遇到的其他问题。 当您更换导航菜单时,菜单不会自动记住并突出显示导航树中当前选定的项目 – 您必须明确记住它并在执行导航后再次重置它。 但是如果你没有更换整个场景内容,导航菜单会记住上次选择的内容并显示它(因为导航菜单本身在导航时不会改变)。

缓存FXML加载节点树和控制器

您只需在应用程序中一次显示一个“创建类”表单。 所以你只需要使用FXMLLoader来加载“创建类”表单一次。 这将为表单创建一个节点树。 定义一个静态HashMap,它将“create classes”映射到CreateClassesController对象(在应用程序中也只有一个)。 当您导航到“创建类”屏幕时,通过从哈希映射中检索控制器,查看您之前是否已经在那里。 如果已存在现有控制器类,请查询它以获取表单的根窗格,并通过用新表单替换场景的中心面板在场景中显示表单。 您可以在控制器上添加额外的方法,您可以调用这些方法来清除表单中的任何现有数据值,或者设置从网络提取任务加载的任何数据值。

除了加速应用程序之外,您现在还可以保留“创建类”表单的状态,直到您或用户决定清除它为止。 这意味着用户可以通过并部分填写表单到应用程序中的其他位置然后返回到表单,它将处于与它们离开时相同的状态,而不是忘记用户之前输入的所有内容。

现在,因为您只加载了“创建类”表单一次,您可以在启动时加载所有表单(并有一个预加载器页面,指示您的应用程序正在初始化)。 这意味着应用程序的初始启动速度会变慢,但应用程序的运行速度会很快。

建议的设计

  1. 在应用程序中创建不同面板部分的表单(导航栏,“创建类”表单,“主屏幕”等)。
  2. 仅在JavaFX UI线程上创建和操作UI元素。
  3. 仅替换导航上的面板部分,而不是整个场景。
  4. 将FXML预编译为类文件。
  5. 如有必要,请使用启动画面预加载器。
  6. 抽象网络和数据将代码提取到自己的线程中。
  7. 重用为面板表单创建的缓存节点树,而不是重新创建它们。
  8. 当新的网络数据可用时,将其传输到UI线程并将其填充到缓存的节点树中。

查看SceneBuilder实现

遵循SceneBuilder实现本身使用的原则 – 它是一个合理大小的JavaFX项目的当前最佳设计示例,该项目使用FXML作为其UI。 SceneBuilder代码是开源的,并以BSD风格的许可证分发,因此它很适合学习。

结果

我对这个答案中提到的一些想法进行了原型设计,这将“创建类”屏幕的初始加载时间从一秒钟缩短到大约400毫秒(第一次加载屏幕)。 我没有用其他东西替换FXMLLoader(我肯定会大大降低400ms的值)。 基于刚刚重新添加到场景的缓存节点树的“创建类”表单的后续加载大约需要4ms – 因此,就用户而言,操作性能是即时的。

其他问题的更新

您是否认为我应该使用Tom Schindl的编译FXML的解决方案,还是“太Beta”?

我的猜测是(截至今天)它“太过于测试”。 但是亲自尝试一下,看看它是否符合您的需求。 有关Tom的FXML => JavaFX编译器的支持,请发布到e(fx)clipse论坛 ,因为该项目属于e(fx)clipse项目的大范围 。

我试过’stage.getScene()。setRoot(service.getValue()。getRoot());’ 但得到了OutOfMemoryError:Java堆空间你认为该行导致它还是不相关?

作为创建此答案的一部分,我正在对您的代码进行一些分析(通过将NetBeans探查器附加到已运行的应用程序实例)。 我注意到每次你的程序加载“创建类”场景时,内存使用量会显着增长并且内存似乎没有被释放。 我没有花时间试图找出原因是什么,但那是未修改的代码分析。 所以我怀疑系统耗尽内存的最终原因与你是换掉一个场景还是换掉一个场景根无关。 我注意到CSS psuedo-classes消耗了大量内存,但我无法告诉你原因。 我的猜测是,如果您遵循本答案中概述的原则,那么总的来说,您的应用程序将更加高​​效,并且您可以规避当前代码中存在的与内存相关的问题。 如果没有,您可以继续分析应用程序内存使用情况,以查看根本问题。

场景的速度将与您从一开始就尝试在场景中初始化的对象数量有很大关系。 因此,如果您可以将该数字降低到最小值并在场景运行时创建其余部分,那么一切都将运行得更顺畅。 如果在您的情况下这是不可能的,可能只是尝试限制其他事情以减少系统的压力。