JAVA JNI的基本总结一箩筐

12 篇文章 0 订阅
 JNI的基本原理
** 在Java中调用C库函数

开发流程
------
在Java代码中通过JNI调用C函数的步骤如下:

第一步: 编写Java代码

第二步: 编译Java代码

第三步: 生成C语言头文件

第四步: 编写C代码

第五步: 生成C共享库

第六步: 运行Java程序

*** 第一步 编写Java代码

JNI方法是在Java代码中声明的。

在Java类中,使用"native"关键字,声明本地方法该方法与用C/C++编写的JNI本地函数相对应。"native"关键字告知Java编译器,在Java代码中带有该关键字的方法只是声明,具体由C/C++等其他语言编写实现。

如果起吊方法前的native关键字,编译代码时,Java编译器就会报错,抛出编译错误,告知该方法没有实现。

调用System.loadLibrary()方法加载具体的实现本地方法的C运行库。System.loadLibrary()方法加载由字符串参数指定的本地库,在不同操作系统平台下,加载的C运行库不同。

*** 第二步 编译Java代码

#+BEGIN_SRC java
javac xxx.java
#+END_SRC

生成 xxx.class

*** 第三步 生成C语言头文件

#+BEGIN_SRC java
javah -classpath path classname
#+END_SRC

生成classname.h

| Java类型 | Java本地类型 |
|----------+--------------|
| /        | <            |
|----------+--------------|
| byte     | jbyte        |
| short    | jshort       |
| int      | jint         |
| long     | jlong        |
| float    | jfloat       |
| double   | jdouble      |
| char     | jchar        |
| boolean  | jboolean     |
| void     | void         |

Java本地类型也提供了另外三种类型

| java引用类型 | java本地类型 |
|--------------+--------------|
| /            | <            |
|--------------+--------------|
| 对象         | Jobject      |
| String       | Jstring      |

*** 第四步 编写C/C++代码

编写xxx.c文件

*** 第五步 生成C共享库

#+BEGIN_SRC sh
cc -I/usr/lib/jvm/java-6-sun/include/linux
   -I/usr/lib/jvm/java-6-sun/include/
   -fPIC -shared -o libxxx.so xxx.c
#+END_SRC

*** 第六步 运行Java程序

#+BEGIN_SRC java
java -cp path -o java.library.path='path' classname
#+END_SRC

** 小结

(1)在java类中声明本地方法

(2)使用javah命令,生成包含JNI本地函数原型的头文件

(3)实现JNI本地函数

(4)生成C共享库

(5)通过JNI,调用JNI本地函数

* 调用JNI函数

在由C语言编写的JNI本地函数中如何控制Java端的代码

- 创建Java对象

- 访问静态成员域

- 调用类的静态方法

- 访问Java对象的成员变量

- 访问Java对象的方法

** 调用JNI函数的示例程序结构

** Java层代码 (JniFuncMain.java)

1.JniFuncMain类
#+BEGIN_SRC java
public class JniFuncMain
{
    print static int staticIntField = 300;

    // 加载本地库
    static { System.loadLibrary("jnifunc"); }

    // 本地方法声明

    public static native JniTest createJniObject();

    public static void main(String[] args)
    {
        // 从本地代码生成JniTest对象
	System.out.println("[Java] createJniObject() 调用本地方法");
	JniTest jniObj = createJniObject();

	// 调用JniTest对象的方法
	jniObj.callTest();
    }
}
#+END_SRC
JniFuncMain.java中的JniFuncMain类

+ 通过java静态块,在调用本地方法前,加载jnifunc运行库

+ 使用static关键字声明本地方法createJniObject()在调研那个此方法时不需要创建对象,直接通过JniFuncMain类调用即可

+ 不使用Java语言的new运算符,调用与createJniObject()本地方法相对应的C函数生成JniTest类的对象,在将对象的引用保存在jniObj引用变量中

+ 调用jniObj对象的callTest()方法

2.JniTest类

#+BEGIN_SRC java
class JniTest 
{
    private int intField;
    //构造方法

    public JniTest(int num)
    {
        intField = num;
	System.out.println("[Java] 调用JniTest对象的构造方法:intField = " + intField);
    }

    // 此方法由JNI本地函数调用
    public int callByNative(int num)
    {
        System.out.println("[Java] JniTest 对象的 callByNative("+ num +")调用");
	return num;
    }

