Android JNI from 0 to 1 Getting Started Tutorial (3)

The previous two blogs introduced jni-related content and how to use it in Android. The demo is relatively simple. This time, let’s talk about the more complicated mutual calls between java and C/C++.

The function we will implement next is to pass the Java object to C++, then use the C++ object to receive the value, and finally pass the value of the C++ object back to the Java layer.

1. Code example

1. Create a java entity class

public class RequestBean {
    public String name;
    public int num;
}

public class ResponseBean {
    public String resName;
    public int resNum;
}

2. Define the native method

public class JNIDemo {

    static {
        //这个库名必须跟Android.mk的LOCAL_MODULE对应,如果是第三方so,也请对应正确
        System.loadLibrary("JniDemo");
    }

    public static native String test();

    public static native ResponseBean getRespFromCPP(RequestBean request);
}

3. Generate the header file com_example_jni_JNIDemo.h

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

#ifndef _Included_com_example_jni_JNIDemo
#define _Included_com_example_jni_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_jni_JNIDemo
 * Method:    test
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_jni_JNIDemo_test
  (JNIEnv *, jclass);

/*
 * Class:     com_example_jni_JNIDemo
 * Method:    getRespFromCPP
 * Signature: (Lcom/example/jni/RequestBean;)Lcom/example/jni/ResponseBean;
 */
JNIEXPORT jobject JNICALL Java_com_example_jni_JNIDemo_getRespFromCPP
  (JNIEnv *, jclass, jobject);

#ifdef __cplusplus
}
#endif
#endif

4. Declare the C++ class header file CResponse.h

#include "string"//string在C++中并不是一个基本类型,而是一个完整的字符串类。要使用需要include其头文件
using std::string; //声明使用空间

class CResponse{
private:
    string name;
    int num;
public:

    string getName();

    int getNum();

    void setValue(string name,int num);
};

5. C++ class implementation source file CResponse.cpp

#include "CResponse.h"
#include "string"

using namespace std;

string CResponse::getName() {
    return this->name;
}

int CResponse::getNum() {
    return this->num;
}

void CResponse::setValue(string name, int num) {
    this->name = name;
    this->num = num;
}

6. JNI implements JNITest.cpp

#include "com_example_jni_JNIDemo.h" //引入刚刚生成的头文件
#include "CResponse.h"
#include "string"

extern "C"
JNIEXPORT jstring JNICALL Java_com_example_jni_JNIDemo_test(JNIEnv * env, jclass clz){
    return env->NewStringUTF("hello world");
}

//jstring转C++ std::string
std::string jstringToString(JNIEnv* env, jstring jstr)
{
    const char *cStr = env->GetStringUTFChars(jstr, nullptr);
    std::string cppStr(cStr);  //这是string.h提供的库函数
    env->ReleaseStringUTFChars(jstr, cStr);//释放掉cStr,防止内存泄漏
    return cppStr;
}

extern "C"
JNIEXPORT jobject JNICALL Java_com_example_jni_JNIDemo_getRespFromCPP(JNIEnv * env, jclass clz, jobject request) {

    //获取传过来的java对象值
    // 1)获取java对象的jclass;
    jclass  jRequestClass = env->FindClass("com/example/jni/RequestBean");
    // 2)获取java对象的字段ID,注意字段名称和签名;
    jfieldID nameId  = env->GetFieldID(jRequestClass, "name", "Ljava/lang/String;");
    // 3)根据字段ID获取该字段的值;
    jstring name = (jstring)env->GetObjectField(request, nameId);
    jfieldID numId  = env->GetFieldID(jRequestClass, "num", "I");
    jint cNum = env->GetIntField(request, numId);

    CResponse *cResp = new CResponse();
    // Java jstring类型转C++ string类型
    string cName = jstringToString(env,name) + " from c++"; //从java获取到name后拼上字符串
    cNum++; //将java对象传过来的num值加1
    //调用函数赋值给C++对象的成员变量
    cResp->setValue(cName,cNum);

    //C++对象转换为java对象
    // 1)获取java ResponseBean对象的jclass;
    jclass jRespClass = env->FindClass("com/example/jni/ResponseBean");
    // 2)获取构造方法ID;
    jmethodID jmId = env->GetMethodID(jRespClass, "<init>", "()V");
    // 3)通过构造方法ID创建Java ResponseBean对象;
    jobject jReturnObj = env->NewObject(jRespClass, jmId);
    // 4)获取ReturnInfo对象的字段ID;
    jfieldID rNameId = env -> GetFieldID(jRespClass, "resName", "Ljava/lang/String;");
    jfieldID rNumId = env -> GetFieldID(jRespClass, "resNum", "I");

    // 5)通过字段ID给每个字段赋值
    jstring rName = env->NewStringUTF(cResp->getName().c_str());
    env->SetObjectField(jReturnObj, rNameId, rName);
    env->SetIntField(jReturnObj, rNumId, cResp->getNum());
    // 返回Java对象;
    return jReturnObj;
}

7. Add library configuration to CMakeLists.txt

#指定CMake的最低版本要求
cmake_minimum_required(VERSION 3.18.1)

# 定义本地库的名称
set(my_lib_name JniDemo)

#添加库配置,如果有多个库,可以添加多个add_library方法
add_library( # 指定生成库的名称,使用上面定义的变量
        ${my_lib_name}
        # 标识是静态库还是动态库
        SHARED
        # C/C++源代码文件路径
        src/main/cpp/JNITest.cpp
        src/main/cpp/CResponse.cpp)

#指定.h头文件的目录
include_directories(src/main/cpp/)

