Android JNI de 0 a 1 Tutorial de introdução (3)

Os dois blogs anteriores apresentaram conteúdo relacionado ao jni e como usá-lo no Android. A demonstração é relativamente simples. Desta vez, vamos falar sobre as chamadas mútuas mais complicadas entre java e C/C++.

A função que implementaremos a seguir é passar o objeto Java para C++, então usar o objeto C++ para receber o valor e, finalmente, passar o valor do objeto C++ de volta para a camada Java.

1. Exemplo de código

1. Crie uma classe de entidade Java

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

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

2. Defina o método nativo

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. Gere o arquivo de cabeçalho 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 o arquivo de cabeçalho da classe C++ 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. Arquivo fonte de implementação de classe C++ 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 implementa 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. Adicione a configuração da biblioteca a 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. Recrie o projeto, gere .so e chame-o na camada Java

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

Efeito:

Estrutura completa do projeto: 

2. Análise dos pontos-chave

1. jstring e std::string

Em JNI jstringe C++ std::stringsão tipos diferentes, possuem características diferentes, por isso precisam ser convertidos ao atribuir valores um ao outro.

  1. jstring: jstring É o tipo que representa a string Java em JNI. É um tipo intermediário para passar dados de string entre Java e código nativo. Em JNI, jstringum ponteiro para um objeto Java String. Em Java, as strings são representadas na codificação UTF-16

  2. std::string: std::string é um tipo de string fornecido pela biblioteca padrão C++. É um tipo comum para representar strings em C++, usado para armazenar e manipular dados de caractere, std::stringe armazena e manipula dados de string na forma de sequências de bytes. Ao usar C++ para processar strings, você precisa prestar atenção ao manuseio e conversão de codificações de caracteres.

 2. Assinatura de campo/método

No código acima podemos ver o seguinte formulário:

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

Aqui, o segundo parâmetro é o nome da variável de membro para o objeto Java e o terceiro parâmetro é o bytecode de assinatura do campo.

A assinatura de um campo Java (Field Signature) é uma representação de string usada para descrever o tipo de campo. Uma assinatura de campo inclui os modificadores do campo, tipo de campo e informações opcionais de tipo genérico.

A assinatura de um campo Java segue certas regras e notações. Aqui estão alguns símbolos e exemplos comuns de assinatura de campo:

  • tipo básico:

    • B:byte
    • C:Caracteres
    • D:dobro
    • F:flutuador
    • I:int
    • J:longo
    • S:curto
    • Z:boleano
  • Tipo de referência:

    • L + nome da classe+  ;: Indica o tipo de referência, e o nome da classe precisa usar uma barra ( /) como separador e ;terminar com ponto e vírgula ( ). Por exemplo, Ljava/lang/String; para representar  java.lang.String o tipo.
  • Tipo de matriz:

    • [: Representa uma matriz unidimensional
    • [[: Representa uma matriz bidimensional
    • [ Por analogia, arrays multidimensionais podem ser representados pela adição de múltiplos 
    • O tipo de array é seguido pela assinatura do tipo de elemento correspondente. Por exemplo, [Ljava/lang/String; denotar  String[] tipo, [[I denotar  int[][] tipo.
  • Tipo genérico:

    • Use  <> uma lista de parâmetros de tipo fechada e use vírgulas ( ,) para separar vários parâmetros de tipo. Por exemplo, Ljava/util/List<Ljava/lang/String;>; para representar  List<String> o tipo.

As assinaturas de campo são frequentemente usadas em cenários como reflexão Java, manipulação de bytecode e carregadores de classe para descrever e distinguir diferentes tipos de campos.

assinatura de método

Uma assinatura de método Java (assinatura de método) é uma representação de string usada para descrever um método. Uma assinatura de método inclui o nome do método, a lista de parâmetros e o tipo de retorno.

A representação em bytecode da assinatura do método é a seguinte:

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

Se o método tiver uma declaração para lançar uma exceção, a informação da exceção na assinatura do método ^é representada pelo símbolo, seguido da representação da classe da exceção.

Por exemplo:

  • public void printMessage(String message) A representação em bytecode de é:(Ljava/lang/String;)V
  • private int calculateSum(int[] numbers) A representação em bytecode de é:([I)I
  • protected boolean checkValidInput(String username, String password) A representação em bytecode de é:(Ljava/lang/String;Ljava/lang/String;)Z
  • public void process() throws IOException A representação em bytecode de é:()V^Ljava/io/IOException;

Deve-se notar que existem algumas diferenças entre a assinatura do campo e a assinatura do método (assinatura do método). A assinatura do campo se concentra principalmente na descrição do tipo de campo, enquanto a assinatura do método inclui o tipo de valor de retorno, lista de parâmetros e exceção Informação.

3. ReleaseStringUTFChars

No JNI, a função ReleaseStringUTFChars é usada para liberar a matriz de caracteres codificados em UTF-8 do objeto jstring obtido pela função GetStringUTFChars. Essas duas funções costumam ser usadas juntas para garantir o gerenciamento adequado da memória e evitar vazamentos de memória.

A função GetStringUTFChars retorna um ponteiro para uma matriz de caracteres codificados em UTF-8 do objeto jstring. A matriz de caracteres precisa permanecer inalterada durante o uso e a memória associada precisa ser liberada quando não for mais usada.

A função da função ReleaseStringUTFChars é notificar a JVM (Java Virtual Machine) que a JNI não precisa mais usar o array de caracteres e liberar os recursos de memória associados a ela. Isso pode evitar vazamentos de memória e liberar o espaço de memória ocupado.

4. Análise das funções relacionadas da biblioteca jni

Um grande número de funções da biblioteca jni é usado no código acima para obter informações sobre objetos Java, e essas funções são todas declaradas no arquivo jni.h. Aqui estão algumas descrições de função comumente usadas:

jclass FindClass(const char * nome)
Encontre e retorne a classe Java correspondente no classpath especificado
jclass GetObjectClass(objeto de trabalho)
Obtenha esta classe através do objeto
jfieldID GetFieldID(jclass clazz, const char* nome, const char* sig)
Obtenha o ID do campo da classe especificada
jmethodID GetMethodID(jclass clazz, const char* nome, const char* sig)
Obtenha o ID do método da classe especificada
jobject GetObjectField(jobject obj, jfieldID fieldID)
Obtenha o objeto objeto especificado de acordo com o ID do campo
jobject NewObject(jclass clazz, jmethodID methodID, ...)
Criar um novo objeto Java
jstring NewStringUTF(const char* bytes)
Converter strings C/C++ em strings Java
const char* GetStringUTFChars(jstring string, jboolean* isCopy)
Converter string Java em string C/C++
void ReleaseStringUTFChars(jstring string, const char* utf)
Libera  GetStringUTFChars() uma string C/C++ obtida por

pós-escrito

Usando JNI em Kotlin

Kotlin e java podem chamar um ao outro, então é o mesmo para JNI. Em Java, use a palavra-chave nativa para declarar o método e em kotlin, use a palavra-chave externa. Segue um exemplo:

class KNIDemo {

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

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

Outros aspectos são os mesmos do Java. As assinaturas das variáveis ​​e métodos de membros do Kotlin são exatamente as mesmas do Java e não há problemas de incompatibilidade.

Acho que você gosta

Origin blog.csdn.net/gs12software/article/details/131681348
Recomendado
Clasificación