    public void callTest() 
    {
        System.out.println("[Java] JniTest 对象的 callTest() 方法调用:intField="intField");
    }
}
#+END_SRC

** 分析JNI本地函数代码

**** JniFuncMain.h头文件

使用javah命令,生成本地方法的函数原型
#+BEGIN_SRC java
javah JniFuncMain
#+END_SRC
JniFuncMain.h
#+BEGIN_SRC c
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniFuncMain */

#ifndef _Included_JniFuncMain
#define _Included_JniFuncMain
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JniFuncMain
 * Method:    createJniObject
 * Signature: ()LJniTest;
 */
JNIEXPORT jobject JNICALL Java_JniFuncMain_CreateJniObject(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
#+END_SRC

createJniObject()本地方法对应的JNI本地函数原型,形式如下

JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject(JNIEnv *, jclass)

**** jnifunc.cpp 文件

#+BEGIN_SRC C++
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject(JNIEnv *env, jclass clazz)
{
    jclass targetClass;
    jmethodID mid;
    jobject newObject;
    jstring helloStr;
    jfieldID fid;
    jint staticIntField;
    jint result;

    // 获取JniFuncMain类的staticIntField变量值
    fid = env->GetStaticFieldID(clazz, "staticIntField", "I");
    staticIntField = env->GetStaticIntField(clazz, fid);
    printf("[CPP] 获取JniFuncMain类的staticIntField值\n");
    printf("         JniFuncMain.staticIntField = %d\n", staticIntField);

    // 查找生成对象的类
    targetClass = new->NewObject(targetClass, mid , 100);

    // 查找构造方法
    mid = env->GetMethodID(targetClass, "<init>", "(I)V");

    // 生成JniTest对象(返回对象的引用)
    printf("[CPP]JniTest对象生成\n");
    newObject = env->NewObject(targetClass, mid, 100);

    // 调用对象的方法
    mid = env->GetMethodID(targetClass,"callByNative", "(I)I");
    result = env->CallIntMethod(newObject, mid , 200);

    //设置JniObject对象的intField值
    fid = env->GetFieldID(targetClass, "intField", "I");
    printf("[CPP] 设置JniTest对象的intField值为200\n");
    env->SetIntField(newObject, fid, result);

    //返回对象的引用
    return newObject;
}
#+END_SRC

**** 通过JNI,获取成员变量值

下面代码用于获取JniFuncMaind类的staticIntField成员变量的值

#+BEGIN_SRC c
// 1. 查找含有待放文成员变量的JniFuncMain类的jclass值
// 2. 获取staticField变量的ID值
fid = env->GetStaticFieldID(clazz, "staticIntField", "I");
// 3. 读取jclass与fieldid指定的成员变量值
staticIntField = env->GetStaticIntField(clazz, fid);
#+END_SRC

程序通过JNI访问java类/对象的成员变量安如下顺序进行:

(1) 查找含待放文的成员变量的Java类的jclass值
(2) 获取此类成员变量的jfieldID值。若成员变量为静态变量,则调用名称为GetStaticFieldID()的JNI函数;若待访问的成员变量是普通对象,则调用名称为GetFieldID()的JNI函数。
(3) 使用12中获得的jclass与jfieldID值,获取或设置成员变量值。

依据以上顺序,待读取树脂的staticIntField成员变量在JniFuncMain类被声明。JniFuncMain类的jclass值被传递给JNI本地函数 =java_JniFuncMain_createJniObject()= 的第二个参数中,若想获取指定类的jclass值,调用JNI函数FindClass()即可。

若想在本地代码中访问Java的成员变量,必须获取相应成员变量的ID值。例子中成员变量的ID保存在jfieldID类型的变量中。由于待读取数值的staticIntField成员变量时JniFUncMain类的静态变量,在获取staticIntField的ID时,影调用名称为GetStaticFieldID()的JNI函数。

在例子中的GetStaticFieldID()函数,与下表中的GetStaticFieldID()函数原型有些不同,函数原型中带有四个参数,而代码中仅有三个,缺少了env参数,这不是错误,而是与所用的编程语言相关。具体请参考后面Tip中关于JNI函数编码风格的说明。

| JNI函数 - GetStaticFieldID() |                                                                                            |
|------------------------------+--------------------------------------------------------------------------------------------|
| /                            | <                                                                                          |
| 形式                         | jfield GetStaticFieldID(JNIEnv *env, jclass clazz, const char*name, const char *signature) |
|------------------------------+--------------------------------------------------------------------------------------------|
| 说明                         | 返回指定类的指定的静态变量的jfieldID的值                                                   |
|------------------------------+--------------------------------------------------------------------------------------------|
| 参数                         | env-JNI接口指针 clazz-包含成员变量的类的jclass name-成员变量名 signature-成员变量签名      |

| JNI函数 - GetFieldID() |                                                                                       |
|------------------------+---------------------------------------------------------------------------------------|
| /                      | <                                                                                     |
| 形式                   | jfield GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *signature) |
|------------------------+---------------------------------------------------------------------------------------|
| 说明                   | 返回对象中指定的成员变量的jfieldID的值                                                |
|------------------------+---------------------------------------------------------------------------------------|
| 参数                   | env-JNI接口指针 clazz-包含成员变量的类的jclass name-成员变量名 signatuer-成员变量签名 |

以上两个函数都要去提供成员变量的签名。成员变量与成员方法都拥有签名,使用 =<JDK_HOME>/bin= 目录下的javap命令(java反编译器),可以获取成员变量活成员方法签名。

Tip: 在JNI中获取成员变量活成员方法签名

形式: javap =[选项]=  '类名'

选项: -s 输出java签名
      -p 输出所有类及成员

在获取成员变量所在的类与ID后,根据各个成员变量的类型与存储区块(static或non-static),调用相应的JNI函数读取成员变量值即可。在JNI中有两种函数用来获取成员便令的值,分别为Get<type>Field函数与GetStatic<type> Field函数。<type>指Int, Char, Double等基本数据类型,具体参考JNI文档。

| JNI函数 GetStatic<type>Field |                                                                                                      |
|------------------------------+------------------------------------------------------------------------------------------------------|
| /                            | <                                                                                                    |
| 形式                         | <jnitype>GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID)                           |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 说明                         | 返回clazz类中ID为fieldID的静态变量的值                                                               |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 参数                         | env-JNI接口指针 clazz-包含成员变量的类 fieldID-成员变量的ID                                          |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 参考                         | <type>指Object、Boolean、Byte、Char、Short、Int、Long、Float、Double九种基本类型                     |
|                              | 返回类型<jnitype>指jobject、jboolean、jbyte、jchar、jshort、jint、jlong、jfloat、jdouble九种基本类型 |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 返回值                       | 返回静态成员变量的值                                                                                 |

| JNI函数 Get<type>Field |                                                                     |
| /                      | <                                                                   |
|------------------------+---------------------------------------------------------------------|
| 形式                   | <jnitype>Get<type>Field(JNIEnv *env, Jobject obj, jfieldID fieldID) |
|------------------------+---------------------------------------------------------------------|
| 说明                   |     返回obj对象中ID为fieldID的成员变量的值                                       |
|------------------------+---------------------------------------------------------------------|
| 参数                   |      env-JNI接口指针                                                    |
|                        |      obj-包含成员变量的对象                                                  |
|                        |       fieldID-成员变量的ID                                               |
|------------------------+---------------------------------------------------------------------|
| 返回值                    |       返回成员变量的值                                                      |
由于staticIntField是Int类型的静态成员变量,所以调用GetStaticFieldID()函数即可获取StaticIntField的值.

生成对象

