java中的native方法

一. 什么是Native Method
   简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。
   "A native method is a Java method whose implementation is provided by non-java code."
   在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非java语言在外面实现的。,下面给了一个示例:    
    public class IHaveNatives
    {
      native public void Native1( int x ) ;
      native static public long Native2() ;
      native synchronized private float Native3( Object o ) ;
      native void Native4( int[] ary ) throws Exception ;
    } 
    这些方法的声明描述了一些非java代码在这些java代码里看起来像什么样子(view).
    标识符native可以与所有其它的java标识符连用,但是abstract除外。这是合理的,因为native暗示这些方法是有实现体的,只不过这些实现体是非java的,但是abstract却显然的指明这些方法无实现体。native与其它java标识符连用时,其意义同非Native Method并无差别,比如native static表明这个方法可以在不产生类的实例时直接调用,这非常方便,比如当你想用一个native method去调用一个C的类库时。上面的第三个方法用到了native synchronized,JVM在进入这个方法的实现体之前会执行同步锁机制(就像java的多线程。)
    一个native method方法可以返回任何java类型,包括非基本类型,而且同样可以进行异常控制。这些方法的实现体可以制一个异常并且将其抛出,这一点与java的方法非常相似。当一个native method接收到一些非基本类型时如Object或一个整型数组时,这个方法可以访问这非些基本型的内部,但是这将使这个native方法依赖于你所访问的java类的实现。有一点要牢牢记住:我们可以在一个native method的本地实现中访问所有的java特性,但是这要依赖于你所访问的java特性的实现,而且这样做远远不如在java语言中使用那些特性方便和容易。
    native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM将控制调用本地方法的所有细节。需要注意当我们将一个本地方法声明为final的情况。用java实现的方法体在被编译时可能会因为内联而产生效率上的提升。但是一个native final方法是否也能获得这样的好处却是值得怀疑的,但是这只是一个代码优化方面的问题,对功能实现没有影响。
    如果一个含有本地方法的类被继承,子类会继承这个本地方法并且可以用java语言重写这个方法(这个似乎看起来有些奇怪),同样的如果一个本地方法被fianl标识,它被继承后不能被重写。
   本地方法非常有用,因为它有效地扩充了jvm.事实上,我们所写的java代码已经用到了本地方法,在sun的java的并发(多线程)的机制实现中,许多与操作系统的接触点都用到了本地方法,这使得java程序能够超越java运行时的界限。有了本地方法,java程序可以做任何应用层次的任务。


二.为什么要使用Native Method
   java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
   与java环境外交互:
   有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。
   与操作系统交互:
   JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎 样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
    Sun's Java
    Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread 的 setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。


三.JVM怎样使Native Method跑起来:
    我们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会回载一次。在这个被加载的字节码的入口维持着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。
    如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的。
   
   最后需要提示的是,使用本地方法是有开销的,它丧失了java的很多好处。如果别无选择,我们可以选择使用本地方法。

以下链接有关于Native Method相当完备的英文论述:http://docs.oracle.com/javase/6/docs/technotes/guides/jni/

二、使用举例:

 Java不是完美的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能。 
  可以将native方法比作Java程序同C程序的接口,其实现步骤:

  1、在Java中声明native()方法,然后编译; 

  2、用javah产生一个.h文件; 

  3、写一个.cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(注意其中又包含了JDK带的jni.h文件); 

  4、将第三步的.cpp文件编译成动态链接库文件; 

  5、在Java中用System.loadLibrary()方法加载第四步产生的动态链接库文件,这个native()方法就可以在Java中被访问了。 

  JAVA本地方法适用的情况 

  1.为了使用底层的主机平台的某个特性,而这个特性不能通过JAVA API访问 

  2.为了访问一个老的系统或者使用一个已有的库,而这个系统或这个库不是用JAVA编写的 

  3.为了加快程序的性能,而将一段时间敏感的代码作为本地方法实现。 

  <1>.首先写好JAVA文件: TestNative.java

package cn.wzb;

public class TestNative {
	public native void displayHelloWorld();
	
	static {
		System.out.println(System.getProperty("java.library.path"));
		System.loadLibrary("test");
	}
	
	public static void main(String[] args) {
		new TestNative().displayHelloWorld();
	} 

}

  <2>然后根据写好的文件编译成CLASS文件 : 在命令行下,执行: 

javac -d . TestNative.java
  <3>然后在class的包所在的根目录下执行:

 javah -jni cn.wzb.TestNative, 

 就会在报所在的根目录中得到一个cn_wzb_TestNative.h的文件,内容如下:

 

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class cn_wzb_TestNative */

#ifndef _Included_cn_wzb_TestNative
#define _Included_cn_wzb_TestNative
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     cn_wzb_TestNative
 * Method:    displayHelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_cn_wzb_TestNative_displayHelloWorld
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
 <4>.根据头文件的内容编写cn_wzb_TestNative.c文件, 该文件的内容如下:

#include "cn_wzb_TestNative.h" 

#include <stdio.h> 

JNIEXPORT void JNICALL Java_cn_wzb_TestNative_displayHelloWorld(JNIEnv *env, jobject obj){
	printf("Hello world!\n"); 
	
	return; 
} 

  <5>. 编译生成DLL文件如“test.dll”,名称与System.loadLibrary("test")中的名称一致 :
   vc的编译方法:cl -I%java_home%\include -I%java_home%\include\win32 -LD cn_wzb_TestNative.c -Fetest.dll 
   最后在运行时加参数-Djava.library.path=[dll存放的路径

F:\wzb\TestNative>javac -d . TestNative.java

F:\wzb\TestNative>javah -jni cn.wzb.TestNative

F:\wzb\TestNative>type cn_wzb_TestNative.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class cn_wzb_TestNative */

#ifndef _Included_cn_wzb_TestNative
#define _Included_cn_wzb_TestNative
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     cn_wzb_TestNative
 * Method:    displayHelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_cn_wzb_TestNative_displayHelloWorld
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

F:\wzb\TestNative>type cn_wzb_TestNative.c
#include "cn_wzb_TestNative.h"

#include <stdio.h>

JNIEXPORT void JNICALL Java_cn_wzb_TestNative_displayHelloWorld(JNIEnv *env, job
ject obj){
        printf("Hello world!\n");

        return;
}

F:\wzb\TestNative>cl -I%java_home%\include -I%java_home%\include\win32 -LD cn_wz
b_TestNative.c -Fetest.dll
用于 80x86 的 Microsoft (R) 32 位 C/C++ 优化编译器 15.00.30729.01 版
版权所有(C) Microsoft Corporation。保留所有权利。

cn_wzb_TestNative.c
Microsoft (R) Incremental Linker Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

/dll
/implib:test.lib
/out:test.dll
cn_wzb_TestNative.obj
   正在创建库 test.lib 和对象 test.exp

F:\wzb\TestNative>java cn.wzb.TestNative
D:\wzb\java\jdk1.6.0_10\bin;.;C:\WINDOWS\Sun\Java\bin;C:\WINDOWS\system32;C:\WIN
DOWS;C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE;C:\Program Files\M
icrosoft Visual Studio 9.0\VC\BIN;C:\Program Files\Microsoft Visual Studio 9.0\C
ommon7\Tools;C:\WINDOWS\Microsoft.NET\Framework\v3.5;C:\WINDOWS\Microsoft.NET\Fr
amework\v2.0.50727;C:\Program Files\Microsoft Visual Studio 9.0\VC\VCPackages;C:
\Program Files\Microsoft SDKs\Windows\v6.0A\bin;D:\wzb\java\jdk1.6.0_10\bin;D:\O
racle\product\10.1.0\Client_2\bin;D:\Oracle\product\10.1.0\Client_2\jre\1.4.2\bi
n\client;D:\Oracle\product\10.1.0\Client_2\jre\1.4.2\bin;C:\WINDOWS\system32;C:\
WINDOWS;C:\WINDOWS\System32\Wbem;C:\Program Files\Common Files\Thunder Network\K
anKan\Codecs;C:\Program Files\Common Files\TTKN\Bin;C:\Program Files\IDM Compute
r Solutions\UltraEdit-32;C:\Program Files\TortoiseSVN\bin;D:\matlabe\runtime\win
32;D:\matlabe\bin;C:\WINDOWS\system32\WindowsPowerShell\v1.0;C:\Program Files\Mi
crosoft SQL Server\100\Tools\Binn\VSShell\Common7\IDE\;C:\Program Files\Microsof
t SQL Server\100\Tools\Binn\;C:\Program Files\Microsoft SQL Server\100\DTS\Binn\

Hello world!
F:\wzb\TestNative>java -Djava.library.path=.  cn.wzb.TestNative
.
Hello world!

F:\wzb\TestNative>java -Djava.library.path=.  cn.wzb.TestNative
.
Hello world!

F:\wzb\TestNative>java -Djava.library.path=%classpath%  cn.wzb.TestNative
.;D:\wzb\java\jdk1.6.0_10\lib\dt.jar;D:\wzb\java\jdk1.6.0_10\lib\tools.jar;
Hello world!

F:\wzb\TestNative>echo %classpath%
.;D:\wzb\java\jdk1.6.0_10\lib\dt.jar;D:\wzb\java\jdk1.6.0_10\lib\tools.jar;


补充: 关于

java可以通过System.getProperty获得系统变量的值

       

java可以通过System.getProperty获得系统变量的值。而java.library.path只是其中的一个,表示系统搜索库文件的路径。

例如这个值可以能是 c;\windows;d:\test;e:\mytest

那当你在程序中装载一个dll库时,系统就是去当前目录和这几个目录找看看有没有这个文件。

这个类作用是很大的,我们可以获取很多信息。

  System.getProperty()参数大全   
   
java.version            Java Runtime Environment version   
java.vendor            Java Runtime Environment vendor   
java.vendor.url            Java vendor URL   
java.home            Java installation directory   
java.vm.specification.version                    Java Virtual Machine specification version   
java.vm.specification.vendor                    Java Virtual Machine specification vendor   
java.vm.specification.name                    Java Virtual Machine specification name   
java.vm.version            Java Virtual Machine implementation version   
java.vm.vendor            Java Virtual Machine implementation vendor   
java.vm.name            Java Virtual Machine implementation name   
java.specification.version                Java Runtime Environment specification version   
java.specification.vendor             Java Runtime Environment specification vendor   
java.specification.name        Java Runtime Environment specification name   
java.class.version                        Java class format version number   
java.class.path                  Java class path   
java.library.path                        List of paths to search when loading libraries   
java.io.tmpdir                Default temp file path   
java.compiler            Name of JIT compiler to use   
java.ext.dirs            Path of extension directory or directories   
os.name                Operating system name   
os.arch                Operating system architecture   
os.version            Operating system version   
file.separator            File separator ("/" on UNIX)   
path.separator            Path separator (":" on UNIX)   
line.separator            Line separator ("\n" on UNIX)   
user.name            User's account name   
user.home            User's home directory   
user.dir                User's current working directory
JNI是Java Native Interface的缩写。从Java 1.1开始,Java Native Interface (JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。 使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的,比如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少保证本地代码能工作在任何Java 虚拟机实现下。
一、JNI(Java Native Interface)的设计目的 
1.The standard Java class library may not support the platform-dependent features needed by your application. 
2.You may already have a library or application written in another programming language and you wish to make it accessible to Java applications 
3.You may want to implement a small portion of time-critical code in a lower-level programming language, such as assembly, and then have your Java application call these functions 
4.编写带有native声明的方法的java类 
5.使用javac命令编译所编写的java类 
6.使用javah ?jni java类名生成扩展名为h的头文件 
7.使用C/C++实现本地方法 
8.将C/C++编写的文件生成动态连接库 

二、JNI(Java Native Interface)的书写步骤
1) 编写java程序:这里以HelloWorld为例。
代码1: 
class HelloWorld { 
  public native void displayHelloWorld(); 
  static { 
   System.loadLibrary("hello"); 
  } 
  public static void main(String[] args) { 
   new HelloWorld().displayHelloWorld(); 
  } 


声明native方法:如果你想将一个方法做为一个本地方法的话,那么你就必须声明改方法为native的,并且不能实现。其中方法的参数和返回值在后面讲述。 Load动态库:System.loadLibrary("hello");加载动态库(我们可以这样理解:我们的方法displayHelloWorld()没有实现,但是我们在下面就直接使用了,所以必须在使用之前对它进行初始化)这里一般是以static块进行加载的。同时需要注意的是System.loadLibrary();的参数"hello"是动态库的名字。 main()方法是函数得入口点。 
2) 编译 javac HelloWorld.java 
3) 生成扩展名为h的头文件 javah HelloWorld 头文件的内容: 
/* DO NOT EDIT THIS FILE - it is machine generated */ 
#include /* Header for class HelloWorld */ 
#ifndef _Included_HelloWorld 
#define _Included_HelloWorld 
#ifdef __cplusplus extern "C" { 
#endif 

