首页>国内 > 正文

转转质检桌面应用程序的架构演进

2023-03-15 10:16:32来源:转转技术

质检是转转履约体系中的重要一环,通过对手机、平板、笔记本、耳机、手表等品类商品的软硬件功能、外观成色等进行全方面检测,为买家、卖家把好质量关,让二手交易变得更透明、更靠谱,促进绿色消费。

在质检环节中,通过标准化产线,结合自动化设备、质检 APP、桌面应用程序等,最终输出全面可信的“质检报告”呈现给用户。其中,桌面应用程序发挥着举足轻重的作用,本文将重点介绍桌面应用程序架构的演进及其落地。


(资料图片)

1 背景

转转质检的桌面应用程序,前期主要由 Qt 构建,C/C++提供底层支持。这些桌面应用的视图层、应用层、以及底层能力支持,均由 C/C++开发人员承担全部开发迭代工作。其次随着业务的不断发展,部分桌面应用程序逐渐暴露出拓展性差、迭代难的问题。

在转转质检技术团队,除了 C/C++开发同学外,还有配套成熟的前端和 Java 后端,综合来说:对于视图层,前端的技术生态、以及开发人员的技术经验,在视图层开发方面具有很大的优势;在应用层方面,Java 技术生态的优势不言而喻,同时 Java 后端同学对整体业务和系统都有着相对全面深入的了解,简而言之,Java 同学在应用层架构设计和落地方面,是非常合适的。

综上,基于团队实际,笔者团队对桌面应用程序,提出了新的技术架构——EJC(Electron、Java、C/C++)。该架构的主要优势在于:

让 C/C++开发同学更偏向于底层能力的研究,发挥更大的价值。Electron 本质是前端技术栈,Java 同学更理解整体业务,在应用层设计经验方面更擅长;且前端和 Java 资源更容易灵活调配。Electron 和 Java 本身是跨端的,对于后续质检桌面应用各端的整合(Windows&Mac),具有不错的优势。2 EJC 架构

简单说,EJC 技术架构即:Electron(视图层/用户层),Java(应用层),C/C++(基础能力层)。

2.1 Electron

Electron 是一个基于 Chromium 和 Node.js 的框架,一套多端生成 Windows、macOS 和 Linux 的跨平台桌面应用程序。

本质是前端技术栈,内置了 Chromium 内核使得应用程序具有最新的 web 标准,开发人员可以专注应用程序的逻辑和界面设计,不用再束手束足做浏览器兼容性操作。整包更新和热更新,使程序保持最新的状态,类似混合移动应用(Hybrid APP)的快感。安全的跨平台运行环境,可以有效地降低程序崩溃和系统错误的机率,实现更加可靠和稳定的应用程序。丰富的 API 在 C/C++能力的加持下,让软硬件结合更加丝滑,扩展能力更进一步。2.2 Java

Java 应用层,主要包括:

通讯模块:提供基于 HTTP、WebSocket 等协议的通信能力。底层交互模块: 封装了 Java 调用本地代码(动态库)的技术(JNI/JNA)。数据存储:使用轻量级数据库 SQLite 来持久化数据,为数据的高可用及容错提供基础能力。事件监听:基于 Spring 的事件和监听机制实现了基于事件驱动的编程模型,有松耦合、高扩展性和可测试性等优点。业务模块:必要的业务逻辑处理。监控模块:对客户端的实时运行参数、硬件异常情况等数据进行记录,定时上报云端。配置管理:定时从云端拉取最新的配置,覆盖本地配置。调度策略:根据设备的历史状态,预判硬件(如货架货位上的 USB 通讯口、USB 集线器等设备)是否出现故障,当判定故障时,可以优先调度其它设备并将故障信息上报。2.3 C/C++

基础能力层,核心 SDK 的实现。提供与 Windows、IOS、安卓、相机、机械臂等底层通用能力。

基于上述说明,在转转质检中,笔者呈现的 EJC 技术架构,如下:

EJC架构图

下面重点对 Java 应用层的前端通讯模块、底层通讯模块进行介绍。

2.4 前端通讯模块

在 Java 应用层兼容了 HTTP 协议、WebSocket 协议的通信方式。接下来介绍几种与前端通讯的方案以及我们在 EJC 架构中的选型和考量:

2.4.1 HTTP 短轮询

客户端周期性的向服务器发送请求,以获得最新的数据,这种方式会造成服务器和网络资源的浪费。适用于实时性要求不高的场景:在 Java 客户端从云端拉取配置时,采用的就是此机制。

2.4.2 HTTP 长轮询

与 HTTP 短轮询相比,HTTP 长轮询能够避免客户端频繁向服务器发送请求,节省了网络和服务器资源的开销,同时能实现更及时和可靠的数据推送。

2.4.3 SSE(Server-Sent Events)

本质上是一个 HTTP 长连接,服务端发送给客户端不是一个数据包,而是一个 stream 流,格式为 text/stream,所以客户端不会关闭连接,会一直等着服务器发过来新的数据流。适合一些只需要服务端单向推送事件给客户端的场景。

在实际的应用场景中,服务端只需要推送一次信息给前端(如:前端调用 Java 服务端获取系统硬件配置信息),我们选择 SSE 作为前后端的通信方式,有以下优势:

比 http 短轮询性能更好。比 http 长轮询更可靠。比 WebSocket 更轻量。可以在现有的基础设施和技术上使用,而不需要进行任何额外的配置或部署。2.4.4 WebSocket

WebSocket 是基于 TCP 的双向通信协议,可以实现实时通信。适合实时性要求很高而且需要双工通信的系统。在实际的应用中,如隐私清除工具,从插入手机到隐私清除完成只需要 3~5 秒,质检人员需要实时的看到手机状态的变更,这时候我们选用 Websocket 实时的将数据状态推送到前端进行展示。

2.5 底层通讯模块

Java 调用 C/C++ 有 JNI (Java Native Interface) 与 JNA (Java Native Access) 两种方式,都是 Java 中用于调用本地底层 SDK 的技术。

下面通过简单的代码示例(获取 IOS 设备名称)来说明 Java 是如何调用底层 SDK 的。为了节约篇幅,仅展示了部分关键代码。

2.5.1 JNI 介绍和使用

Java 语言提供的标准接口,它提供了一组函数和数据类型,允许 Java 应用程序调用和被 C/C++ 语言调用。JNI 通过编写本地方法实现与 C/C++ 语言的交互。

使用 native 关键字声明本地方法。
public class JniDemo {    /**     * 获取IOS设备的名称     * @param udid 设备UDID     * @return 设备名称     */    public native String getDeviceNameByUDID(String udid);}
通过 javah 命令,将代码中的 native 方法生成对应的 C 语言的头文件。
> javah JniDemo// JDK10+已经移除了javah命令工具,使用以下命令> javac JniDemo.java -h outputDir
执行上述命令后,将生成一个名为 JniDemo.h 的 C/C++ 头文件。
#include /* Header for class JniDemo */#ifndef _Included_JniDemo#define _Included_JniDemo#ifdef __cplusplusextern "C" {#endif/* * 包含了 getDeviceNameByUDID 方法的声明 * Class:     JniDemo * Method:    getDeviceNameByUDID * Signature: (Ljava/lang/String;)Ljava/lang/String; */JNIEXPORT jstring JNICALL Java_JniDemo_getDeviceNameByUDID  (JNIEnv *, jobject, jstring);#ifdef __cplusplus}#endif#endif
C/C++ 实现头文件来实现 Java_JniDemo_getDeviceNameByUDID 方法,并将其编译为动态库。
#include "jnidemo.h"JNIEXPORT jstring JNICALL Java_JniDemo_getDeviceNameByUDID(JNIEnv *env, jobject, jstring udid){    string udid_cpp = jstringTostring(env, udid);    LHW_INFO("udid_cpp = " << udid_cpp);    IOS_Device_Interface idi;    string device_name = idi.get_device_name_by_udid(udid_cpp);    LHW_INFO("device_name = " << device_name);    return stringTojstring(env, device_name.c_str());}
Java 使用
public class JniDemo {    /**     * 获取IOS设备的名称     * @param udid 设备UDID     * @return 设备名称     */    public native String getDeviceNameByUDID(String udid);    public static void main(String[] args) {        System.loadLibrary("jniDemo");        JniDemo obj = new JniDemo();        String result = obj.getDeviceNameByUDID("00008110-001518392EE3801E");        System.out.println("Result is " + result);        // Result is iphone 13 pro    }}
2.5.2 JNA 介绍和使用

JNA 是在 JNI 基础上实现的编程框架,实现了 Java 类型到 C 类型的自动转换。Java 开发人员只要在一个 Java 接口中描述目标 native library 的函数与结构,不再需要编写任何 Native/JNI 代码,极大的降低了 Java 调用动态库的开发难度。

编写 C/C++代码,声明头文件(需要使用 extern “C”关键字才能被 JNA 调用)。
#pragma once#include "pch.h"#ifndef JNADemoAPI#define JNADemoAPI __declspec(dllexport)#endif // !_Included_JnaDemo#ifdef __cplusplusextern "C" {#endif // __cplusplus    JNADemoAPI const char* getDeviceNameByUDID(const char *udid);#ifdef __cplusplus}#endif // __cplusplus
实现头文件,并将其编译为动态库。
#include "pch.h"#include "JnaDemo.h"#include "IOSDevice/ios_device_pimpl.h"string IOS_Device_Interface::getDeviceNameByUDID(string udid) {    return ios_device->get_deviceName_by_udid(udid);}
Java 中使用。

首先在项目中引入 JNA 库:

  com.sun.jna  jna  5.12.1

声明与动态库对应的 Java 接口类:

/** *  定义动态库接口 */public interface JnaDemo extends Library {    /**     * 与 C/C++ 中的函数名对应     * @param 设备UDID     * @return 设备名称     */    String getDeviceNameByUDID(String udid);}

加载动态库并调用方法:

/** * 通过 JNA 调用 C/C++ 函数 * */public class JnaDemoTest {    public static void main(String[] args) {        // 加载名为 jnaDemo 动态库        JnaDemo jnaDemo = Native.load("JnaDemo", JnaDemo.class);        // 调用方法并获取结果        String result = jnaDemo.getDeviceNameByUDID("00008110-001518392EE3801E");        System.out.println("Result is " + result);        // Result is iphone 13 pro    }}
2.5.3 选型和考量

通过上述的示例代码我们对比了两种方案的优缺点,并进行了性能测试。

JNI

JNA

优点

本地方法编译后可以选择 C 或 C++ 来实现。调用本地方法时效率相对 JNA 高。

封装了系统常用的动态库,可以直接使用。开发效率相对 JNI 高,无需 Java 编写本地方法。

缺点

开发效率相对较低,需要 Java 编写本地方法并编译生成 C/C++头文件,C/C++ 需要按照生成的头文件进行编码实现。

不支持 C++编译生成的动态库,需要在 C++ 接口的上层用 C 语言进行一次封装。

从开发者的角度来说:JNA 对 Java 开发者比较友好,JNI 则对 C/C++开发者比较友好。

同时我们分别用 JNI 和 JNA 进行 100 次到 500 次读取 IOS 设备名称的性能测试,得到耗时对比。在 8 核 16G 机器上运行得到如下结果:

计算数量(百次)

JNI

JNA

1

1197ms

26957ms

2

2196ms

52800ms

3

2759ms

79260ms

4

4573ms

106377ms

5

6299ms

132482ms

通过上面的对比和性能测试,我们制定了如下选型标准:

自研的 SDK:高优使用 JNI 作为底层通信方式。优势在于:JNI 的性能更好,底层数据交互的接口由 Java 定义,C/C++开发者可以选择 C 或 C++进行实现,有更多的选择性及灵活性。外部厂商提供的 SDK:优先调用厂商自带的 SDK。优势在于:无需 C/C++再次封装一层动态库,减少开发资源的投入。3 EJC 架构的落地

EJC 架构在转转质检已成功落地了多个应用,下面主要介绍 EJC 在 Windows 笔记本质检工具中的落地。

3.1 项目背景

随着质检业务的发展,笔记本质检量再创新高。早期由 C/C++开发、使用 Qt 构建的笔记本验机工具已不能满足业务的需求,主要体现有以下几点:

维护成本高:代码的复杂性较高,维护需要开发者投入更多的时间和精力。覆盖率低:功能不够完善,易用性较差,使用覆盖率低。移植性差:无法移植到 Mac 平台。

基于上述的项目背景,我们使用了 EJC 架构来重构笔记本验机工具。

3.2 架构实现3.2.1 名词解释WMI:Windows Management Instrumentation;是 Windows 系统标准的信息服务。WinAPI:Windows 系统提供的底层接口。DLL(C):即 EJC 中的 C,由我们 C/C++的同学研发的底层 SDK。3.2.2 流程描述录入获取数据流程:Electron 启动后 -> 后台异步启动 Java 服务端 -> 开启异步全局扫描笔记本基本数据项注解 -> 获得需读取的电脑属性 -> 调度器分类执行属性获取命令 -> 执行获取命令执行链(可横向扩展获取方式)-> 数据加工 -> 数据纠错 -> 经 SSE 通道推送页面渲染。质检流程辅助流程:进入质检流程 -> 请求质检项辅助(某个功能,例如指纹是否正常)-> Java 调用底层(Dll、Wmi 等其他方式)-> 返回辅助质检结果 -> 页面回传渲染质检项支持结果。3.2.3 流程释义执行链:考虑到电脑某项属性需要多种方式获取并互相就纠正,因此可以针对性的配置其特有的执行链,以达到更好的读取准确率,也更加方便扩展。数据纠错:某些属性比如电池健康值;默认采取 WMI 读取;但是部分厂商没有按照 WMI 的标准写值,导致获取为空;因此需要调取 DLL(C)的其他获取方式作为补充。3.3 项目呈现3.3.1 录入模块

通过 Java、C/C++读取笔记本关键信息,获取笔记本基本情况。通过录入功能,辅助一线人员选择系统标品项,同时与质检码进行关联入库。在此基础上产生原始信息与标品 ID 的映射关系,减少下次相同机型的一个操作步骤,方便一线操作人员在质检相同机型的一个操作便携性。

笔记本录入模块

3.3.2 质检模块

通过品牌机型获取系统对应的质检模版,提供自动&辅助质检能力,协助一线质检人员对笔记本的质检能力更快捷、精准。

4 总结

本文对转转质检的 EJC 架构做了一些分享,并给出一些实践经验,希望能为大家解决类似的问题提供一些帮助。目前 EJC 架构体系已经在质检业务中上线了多个桌面应用并稳定运行,未来将会覆盖更多的应用场景,助力业务得到实质发展。

5 参考链接​​https://www.electronjs.org​​​​http://java-native-access.github.io/jna/5.13.0/javadoc/overview-summary.html#overview_description​​​​https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/types.html#wp914​​​​https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events​​

关键词:

相关新闻

Copyright 2015-2020   三好网  版权所有 联系邮箱:435 22 640@qq.com  备案号: 京ICP备2022022245号-21