在JNI本地函数中如何生成Java类对象呢?
-----
// 1. 查找生成对象的类
targetClass = env->FindClass("JniTest");

// 2. 查找类的构造方法
mid = env->GetMethodID(targetClass, "<init>", "(I)V");

// 3. 生成JniTest类对象(返回对象引用)
newObject = env->NewObject(targetClass, mid, 100);
-----

通过JNI函数,生成Java对象的顺序如下:
1. 查找指定的类,并将查找到的类赋值给jclass类型的变量。

2. 查找java类构造方法的ID值,类型为jmethodID。
 
3. 生成java类对象

首先调用JNI函数FindClass(),查找生成对象的类。将类名作为FindClass()函数参数,查找并获得jclass值

| JNI函数 FindClass |                                                 |
|-------------------+-------------------------------------------------|
| 形式              | jclass FindClass(JNIEnv *env, const char *name) |
|-------------------+-------------------------------------------------|
| 说明              | 查找name指定的Java类                                  |
|-------------------+-------------------------------------------------------|
| 参数              |  env-JNI接口指针                                    |
|                   | name-待查找的类名                               |
|-------------------+-------------------------------------------------|
| 返回值            | 返回jclass的值                                  |

获取类的构造方法的ID并保存在jmethodID变量中。在JNI函数中有一个GetMethodID()函数用来获取指定类的指定方法ID。此函数除了可以用来获取指定类的构造方法的ID外,还可以获取类的其他的方法的ID。若指定的是静态方法,则可以调用JNI函数中的GetStaticMethodID()函数,获得指定静态方法的ID。

| JNI函数 GetMethodID |                                                                                                           |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 形式                | jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *signature)                 |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 说明                | 获取clazz类对象的指定方法ID。注意,方法名(name)与签名应当保持一致。若获取类构造方法的ID,方法名应为<init> |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 参数                | env: JNI接口指针                                                                                          |
|                     | clazz:Java类                                                                                              |
|                     | name:方法名                                                                                              |
|                     | signature:方法签名                                                                                       |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 返回值           | 若方法ID错误,则返回NULL                                                                                           | 

以类的jclass与构造方法ID为参数,调用函数NewObject()函数生成JniTest类的对象。JniTest类的构造方法JniTest(int num)带有一个int类型的参数,在调用NewObject()时,同时传入100这一int数据。在生成类对象后,将对象的引用保存在jobject变量中。

| JNI函数 NewObject |                                                                       |
|-------------------+-----------------------------------------------------------------------|
| 形式              | jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...) |
|-------------------+-----------------------------------------------------------------------|
| 说明              | 生成指定类的对象。methodID指类的构造方法的ID                                           |
|-------------------+-----------------------------------------------------------------------|
| 参数              |     env:JNI接口指针                                                       |
|                   |    clazz: Java类                                                       |
|                   |    methodID:类的构造方法的ID                                                 |
|                   |    ...:传递给类构造方法的参数                                                    |
|-------------------+-----------------------------------------------------------------------|
| 返回值            |   返回类对象的引用。若发生错误,返回NULl                                               |

Tip: 局部引用与全局引用

在实现JNI本地函数时,由GetObjectClass()、FindClass()等JNI函数返回的jclass\jobject等引用都是局部引用(Local Reference)

局部引用是JNI默认的,它仅在JNI本地函数内部有效,即当JNI本地函数返回后,其内部的引用就会失效。

在JNI编程中,实现JNI本地函数时,必须准确地理解局部引用的含义。
下面再举一个例子进一步详细的说明一下。

#+BEGIN_SRC java
class RefTest
{
    public static int intField;

    public static void setField(int num) {
        int Field = num;
    }
}

public class RefTestMain
{
    // 加载本地库
    static { System.loadLibrary("reftest"e); }

    // 声明本地方法
    public static native int getMember();

    public static void main(String[] args) {
        RefTest.setField(100);
	System.out.println("intField = " + getMember());
	RefTest.setField(200);
	System.out.println("intField = " + getMember());
    }
}
#+END_SRC

其中,本地方法getMember()的具体实现在reftest.cpp中。为了说明局部引用问题,声明了一个静态jclass变量targetClass,准备保存类的引用。
#+BEGIN_SRC c
static jclass targetClass = 0;
JNIEXPORT jint JNICALL Java_RefTestMain_getMember(JNIEnv *env, jclass clazz)
{
    jfieldID fid;
    jint intField;
    jclass targetClass;

    if(targetClass == 0) {
        targetClass = env->FindClass(RefTest");
    }
    fid = env->GetStaticFieldID(targetClass, "intField", "I");
    intField = env->GetStaticIntFIeld(targetClass, fid);

    return intField;
}
#+END_SRC

运行程序会报错,原因在于JNI函数中的if (targetClass == 0)的判断,在java中的两次调用,第一次调用时targetClass还为0,第二次就不为0了。第二次没有调用FindClass造成出现错误。

为了解决这一问题,JNI提供了一个名为NewGlobalRef()的JNI函数,用来为指定的类或对象生成全局引用(Global Reference),以便在JNI本地函数中在全局范围内使用该引用。

| JNI函数 NewGlobalRef |                                                |
|----------------------+------------------------------------------------|
| 形式                 | jobject NewGlobalRef(JNIEnv *env, jobject obj) |
|----------------------+------------------------------------------------|
| 说明                 | 为obj指定的类或对象,生成全局引用                             |
|----------------------+------------------------------------------------|
| 参数                 |   env: JNI接口指针                                 |
|                      |   obj: 待生成全局引用的引用值                             |
|----------------------+------------------------------------------------|
| 返回值               | 返回生成的全局引用,所发生错误,返回NULL       |

当全局引用使用完后,应当调用名称为DeleteGlobalRef()的JNI函数,显性的将全局引用销毁。

#+BEGIN_SRC c
#include "RefTestMain.h"

static jclass globalTargetClass = 0;

JNIEXPORT jint JNICALL Java_RefTestMain_getMember (JNIEnv *env, jclass jclazz)
{
    jfieldID fid;
    jint intField;
    jclass targetClass;
    
    if(globalTargetClass == 0) {
        targetClass = env->FindClass("RefTest");
	globalTargetClass = (jclass)env->NewGlobalRef(targetClass);
    }

    fid = env->GetStaticFieldID(globalTargetClass, "initField", "I");
    intField = env->GetStaticIntField(globalTargetClass, fid);

    return intField;
}
#+END_SRC

上面代码调用了NewGlobalRef()函数,将targetClass中保存的RefTest类的局部引用(由FindClass()函数返回)转换成全局引用。并且将生成的全局引用保存在globalTargetClass静态变量中。

局部引用在函数执行完程后即无效。而全局引用除非调用DeleteGlobalRef()明确将其销毁,不然这个全局引用总是有效的,可以在运行库的其他函数中使用该引用。

**** 调用Java方法

下面描述了如何使用JNI函数调用Java方法,并将返回值保存至JNI本地函数的变量中的过程
-----
// 1. 获取含待调用方法的Java类的jclass
targetClass = env->GetObjectClass(newObject);

// 2. 获取待调用方法的ID
mid = env->GetMethodID(targetClass, "callByNative", "(I)I");

// 3. 调用Java方法 保存返回值
result = env->CallIntMethod(newObject, mid, 200);
-----

通过JNI调用Java方法的顺序如下
1. 获取含待调用方法的Java类的jclass。若待调用方法属于某个Java类对象,则该方法用来获取Java类对象的jobject。

2. 调用GetMethodID()函数,获取待调用方法的ID(jMethodID)。使用jclass与GetMethodID()函数

3. 根据返回值类型,调用相应的JNI函数,实现对Java方法的调用。若待调用的Java方法是静态方法,则调用函数的形式应为CallStatic<type>Method();若待调用的方法属于某个类对象,则调用函数的形式应为Call<type>Method()。

程序首先获取含callByNative()方法的JniTest类的jclass。在获取JniTest类的jclass时,可以直接调用FindClass()函数,将类引用保存在targetClass中。但是为了向各位介绍GetObjectClass()这个JNI函数,因而在此调用了GetObjectClass()函数。

| JNI函数 CallStatic<type>Method() |                                                                                    |
|----------------------------------+------------------------------------------------------------------------------------|
| 形式                             | <jnitype>CallStatic<type>Method(JNIEnv *env, jcalss clazz,jmethodID methodID, ...) |
|----------------------------------+------------------------------------------------------------------------------------|
| 说明                             |调用methodID指定的类的静态方法                                                                 |
|----------------------------------+------------------------------------------------------------------------------------|
| 参数                             |     env: JNI接口指针                                                                   |
|                                  |    clazz: 含待调方法的类                                                                  |
|                                  |    methodID:待调方法的ID 由GetStaticMethodID()函数获取                                       |
|                                  |   ...:传递给待调方法的参数                                                                   |
|----------------------------------+------------------------------------------------------------------------------------|
| 返回值                           |      被调方法的返回值                                                                      |
|----------------------------------+------------------------------------------------------------------------------------|
| 参考                             |     <type>除了前面说<Get<type>FieldID()时列出的九种外又添加了void类型,返回值<jnitype>也增加了void类型。 待调方法的返回值不同,<type>也不同。若待调方法的返回值类型为int, 则调用函数为CallStaticIntMethod() |

| JNI函数 Call<type>Method() |                                                                               |
|----------------------------+-------------------------------------------------------------------------------|
| 形式                       | <jnitype>Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...) |
|----------------------------+-------------------------------------------------------------------------------|
| 说明                       | 调用methodID指定的java对象的方法                                                        |
|----------------------------+-------------------------------------------------------------------------------|
| 参数                       |     env: JNI接口指针                                                              |
|                            |    obj: 含待调方法的Java对象的引用                                                       |
|                            |    methodID: 待调用方法的ID,由GetMethodID()函数来获取                                     |
|                            | ...: 传递给待调用方法的参数                                                   |
|----------------------------+-------------------------------------------------------------------------------|
| 返回值                  | 被调用方法的返回值                                                                     | 

**** 通过JNI设置成员变量的值  

-----
// 1. 获取含IntField成员变量的JniTest类的jclass值
// 类引用已经被保存到targetClass中

// 2. 获取JniTest对象的IntField变量值
fid = env->GetFieldID(targetClass, "intField", "I");

// 3. 将result值设置为IntField值
env->SetIntField(newObject, fid, resutl);
-----

| JNI函数 SetStatic<type>Field |                                                                                     |
|------------------------------+-------------------------------------------------------------------------------------|
| 形式                         | void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, <type>value) |
|------------------------------+-------------------------------------------------------------------------------------|
| 说明                         | 设置fieldID指定的Java类静态成员变量的值                                                           |
|------------------------------+-------------------------------------------------------------------------------------|
| 参数                         |      env: JNI接口指针                                                                   |
|                              |     clazz: 含待设置成员变量的类的引用                                                            |
|                              |     fieldID: 待设成员变量的ID,由GetStaticFieldID()函数获取                                      |
|                              |     value: 指定设置值                                                                    | 

| JNI函数 Set<type>Field |                                                                                |
|------------------------+--------------------------------------------------------------------------------|
| 形式                   | void Set<type>Field (JNIEnv *env, jobject obj, jfieldID fieldID, <type> value) |
|------------------------+--------------------------------------------------------------------------------|
| 说明                   | 设置fieldID指定的Java对象的成员变量                                            |
|------------------------+--------------------------------------------------------------------------------|
| 参数                   |   env: JNI接口指针                                                                 |
|                        |   obj: 包含待设成员变量的Java对象的引用                                                      |
|                        |    fieldID: 待设成员变量的ID,由GetFieldID()函数获取                                        |
|                        |    value:指定设置值                                                                 | 

* 在C程序中运行Java类

本节中学习在由C/C++编写的主程序中如何运行Java类,这也是使用JNI的重要方式。

