安卓JNI精細化講解,讓你徹底瞭解JNI(二):用法解析

来源:https://www.cnblogs.com/qixingchao/archive/2019/11/22/11911787.html
-Advertisement-
Play Games

目錄 "用法解析" "├── 1、JNI函數" "│ ├── 1.1、extern "C"" "│ ├── 1.2、JNIEXPORT、JNICALL" "│ ├── 1.3、函數名" "│ ├── 1.4、JNIEnv" "│ ├── 1.5、jobject" "├── 2、Java、JNI、C/ ...


目錄

用法解析
├── 1、JNI函數
│ ├── 1.1、extern "C"
│ ├── 1.2、JNIEXPORT、JNICALL
│ ├── 1.3、函數名
│ ├── 1.4、JNIEnv
│ ├── 1.5、jobject
├── 2、Java、JNI、C/C++基本類型映射關係
├── 3、JNI描述符(簽名)
├── 4、函數靜態註冊、動態註冊
│ ├── 4.1、動態註冊原理
│ ├── 4.2、靜態註冊原理
│ ├── 4.3、Java調用native的流程

當通過AndroidStudio創建了Native C++工程後,首先面對的是*.cpp文件,對於不熟悉C/C++的開發人員而言,往往是望“類”興嘆,無從下手。為此,咱們系統的梳理一下JNI的用法,為後續Native開發做鋪墊。

1、JNI函數

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_qxc_testnativec_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

通常,大家看到的JNI方法如上圖所示,方法結構與Java方法類似,同樣包含方法名、參數、返回類型,只不過多了一些修飾詞、特定參數類型而已。

1.1、extern "C"

作用:避免編繹器按照C++的方式去編繹C函數

該關鍵字可以刪掉嗎?
我們不妨動手測試一下:去掉extern “C” , 重新生成so,運行app,結果直接閃退了:

咱們反編譯so文件看一下,原來去掉extern “C” 後,函數名字竟然被修改了:

//保留extern "C"
000000000000ea98 T 
Java_com_qxc_testnativec_MainActivity_stringFromJNI

//去掉extern "C"
000000000000eab8 T 
_Z40Java_com_qxc_testnativec_MainActivity_stringFromJNIP7_JNIEnvP8_jobject

原因是什麼呢?
其實這跟C和C++的函數重載差異有關係:

1、C不支持函數的重載,編譯之後函數名不變;
2、C++支持函數的重載(這點與Java一致),編譯之後函數名會改變;

原因:在C++中,存在函數的重載問題,函數的識別方式是通過:函數名,函數的返回類型,函數參數列表
三者組合來完成的。

所以,如果希望編譯後的函數名不變,應通知編譯器使用C的編譯方式編譯該函數(即:加上關鍵字:extern “C”)。

擴展:
如果即想去掉關鍵字 extern “C”,又希望方法能被正常調用,真的不能實現嗎?

非也,還是有解決辦法的:“函數的動態註冊”,這個後面再介紹吧!!!
1.2、JNIEXPORT、JNICALL
作用:

JNIEXPORT 用來表示該函數是否可導出(即:方法的可見性)
JNICALL 用來表示函數的調用規範(如:__stdcall)

我們通過JNIEXPORT、JNICALL關鍵字跳轉到jni.h中的定義,如下圖:

通過查看 jni.h 中的源碼,原來JNIEXPORT、JNICALL是兩個巨集定義

對於安卓開發者來說,巨集可這樣理解:

├── 巨集 JNIEXPORT 代表的就是右側的表達式: __attribute__ ((visibility ("default")))
├── 或者也可以說: JNIEXPORT 是右側表達式的別名

巨集可表達的內容很多,如:一個具體的數值、一個規則、一段邏輯代碼等;

attribute___((visibility ("default"))) 描述的是“可見性”屬性 visibility

1、default :表示外部可見,類似於public修飾符 (即:可以被外部調用)
2、hidden :表示隱藏,類似於private修飾符 (即:只能被內部調用)
3、其他 :略

如果,我們想使用hidden,隱藏我們寫的方法,可這麼寫:

#include <jni.h>
#include <string>

extern "C" __attribute__ ((visibility ("hidden"))) jstring JNICALL
Java_com_qxc_testnativec_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

重新編譯、運行,結果閃退了。
原因:函數Java_com_qxc_testnativec_MainActivity_stringFromJNI已被隱藏,而我們在java中調用該函數時,找不到該函數,所以拋出了異常,如下圖:

