习惯了Electron的开发模式,我想用Java简单地模仿Chromium的架构,实现一个类似Electron的框架。毕竟,Electron的性能问题一直是人们诟病的地方,而Java作为一门成熟的语言,性能上有着很大的优势。当然,Chromium的架构非常复杂,这里只是一个简单的模仿.
Chromium架构简介
Chromium是一个开源的浏览器项目,它的架构非常复杂,但是也非常强大。它的架构图如下:

从图中可以看到,Chromium的架构分为多个层次,核心是一个Main Thread
然后浏览器通过IPC通信与渲染进程Render通信. 每个Render进程都有一个独立的渲染线程,用于渲染网页。
这样的架构使得Chromium非常稳定,同时也非常高效。
这样独立每个的渲染进程使得Chromium的鲁棒性更好,各个模块之间的耦合度更低,同时也更容易实现多进程并行处理。
Electron也使用了类似的架构,但是它是基于Node.js的.
Electron中,主要的模块为Main.js, preload.js, 和Render.js.
Main.js负责创建窗口,处理窗口事件,preload.js负责在渲染进程中注入一些Node.js的API, Render.js负责渲染网页。
而要在Java中模仿这样的架构,我们需要先搞清楚Chromium的架构更多细节.
ResourceDispatcherHost
ResourceDispatcherHost是Chromium中的一个重要模块,它负责处理网络请求。它的主要功能是将网络请求分发给不同的Render进程,然后将渲染结果返回给浏览器进程。这样的架构使得Chromium的网络请求非常高效。
假设我们是Render进程,如果我们要加载一个网页的图片,我们会向ResourceDispatcherHost发送一个请求.实际处理网络请求的是ResourceDispatcherHost,而Render只负责渲染结果.
IPC通信
Chromium中的IPC通信是非常重要的,它负责浏览器进程和渲染进程之间的通信。IPC通信的实现也是分发器.在Java中,我们可以简单地使用共享内存来实现IPC通信.
渲染进程
渲染进程是Chromium中的一个重要模块,它负责渲染网页。渲染进程是一个独立的进程,它有自己的渲染线程,用于渲染网页。在Java中,我将采用JavaFX来实现渲染进程.
Java模仿Chromium架构
事件处理和消息传递
在Java中,我们首先需要创建一个事件监听系统。
这个系统能够让不同的组件注册事件监听器,并在特定事件发生时调用这些监听器。
这样的设计模式可以让我们在Java中模仿类似Electron的事件监听和处理机制。
事件类(Event):
- 定义一个事件类,用于封装事件信息。这个类可以包含事件类型、源、以及任何相关数据。
事件监听器接口(EventListener):
- 创建一个事件监听器接口,定义一个方法,例如
onEvent(Event event),用于处理事件。
事件分发器(EventDispatcher):
- 管理事件监听器的注册和注销。
- 负责在事件发生时通知所有注册的监听器。
IPC通信类(IPCCommunication):
- 用于进程间的通信。
- 能够发送事件和接收来自其他进程的事件。
- 当接收到事件时,将其封装为
Event对象并通过EventDispatcher分发。
具体的事件处理类:
- 实现
EventListener接口,定义对特定事件的响应逻辑。
以下是一个简单的代码示例来说明这些概念:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
| @FunctionalInterface public interface Callback { void call(String data); }
public class Event { private String type; private Object data; private Callback callback;
public Event(String type, Object data) { this.type = type; this.data = data; this.callback = null; }
public Event(String type, Object data, Callback callback) { this.type = type; this.data = data; this.callback = callback; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public Object getData() { return data; }
public void setData(Object data) { this.data = data; }
public Callback getCallback() { return callback; }
public void setCallback(Callback callback) { this.callback = callback; }
}
public interface EventListener { void onEvent(Event event); }
public class EventDispatcher { private Map<String, List<EventListener>> listeners = new HashMap<>();
public void registerListener(String eventType, EventListener listener) { this.listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener); }
public void unregisterListener(String eventType, EventListener listener) { this.listeners.getOrDefault(eventType, new ArrayList<>()).remove(listener); }
public void dispatchEvent(Event event) { List<EventListener> eventListeners = listeners.getOrDefault(event.getType(), new ArrayList<>()); for (EventListener listener : eventListeners) { listener.onEvent(event); } } }
public class IPCCommunication { private final EventDispatcher dispatcher;
public IPCCommunication(EventDispatcher dispatcher) { this.dispatcher = dispatcher; }
public void sendMessage(String message) {
Event event = new Event("send-message", message); dispatcher.dispatchEvent(event); } }
public class Render extends Thread{ private final IPCCommunication ipc;
public Render(IPCCommunication ipc) { this.ipc = ipc; }
@Override public void run() {
} }
public class Main { public static void main(String[] args) { EventDispatcher dispatcher = new EventDispatcher(); IPCCommunication ipc = new IPCCommunication(dispatcher); new Thread(new Render(ipc)).start();
class QuickHideAndShowListener implements EventListener { @Override public void onEvent(Event event) { if ("set-quick-hide-and-show".equals(event.getType())) { System.out.println("set quickHideAndShow" + event.getData()); } } } dispatcher.registerListener("set-quick-hide-and-show", new QuickHideAndShowListener()); } }
|
在这个设计中,在主进程中创建EventDispatcher和IPCCommunication的实例,并在QuickHideAndShowListener中定义对特定事件的处理逻辑。然后,当IPC通信接收到消息时,它会创建一个事件并通过EventDispatcher分发给所有注册的监听器。这种设计模式允许在Java中模拟类似Electron的事件监听和处理机制。
我们使用IPCCommunication类来实现Preload的功能。IPCCommunication充当了Preload的角色,可以给Render提供一些能力,例如发送消息给主进程。
渲染进程的基本实现
由于使用JavaFX作为GUI框架,我们可以包装一些JavaFX的功能。
1. 页面管理和切换
首先,我们定义一个PageInterface,每个页面类都实现这个接口, 每个页面都需要有一个render函数(render函数中用JavaFX展示页面)。
1 2 3
| public interface PageInterface { void render(Stage stage); }
|
这样,所有页面都实现PageInterface。这个接口中的render方法负责在给定的Stage上渲染页面。
2. 事件处理和消息传递
要处理用户的操作,如按钮点击和输入验证,您可以在每个页面内部定义事件处理器。例如,对于按钮点击事件,您可以在ConnectPage内部定义一个方法来处理连接操作。
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class ConnectPage implements PageInterface { private void handleConnectButtonClick() { }
@Override public void render(Stage stage) { Button connectButton = new Button("Connect"); connectButton.setOnAction(event -> handleConnectButtonClick()); stage.show(); } }
|
3. 封装JavaFX组件
对于切换页面,您可以在PageInterface中定义一个switchToPage方法,用于切换页面。然后,可以在ConnectPage中调用这个方法来切换到ChatPage。
4. 统一页面渲染逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class UIManager { private Stage primaryStage;
public UIManager(Stage primaryStage) { this.primaryStage = primaryStage; }
public void switchToPage(PageInterface page) { Platform.runLater(() -> page.render(primaryStage)); } }
UIManager uiManager = new UIManager(primaryStage);
uiManager.switchToPage(new ConnectPage());
|
优点
- 通过事件处理和消息传递,可以在Java中模拟类似Electron的事件监听和处理机制。
- 通过封装JavaFX组件,可以实现页面管理和切换。
- 通过统一页面渲染逻辑,可以简化页面切换的代码。
使用实例
用简单的代码完整实现登录功能举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| public class Main { class login implements EventListener { @Override public void onEvent(Event event) { if ("login".equals(event.getType())) { JSONObject json = new JSONObject(event.getData().toString()); String address = json.get("address").toString(); String port = json.get("port").toString(); String user = json.get("user").toString(); Socket socket = new Socket(address, Integer.parseInt(port)); dispatcher.registerListener("login-success", new EventListener() { @Override public void onEvent(Event event1) { if(event1.getType().equals("login-success") && event1.getData().toString().equals("success")){ event.getCallback().call("success"); }else{ event.getCallback().call(event1.getData().toString()); } dispatcher.unregisterListener("login-success", this); } }); sendRequestToServer("login", new JSONObject().put("user", user)); } } } public static void main(String[] args) { EventDispatcher dispatcher = new EventDispatcher(); IPCCommunication ipc = new IPCCommunication(dispatcher); Render.setIPC(ipc); Render.launch(Render.class, args);
dispatcher.registerListener("login", new login()); } }
public class IPCCommunication{ public void login(JSONObject data, Callback callback) { Event event = new Event("login", data, callback); dispatcher.dispatchEvent(event); } }
public class Render extends Application { public static IPCCommunication ipc;
public static void setIPC(IPCCommunication i) { ipc = i; }
public static void login(String address, int port, String user){ JSONObject data = new JSONObject(); data.put("address", address); data.put("port", port); data.put("user", user); try { ConnectPage.showLoading(); ipc.login(data, res -> { if(res.equals("success")){ logger.info("login success"); serverMode = false; ConnectPage.hideLoading(); ChatPage.HistoryMode = false; uiManager.switchToPage(new ChatPage()); }else{ logger.error("login failed: " + res); ConnectPage.showError(res); } }); } catch (Exception e) { logger.error("login failed: " + e.getMessage()); ConnectPage.showError("login failed"); } } }
public class ConnectPage implements PageInterface{ @Override public void render(Stage primaryStage) { Button connectButton = new Button("Connect"); connectButton.setOnAction(event -> { Render.login(address, port, user); }); primaryStage.show(); } }
|
一些挑战
- Java虚拟机的内存管理方式与Electron的V8引擎有很大不同,这可能会对性能和资源管理产生影响。
- JavaFX强制需要在主线程中运行,对于任何修改,都必须添加到修改队列,然后稍后执行,这可能会对性能产生影响。
- 这个模仿未在性能上进行优化,可能会有性能问题。
可能的优化与提升
- 使用线程池来处理事件分发,以提高性能。目前的设计中,事件都是同步事件,如果事件处理时间过长,会阻塞主线程.后续需要添加异步事件的支持
- 目前主进程如果需要调用渲染进程的函数,直接使用了静态函数,这样的设计耦合度较高,后续需要添加更好的设计模式来解耦
- 目前的设计中,事件分发器是单例模式,这样的设计可能会导致线程安全问题,后续需要添加线程安全的支持
- 目前的设计都是基于单窗口的应用考虑的,对于多窗口的进程管理与通信,还需要进一步的设计与实现
- 对于目前用事件分发来实现的回调函数,后续可以进一步包装成Promise模式,使得代码更加简洁
- 在评论区补充吧…
总结
对于Java桌面端应用的开发,可能这个只是一个初步的尝试. 希望这个设计可以提高开发的效率~
github仓库链接:
JElectron-demo
参考资料: