质检是转转履约体系中的重要一环,通过对手机、平板、笔记本、耳机、手表等品类商品的软硬件功能、外观成色等进行全方面检测,为买家、卖家把好质量关,让二手交易变得更透明、更靠谱,促进绿色消费。
在质检环节中,通过标准化产线,结合自动化设备、质检 APP、桌面应用程序等,最终输出全面可信的“质检报告”呈现给用户。其中,桌面应用程序发挥着举足轻重的作用,本文将重点介绍桌面应用程序架构的演进及其落地。
转转质检的桌面应用程序,前期主要由 Qt 构建,C/C++提供底层支持。这些桌面应用的视图层、应用层、以及底层能力支持,均由 C/C++开发人员承担全部开发迭代工作。其次随着业务的不断发展,部分桌面应用程序逐渐暴露出拓展性差、迭代难的问题。
在转转质检技术团队,除了 C/C++开发同学外,还有配套成熟的前端和 Java 后端,综合来说:对于视图层,前端的技术生态、以及开发人员的技术经验,在视图层开发方面具有很大的优势;在应用层方面,Java 技术生态的优势不言而喻,同时 Java 后端同学对整体业务和系统都有着相对全面深入的了解,简而言之,Java 同学在应用层架构设计和落地方面,是非常合适的。
综上,基于团队实际,笔者团队对桌面应用程序,提出了新的技术架构——EJC(Electron、Java、C/C++)。该架构的主要优势在于:
简单说,EJC 技术架构即:Electron(视图层/用户层),Java(应用层),C/C++(基础能力层)。
Electron 是一个基于 Chromium 和 Node.js 的框架,一套多端生成 Windows、macOS 和 Linux 的跨平台桌面应用程序。
Java 应用层,主要包括:
基础能力层,核心 SDK 的实现。提供与 Windows、IOS、安卓、相机、机械臂等底层通用能力。
基于上述说明,在转转质检中,笔者呈现的 EJC 技术架构,如下:
下面重点对 Java 应用层的前端通讯模块、底层通讯模块进行介绍。
在 Java 应用层兼容了 HTTP 协议、WebSocket 协议的通信方式。接下来介绍几种与前端通讯的方案以及我们在 EJC 架构中的选型和考量:
客户端周期性的向服务器发送请求,以获得最新的数据,这种方式会造成服务器和网络资源的浪费。适用于实时性要求不高的场景: Java 客户端从云端拉取配置时,采用的就是此机制。
与 HTTP 短轮询相比,HTTP 长轮询能够避免客户端频繁向服务器发送请求,节省了网络和服务器资源的开销,同时能实现更及时和可靠的数据推送。
本质上是一个 HTTP 长连接,服务端发送给客户端不是一个数据包,而是一个 stream 流,格式为 text/stream,所以客户端不会关闭连接,会一直等着服务器发过来新的数据流。适合一些只需要服务端单向推送事件给客户端的场景。
在实际的应用场景中,服务端只需要推送一次信息给前端(如:前端调用 Java 服务端获取系统硬件配置信息),我们选择 SSE 作为前后端的通信方式,有以下优势:
WebSocket 是基于 TCP 的双向通信协议,可以实现实时通信。适合实时性要求很高的而且需要双工通信的系统。在实际的应用中,如隐私清除工具,从插入手机到隐私清除完成只需要 3~5 秒,质检人员需要实时的看到手机状态的变更,这时候我们选用 Websocket 实时的将数据的状态推送到前端进行展示。
Java 调用 C/C++ 有 JNI (Java Native Interface) 与 JNA (Java Native Access) 两种方式,都是 Java 中用于调用本地底层 SDK 的技术。
下面通过简单的代码示例(获取 IOS 设备名称)来说明 Java 是如何调用底层 SDK 的。为了节约篇幅,仅展示了部分关键代码。
Java 语言提供的标准接口,它提供了一组函数和数据类型,允许 Java 应用程序调用和被 C/C++ 语言调用。JNI 通过编写本地方法实现与 C/C++ 语言的交互。
public class JniDemo {
/**
* 获取IOS设备的名称
* @param udid 设备UDID
* @return 设备名称
*/
public native String getDeviceNameByUDID(String udid);
}
复制代码
> javah JniDemo
// JDK10+已经移除了javah命令工具,使用以下命令
> javac JniDemo.java -h outputDir
复制代码
#include
/* Header for class JniDemo */
#ifndef _Included_JniDemo
#define _Included_JniDemo
#ifdef __cplusplus
extern "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
复制代码
#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());
}
复制代码
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
}
}
复制代码
JNA 是在 JNI 基础上实现的编程框架,实现了 Java 类型到 C 类型的自动转换。Java 开发人员只要在一个 Java 接口中描述目标 native library 的函数与结构,不再需要编写任何 Native/JNI 代码,极大的降低了 Java 调用动态库的开发难度。
#pragma once
#include "pch.h"
#ifndef JNADemoAPI
#define JNADemoAPI __declspec(dllexport)
#endif // !_Included_JnaDemo
#ifdef __cplusplus
extern "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);
}
复制代码
首先在项目中引入 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
}
}
复制代码
通过上述的示例代码我们对比了两种方案的优缺点,并进行了性能测试。
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 |
通过上面的对比和性能测试,我们制定了如下选型标准:
EJC 架构在转转质检已成功落地了多个应用,下面主要介绍 EJC 在 Windows 笔记本质检工具中的落地。
随着质检业务的发展,笔记本质检量再创新高。早期由 C/C++开发、使用 Qt 构建的笔记本验机工具已不能满足业务的需求,主要体现有以下几点:
基于上述的项目背景,我们使用了 EJC 架构来重构笔记本验机工具。
通过 Java、C/C++读取笔记本关键信息,获取笔记本基本情况。通过录入功能,辅助一线人员选择系统标品项,同时与质检码进行关联入库。在此基础上产生原始信息与标品 ID 的映射关系,减少下次相同机型的一个操作步骤,方便一线操作人员在质检相同机型的一个操作便携性。
通过品牌机型获取系统对应的质检模版,提供自动&辅助质检能力,协助一线质检人员对笔记本的质检能力更快捷、精准。
本文对转转质检的 EJC 架构做了一些分享,并给出一些实践经验,希望能为大家解决类似的问题提供一些帮助。目前 EJC 架构体系已经在质检业务中上线了多个桌面应用并稳定运行,未来将会覆盖更多的应用场景,助力业务得到实质发展