巨集JNICALL 右邊是空的,說明只是個空定義。上面講了,巨集JNICALL代表的是右邊定義的內容,那麼,我們代碼也可直接使用右邊的內容(空)替換調JNICALL(即:去掉JNICALL關鍵字),編譯後運行,調用so仍然是正確的:

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring
Java_com_qxc_testnativec_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
JNICALL 知識擴展:

JNICALL的定義,並非所有平臺都像Linux一樣是空的,如windows平臺:
#ifndef _JAVASOFT_JNI_MD_H_  
#define _JAVASOFT_JNI_MD_H_  
#define JNIEXPORT __declspec(dllexport)  
#define JNIIMPORT __declspec(dllimport)  
#define JNICALL __stdcall  
typedef long jint;  
typedef __int64 jlong;  
typedef signed char jbyte;  
#endif
1.3、函數名

看到.cpp中的函數"Java_com_qxc_testnativec_MainActivity_stringFromJNI",大部分開發人員都會有疑問:我們定義的native函數名stringFromJNI,為什麼對應到cpp中函數名會變成這麼長呢?

public native String stringFromJNI();

這跟JNI native函數的註冊方式有關

JNI Native函數有兩種註冊方式(後面會詳細介紹):
1、靜態註冊:按照JNI介面規範的命名規則註冊;
2、動態註冊:在.cpp的JNI_OnLoad方法里註冊;

JNI介面規範的命名規則:
一般是 Java_ ,當我們在Java中調用native方法時,JVM 也會根據這種命名規則來查找、調用native方法對應的 C 方法。

1.4、JNIEnv

JNIEnv 代表了Java環境,通過JNIEnv*就可以對Java端的代碼進行操作,如:
├──創建Java對象
├──調用Java對象的方法
├──獲取Java對象的屬性等

我們跳轉、查看JNIEnv的源碼實現,如下圖:

JNIEnv指向_JNIEnv,而_JNIEnv是定義的一個C++結構體,裡面包含了很多通過JNI介面(JNINativeInterface)對象調用的方法。

那麼,我們通過JNIEnv操作Java端的代碼,主要使用哪些方法呢?
| 函數名稱 | 作用 |
|:-----------------:| :-----------------:|
| NewObject | 創建Java類中的對象 |
| NewString | 創建Java類中的String對象 |
| NewArray | 創建類型為Type的數組對象 |
| GetField | 獲得類型為Type的欄位 |
| SetField | 設置類型為Type的欄位 |
| GetStaticField | 獲得類型為Type的static的欄位 |
| SetStaticField | 設置類型為Type的static的欄位 |
| CallMethod | 調用返回值類型為Type的static方法 |
| CallStaticMethod | 調用返回值類型為Type的static方法 |
具體用法,後面案例再進行演示。

1.5、jobject

jobject 代表了定義native函數的Java類 或 Java類的實例:

├── 如果native函數是static,則代表類Class對象
├── 如果native函數非static,則代表類的實例對象

我們可以通過jobject訪問定義該native方法的成員方法、成員變數等。

2、Java、JNI、C/C++基本類型映射關係

上面,已經介紹了.cpp方法的基本結構、主要關鍵字。當我們定義了具體方法,寫C/C++方法實現時,會用到各種參數類型。那麼,在JNI開發中,這些類型應該是怎麼寫呢?
舉例:定義加、減、乘、除的方法

//加
jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a+b;
}
//減
jint subNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a-b;
}
//乘
jint mulNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a*b;
}
//除
jint divNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a/b;
}

通過上面案例可以看到,幾個方法的後兩個參數、返回值,類型都是 jint

jint 是JNI中定義的類型別名,對應的是Java、C++中的int類型

我們先源碼跟蹤、看下jint的定義,jint 原來是 jni.h中 定義的 int32_t 的別名,如下圖:

根據 int32_t 查找,發現 int32_t 是 stdint.h中定義的 __int32_t的別名,如下圖:

再根據 __int32_t 查找,發現 __int32_t 是 stdint.h中定義的 int 的別名(這個也就是C/C++中的int類型了),如下圖:

Java 、C/C++都有一些常用的數據類型,分別是如何與JNI類型對應的呢?如下所示:

Java 、C/C++中的常用數據類型的映射關係表(通過源碼跟蹤查找列出來的)
JNI中定義的別名 Java類型 C/C++類型
jint / jsize int int
jshort short short
jlong long long / long long (__int64)
jbyte byte signed char
jboolean boolean unsigned char
jchar char unsigned short
jfloat float float
jdouble double double
jobject Object _jobject*

3、JNI描述符 (簽名)

JNI開發時,我們除了寫本地C/C++實現,還可以通過 JNIEnv *env 調用Java層代碼,如獲得某個欄位、獲取某個函數、執行某個函數等:

