Java跨平台开发神器之JNI

        之前应公司要求,利用java开发一个门禁系统,其要求是用封装好的DLL(C++写的)来做为接口,用java去调用该DLL库中暴露出来的接口,已完成门禁系统的逻辑开发。

        对于我这样的实习僧来说实为当头一棒,从来没有接触过java还能跨平台,我了个ca,于是乎在内心几近崩溃的条件下,开始查资料,垒代码,最终算是完成了要求,现在分享如下,望需要的小伙伴们拾到干货。


        首先,企业级开发吗,虽然对我这实习的不太实用,但是多少还要遵守流程的,首先是查资料。Java跨平台  有很多种方法,如JNI,Jawin,Jacob,JNative等,但是后者皆是以JNI为基础进行的开发,所以说JNI是最底层的,最强大的,读者想知道其他几种方法可以起官方文档查看。

Jawin官网文档

Jacob官方文档

知道了用JNI方式进行开发后,我们接下来就要了解JNI并进行一些简单的实现,比如打印我们的老朋友HelloWorld:


        JNI -- Java Native Interface

1.简介.

    JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。

    sun相关文档:http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/jniTOC.html

2.简单实例.

    Java调用c++的dll实现在console端打印“Hello World !!!”

3.步骤.

     ①编写Java类,用该类将DLL对外提供的函数服务进行声明,其中的Java方法均声明为native,其方法签名可以自定义,不用实现函数体。实现如下:

<span style="font-size:18px;">public class HelloWorld {
    public native void show();
    static {
        System.loadLibrary("hello");
    }
    public static void main(String[] args) {
        TestJNI t = new TestJNI();
        t.show();
    }
}</span>

②win+r打开cmd,cd 到java的到工程目录下的TestJNI.java文件处,执行

Javac TestJNI.java

Javah TestJNI

可以看到,在该文件目录下生成两连个文件,TestJNI.classes和TestJNI.h,其中TestJNI.h用来在c中调用。

TestJNI.h文件内容如下  

<span style="font-size:18px;">/* DO NOT EDIT THIS FILE - it is machine generated */
  #include <jni.h>
  /* Header for class HelloWorld */
  #ifndef _Included_HelloWorld
  #define _Included_HelloWorld
  #ifdef __cplusplus
  extern "C" {
  #endif
   /*
  * Class: HelloWorld
  * Method: show
  * Signature: ()V
  */
  JNIEXPORT void JNICALL Java_HelloWorld_show
  (JNIEnv *, jobject);
  #ifdef __cplusplus
  }
  #endif
  #endif</span>

这里JNIEXPORT和JNICALL都是JNI的关键字,表示此函数是要被JNI调用的。而jint、jstring是以JNI为中介使JAVA中的数据类型与C/C++中的数据类型之间的一种中间类型。jint可以直接当做int使用,但是jstring不能和char *等同,需要做一定的转换。函数的名称是JAVA_再加上java程序的package路径再加函数名组成的。参数中,我们也只需要关心在JAVA程序中存在的参数,至于JNIEnv*和jclass我们一般没有必要去碰它。

④实现c/c++头文件HelloWorld.h的头文件 HelloWorld.cpp如下:(自己先建个win32的工程,就是可以生成dll的工程,不同编译器创建dll工程的方式不同,这个大家自己问度娘)

<span style="font-size:18px;">#include "jni.h"
#include "TestJNI.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_TestJNI_show
(JNIEnv *env, jobject obj)
{
    printf("Hello World !!!");
    return;
}</span>

⑤编译生成hello.dll动态链接库

⑥在java编译器中,选择File->Import->FileSystem->Broswer->dll文件所在目录->勾选要导入的dll文件。

⑦运行 java HelloWorld(在编译器或者cmd下都可以)。

⑧如果在编译器下运行(STS为例),则Console中会出现Hello World!!!、


简单说,我们的流程就是在java中先建立java文件,用

<span style="font-size:18px;">static {
        System.loadLibrary("hello");
    }</span>
导入要调用的dll包,dll包创建过程如上。

        简单的我们了解了。helloworld我也打印了,接下来进行比较高端的,其实也并不高端,就是你想调用的函数(如上的show()函数)里面有参数了,而且万一参数是指针,那怎么办,java中可没有指针,这里,大家先要了解JNI中的数据类型了。表如下(摘下来的):

    Java类型                本地类型                 JNI中定义的别名    