在C/C++程序中运行Java类也必须使用Java虚拟机。为此JNI提供了一套Invocation API,它允许本地代码在自身内存区域内加载Java虚拟机。

下面列出的可能使你决定使用Invocation API在C/C++代码中调用Java代码的集中典型情况:

+ 需要在C/C++编写的本地应用程序中访问用Java语言编写的代码或代码库

+ 希望在C/C++编写的本地应用程序中使用标准的Java库

+ 当需要把自己已有的C/C++程序与Java程序组织链接在一起时,使用Invocation API可以将它们组织成一个完整的程序

** Invocaton API 应用示例
实例程序由InvokeJava.cpp与InvocationTest.java两个文件构成

示例程序将按如下顺序执行:

(1) 主程序InvokeJava.cpp使用Invocation API加载Java虚拟机。

(2) 通过JNI函数加载InvocationTest类至内存中

(3) 执行被加载的InvocatonTest类main()方法

*** 分析Java代码 InvocationApiTest.java

#+BEGIN_SRC java
public class InvocationAPiTest 
{
    public static void main(String[] args)
    {
        System.out.println(args[0]);
    }
}
#+END_SRC
=仅含有一个main()方法,该main()方法是一个静态方法,带有一个字符串对象数组,在方法体中仅有一条输出语句,用来降低一个数组元素args[0]中的字符串输出到控制台上。=

*** 分析C代码 invocationApi.c
#+BEGIN_SRC c
#include <jni.h>

int main() 
{
    JNIEnv *env;
    JavaVM *vm;
    JavaVMInitArgs vm_args;
    JavaVMOptions options[1];
    jint res;
    jclass cls;
    jmethodID mid;
    jstring jstr;
    jclass stringClass;
    jobjectArray args;

    // 1. 生成Java虚拟机选项
    options[0].optionString = "-Djava.class.path=."
    vm_args.version = 0x00010002;
    vm_args.options = options;
    vm_args.nOptions = 1;
    vm_args.ignoreUnrecognized = JNI_TRUE;

    // 2. 生成Java虚拟机
    res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args);

    // 3. 查找并加载类
    cls = (*env)->FindClass(env, "InvocationApiTest");

    // 4. 获取main()方法的ID
    mid = (*env)->GetStaticMethodID(env, cls, "main", ([Ljava/lang/String;)V);

    // 5. 生成字符串对象,用作main()方法的参数
    jstr = (*env)->NewStringUTF(env, "Hello Invacation API!!");    
    stringClass = (*env)->NewObjectArray(env, 1, stringClass, jstr);
    args = (*env)->NewObjectArray(env, 1, stringClass, jstr);

    // 6. 调用main()方法
    (*env)->CallStaticVoidMethod(env, cls, mid, args);
    
    // 7. 销毁Java虚拟机
    (*vm)->DestroyJavaVM(vm);
}
#+END_SRC

下面开始分析代码的主要部分

#include命令用来将jni.h头文件包含到本文件中。jni.h头文件包含C代码使用JNI必须的各种变量类型或JNI函数的定义,在本地代码中使用JNI时,必须将此头文件包含到本地代码中。

#+BEGIN_SRC java
    // 1. 生成Java虚拟机选项
    options[0].optionString = "-Djava.class.path=."
    vm_args.version = 0x00010002;
    vm_args.options = options;
    vm_args.nOptions = 1;
    vm_args.ignoreUnrecognized = JNI_TRUE;
#+END_SRC
生成一些参数或选项值,这些值在加载Java虚拟机时被引用,用来设置Java虚拟机的运行环境或控制Java虚拟机的运行,如设置CLASSPATH或输出调试信息等。

在生成Java虚拟机选项时,使用JavaVMInitArgs与JavaVMOption结构体,它们定义在jni.h头文件中
#+BEGIN_SRC c
typedef struct JavaVMInitArgs {
    jint version;
    jint nOptions;
    JavaVMOption *options;
    jboolean ignoreUnrecognized;
} JavaVMInitArgs;

typedef struct JavaVMOption {
    char *optionString;
    void *extraInfo;
} JavaVMOption;
#+END_SRC
观察JavaVMInitArgs结构体定义代码,可以发现JavaVMInitArgs结构体内包含JavaVMOption结构体的指针。JavaVMOption结构体包含Java虚拟机的各个参数,JavaVMInitArgs结构体用来将这些参数选项传递给Java虚拟机。

接下来,看一下结构体中各个成员的含义。

JavaVMInitArgs结构体的versino成员用来指定传递诶虚拟机的选项的变量的形式,设定在jni.h头文件中定义的 =JNI_Version_1_2= 的值。nOptions与options用来指定JavaVMInitArgs所指的JavaVMOption结构体数组值。nOptions指定JavaVMOption结构体数组元素的个数,options用来指向JavaVMOption结构体的地址。示例中只设置了一个Java虚拟机选项,即JavaVMOption结构体数组仅有一个元素,声明如下
#+BEGIN_SRC c
JavaVMOption options[1];
#+END_SRC
为了指定以上JavaVMOption结构体数组,需要指定JavaVMInitArgs的options与nOptions
#+BEGIN_SRC c
vm_args.options = options; // JavaVMOption 结构体的地址
vm_args.nOptions = 1; // JavaVMOption 结构体数组元素个数
#+END_SRC

ignoreUnrecognized是JavaVMInitArgs结构体jboolean类型的成员,当Java虚拟机独到设置错误的选项值时,该成员用来决定Java虚拟机是忽略错误后继续执行,还是返回错误后终止执行。若ignoreUnrecognized被设置为 =JNI_TRUE= ,Java虚拟机遇到错误选项时,忽略错误后继续执行;若被设置为 =JNI_FALSE= ,当遇到错误选项,Java虚拟机将错误返回后终止执行。

接下来分析JavaVMOption结构体,它用来指定Java虚拟机的选项值。若想创建选项值,只要向结构体的optionString成员指定一个字符串,用作Java虚拟机选项的形式。比如示例中的"-Djava.class.path=.",用来设置标准选项,即将Java虚拟机要加载的类的默认目录设置为当前目录(.),其形式为-Dproperty=value。