//獲得某類中定義的欄位id
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
    { return functions->GetFieldID(this, clazz, name, sig); }

//獲得某類中定義的函數id
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    { return functions->GetMethodID(this, clazz, name, sig); }

上面的函數與Java的反射比較類似,參數:

clazz : 類的class對象
name : 欄位名、函數名
sig : 欄位描述符(簽名)、函數描述符(簽名)

寫過反射的開發人員對clazz、name這兩個參數應該比較熟悉,對sig稍微陌生一些。

sig 此處是指的:

1、如果是欄位,表示欄位類型的描述符
2、如果是函數,表示函數結構的描述符,即:每個參數類型描述符 + 返回值類型描述符

舉例( int 類型的描述符是 大寫的 I ):

Java代碼:

public class Hello{
     public int property;
     public int fun(int param, int[] arr){
          return 100;
     }
}
JNI C/C++代碼:

JNIEXPORT void Java_Hello_test(JNIEnv* env, jobject obj){
    jclass myClazz = env->GetObjectClass(obj);
    jfieldId fieldId_prop = env -> GetFieldId(myClazz, "property", "I");
    jmethodId methodId_fun = env -> GetMethodId(myClazz, "fun", "(I[I)I");
}

由上面的示例可以看到,Java類中的欄位類型、函數定義分別對應的描述符:

int  類型 對應的是  I
fun  函數 對應的是  (I[I)I

其他類型的描述符(簽名)如下表:
| Java類型 | 欄位描述符(簽名) | 備註|
|:-----------------:| :-----------------:|:-----------------:|
| int | I |int的首字母、大寫|
| float | F |float的首字母、大寫|
| double | D |double的首字母、大寫|
| short | S |short的首字母、大寫|
| long | L |long的首字母、大寫|
| char | C |char的首字母、大寫|
| byte | B |byte的首字母、大寫|
| boolean | Z |因B已被byte使用,所以JNI規定使用Z|
| object | L + /分隔完整類名 |String 如: Ljava/lang/String|
| array | [ + 類型描述符 |int[] 如:[I|

Java函數 函數描述符(簽名) 備註
void V 無返回值類型
Method (參數欄位描述符...)返回值欄位描述符 int add(int a,int b) 如:(II)I

4、函數靜態註冊、動態註冊

JNI開發中,我們一般定義了Java native方法,又寫了對應的C方法實現。
那麼,當我們在Java代碼中調用Java native方法時,虛擬機是怎麼知道並調用SO庫的對應的C方法的呢?

Java native方法與C方法的對應關係,其實是通過註冊實現的,Java native方法的註冊形式有兩種,一種是靜態註冊,另一種是動態註冊:

靜態註冊:按照JNI規範書寫函數名:java_類路徑_方法名(路徑用下劃線分隔)
動態註冊:JNI_OnLoad中指定Java Native函數與C函數的對應關係

兩種註冊方式的使用對比:

靜態註冊:
1、優缺點:
系統預設方式,使用簡單;
靈活性差(如果修改了java native函數所在類的包名或類名,需手動修改C函數名稱(頭文件、源文件));

2、實現方式:
1)函數名可以根據規則手寫
2)也可使用javah命令自動生成

3、示例:
extern "C" JNIEXPORT jstring
Java_com_qxc_testnativec_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
動態註冊:
1、優缺點:
函數名看著舒服一些,但是需要在C代碼中維護Java Native函數與C函數的對應關係;
靈活性稍高(如果修改了java native函數所在類的包名或類名,僅調整Java native函數的簽名信息)

2、實現方式
env->RegisterNatives(clazz, gMethods, numMethods)

3、示例:
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){
    //列印日誌
    __android_log_print(ANDROID_LOG_DEBUG,"JNITag","enter jni_onload");
    JNIEnv* env = NULL;
    jint result = -1;
    // 判斷是否正確
    if((*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_6)!= JNI_OK){
        return result;
    }
    // 定義函數映射關係(參數1:java native函數,參數2:函數描述符,參數3:C函數)
    const JNINativeMethod method[]={
            {"add","(II)I",(void*)addNumber},
            {"sub","(II)I",(void*)subNumber},
            {"mul","(II)I",(void*)mulNumber},
            {"div","(II)I",(void*)divNumber}
    };
    //找到對應的JNITools類
    jclass jClassName=(*env)->FindClass(env,"com/qxc/testpage/JNITools");
    //開始註冊
    jint ret = (*env)->RegisterNatives(env,jClassName,method, 4);
     //如果註冊失敗,列印日誌
    if (ret != JNI_OK) {
        __android_log_print(ANDROID_LOG_DEBUG, "JNITag", "jni_register Error");
        return -1;
    }
    return JNI_VERSION_1_6;
}

