Linux C++ JNI封裝、打包成jar包供Java調用詳細介紹

在前面 Android專欄 中詳細介紹了如何在Android Studio中調用通過jni封裝的c++庫。

在Android使用 opencv c++代碼,需要準備opencv4android,也就是c++的任何代碼,是使用Android NDK編譯的,相當于在windows/mac上使用Android stdido交叉編譯。

本文再介紹服務端的使用方式,c++通過jni封裝的庫,直接被java后端服務代碼調用。 這里c++依賴庫都是linux主機上,jni有關庫也都是linux上的,因此就不存在交叉編譯。

最后,還將項目打包成 jar包。

實際項目參考 GitCode FLowMeasurem。

文章目錄

  • 1、環境準備
    • 1.1、java sdk安裝
    • 1.2、opencv
    • 1.3、gcc/g++ 和 cmake
  • 2、項目實現
    • 2.1、java端代碼
    • 2.2、生成頭文件
    • 2.4、C++實現
    • 2.5、編譯共享庫
      • 2.5.1、命令行編譯
      • 2.5.2、cmake編譯
  • 2.6、測試運行
  • 3、導出Jar包給后端直接使用
    • 3.1、項目結構
    • 3.2、項目準備
      • 3.2.1、創建Java類
      • 3.2.2、生成JNI頭文件
      • 3.2.3、實現c++代碼
      • 3.2.4、編寫CMake構建文件
      • 3.2.5、構建動態庫
    • 3.3、 打包jar
      • 3.3.1、編譯java代碼
      • 3.3.2、創建MANIFEST.MF
      • 3.3.3 打包JAR
      • 3.3.4、jar包中添加so動態庫
    • 3.4、測試jar包
    • 3.5、包聲明問題
  • 4、優化 Jar 包代碼和結構

1、環境準備

1.1、java sdk安裝

這里直接使用apt安裝(可能還需要配置環境變量),

sudo apt install openjdk-8-jdk

之后使用 javacjavah 工具,能正常使用即可。

1.2、opencv

簡單起見,直接

sudo apt install libopencv-dev

1.3、gcc/g++ 和 cmake

不贅述。

2、項目實現

2.1、java端代碼

我們定義一個 OpenCVJNI.java 類,里面包含native函數,以及測試代碼main函數。

public class OpenCVJNI {// 加載本地庫static {System.loadLibrary("OpenCVJNI");}// 聲明本地方法public native int detectFaces(String imagePath, String outputPath);// 測試main函數public static void main(String[] args) {if (args.length < 2) {System.out.println("Usage: java OpenCVJNI <inputImage> <outputImage>");return;}OpenCVJNI ocv = new OpenCVJNI();int faceCount = ocv.detectFaces(args[0], args[1]);System.out.println("Detected " + faceCount + " faces.");}
}

2.2、生成頭文件

編譯Java類的命令 javac OpenCVJNI.java 此時,會在當前目錄生成 OpenCVJNI.class 文件;
繼續執行 javah -jni OpenCVJNI 會繼續在當前目錄生成 OpenCVJNI.h 文件。

我們可以使用 javac OpenCVJNI.java -h ./ 直接在當前目錄生成class文件 ( -s 指定class保存目錄), 和 -h 指定目錄下保存生的 h 文件。

內容如下

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class OpenCVJNI */#ifndef _Included_OpenCVJNI
#define _Included_OpenCVJNI
#ifdef __cplusplus
extern "C" {
#endif
/** Class:     OpenCVJNI* Method:    detectFaces* Signature: (Ljava/lang/String;Ljava/lang/String;)I*/
JNIEXPORT jint JNICALL Java_OpenCVJNI_detectFaces(JNIEnv *, jobject, jstring, jstring);#ifdef __cplusplus
}
#endif
#endif

2.4、C++實現