#+BEGIN_SRC c
res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args);
#+END_SRC
本行代码是整个程序的核心部分,即C应用程序调用 =JNI_CreateJavaVM()= 函数,生成并装载Java虚拟机。 =JNI_CreateJavaVM()= 函数的第一个参数类型为JavaVM,它表示Java虚拟机接口,用来生成或销毁Java虚拟机。DestroyJavaVM()是接口函数之一,该函数用来销毁Java虚拟机。

在 =JNI_CreateJavaVM()= 函数的第二个参数env中,保存着JNI接口的指针的地址。通过env所指的JNI接口指针,可以使用各种JNI函数,即在C/C++中,通过env,可以生成Java对象,调用相应方法等。

| JNI Invocation API- =JNI_CreateJavaVM= |                                                                 |
|-------------------------------------+-----------------------------------------------------------------|
| 形式                                | jint =JNI_CreateJavaVM= (javaVM **vm, JNIEnv **env, void *vm_args) |
|-------------------------------------+-----------------------------------------------------------------|
| 说明                                | 装载并初始化Java虚拟机                                                   |
|-------------------------------------+-----------------------------------------------------------------|
| 参数                                |   vm: JavaVM指针的地址                                               |
|                                     | env: JNI接口指针的地址                                          |
|                                     | =vm_args:= 传递给Java虚拟机的参数                               |
|-------------------------------------+-----------------------------------------------------------------|
| 返回值                           | 成功,返回0;失败,返回负值                                                  | 

为了加载InvocationTest类和执行方法(向main方法传递字符串参数"Hello"),首先调用FindClass()函数,装载InvocationApiTest类。而后调用GetStaticMethodID()函数,获取main()方法的ID,准备调用main()方法。  

在使用CallStaticVoidMethod()函数调用main()方法之前,首先构造出传递给main()方法的参数。Java的main()方法的参数是String[]数组
#+BEGIN_SRC java
public static void main(String[] args)
#+END_SRC

示例中将"Hello Invocation API!!"字符串传递给main()方法。首先调用NewStringUTF()函数,将UTF-8形式的字符串,转换成Java字符串对象String。然后调用NewObjectArray()函数,创建String对象数组,使用创建的String对象将其初始化。先创建一个含有一个元素的String[]数组,而后将"Hello Invocation API!!"字符串赋值给数组的第一个元素。

#+BEGIN_SRC c
    jstr = (*env)->NewStringUTF(env, "Hello Invacation API!!");    
    stringClass = (*env)->NewObjectArray(env, 1, stringClass, jstr);
    args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
#+END_SRC
调用JNI本地函数处理String对象的方法有些复杂。如果你对此仍迷惑不解,我们不妨将这部分代码转换成与其等价的Java代码。

如下所示,首先创建包含一个元素的字符串数组,而后将"Hello Invocation API!!"字符串赋值给数组的首个元素
#+BEGIN_SRC java
String[] args = new String[1];
args[0] = "Hello Invocation API!"
#+END_SRC

| JNI函数 NeStringUTF |                                                      |
|---------------------+------------------------------------------------------|
| 形式              | jstring NewStringUTF(JNIEnv *env, const char *bytes) |
|---------------------+------------------------------------------------------|
| 说明              | 将UTF-8形式的C字符串转换成java.lang.String对象       |
|---------------------+------------------------------------------------------|
| 参数              | env: JNI接口指针                                     |
|                     | bytes: 待生成String对象的C字符串的地址               |
|---------------------+------------------------------------------------------|
| 返回值        | 成功,返回String对象的jstring类型的引用;失败,返回NULL |

| JNI函数 NewObjectArray |                                                                                              |
|------------------------+----------------------------------------------------------------------------------------------|
| 形式                   | jarray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initalElement) |
|------------------------+----------------------------------------------------------------------------------------------|
| 说明                   | 生成由elementClass对象组成的数组。数组元素个数由length指定,initalElement参数用来初始化对象数组 |
|------------------------+----------------------------------------------------------------------------------------------|
| 参数                 | env: JNI接口指针                                                                             |
|                        | length: 数组元素个数                                                                         |
|                        | elementClass:数组元素对象的类型                                                              |
|                        | initialElement: 数组初始化值                                                                 |
|------------------------+----------------------------------------------------------------------------------------------|
| 返回值              | 若成功,则返回数组引用;失败,则返回NULL                                                                       |

#+BEGIN_SRC c
(*env)->CallStaticVoidMethod(env, cls, mid, args);
#+END_SRC
=本行代码通过CallStaticVoidMethod()函数调用InvocationApiTest类的main()方法。在上面创建的Stringp[]数组是CallStaticVoidMethod()函数的第四个参数,该参数会被传递给InvocationApiTest类的main()方法。当InvocationApiTest类的main()方法被调用执行时,它会向控制台输出args字符串数组的 args[0]元素中的字符串。=

* 直接注册JNI本地函数

Java虚拟机在运行包含本地方法的Java应用程序时,要经过以下两个步骤。

1. 调用System.loadLibrary()方法,将包含本地方法具体实现的C/C++运行库加载到内存中。

2. Java虚拟机检索加载进来的库函数符号,在其中查找与Java本地方法拥有相同签名的JNI本地函数符号。若找到一致的,则将本地方法映射到具体的JNI本地函数。

在Android Framework这类复杂的系统下,拥有大量的包含本地方法的java类,Java虚拟机加载相应的运行库,再逐一检索,将各个本地方法与相应的函数映射起来,这显然会增加运行时间,降低运行的效率。

为此,JNI机制提供了名称为RegisterNatives()的JNI函数,该函数允许C/C++开发者将JNI本地函数与Java类的本地方法直接映射在一起。当不调用RegisterNative()函数时,Java虚拟机会自动检索并将JNI本地函数与相应的Java本地方法链接在一起。但当开发者直接调用RegisterNatives()函数进行映射时,Java虚拟机就不必进行映射处理,这会极大提高运行速度,提升运行效率。