//加
jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a+b;
}
//減
jint subNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a-b;
}
//乘
jint mulNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a*b;
}
//除
jint divNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a/b;
}

上面,帶著大家瞭解了兩種註冊方式的基本知識。接下來,咱們再深入瞭解一下動態註冊和靜態註冊的底層差異、以及實現原理。

4.1、動態註冊原理

動態註冊是Java代碼調用中System.loadLibray()時完成的

那麼,我們先瞭解一下System.loadLibray載入動態庫時,底層究竟做了哪些操作:

System.loadLibray的流程圖(為了便於大家理解,此圖省略了部分流程)

底層源碼:/dalvik/vm/Native.cpp

dvmLoadNativeCode() -> JNI_OnLoad()
//省略的代碼......
//將pNewEntry保存到gDvm全局變數nativeLibs中,下次可以直接通過緩存獲取
SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
//省略的代碼......
//第一次載入so時,調用so中的JNI_OnLoad方法
vonLoad = dlsym(handle, "JNI_OnLoad");

通過System.loadLibray的流程圖,不難看出,Java中載入.so動態庫時,最終會調用so中的JNI_OnLoad方法,這也是為什麼我們要在C的JNIEXPORT jint JNI_OnLoad(JavaVM vm, void* reserved)方法中註冊的原因。

接下來,咱們再深入瞭解一下動態註冊的具體流程:

動態註冊的具體流程圖(為了便於大家理解,此圖省略了部分流程)

如上圖所示:

流程1:是指執行 System.loadLibray函數;
流程2:是指底層預設調用so中的JNI_OnLoad函數;
流程3:是指開發人員在JNI_OnLoad中寫的註冊方法,例如: (*env)->RegisterNatives(env,.....)
流程4:需要重點講解一下:
├── 在Android中,不管是Java函數還是Java Native函數,它在虛擬機中對應的都是一個Method*對象
├── 如果是Java Native函數,那麼Method*對象的nativeFunc會指向一個bridge函數dvmCallJNIMethod
├── 當調用Java Native函數時,就會執行該bridge函數,bridge函數的作用是調用該Java Native方法對應的
JNI方法,即: method.insns

流程4的主要作用,如圖所示,為Java Native函數對應的Method*對象,綁定屬性,建立對應關係:
├── nativeFunc 指向函數 dvmCallJNIMethod(通常情況下)
├── insns 指向native層的C函數指針 (我們寫的C函數)

我們再從源碼層面,重點分析一下動態註冊的流程3和流程4吧。

流程3:開發人員在JNI_OnLoad中寫的註冊方法,註冊對應的C函數

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){
    //列印日誌
    __android_log_print(ANDROID_LOG_DEBUG,"JNITag","enter jni_onload");
    JNIEnv* env = NULL;
    jint result = -1;
    // 判斷是否正確
    if((*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_6)!= JNI_OK){
        return result;
    }
    // 定義函數映射關係(參數1:java native函數,參數2:函數描述符,參數3:C函數)
    const JNINativeMethod method[]={
            {"add","(II)I",(void*)addNumber},
            {"sub","(II)I",(void*)subNumber},
            {"mul","(II)I",(void*)mulNumber},
            {"div","(II)I",(void*)divNumber}
    };
    //找到對應的JNITools類
    jclass jClassName=(*env)->FindClass(env,"com/qxc/testpage/JNITools");
    //開始註冊
    jint ret = (*env)->RegisterNatives(env,jClassName,method, 4);
     //如果註冊失敗,列印日誌
    if (ret != JNI_OK) {
        __android_log_print(ANDROID_LOG_DEBUG, "JNITag", "jni_register Error");
        return -1;
    }
    return JNI_VERSION_1_6;
}

//加
jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a+b;
}
//減
jint subNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a-b;
}
//乘
jint mulNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a*b;
}
//除
jint divNumber(JNIEnv *env,jclass clazz,jint a,jint b){
     return a/b;
}

C函數的定義比較簡單,共加減乘除4個函數。當動態註冊時,需調用函數 (env)->RegisterNatives(env,jClassName,method, 4)(該方法有不同參數的多個方法重載),我們主要關註的參數:jclass clazz、JNINativeMethod methods、jint nMethods

clazz 表示:定義Java Native方法的Java類;
methods 表示:Java Native方法與C方法的對應關係;
nMethods 表示:methods註冊方法的數量,一般設置成methods數組的長度;