#include <jni.h>
#include <opencv2/opencv.hpp>
#include "OpenCVJNI.h"using namespace cv;JNIEXPORT jint JNICALL Java_OpenCVJNI_detectFaces(JNIEnv *env, jobject obj, jstring imagePath, jstring outputPath) {// 將Java字符串轉換為C字符串const char* inputPath = env->GetStringUTFChars(imagePath, 0);const char* outPath = env->GetStringUTFChars(outputPath, 0);// 加載圖像Mat image = imread(inputPath);if(image.empty()) {env->ReleaseStringUTFChars(imagePath, inputPath);env->ReleaseStringUTFChars(outputPath, outPath);return -1;}// 轉換為灰度圖像Mat gray;cvtColor(image, gray, COLOR_BGR2GRAY);// 加載預訓練的人臉檢測器CascadeClassifier faceDetector;String faceCascadePath = "/usr/share/opencv4/haarcascades/haarcascade_frontalface_default.xml";if(!faceDetector.load(faceCascadePath)) {env->ReleaseStringUTFChars(imagePath, inputPath);env->ReleaseStringUTFChars(outputPath, outPath);return -2;}// 檢測人臉std::vector<Rect> faces;faceDetector.detectMultiScale(gray, faces, 1.1, 3, 0, Size(30, 30));// 在檢測到的人臉周圍繪制矩形for(size_t i = 0; i < faces.size(); i++) {rectangle(image, faces[i], Scalar(0, 255, 0), 2);}// 保存結果圖像imwrite(outPath, image);// 釋放資源env->ReleaseStringUTFChars(imagePath, inputPath);env->ReleaseStringUTFChars(outputPath, outPath);return faces.size();
}

2.5、編譯共享庫

2.5.1、命令行編譯

使用g++命令行編譯

g++ -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -I/usr/include/opencv4 \-shared -fPIC -o libOpenCVJNI.so OpenCVJNI.cpp \-lopencv_core -lopencv_imgproc -lopencv_objdetect -lopencv_highgui

若提示錯誤找不到jni有關頭文件,請配置環境變量 JAVA_HOME, 例如這里的為 /usr/lib/jvm/java-8-openjdk-amd64

編譯之后,會在當前目錄下生成 libOpenCVJNI.so 文件。

2.5.2、cmake編譯

在當前目錄創建 CMakeLists.txt:

cmake_minimum_required(VERSION 3.5)
project(OpenCVJNI)find_package(Java REQUIRED)
find_package(JNI REQUIRED)
find_package(OpenCV REQUIRED)include_directories(${JNI_INCLUDE_DIRS})
include_directories(${OpenCV_INCLUDE_DIRS})add_library(OpenCVJNI SHARED OpenCVJNI.cpp)
target_link_libraries(OpenCVJNI ${OpenCV_LIBS})

執行以下命令,在當build目錄下生成 libOpenCVJNI.so 文件。

mkdir build
cd build
cmake ..
make

2.6、測試運行

以cmake方式為例,給出當前項目目錄結構

OpenCVJNIProject/
├── CMakeLists.txt
├── lena.png
├── OpenCVJNI.java
├── OpenCVJNI.cpp
├── OpenCVJNI.h
└── build/└── libOpenCVJNI.so

我們運行時,需要將編譯生成的 libOpenCVJNI.so ,復制到Java庫路徑或者指定路徑,之后在OpenCVJNI.class的目錄下執行。

  • 方式1
cd build
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
cd ..
java -Djava.library.path=. OpenCVJNI ../lena.png output.jpg
  • 方式2

在項目目錄下指定so目錄path

java -Djava.library.path=./build OpenCVJNI ../lena.png output.jpg
  • 方式3

將 OpenCVJNI.class 和 libOpenCVJNI.so 放在同一目錄(當前命令也包含圖片),直接運行

java OpenCVJNI ../lena.png output.jpg

在這里插入圖片描述

3、導出Jar包給后端直接使用

前面OpenCVJNI.java 就包含了接口,也包含了測試代碼。 這里,我們將前面人臉識別的接口進行封裝成一個jar包,供其他java項目直接調用。

3.1、項目結構

我們按照java調用的格式創建一個目錄結構

MyOpenCVProject/
├── native/                  # 本地代碼(C/C++)目錄
│   ├── CMakeLists.txt       # C++構建配置
│   ├── src/                 # C++源代碼
│   └── lib/                 # 生成的動態庫
├── java/                    # Java代碼目錄
│   ├── src/                 # Java源代碼
│   └── target/              # 構建輸出
└── dist/                    # 最終分發目錄

3.2、項目準備

3.2.1、創建Java類