# 指定构建输出路径
set_target_properties(${my_lib_name} PROPERTIES
        LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}")

8. Rebuild the project, generate .so, and then call it in the Java layer

RequestBean bean = new RequestBean();
bean.name = "张三";
bean.num = 0;
ResponseBean resp = JNIDemo.getRespFromCPP(bean);
tvMsg.setText("name: " + resp.resName + " num:"+resp.resNum);

Effect:

Complete project structure: 

2. Analysis of key points

1. jstring and std::string

In JNI, jstringand C++ std::stringare different types, they have different characteristics, so they need to be converted when assigning values ​​to each other.

  1. jstring: jstring It is the type representing Java string in JNI. It is an intermediate type for passing string data between Java and native code. In JNI, jstringa pointer to a Java String object. In Java, strings are represented in UTF-16 encoding

  2. std::string: std::string It is a string type provided by the C++ standard library. It is a common type for representing strings in C++, used to store and manipulate character data, std::stringand it stores and manipulates string data in the form of byte sequences. When using C++ to process strings, you need to pay attention to the handling and conversion of character encodings.

 2. Field/method signature

In the above code we can see the following form:

env->GetFieldID(jRequestClass, "name", "Ljava/lang/String;")

Here the second parameter is the member variable name for the Java object, and the third parameter is the signature bytecode of the field.

The signature of a Java field (Field Signature) is a string representation used to describe the field type. A field signature includes the field's modifiers, field type, and optional generic type information.

The signature of a Java field follows certain rules and notations. Here are some common field signature symbols and examples:

  • basic type:

    • B:byte
    • C:char
    • D:double
    • F:float
    • I:int
    • J:long
    • S:short
    • Z:boolean
  • Reference type:

    • L + class name+  ;: Indicates the reference type, and the class name needs to use a slash ( /) as a separator and ;end with a semicolon ( ). For example, Ljava/lang/String; to represent  java.lang.String the type.
  • Array type:

    • [: Represents a one-dimensional array
    • [[: Represents a two-dimensional array
    • [ By analogy, multidimensional arrays can be represented by adding multiple 
    • The array type is followed by the signature of the corresponding element type. For example, [Ljava/lang/String; denote  String[] type, [[I denote  int[][] type.
  • Generic type:

    • Use  <> an enclosed type parameter list, and use commas ( ,) to separate multiple type parameters. For example, Ljava/util/List<Ljava/lang/String;>; to represent  List<String> the type.

Field signatures are often used in scenarios such as Java reflection, bytecode manipulation, and class loaders to describe and distinguish different types of fields.

method signature

A Java method signature (Method Signature) is a string representation used to describe a method. A method signature includes the method name, parameter list, and return type.

The bytecode representation of the method signature is as follows:

(L参数类型1;L参数类型2;...;)返回类型

If the method has a declaration to throw an exception, the exception information in the method signature ^is represented by the symbol, followed by the representation of the exception class.

For example:

  • public void printMessage(String message) The bytecode representation of is:(Ljava/lang/String;)V
  • private int calculateSum(int[] numbers) The bytecode representation of is:([I)I
  • protected boolean checkValidInput(String username, String password) The bytecode representation of is:(Ljava/lang/String;Ljava/lang/String;)Z
  • public void process() throws IOException The bytecode representation of is:()V^Ljava/io/IOException;

It should be noted that there are some differences between the field signature and the method signature (Method Signature). The field signature mainly focuses on the description of the field type, while the method signature includes the return value type, parameter list, and exception information.

3.ReleaseStringUTFChars

In JNI, the ReleaseStringUTFChars function is used to release the UTF-8 encoded character array of the jstring object obtained by the GetStringUTFChars function. These two functions are often used together to ensure proper memory management and avoid memory leaks.

The GetStringUTFChars function returns a pointer to an array of UTF-8 encoded characters of the jstring object. The character array needs to remain unchanged during use, and the associated memory needs to be freed when it is no longer used.

The function of the ReleaseStringUTFChars function is to notify the JVM (Java Virtual Machine) that JNI no longer needs to use the character array and release the memory resources associated with it. This can avoid memory leaks and release the occupied memory space.

4. Analysis of related jni library functions

A large number of jni library functions are used in the above code to obtain information about Java objects, and these functions are all declared in the jni.h file. Here are some commonly used function descriptions:

jclass FindClass(const char* name)
Find and return the corresponding Java class in the specified classpath
jclass GetObjectClass(jobject obj)
Get this class through the object
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
Get the ID of the field of the specified class
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
Get the ID of the method of the specified class
jobject GetObjectField(jobject obj, jfieldID fieldID)
Get the specified object object according to the field ID
jobject NewObject(jclass clazz, jmethodID methodID, ...)
Create a new Java object
jstring NewStringUTF(const char* bytes)
Convert C/C++ strings to Java strings
const char* GetStringUTFChars(jstring string, jboolean* isCopy)
Convert Java String to C/C++ String
void ReleaseStringUTFChars(jstring string, const char* utf)
Frees  GetStringUTFChars() a C/C++ string obtained by

postscript

Using JNI in kotlin

Kotlin and java can call each other, so it is the same for JNI. In Java, use the native keyword to declare the method, and in kotlin, use the external keyword. The following is an example:

class KNIDemo {

    companion object {
        init {
            System.loadLibrary("JniDemo")
        }
    }

    external fun test(): String
    external fun getRespFromCPP(request: RequestBean?): ResponseBean?
}

Other aspects are the same as Java. The signatures of Kotlin's member variables and methods are exactly the same as those of Java, and there are no incompatibility issues.

Guess you like

Origin blog.csdn.net/gs12software/article/details/131681348