/* 
* Class: HelloWorld 
* Method: displayHelloWorld 
* Signature: ()V 
*/ 

JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld(JNIEnv *, jobject); 
#ifdef __cplusplus 

#endif 
#endif 
(这里我们可以这样理解:这个h文件相当于我们在java里面的接口,这里声明了一个Java_HelloWorld_displayHelloWorld (JNIEnv *, jobject);方法,然后在我们的本地方法里面实现这个方法,也就是说我们在编写C/C++程序的时候所使用的方法名必须和这里的一致)。 
4) 编写本地方法(c/c++程序)实现和由javah命令生成的头文件里面声明的方法名相同的方法。
代码2: 
  #include "jni.h" 
  #include "HelloWorld.h" 
  #include ...... 
  JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld(JNIEnv *env, jobject obj) { 
    printf("Hello world! "); 
    return; 
  } 
  
  注意第1行,需要将jni.h(该文件可以在%JAVA_HOME%/include文件夹下面找到)文件引入,因为在程序中的JNIEnv、jobject等类型都是在该头文件中定义的;另外在第2行需要将HelloWorld.h头文件引入(我是这么理解的:相当于我们在编写java程序的时候,实现一个接口的话需要声明才可以,这里就是将HelloWorld.h头文件里面声明的方法加以实现。当然不一定是这样)。然后保存为HelloWorldImpl.c就ok了。 