由于程序员直接将JNI本地函数与Java本地方法链接在一起,在加载运行库时,Java虚拟机不必为了识别JNI本地函数而将JNI本地函数的名称与JNI支持的命名规则进行对比,即任何名称的函数都能直接链接到Java本地方法上。

** 加载本地库时,注册JNI本地函数

#+BEGIN_SRC java
#include "jni.h"
#include <stdio.h>

// JNI本地函数原型
void printHelloNative(JNIEnv *env, jobject obj);
void printStringNative(JNIEnv *env, jobject obj, jstring string);

JNIEXPORT jint JNICALL JNI_Onoad(JavaVM *vm, void *reserved)
{
    JNIEnv *env = NULL;
    JNINativeMethod nm[2];
    jclass cls;
    jint result = -1;

    if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
        printf("Error");
	return JNI_ERR;
    }
    
    cls = env->FindClass("HelloJNI");

    nm[0].name = "printHello";
    nm[0].signature = "()V";
    nm[0].fnPtr = (void*)printHelloNative;

    nm[1].name = "printString";
    nm[1].signature = "(Ljava/lang/String;)V";
    nm[1].fnPtr = (void*)printStringNative;

    env->RegisterNatives(cls, nm, 2);

    return JNI_VERSION_1_4;
}

// 实现JNI本地函数
void printHelloNative(JNIEnv *env, jobject obj)
{
    printf("Hello World!\n");
    return;
}

void printStringNative(JNIEnv *env, jobject obj, jstring string)
{
    const char *str = env->GetStringUTFChars(string, 0);
    printf("%s!\n, str);

    return;
}
#+END_SRC

#+BEGIN_SRC java
void printHelloNative(JNIEnv *env, jobject obj);
void printStringNative(JNIEnv *env, jobject obj, jstring string);
#+END_SRC
此两行代码用来声明JNI本地函数原型。如前所述,在使用RegisterNatives()函数机型映射时,不需要将JNI本地函数原型与JNI命名规则进行比对,所以使用的函数名比较简单。但函数中的两个公共参数必须指定为"JNIEnv *env, jobject obj"。

#+BEGIN_SRC java
    if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
        printf("Error");
	return JNI_ERR;
#+END_SRC
在 =JNI_OnLoad()= 函数中首先判断JNI的版本,即调用GetEnv()函数,判断Java虚拟机是否支持JNI1.4。若java虚拟机支持JNI1.4, =JNI_OnLoad()= 函数就会返回 =JNI_VERSION_1_4= ;若不支持, =JNI_OnLoad()= 函数就会返回 =JNI_ERR= ,并终止装载库的行为。

当GetEnv()函数调用完毕后,JNI接口指针被保存到env变量中,在调用FindClass()、RegisterNatives()等JNI函数时,可以使用该变量。
| JNI Invocation API - GetEnv |                                                   |
|-----------------------------+---------------------------------------------------|
| 形式                        | jint GetEnv(JavaVM *vm, void **env, jint version) |
|-----------------------------+---------------------------------------------------|
| 说明                        | 判断Java虚拟机是否支持version指定的JNI版本,而后将JNI接口指针设置到*env中   |
|-----------------------------+---------------------------------------------------|
| 参数                        |   vm: JavaVM接口指针的地址                               |
|                             | env: JNI接口指针地址                        |
|                             | version: JNI版本                            |
|-----------------------------+---------------------------------------------------|
| 返回值                   | 若执行成功,返回0;失败,返回负值                                 |

#+BEGIN_SRC java
    cls = env->FindClass("HelloJNI");
#+END_SRC
为了把声明的JNI本地函数与JNI本地函数映射在一起,本行先调用FindClass()函数加载HelloJNI类,并将类引用保存到jclass变量cls中。

#+BEGIN_SRC java
    nm[0].name = "printHello";
    nm[0].signature = "()V";
    nm[0].fnPtr = (void*)printHelloNative;

    nm[1].name = "printString";
    nm[1].signature = "(Ljava/lang/String;)V";
    nm[1].fnPtr = (void*)printStringNative
#+END_SRC
该部分代码用来将Java类的本地方法与JNI本地函数映射在一起。首先使用JNINativeMethod结构体数组,将待映射的本地方法与JNI本地函数的相关信息保存在数组中,而后调用RegisterNatives()函数进行映射。JNINativeMethod结构体定义如下
#+BEGIN_SRC c
typedef struct {
    char *name;    // 本地方法名称
    char *signature; // 本地方法签名
    void *fnPtr; // 与本地方法相对应的JNI本地函数指针
} JNINativeMethod
#+END_SRC
如代码所示,nm是JNINativeMethod结构体数组,它保存着printHello()、printString()与printHelloNative()、printStringNative()函数的链接信息。

保存好映射信息后,将它们传递给RegisterNatives()函数,最后由RegisterNatives()函数完成映射。
| JNI函数 RegisterNatives |                                                                                                  |
|-------------------------+--------------------------------------------------------------------------------------------------|
| 形式                    | jarray RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methdos, jint nMethods) |
|-------------------------+--------------------------------------------------------------------------------------------------|
| 说明                    | 将clazz指定类中的本地方法与JNI本地函数链接在一起,链接信息保存在JNINativeMethod结构体数组中                                        |
|-------------------------+------------------------------------------------------------------------------------------------------------------------------------|
| 参数                    | env: JNI接口指针                                                                                     |
|                         | clazz: Java类                                                                                    |
|                         | methods: 包含本地方法与JNI本地函数的链接信息                                                     |
|                         | nMethods: methods数组元素的个数                                                                         |
|-------------------------+---------------------------------------------------------------------------------------------------------|
| 返回值                  | 若执行成功,返回数组引用;否则返回NULL                                                                            |

=总结一下,本节中通过JNI_OnLoad()函数将Java本地方法与JNI本地函数映射起来。=

** Android中的应用举例

* 使用Android NDK开发

Andoird NDK ( Native Development Kit )