在創建目錄 java/src/com/magicsky/OpenCVWrapper,在當前包下創建OpenCVWrapper.java 文件.

package com.magicsky.OpenCVWrapper;public class OpenCVWrapper {static {System.loadLibrary("opencv_jni"); // 加載動態庫}// 聲明本地方法public native int detectFaces(String inputPath, String outputPath);// 輔助方法:獲取當前平臺對應的庫名稱private static String getLibraryName() {String osName = System.getProperty("os.name").toLowerCase();if (osName.contains("linux")) {return "opencv_jni";} else if (osName.contains("win")) {return "opencv_jni";} else if (osName.contains("mac")) {return "opencv_jni";}return "opencv_jni";}
}

3.2.2、生成JNI頭文件

首先編譯生成class文件,并導出頭文件,這里一步到位

cd java/src
javac -h ../../native/src com/magicsky/OpenCVWrapper/OpenCVWrapper.java

運行之后,會在 OpenCVWrapper.java 同級目錄下生成 OpenCVWrapper.class文件。

類似Android中,在 native/src下創建了一個文件 com_magicsky_OpenCVWrapper_OpenCVWrapper.h

3.2.3、實現c++代碼

native/src/下創建opencv_jni.cpp 代碼,除了引用目錄和函數命名,其他內容和前述 OpenCVJNI.java 內容一致。

#include <jni.h>
#include <opencv2/opencv.hpp>
#include "com_magicsky_OpenCVWrapper_OpenCVWrapper.h"using namespace cv;JNIEXPORT jint JNICALL Java_com_magicsky_OpenCVWrapper_OpenCVWrapper_detectFaces(JNIEnv *env, jobject obj, jstring imagePath, jstring outputPath) {// 實現代碼與之前示例相同// ...
}

3.2.4、編寫CMake構建文件