5) 生成动态库这里以在Windows中为例,需要生成dll文件。在保存HelloWorldImpl.c文件夹下面,使用VC的编译器cl生成。
  命令行如下:
  cl -I%java_home%\include -I%java_home%\include\win32 -LD HelloWorldImp.c -Fehello.dll 
  注意:生成的dll文件名在选项-Fe后面配置,这里是hello,因为在HelloWorld.java文件中我们loadLibary的时候使用的名字是hello。当然这里修改之后那里也需要修改。另外需要将-I%java_home%\include -I%java_home%\include\win32参数加上,因为在第四步里面编写本地方法的时候引入了jni.h文件。 
6) 运行程序 java HelloWorld就ok。
三、JNI(Java Native Interface)的简要使用例子 下面是一个简单的例子实现打印一句话的功能,但是用的c的printf最终实现。一般提供给java的jni接口包括一个so文件(封装了c函数的实现)和一个java文件(需要调用path的类)。 
1. JNI的目的是使java方法中能够调用c实现的一些函数,比如以下的java类,就需要调用一个本地函数testjni(一般声明为private native类型),
    首先需要创建文件weiqiong.java,内容如下: 
    class weiqiong { 
     static { 
      System.loadLibrary("testjni");//载入静态库,test函数在其中实现 
     } 
     private native void testjni(); //声明本地调用 
     public void test() { 
      testjni(); 
     } 
     public static void main(String args[]) { 
      weiqiong haha = new weiqiong(); 
      haha.test(); 
     } 
    } 
2.然后执行javac weiqiong.java,如果没有报错,会生成一个weiqiong.class。 
3.然后执行javah weiqiong,会生成一个文件weiqiong.h文件,其中有一个函数的声明如下: 
  JNIEXPORT void JNICALL Java_weiqiong_testjni(JNIEnv *, jobject); 
4.创建文件testjni.c将上面那个函数实现,内容如下: 
  #include "jni.h" 
  #include weiqiong.h 
  
  JNIEXPORT void JNICALL Java_weiqiong_testjni(JNIEnv *env, jobject obj) { 
   printf("haha---------go into c!!! "); 
  } 