intlongjint
long_int64jlong
bytesigned charjbyte
booleanunsigned charjboolean
charunsigned shortjchar
shortshortjshort
floatfloatjfloat
doubledoublejdouble
Object_jobject*jobject
知道数据类型还不够,我们还得知道一个叫方法签名的东西:

        方法签名包括两部分:参数类型和返回值类型;具体的格式:(参数1类型签名参数2类型签名)返回值类型签名。下面是java类型和年名类型的对照的一个表

 

    Java类型     对应的签名
booleanZ
byteB
charC
shrotS
intI
longL
floatF
doubleD
voidV
ObjectL用/分割包的完整类名;  Ljava/lang/String;
Array[签名       [I       [Ljava/lang/String;
这个表要用,非常重要!!!

       

        基本知识知道的差不多啦,接下来就得搞一下复杂的东西啦,在这里,我以工作中遇到的复杂类以及解决办法为例子,给大家直接上实例,我想,如果已经琢磨过了很久JNI的人,看过我这些例子之后就能明白很多问题,废话不多说,直接上例子:

    

        首先。我们有一个需要调用的第三方DLL我们只能通过文档等手段了解到这个DLL的信息,比如接口,函数名,参数,返回值,功能。那么我们需要用JAVA去掉用,首先要查看DLL的函数名,这里推荐一些软件,在度娘中搜索DLL函数查看器,就可以找到很多可以查DLL函数名的软件,比如我用某款软件查到的函数名如下:

                                                 序数   函数名                               地址          参数量  
                                                ------------------------------------------------------------------
                                                  1      _PDB_AddDataBase@4                   06761E20      1       
                                                  2      _PDB_ChangeFeature@16                06761FD0      4       
                                                  3      _PDB_DeleteDataBase@4                06761D40      1       
                                                  4      _PDB_DeleteFeature@8                 06761980      2       
                                                  5      _PDB_MatchFeature@28                 06761A70      7       
                                                  6      _PDB_ResetDataBase@4                 06761B70      1       
                                                  7      _PDB_StoreFeature@12                 06761860      3       
                                                  8      _PFD_AgrRecogImg@12                  06763480      3       
                                                  9      _PFD_BlinkRecogImg@12                06763B60      3       
                                                 10     _PFD_DirectRecogImg@12               06763910      3       
                                                 11     _PFD_Exit@0                          06761650      0       
                                                 12     _PFD_FaceRecog@16                    06763230      4       
                                                 13     _PFD_FeatureMatching@16              067617A0      4       
                                                 14     _PFD_GetFeature@16                   067620D0      4       
                                                 15     _PFD_GetFeatureByFaceInfo@24         06762C90      6       
                                                 16     _PFD_GetFeatureByManual@20           06762560      5       
                                                 17     _PFD_Init@4                          06761330      1

                                                 18     _PFD_SmileRecogImg@12                067636B0      3  

        怎么样,巨恶心吧,不过不要紧,我们一点点分析,我通过找资料,明白了,这种方式定义的函数名是遵循__stdcall方式来进行引用的,这里__stdcall大家需要记住,当然了,还有很多别的引用方式,大家可以度娘。

        接下来我们给出这个DLL文档中给出的一小撮结构体定义以及用到这些结构体作为参数的函数:

typedef struct _pfd_face_position {
    short    conf;
    short    rect_l;
    short    rect_r;
    short    rect_t;
    short    rect_b;
    short    eye_lx;
    short    eye_ly;
    short    eye_rx;
    short    eye_ry;
} PFD_FACE_POSITION;

typedef struct _pfd_detect_info{
    PFD_FACE_POSITION faceInfo;
    short ageConf;
    short genConf;
    short age;
    short gen;
    short smile;
    short pitch; 
    short yaw;
    short roll; 
    short lb;
    short rb;
    short flen;
}PFD_DETECT_INFO;

typedef struct _pfd_face_detect {
    short    num;
    PFD_DETECT_INFO    info[PFD_MAX_FACE_NUM];
} PFD_FACE_DETECT;
/*
DLL_API int __stdcall PFD_FaceRecog(unsigned char * bmpData,PFD_FACE_DETECT* faceInfo,int faceInfoFlag,short faceRote);
DLL_API int __stdcall PFD_GetFeature(unsigned char * bmpData,PFD_FACE_DETECT* faceInfo,unsigned char ** feature,short faceRote);......*/

知道了结构体的内容后,和DLL中函数的声明以后,我们就到重点了,路漫漫其修远兮啊、、、PFD_FaceRecog()这个函数的参数有unsigned char*,结构体指针,这怎么调用?java中并没有这种数据类型啊。


        这里我们用到了封装的思想,即把一个复杂的DLL封装成一个参数简单,易于调用的DLL,比如我们可以将其封装成java中都有的数据类型,以方便调用。那我们就得想怎么变成java中的数据类型,在这里我是这样对PFD_FaceRecog()这个函数中的参数做变化的。我在java中做了这样的函数:

public native int FaceRecog(byte[] bmpData, PFD_FACE_DETECT faceInfo, int faceInfoFlag, short faceRote);
即,将unsigned char*->byte[],结构体指针->java中的类

所以按照上面helloworld的方式,我就做了这样的java类:

public class getFunction {
	public native int FaceRecog(byte[] bmpData, PFD_FACE_DETECT faceInfo, int faceInfoFlag, short faceRote);
	public native int GetFeature(byte[] bmpData,PFD_FACE_DETECT faceInfo,byte[] feature,short faceRote);
	/*......*/
        static {
		System.loadLibrary("test");
	}
这里,我那个作为封装dll的dll的名为test.dll;


有了这个java文件后,我们就参照上方的HelloWorld的步骤,创建一个空的dll文件,来做为封装文件。我就直接把我做的对PFD_FaceRrecog()函数和GetFeature()函数的封装给大家,参数比较典型,大家可以对照,看看我是如何在这个里面进行java和c++参数类型转换的,如下(这里面凡是碰到你没见过的函数或执行方式,就去问度娘,度娘解释的很多,很简单):

#include<windows.h>
#include<jni.h>
#include"face.h"
#include"eval.h"

typedef int(__stdcall *FaceRecog)(unsigned char *, PFD_FACE_DETECT*, int, short);

JNIEXPORT jint JNICALL Java_getFunction_FaceRecog(JNIEnv *env, jobject, 
	jbyteArray bmpData, jobject faceInfo, jint faceInfoFlag, jshort faceRote) {
		//获取java中实例类PFD_FACE_POSITION
		jclass jcpfp = env->FindClass("PFD_FACE_POSITION");
		//short	conf
		jfieldID jfconf = env->GetFieldID(jcpfp, "conf", "S");
    	//short	rect_l
		jfieldID jfrect_l = env->GetFieldID(jcpfp, "rect_l", "S");
    	//short	rect_r
		jfieldID jfrect_r = env->GetFieldID(jcpfp, "rect_r", "S");
    	//short	rect_t
		jfieldID jfrect_t = env->GetFieldID(jcpfp, "rect_t", "S");
    	//short	rect_b
		jfieldID jfrect_b = env->GetFieldID(jcpfp, "rect_b", "S");
    	//short	eye_lx
		jfieldID jfeye_lx = env->GetFieldID(jcpfp, "eye_lx", "S");
    	//short	eye_ly
		jfieldID jfeye_ly = env->GetFieldID(jcpfp, "eye_ly", "S");
    	//short	eye_rx
		jfieldID jfeye_rx = env->GetFieldID(jcpfp, "eye_rx", "S");
    	//short	eye_ry
		jfieldID jfeye_ry = env->GetFieldID(jcpfp, "eye_ry", "S");


		//获取java中实例类PFD_DETECT_INFO
		jclass jcpdi = env->FindClass("PFD_DETECT_INFO");
		//PFD_FACE_POSITION faceInfo
		jfieldID jfpfp = env->GetFieldID(jcpdi, "faceInfo", "LPFD_FACE_POSITION;");
		//short ageConf
		jfieldID jfageConf = env->GetFieldID(jcpdi, "ageConf", "S");
		//short genConf
		jfieldID jfgenConf = env->GetFieldID(jcpdi, "genConf","S");
		//short age
		jfieldID jfage = env->GetFieldID(jcpdi, "age","S");
    	//short gen
		jfieldID jfgen = env->GetFieldID(jcpdi, "gen","S");
    	//short smile
		jfieldID jfsmile = env->GetFieldID(jcpdi, "smile", "S");
    	//short pitch
		jfieldID jfpitch = env->GetFieldID(jcpdi, "pitch","S");
    	//short yaw
		jfieldID jfyaw = env->GetFieldID(jcpdi, "yaw", "S");
    	//short roll
		jfieldID jfroll = env->GetFieldID(jcpdi, "roll", "S");
    	//short lb
		jfieldID jflb = env->GetFieldID(jcpdi, "lb", "S");
    	//short rb
		jfieldID jfrb = env->GetFieldID(jcpdi, "rb","S");
    	//short flen
		jfieldID jfflen = env->GetFieldID(jcpdi, "flen", "S");


		//获取java中实例类PFD_FACE_DETECT
		jclass jcpfd = env->FindClass("PFD_FACE_DETECT");
		//short num
		jfieldID jfnum = env->GetFieldID(jcpfd, "num", "S");//short->S
		//PFD_DETECT_INFO[] info
		jfieldID jfpdi = env->GetFieldID(jcpfd, "info", "[LPFD_DETECT_INFO;");

		PFD_FACE_DETECT pfd;//c结构体

		//获取实例中的num值给c结构体的num值
		pfd.num = env->GetShortField(faceInfo, jfnum);
		//获取实例中的PFD_DETECT_INFO类的值给c结构体的PFD_DETECT_INFO值
		//jobjectArray joinfo = (jobjectArray)env->GetObjectField(faceInfo, jfpdi);
		//int infolen = env->GetArrayLength(joinfo);
		jobject joinfo = env->GetObjectField(faceInfo, jfpdi);
		//PFD_DETECT_INFO *info1 =(PFD_DETECT_INFO *)env->GetObjectArrayElement(joinfo, 0); 
		//java中的PFD_DETECT_INFO[]对象数组 --> c中的PFD_DETECT_INFO[]结构体数组,需要上面的注释吗?还是直接像这句话这样。
		//memcpy(pfd.info, joinfo, infolen);
		pfd.info->ageConf = env->GetShortField(joinfo, jfageConf);
		pfd.info->genConf = env->GetShortField(joinfo, jfgenConf);
		pfd.info->age = env->GetShortField(joinfo, jfage);
		pfd.info->gen = env->GetShortField(joinfo, jfgen);
		pfd.info->smile = env->GetShortField(joinfo, jfsmile);
		pfd.info->pitch = env->GetShortField(joinfo, jfpitch);
		pfd.info->yaw = env->GetShortField(joinfo, jfyaw);
		pfd.info->roll = env->GetShortField(joinfo, jfroll);
		pfd.info->lb = env->GetShortField(joinfo, jflb);
		pfd.info->rb = env->GetShortField(joinfo, jfrb);
		pfd.info->flen = env->GetShortField(joinfo, jfflen);
		//获取PFD_FACE_POSITION对象
		jobject jopfp = env->GetObjectField(joinfo, jfpfp);
		pfd.info->faceInfo.conf = env->GetShortField(jopfp, jfconf);
		pfd.info->faceInfo.rect_l = env->GetShortField(jopfp, jfrect_l);
		pfd.info->faceInfo.rect_r = env->GetShortField(jopfp, jfrect_r);
		pfd.info->faceInfo.rect_t = env->GetShortField(jopfp, jfrect_t);
		pfd.info->faceInfo.rect_b = env->GetShortField(jopfp, jfrect_b);
		pfd.info->faceInfo.eye_lx = env->GetShortField(jopfp, jfeye_lx);
		pfd.info->faceInfo.eye_ly = env->GetShortField(jopfp, jfeye_ly);
		pfd.info->faceInfo.eye_rx = env->GetShortField(jopfp, jfeye_rx);
		pfd.info->faceInfo.eye_ry = env->GetShortField(jopfp, jfeye_ry);
		HINSTANCE hDLL = LoadLibrary("disanfang.dll");
		if(!hDLL) {
			printf("cannot get DLL!");
		}
		int k;
		FaceRecog fr = (FaceRecog) GetProcAddress(hDLL, "_PFD_FaceRecog@16"); 
		if(fr) {
			unsigned char *bmpData1 = (unsigned char*)env->GetByteArrayElements(bmpData, 0);
			k = fr(bmpData1, &pfd, faceInfoFlag, faceRote);
			printf("get FaceRecog success\n");
			return k;
		} else {
			printf("get FaceRecog failed\n");
			return 0;
		}
}







#include<windows.h>
#include<jni.h>
#include"face.h"
#include"eval.h"

typedef int(__stdcall *GetFeature)(unsigned char *, PFD_FACE_DETECT*, unsigned char *, short);

JNIEXPORT jint JNICALL Java_getFunction_GetFeature(JNIEnv *env, jobject,
    jbyteArray bmpData, jobject faceInfo, jstring feature, jshort faceRote) {

        //获取java中实例类PFD_FACE_POSITION
        jclass jcpfp = env->FindClass("PFD_FACE_POSITION");
        //short    conf
        jfieldID jfconf = env->GetFieldID(jcpfp, "conf", "S");
        //short    rect_l
        jfieldID jfrect_l = env->GetFieldID(jcpfp, "rect_l", "S");
        //short    rect_r
        jfieldID jfrect_r = env->GetFieldID(jcpfp, "rect_r", "S");
        //short    rect_t
        jfieldID jfrect_t = env->GetFieldID(jcpfp, "rect_t", "S");
        //short    rect_b
        jfieldID jfrect_b = env->GetFieldID(jcpfp, "rect_b", "S");
        //short    eye_lx
        jfieldID jfeye_lx = env->GetFieldID(jcpfp, "eye_lx", "S");
        //short    eye_ly
        jfieldID jfeye_ly = env->GetFieldID(jcpfp, "eye_ly", "S");
        //short    eye_rx
        jfieldID jfeye_rx = env->GetFieldID(jcpfp, "eye_rx", "S");
        //short    eye_ry
        jfieldID jfeye_ry = env->GetFieldID(jcpfp, "eye_ry", "S");


        //获取java中实例类PFD_DETECT_INFO
        jclass jcpdi = env->FindClass("PFD_DETECT_INFO");
        //PFD_FACE_POSITION faceInfo
        jfieldID jfpfp = env->GetFieldID(jcpdi, "faceInfo", "LPFD_FACE_POSITION;");
        //short ageConf
        jfieldID jfageConf = env->GetFieldID(jcpdi, "ageConf", "S");
        //short genConf
        jfieldID jfgenConf = env->GetFieldID(jcpdi, "genConf","S");
        //short age
        jfieldID jfage = env->GetFieldID(jcpdi, "age","S");
        //short gen
        jfieldID jfgen = env->GetFieldID(jcpdi, "gen","S");
        //short smile
        jfieldID jfsmile = env->GetFieldID(jcpdi, "smile", "S");
        //short pitch
        jfieldID jfpitch = env->GetFieldID(jcpdi, "pitch","S");
        //short yaw
        jfieldID jfyaw = env->GetFieldID(jcpdi, "yaw", "S");
        //short roll
        jfieldID jfroll = env->GetFieldID(jcpdi, "roll", "S");
        //short lb
        jfieldID jflb = env->GetFieldID(jcpdi, "lb", "S");
        //short rb
        jfieldID jfrb = env->GetFieldID(jcpdi, "rb","S");
        //short flen
        jfieldID jfflen = env->GetFieldID(jcpdi, "flen", "S");


        //获取java中实例类PFD_FACE_DETECT
        jclass jcpfd = env->FindClass("PFD_FACE_DETECT");
        //short num
        jfieldID jfnum = env->GetFieldID(jcpfd, "num", "S");//short->S
        //PFD_DETECT_INFO[] info
        jfieldID jfpdi = env->GetFieldID(jcpfd, "info", "[LPFD_DETECT_INFO;");

        PFD_FACE_DETECT pfd;//c结构体

        //获取实例中的num值给c结构体的num值
        pfd.num = env->GetShortField(faceInfo, jfnum);
        //获取实例中的PFD_DETECT_INFO类的值给c结构体的PFD_DETECT_INFO值
        //jobjectArray joinfo = (jobjectArray)env->GetObjectField(faceInfo, jfpdi);
        //int infolen = env->GetArrayLength(joinfo);
        jobject joinfo = env->GetObjectField(faceInfo, jfpdi);
        //PFD_DETECT_INFO *info1 =(PFD_DETECT_INFO *)env->GetObjectArrayElement(joinfo, 0); 
        //java中的PFD_DETECT_INFO[]对象数组 --> c中的PFD_DETECT_INFO[]结构体数组,需要上面的注释吗?还是直接像这句话这样。
        //memcpy(pfd.info, joinfo, infolen);
        pfd.info->ageConf = env->GetShortField(joinfo, jfageConf);
        pfd.info->genConf = env->GetShortField(joinfo, jfgenConf);
        pfd.info->age = env->GetShortField(joinfo, jfage);
        pfd.info->gen = env->GetShortField(joinfo, jfgen);
        pfd.info->smile = env->GetShortField(joinfo, jfsmile);
        pfd.info->pitch = env->GetShortField(joinfo, jfpitch);
        pfd.info->yaw = env->GetShortField(joinfo, jfyaw);
        pfd.info->roll = env->GetShortField(joinfo, jfroll);
        pfd.info->lb = env->GetShortField(joinfo, jflb);
        pfd.info->rb = env->GetShortField(joinfo, jfrb);
        pfd.info->flen = env->GetShortField(joinfo, jfflen);
        //获取PFD_FACE_POSITION对象
        jobject jopfp = env->GetObjectField(joinfo, jfpfp);
        pfd.info->faceInfo.conf = env->GetShortField(jopfp, jfconf);
        pfd.info->faceInfo.rect_l = env->GetShortField(jopfp, jfrect_l);
        pfd.info->faceInfo.rect_r = env->GetShortField(jopfp, jfrect_r);
        pfd.info->faceInfo.rect_t = env->GetShortField(jopfp, jfrect_t);
        pfd.info->faceInfo.rect_b = env->GetShortField(jopfp, jfrect_b);
        pfd.info->faceInfo.eye_lx = env->GetShortField(jopfp, jfeye_lx);
        pfd.info->faceInfo.eye_ly = env->GetShortField(jopfp, jfeye_ly);
        pfd.info->faceInfo.eye_rx = env->GetShortField(jopfp, jfeye_rx);
        pfd.info->faceInfo.eye_ry = env->GetShortField(jopfp, jfeye_ry);
        HINSTANCE hDLL = LoadLibrary("EVAL_x86_Accuracy.dll");
        if(!hDLL) {
            printf("cannot get EVAL_x86_Accuracy.dll!");
        }
        int k=0;
        GetFeature gf = (GetFeature) GetProcAddress(hDLL, "_PFD_GetFeature@16");
        if(gf) {
            unsigned char *bmpData1 = (unsigned char*)env->GetByteArrayElements(bmpData,0);
            unsigned char *feature1 = (unsigned char*)env->GetStringUTFChars(feature,0);
            k = gf(bmpData1, &pfd, feature1, faceRote);
            env->ReleaseStringUTFChars(feature, (const char*)feature1);
            printf("get GetFeature success\n");
            return k;
        } else {
            printf("get GetFeature failed\n");
            return 0;
        }
} 

这里面其实是很有逻辑的,你只要明白了FindClass和GetFieldID就能很好的做出来这样的转换。之后就简单了,我们运行,然后把得出的dll带如我们那个java文件的工程里,记住,要把那个disanfang.dll也一起导进去,然后我们运行java文件,这里我们知道第三方的DLL文件的文档中给出,PFD_FaceRecog()函数返回的是整数,没种整数代表返回的状态,-1代表异常结束,0代表正常结束,-2验证失败,我们运行后发现,console显示-2,先不管结果对不对,有结果说明我们的数据流已经走通了,证明这一系列的步骤是正确的,之所以是验证失败,是因为我的这个dll是有安全保护的,需要插上key才会成功运行与成功验证。



以上即为笔者用JAVA调用第三方dll的过程,虽然过程艰辛,但是最后得出正确结果的时候内心还是异常欢喜的,只要我们认真的去分析,去思考,去实践,去阅览,那没有什么问题可以难倒我们。







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值