JNINativeMethod如何表示Java Native方法與C方法的對應關係的呢?查看其源碼定義:

jni.h

//結構體
typedef struct {
    const char* name;   //Java 方法名稱
    const char* signature;  //Java 方法描述符(簽名)
    void*       fnPtr;  //C/C++方法實現
} JNINativeMethod;

瞭解了JNINativeMethod結構,那麼,JNINativeMethod對象是如何與虛擬機中的Method*對象對應的呢?這個有點複雜了,咱們通過流程圖簡單描述一下吧:

動態註冊的源碼流程圖(為了便於大家理解,此圖省略了部分流程)

dvmSetNativeFunc源碼分析
如果還希望更清晰的瞭解底層源碼的實現邏輯,可下載Android源碼,自行分析一下吧。

4.2、靜態註冊原理

靜態註冊是在首次調用Java Native函數時完成的

靜態註冊的具體流程圖(為了便於大家理解,此圖省略了部分流程)
如上圖所示:

流程1:Java代碼中調用Java Native函數;
流程2:獲得Method*對象,預設為該函數的Method*設置nativeFunc(dvmResolveNativeMethod);
流程3:dvmResolveNativeMethod函數中按照特定名稱查找對應的C方法;
流程4:如果找到了對應的C方法,重新為該方法設置Method*屬性;

註意:當Java代碼中第二次再調用Java Native函數時,Method*的nativeFunc已經有值了
(即:dvmCallJNIMethod,可參考動態註冊流程內容),會直接執行Method*的nativeFunc的函數,不會在
重新執行特定名稱查找了。

靜態註冊流程2 源碼分析

靜態註冊流程3、4 源碼分析

4.3、Java調用native的流程

Java代碼中調用Java native的流程圖(為了便於大家理解,此圖省略了部分流程)
經過對動態註冊、靜態註冊的實現原理的梳理之後,再看Java代碼中調用Java native方法的流程圖,就比較簡單了:

1、如果是動態註冊的Java native函數,System.loadLibray時就已經設置好了Java native函數與C函數的對應關係,當Java代碼中調用Java native方法時,直接執行dvmCallJNIMethod橋函數即可(該函數中執行C函數)。

2、如果是靜態註冊的Java native函數,當Java代碼中調用Java native方法時,預設為Method.nativeFunc賦值為dvmResolveNativeMethod,並按特定名稱查找C方法,重新賦值Method*,最終仍然是執行dvmCallJNIMethod橋函數(只不過Java代碼中第二次再調用靜態註冊的Java native函數時,不會再執行黃色部分的流程圖了)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 如果你的系統有高併發的要求,可以嘗試使用SQL Server記憶體優化表來提升你的系統性能。你甚至可以把它當作Redis來使用。 ...
  • 對比結論 1. 性能上: 性能上都很出色,具體到細節,由於Redis只使用單核,而Memcached可以使用多核,所以平均每一個核上Redis在存儲小數據時比Memcached性能更高。而在100k以上的數據中,Memcached性能要高於Redis,雖然Redis最近也在存儲大數據的性能上進行優化 ...
  • select * from table1 t where (select count(*) from table1 where column1=t.column1 AND column2=t.column2 and column3=t.column3)>1 ...
  • USE [SPECIAL_BLD]GO SET ANSI_NULLS ONGO SET QUOTED_IDENTIFIER ONGO CREATE FUNCTION [dbo].[get_upper] ( @num numeric(18,5))RETURNS VARCHAR(500)ASBEGIN ...
  • 一、Atlas是什麼? 在當今大數據的應用越來越廣泛的情況下,數據治理一直是企業面臨的巨大問題。 大部分公司只是單純的對數據進行了處理,而數據的血緣,分類等等卻很難實現,市場上也急需要一個專註於數據治理的技術框架,這時Atlas應運而生。 Atlas官網地址: "https://atlas.apac ...
  • 看如下一條sql語句: # table T (id int, name varchar(20)) delete from T where id = 10; MySQL在執行的過程中,是如何加鎖呢? 在看下麵這條語句: select * from T where id = 10; 那這條語句呢?其實這 ...
  • 在MySQL中經常出現未按照理想情況使用索引的情況,今天記錄一種Order by語句的使用導致未按預期使用索引的情況。 1. 問題現象 1.1 SQL語句: SELECT DISTINCT p.* FROM tb_name p WHERE 1=1 AND p.createDate >= '2019- ...
  • [20191122]oracel SQL parsing function qcplgte.txt--//昨天看了鏈接:https://nenadnoveljic.com/blog/memory-leak-parsing/ =>Memory Leak During Parsingqcplgteqcp ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...