5.为了生成.so文件,创建makefile文件如下: 
  libtestjni.so:testjni.o makefile 
  gcc -Wall -rdynamic -shared -o libtestjni.so testjni.o 
  testjni.o:testjni.c weiqiong.h gcc -Wall -c testjni.c -I./ -I/usr/java/j2sdk1.4.0/include -I/usr/java/j2sdk1.4.0/include/linux 
  cl: rm -rf *.o *.so 
  注意:gcc前面是tab空,j2sdk的目录根据自己装的j2sdk的具体版本来写,生成的so文件的名字必须是loadLibrary的参数名前加“lib”。 
6.export LD_LIBRARY_PATH=.,由此设置library路径为当前目录,这样java文件才能找到so文件。一般的做法是将so文件copy到本机的LD_LIBRARY_PATH目录下。 
7.执行java weiqiong,打印出结果:“haha---------go into c!!!” 

四.JNI(Java Native Interface)调用中考虑的问题 在首次使用JNI的时候有些疑问,后来在使用中一一解决,下面就是这些问题的备忘: 
1。 java和c是如何互通的? 
  其实不能互通的原因主要是数据类型的问题,jni解决了这个问题,例如那个c文件中的jstring数据类型就是java传入的String对象 ,经过jni函数的转化就能成为c的char*。 对应数据类型关系如下表: 
  Java 类型   本地C类型   说明 
  boolean    jboolean    无符号,8 位 
  byte      jbyte     无符号,8 位 
  char      jchar     无符号,16 位 
  short     jshort     有符号,16 位 
  int      jint      有符号,32 位 
  long      jlong     有符号,64 位 
  float     jfloat     32 位 
  double     jdouble    64 位 
  void      void      N/A 
  JNI 还包含了很多对应于不同 Java 对象的引用类型 
2. 如何将java传入的String参数转换为c的char*,然后使用? 
  java传入的String参数,在c文件中被jni转换为jstring的数据类型,在c文件中声明char* test,然后test = (char*)(*env)->GetStringUTFChars(env, jstring, NULL);
  注意:test使用完后,通知虚拟机平台相关代码无需再访问:(*env)->ReleaseStringUTFChars(env, jstring, test); 
3. 将c中获取的一个char*的buffer传递给java?
  这个char*如果是一般的字符串的话,作为string传回去就可以了。如果是含有’\0’的buffer,最好作为bytearray传出,因为可以制定copy的length,如果copy到string,可能到’\0’就截断了。 
  有两种方式传递得到的数据: 
   一种是在jni中直接new一个byte数组,然后调用函数(*env)->SetByteArrayRegion(env, bytearray, 0, len, buffer);将buffer的值copy到bytearray中,函数直接return bytearray就可以了。 
   一种是return错误号,数据作为参数传出,但是java的基本数据类型是传值,对象是传递的引用,所以将这个需要传出的byte数组用某个类包一下,
   如下: 
   class RetObj { 
    public byte[] bytearray; 
   } 
   这个对象作为函数的参数retobj传出,通过如下函数将retobj中的byte数组赋值便于传出。代码如下: 
    jclass cls; 
    jfieldID fid; 
    jbyteArray bytearray; 
    bytearray = (*env)->NewByteArray(env,len); 
    (*env)->SetByteArrayRegion(env, bytearray, 0, len, buffer); 
    cls = (*env)->GetObjectClass(env, retobj); 
    fid = (*env)->GetFieldID(env, cls, "retbytes", "[B"]); 
    (*env)->SetObjectField(env, retobj, fid, bytearray);
4. 不知道占用多少空间的buffer,如何传递出去呢? 
   在jni的c文件中new出空间,传递出去。java的数据不初始化,指向传递出去的空间即可。
五.JNI中对JAVA传入数据的处理 
  如果传入的是bytearray的话,作如下处理得到buffer: 
    char *tmpdata = (char*)(*env)->GetByteArrayElements(env, bytearray, NULL); 
    (*env)->ReleaseByteArrayElements(env, bytearray, tmpdata, 0); 
六.JAVA调用本地接口后的数据处理问题 
1. java得到的数据是String的话,直接处理就可以了。 
2. 得到的如果是bytearray的话,作如下处理: 
  DataInputStream in = new DataInputStream(new ByteArrayInputStream(bytearray)); 
  byte []byte1 = new byte[36]; in.read(byte1,0,36); 
  String string = new String(byte1); 
  System.out.println("读出的第一个字段为:"+string);



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值