cmake_minimum_required(VERSION 3.5)
project(OpenCVJNI)find_package(Java REQUIRED)
find_package(JNI REQUIRED)
find_package(OpenCV REQUIRED)include_directories(${JNI_INCLUDE_DIRS})
include_directories(${OpenCV_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib)add_library(opencv_jni SHARED src/opencv_jni.cpp)
target_link_libraries(opencv_jni ${OpenCV_LIBS})

3.2.5、構建動態庫

cd native
mkdir -p build
cd build
cmake …
make
生成的動態庫會在native/lib/目錄下,名為libopencv_jni.so

查看項目目錄結構主要文件如下
在這里插入圖片描述

3.3、 打包jar

有了以上so文件,按照jar包規則將需要的數據組織起來。

3.3.1、編譯java代碼

其實就是生成class文件。可以之前復制之前生成的。這里使用命令 javac -d 生成并存入指定位置。

cd java/src
javac -d ../target com/magicsky/OpenCVWrapper/OpenCVWrapper.java

運行后在 java/target 目錄下戶集成一個多級目錄,并創建文件 java/target/com/magicsky/OpenCVWrapper/OpenCVWrapper.class

-s 的結果為 java/target/OpenCVWrapper.class

3.3.2、創建MANIFEST.MF

創建 java/target/META-INF/ 目錄,并添加 `MANIFEST.MF文件 ,內容如下

Manifest-Version: 1.0
Class-Path: .

3.3.3 打包JAR

先試用 jar 工具 打包 MANIFEST.MF 和 OpenCVWrapper.class ,命令如下

mkdir dist
cd java/target
jar cvfm ../../dist/OpenCVWrapper.jar META-INF/MAINFEST.MF com/

運行結果如下

$ mkdir dist
$ cd java/target/
$ jar cvfm ../../dist/OpenCVWrapper.jar META-INF/MAINFEST.MF com/
added manifest
adding: com/(in = 0) (out= 0)(stored 0%)
adding: com/magicsky/(in = 0) (out= 0)(stored 0%)
adding: com/magicsky/OpenCVWrapper/(in = 0) (out= 0)(stored 0%)
adding: com/magicsky/OpenCVWrapper/OpenCVWrapper.class(in = 829) (out= 507)(deflated 38%)

3.3.4、jar包中添加so動態庫

將動態庫打包到JAR中特定目錄(如native/linux-x86_64):

cd dist
mkdir -p native/linux-x86_64
cp ../native/lib/libopencv_jni.so native/linux-x86_64/jar uf OpenCVWrapper.jar native/linux-x86_64/libopencv_jni.so

打包好后下載到本地,解壓查看jar文件結構和內容
在這里插入圖片描述
至此,jar打包完成。

還可以使用更高級的 Maven/Gradle 構建

3.4、測試jar包

準備目錄結構,并拷貝對應文件

$ tree
.
├── dist
│   ├── native
│   │   └── linux-x86_64
│   │       └── libopencv_jni.so
│   └── OpenCVWrapper.jar
├── lena.png
├── Main.java

在項目根目錄中添加 Main.java 文件,內容為

import com.magicsky.OpenCVWrapper.OpenCVWrapper;public class Main {public static void main(String[] args) {OpenCVWrapper wrapper = new OpenCVWrapper();int faceCount = wrapper.detectFaces("lena.png", "output.jpg");System.out.println("Detected " + faceCount + " faces.");}
}

使用 javac -cp dist/OpenCVWrapper.jar Main.java 編譯生成 Main.class 文件。

之后執行命令,需要指定so目錄, jar 目錄等 (根目錄下執行)
java -Djava.library.path=dist/native/linux-x86_64 -cp dist/OpenCVWrapper.jar:. Main

運行成功,截圖如下
在這里插入圖片描述

3.5、包聲明問題

前面的測試代碼,Main.java在根目錄,不存在包聲明。下面結構中,存在一個test包。那么在Main.java文件第一行為 "package test;"

$ tree
.
├── dist
│   ├── native
│   │   └── linux-x86_64
│   │       └── libopencv_jni.so
│   └── OpenCVWrapper.jar
├── lena.png
├── test
│   ├── Main.class
│   └── Main.java

有包聲明時,命令需要修改 (根目錄下執行,包聲明中最上一層)
java -Djava.library.path=./dist/native/linux-x86_64 -cp ./dist/OpenCVWrapper.jar:. test.Main

4、優化 Jar 包代碼和結構

前面jar包在使用時,沒有使用jar包中的 native/lib/libopencv_jni.so 文件,而是拷貝了一份運行時再指定路徑。

我們應該在用戶使用該jar時,解壓jar中的so并使用。
修改 java 文件如下 :

package com.magicsky.OpenCVWrapper;import java.io.*;
import java.nio.file.*;public class OpenCVWrapper {// static {//     System.loadLibrary("opencv_jni"); // 加載動態庫// }static {loadLibrary();}private static void loadLibrary() {try {String libName = getLibraryName();String libPath = "/native/" + getPlatform() + "/lib" + libName + ".so";// 從JAR中提取庫到臨時目錄InputStream in = OpenCVWrapper.class.getResourceAsStream(libPath);if (in == null) {throw new RuntimeException("Native library not found in JAR: " + libPath);}Path tempDir = Files.createTempDirectory("native-lib");tempDir.toFile().deleteOnExit();Path tempLib = tempDir.resolve("lib" + libName + ".so");Files.copy(in, tempLib, StandardCopyOption.REPLACE_EXISTING);in.close();// 加載庫System.load(tempLib.toAbsolutePath().toString());} catch (IOException e) {throw new RuntimeException("Failed to load native library", e);}}// 聲明本地方法public native int detectFaces(String inputPath, String outputPath);// 輔助方法:獲取當前平臺對應的庫名稱private static String getLibraryName() {String osName = System.getProperty("os.name").toLowerCase();if (osName.contains("linux")) {return "opencv_jni";} else if (osName.contains("win")) {return "opencv_jni";} else if (osName.contains("mac")) {return "opencv_jni";}return "opencv_jni";}private static String getPlatform() {String osName = System.getProperty("os.name").toLowerCase();String osArch = System.getProperty("os.arch").toLowerCase();if (osName.contains("linux")) {return "linux-" + osArch;} else if (osName.contains("win")) {return "win-" + osArch;} else if (osName.contains("mac")) {return "mac-" + osArch;}throw new UnsupportedOperationException("Unsupported platform: " + osName + "/" + osArch);}
}

重新編譯并打包

cd java/src
javac -d ../target/ com/magicsky/OpenCVWrapper/OpenCVWrapper.java
cd ../target
jar cvfm ../../dist/OpenCVWrapper.jar META-INF/MAINFEST.MF com/
cd ../../dist
jar uf opencv-wrapper.jar native/linux-x86_64/libopencv_jni.so

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/79148.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/79148.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/79148.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

4.1 模塊概述

1.Python結構 工程 > 包 > 模塊 Python工程: “Python項目中最大的文件夾(本質就是一個文件夾)” --- 左側的 CODE文件夾 為Python工程 Python包: 本質就是一個文件夾,但是python包中具備具體的標識,如果沒有標識則不能導入 --- 左側的 01.Python基礎 文件夾為python包 P…

AJAX 實例

AJAX 實例 引言 Ajax&#xff08;Asynchronous JavaScript and XML&#xff09;是一種在無需重新加載整個頁面的情況下&#xff0c;與服務器交換數據并更新部分網頁的技術。Ajax通過在后臺與服務器交換數據&#xff0c;實現了頁面的動態更新&#xff0c;從而提高了用戶體驗和…

相機的基礎架構

&#x1f4f7; 相機相關基礎架構學習路徑 一、了解手機相機系統架構 Android Camera HAL&#xff08;如果你是做 Android 平臺&#xff09; 學習 Camera HAL3 架構&#xff08;基于 camera_device_t, camera3_device_ops 接口&#xff09; 熟悉 CameraService → CameraProvid…

MLX Chat - 基于 Streamlit 的 MLX 前端界面

本文翻譯整理自&#xff1a;https://github.com/da-z/mlx-ui 一、關于 MLX Chat 一個基于 Streamlit 的簡單 UI/網頁前端&#xff0c;用于 MLX mlx-lm 項目。 相關鏈接資源 github : https://github.com/da-z/mlx-uiMLX 社區模型庫&#xff1a;https://huggingface.co/mlx-co…

el-table 自定義列、自定義數據

一、對象數組格式自定義拆分為N列 1-1、數據格式&#xff1a; const arrayList ref([{"RACK_NO": "A-1-001"},{"RACK_NO": "A-1-002"},{ "RACK_NO": "A-1-003"},//省略多個{"RACK_NO": "A-1-100…

JVM 如何使用性能分析工具定位代碼中的性能問題?

核心思想&#xff1a; 通過工具觀察程序在特定負載下的運行狀態&#xff0c;識別消耗資源最多的代碼段&#xff08;熱點代碼&#xff09;、異常的內存分配模式或線程阻塞情況&#xff0c;然后針對性的優化代碼。 通用步驟&#xff1a; 確定問題&#xff1a; 首先明確遇到了什…

Python虛假新聞檢測識別

程序示例精選 Python虛假新聞檢測識別 如需安裝運行環境或遠程調試&#xff0c;見文章底部個人QQ名片&#xff0c;由專業技術人員遠程協助&#xff01; 前言 這篇博客針對《Python虛假新聞檢測識別》編寫代碼&#xff0c;代碼整潔&#xff0c;規則&#xff0c;易讀。 學習與應…

網絡原理 - 12(HTTP/HTTPS - 3 - 響應)

目錄 認識“狀態碼”&#xff08;status code&#xff09; 200 OK 404 Not Found 403 Forbidden 405 Method Not Allowed 500 Internal Server Error 504 Gateway Timeout 302 Move temporarily 301 Moved Permanently 418 I am a teaport 狀態碼小結&#xff1a; …

Spring Boot中集成Guava Cache或者Caffeine

一、在Spring Boot(1.x版本)中集成Guava Cache 注意&#xff1a; Spring Boot 2.x用戶&#xff1a;優先使用Caffeine&#xff0c;性能更優且維護活躍。 1. 添加依賴 在pom.xml中添加Guava依賴&#xff1a; <dependency><groupId>com.google.guava</groupId&…

Linux工作臺文件操作命令全流程解析

全文目錄 1 確認當前工作路徑2 導航與目錄管理2.1 關鍵命令2.2 邏輯銜接 3 文件基礎操作3.1 創建 → 備份 → 重命名 → 清理3.2 文件查看和編輯3.3 文件鏈接3.4 文件diff 4 文件權限與所有權管理5 文件打包與歸檔6 參考文獻 寫在前面 shell是一種命令解釋器&#xff0c;它提供…

LeetCode第183題_從不訂購的客戶

LeetCode 第183題&#xff1a;從不訂購的客戶 題目描述 表: Customers ---------------------- | Column Name | Type | ---------------------- | id | int | | name | varchar | ---------------------- id 是該表的主鍵。 該表包含消費者的 id 和…

c語言的常用關鍵字

c語言的常用關鍵字 c語言的關鍵字表示數據類型的關鍵字autocharfloatdoubleintlongshortvoidsignedstruct、enum、unionunsigned 表示分支語句的關鍵字ifelseswitchbreakcasecontinuedefault 表示循環語句的關鍵字whiledoforgoto 用于修飾變量或函數的關鍵字constconst修飾變量…

MCU通用輸入輸出端口(GPIO)設計指南

在嵌入式系統開發中&#xff0c;MCU的GPIO接口是一個基礎但非常實用的功能模塊。GPIO全稱是通用輸入輸出端口&#xff0c;它讓MCU可以靈活地與外部設備進行交互。 GPIO的主要特點包括&#xff1a; 多功能性&#xff1a;每個引腳都可以單獨配置為輸入或輸出 可編程性&#xff…

STM32完整內存地址空間分配詳解

在STM32這類基于ARM Cortex-M的32位微控制器中&#xff0c;整個4GB的地址空間(從0x00000000到0xFFFFFFFF)有著非常系統化的分配方案&#xff0c;每個區域都有其特定的用途。下面我將詳細介紹這些地址區域的分配及其功能&#xff1a; STM32完整內存地址空間分配詳解(0x00000000…

實現水平垂直居中的多種方法

在前端開發中&#xff0c;元素的居中是一個常見但又經常讓人頭疼的問題。本文將全面總結各種CSS居中方法&#xff0c;特別是如何實現一個div的水平垂直居中。 為什么居中這么重要&#xff1f; 居中布局是現代網頁設計中最基礎也最重要的布局方式之一。無論是導航菜單、登錄框…

如何實現服務的自動擴縮容(Auto Scaling)

在云計算和分布式系統的時代,系統的彈性和適應性已成為企業構建高效IT基礎設施的核心需求。自動擴縮容(Auto Scaling)作為一種關鍵技術,旨在根據實時負載變化動態調整計算資源,以確保系統性能穩定,同時優化資源利用效率。簡單來說,自動擴縮容是指系統能夠根據預設規則或…

uniapp+vue3+ts 使用canvas實現安卓端、ios端及微信小程序端二維碼生成及下載

加粗樣式uniapp多端生成帶二維碼海報并保存至相冊的實現 在微信小程序開發中&#xff0c;我們常常會遇到生成帶有二維碼的海報并保存到手機相冊的需求&#xff0c;比如分享活動海報、產品宣傳海報等。今天就來和大家分享一下如何通過代碼實現這一功能。 準備工作 在開始之前&am…

架構師面試(三十八):注冊中心架構模式

題目 在微服務系統中&#xff0c;當服務達到一定數量時&#xff0c;通常需要引入【注冊中心】組件&#xff0c;以方便服務發現。 大家有沒有思考過&#xff0c;注冊中心存在的最根本的原因是什么呢&#xff1f;注冊中心在企業中的最佳實踐是怎樣的&#xff1f;注冊中心的服務…

Day.js和Moment.js對比,日期時間庫怎么選?

在JavaScript的日期處理庫中&#xff0c;Moment.js 和 Day.js 是兩個非常流行的選擇。本文將基于從npmtrends的數據&#xff0c;對這兩個庫進行詳細的對比分析。 Moment.js的重度使用者。凡是遇到時間和日期的操作&#xff0c;就把Moment.js引用上。 直到有天我發現加載的mome…

羅默如何用木星衛星“宇宙鐘表”測量光速?

一、17世紀的“宇宙級實驗” 1676年&#xff0c;丹麥天文學家奧勒羅默&#xff08;Ole Rmer&#xff09;在巴黎天文臺做出驚人發現&#xff1a; 木星衛星的“遲到早退”現象&#xff0c;竟能揭示光速的秘密&#xff01; 通過觀察木衛一&#xff08;Io&#xff09;的軌道周期變…