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 jstring
e C++ std::string
são tipos diferentes, possuem características diferentes, por isso precisam ser convertidos ao atribuir valores um ao outro.
-
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,jstring
um ponteiro para um objeto Java String. Em Java, as strings são representadas na codificação UTF-16 -
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::string
e 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
:byteC
:CaracteresD
:dobroF
:flutuadorI
:intJ
:longoS
:curtoZ
: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 representarjava.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;
denotarString[]
tipo,[[I
denotarint[][]
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 representarList<String>
o tipo.
- Use
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.