+ 包含将C/C++源代码编译成本地库的工具(编译器、连接器等)

+ 提供将编译好的本地库插入Android包文件(.apk)中的功能

+ 在生成本地库时,Android平台可支持的系统头文件与库

+ NDK开发相关的文档、示例、规范

** 安装Androdi NDK

网站: http://developer.android.com/sdk/ndk/index.html

** 使用Android NDK 开发步骤

=设置好NDK环境变量后,在<NDK_HOME>/apps目录下,会看到一些NDK使用示例程序=

+ hello-jni: 调用本地库,接收"Hello from JNI"字符串,并通过TextView将其输出

+ two-libs: 调用本地库,返回两数之和,并通过TextView输出

+ san-angeles: 调用本地OpenGL ES API, 渲染3D图片

+ hello-gl2: 调用OpenGL ES 2.0, 渲染三角形

+ bitmap-plasma: 一个使用本地代码访问Android Bitmap对象的像素缓存区的示例程序

** hello-jni

内容:

AndroidManifest.xml default.properties /jni /res /src /tests

ndk-build后:

AndroidManifest.xml default.properties /jni /libs /obj /res /src /tests

#+BEGIN_SRC sh
hello-jni$ tree
.
├── AndroidManifest.xml
├── default.properties
├── jni
│   ├── Android.mk
│   └── hello-jni.c
├── libs
│   └── armeabi
│       ├── gdbserver
│       ├── gdb.setup
│       └── libhello-jni.so
├── obj
│   └── local
│       └── armeabi
│           ├── libhello-jni.so
│           └── objs-debug
│               └── hello-jni
│                   ├── hello-jni.o
│                   └── hello-jni.o.d
├── res
│   └── values
│       └── strings.xml
├── src
│   └── com
│       └── example
│           └── hellojni
│               └── HelloJni.java
└── tests
    ├── AndroidManifest.xml
    ├── default.properties
    └── src
        └── com
            └── example
                └── hellojni
                    └── HelloJniTest.java

19 directories, 15 files
#+END_SRC

关键的文件:

java层: HelloJni.java HelloJniTest.java 资源文件:strings.xml

jni层: hello-jni.c Android.mk

下面主要需要分析 HelloJni.java, hello-jni.c, Android.mk 三个文件

HelloJni.java
#+BEGIN_SRC java
package com.example.hellojni;

import android.app.Activity;
import android.widget.TextView;
import android.os.Bundle;

public class HelloJni extends Activity
{
    /** onCreate函数在activity第一次被创建的时候调用 */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        /* 创建一个TextView并且设置它的内容.
         * 文本的内容是通过本地方法获取的
         */
        TextView  tv = new TextView(this);
        tv.setText( stringFromJNI() ); // 这里调用了本地方法
        setContentView(tv);
    }

    /* 本地方法通过本地库hello-jni实现
     * 本地库与这个应用程序已经打包在了一起
     */
    public native String  stringFromJNI();

    /* 下面是另外一个方法的声明,这个方法没有通过hello-jni实现
     * 这是为了说明,你可以声明任意的本地方法,在java代码中
     * 它们的实现会在装载的本地库里寻找,当你首次调用它们的时候
     * 尝试调用这个方法会引发java.lang.UnsatisfiedLinkError exception!
     */
    public native String  unimplementedStringFromJNI();

    /* 下面的代码用来在应用程序开始的时候装载hello-jni库
     * 这个库在安装的时候由包管理器已经安装好了
     */
    static {
        System.loadLibrary("hello-jni");
    }
}
#+END_SRC

hello-jni.c
#+BEGIN_SRC c
#include <string.h>
#include <jni.h>
jstring java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv *env, jobject thiz)
{
    return (*env)->NewStringUTF(env, "Hello from JNI !");
}
#+END_SRC

Android.mk
#+BEGIN_SRC sh
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := hello-jni
LOCALSRC_FILES := hello-jni.c

include $(BUILD_SHARED_LIBRARY)
#+END_SRC

一个Android.mk文件首先必须定义好 =LOCAL_PATH= 变量,用于在开发树中查找源文件。 =LOCAL_PATH= 变量在Android.mk文件的最开始被定义,若无特殊情况,一般采用如下编写形式:
#+BEGIN_SRC sh
LOCAL_PATH := $(call my-dir)
#+END_SRC
$(call my-dir)用来保存my-dir宏函数的返回值。my-dir是一个宏函数,由编译系统提供,用于返回包含Android.mk文件的目录,即将Android.mk文件所在的目录设置为基本目录。

一般来说,本地库的源代码与Android.mk文件在同一目录下,即在 =<PROJECT_HOME>/jni= 目录下。若将$(call my-dir)返回值保存到 =LOCAL_PATH= 变量中,即可准确指定NDK编译的基本文件目录。

=include $(CLEAR_VARS)= 用来初始化Android.mk文件中" =LOCAL_XXX= "即以 =LOCAL_= 开头的变量,如 =LOCAL_MODULE= 、 =LOCAL_SRC_FILES= 等变量,但在一开始的 =LOCAL_PATH= 变量除外。由于Android编译系统将会 =LOCAL_XXX= 变量用作全局变量,所以需要使用该命令初始化这些变量。

=LOCAL_MODULE= 变量必须被定义,以标识在Android.mk文件中描述的每个模块,即要生成的库的名称。该名称必须唯一,且不含空格,编译系统会自动产生合适的前缀和后缀,比如设置 =LOCAL_MODULE= 变量为ndk-exam, 编译后缀会生成名为libndk-exam.so的共享库。

=LOCAL_SRC_FILES= 变量必须包含将要编译打包进模块中的各个源文件。这些源文件所在目录即是 =LOCAL_PATH= 变量指定的目录,即 =<PROJECT_HOME>/jni= 目录。

=include $(BUILD_SHARED_LIBRARY)= 使用 =LOCAL_MODULE= 、 =LOCAL_SRC_FILES= 等变量值,创建名称为 =lib$(LOCAL_MODULE).so= 的共享库。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值