Android JNI 复杂数据demo ,字符串、数组对象等数据操作讲解-Android JNI 复杂数据demo ,字符串,数组,对象等数据操作

时间:2024-03-11 19:11:34

文章目录

  • Android JNI 复杂数据demo ,字符串,数组,对象等数据操作
    • 一、前言
    • 二、Jni复制数据demo
      • 1、MainActivity.java代码
      • 2、native-lib.cpp 代码
      • 3、Demo代码运行后日志打印
    • 三、其他
      • 1、示例中cpp代码调用的主要api函数
      • 2、string 类型的转换和打印
        • jstring打印示例代码:
        • 指针数据合并示例
      • 3、 Android jni cpp代码中接收jintArray并返回jintArray数据示例代码
      • 4、ArrayList\ 数据类型处理
  • 共勉: 生活不是等待暴风雨过去,而是学会在雨中跳舞 。

一、前言

Android JNI复杂数据的传输,比如数组,字符串等数据传给底层处理,对于JNI使用不多的开发者,可能开发会比较吃力。

本文主要介绍JNI复制数据的传递和打印,不是很复杂的JNI Demo代码,但是对于复杂数据的处理的使用是很有帮助的。

Android JNI的基础知识介绍,之前已经有介绍,不熟悉的可以先看看:

Android Jni的介绍和简单Demo实现:

https://blog.csdn.net/wenzhi20102321/article/details/136291126

二、Jni复制数据demo

1、MainActivity.java代码

package com.demo.jniobject;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import java.util.Arrays;

public class MainActivity extends AppCompatActivity {

    String TAG = "MainActivity.java";
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.i(TAG, "onCreate");

        TextView tv = findViewById(R.id.sample_text);
        //返回cpp的字符串
        String jniString = stringFromJNI();
        Log.i(TAG, "onCreate jniString = " + jniString);
        tv.setText(jniString);

        //传递int值,cpp 返回int值
        int intToJNIBack = intToJNI(16);
        Log.i(TAG, "onCreate intToJNIBack = " + intToJNIBack);

        //传递String值,cpp 返回String值
        String stringToJNIBack = stringToJNI("liwenzhi");
        Log.i(TAG, "onCreate stringToJNIBack = " + stringToJNIBack);
        
        //传递String和int值,cpp返回String值
        String stringAndIntToJNIBack = stringAndIntToJNI("陈wang",18);
        Log.i(TAG, "onCreate stringAndIntToJNIBack = " + stringAndIntToJNIBack);
        
        //传递String数值和int数组值,cpp返回String数值的值
        String[] listStringAndListIntToJNIBack = listStringAndListIntToJNI(new String[] {"姚pengtao", "朱dejiu","周fuping"},new int[] {20,21,30});
        Log.i(TAG, "onCreate listStringAndListIntToJNIBack = " + Arrays.asList(listStringAndListIntToJNIBack));

        Log.i(TAG, "onCreate End jniString = " + jniString);
    }


    //Java到cpp并且获取返回数据方法
    public native String stringFromJNI(); //返回cpp的字符串
    public native int intToJNI(int age); //传递int值,cpp 返回int值
    public native String stringToJNI(String name); //传递String值,cpp 返回String值
    public native String stringAndIntToJNI(String name, int age); //传递String和int值,cpp返回String值
    public native String[] listStringAndListIntToJNI(String[] names, int[] ages); //传递String数值和int数组值,cpp返回String数值的值

}

上面的方法包含了传递int,String,数组和返回int,String,数值的示例方法调用。

2、native-lib.cpp 代码

#include <jni.h>
#include <string>

#include <android/log.h>
#define LOG_TAG "native-lib.cpp"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

//Java 方法: public native String stringFromJNI()
extern "C" JNIEXPORT jstring JNICALL
Java_com_demo_jniobject_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    LOGI("stringFromJNI hello = %s", hello.c_str());
    return env->NewStringUTF(hello.c_str());
}

//Java 方法: public native int intToJNI(int age)
extern "C" JNIEXPORT jint JNICALL
Java_com_demo_jniobject_MainActivity_intToJNI(JNIEnv *env, jobject thiz, jint age) {
    int cppAge = age +10;
    return cppAge;
}

//Java 方法:public native String stringToJNI(String name)
extern "C" JNIEXPORT jstring JNICALL
Java_com_demo_jniobject_MainActivity_stringToJNI(JNIEnv *env, jobject thiz, jstring name) {
return name;//这里直接返回,想要修改字符串内容可以看看下面代码
}

//Java 方法:public native String stringAndIntToJNI(String name, int age)
extern "C" JNIEXPORT jstring JNICALL
Java_com_demo_jniobject_MainActivity_stringAndIntToJNI(JNIEnv *env, jobject thiz, jstring name,jint age) {

    // 将jstring转换为C字符串
    const char* c_str1 = env->GetStringUTFChars(name, nullptr);
    // 进行字符串拼接,不能像Java一样,"" + 5,这样会报错,要先转换类型
    // 将jint转换为C++字符串
    std::string numStr = std::to_string(age);
    std::string result = std::string(c_str1) + "cpp 拼接 age = " + numStr;

    LOGD("stringAndIntToJNI name = %s", c_str1);
    // 释放GetStringUTFChars函数申请的资源
    env->ReleaseStringUTFChars(name, c_str1);

    // 将C字符串转换为jstring
    return env->NewStringUTF(result.c_str());
}

//Java 方法:public native String[] listStringAndListIntToJNI(String[] names, int[] ages)
extern "C" JNIEXPORT jobjectArray JNICALL
Java_com_demo_jniobject_MainActivity_listStringAndListIntToJNI(JNIEnv *env, jobject thiz,jobjectArray names, jintArray ages) {
    // 获取数组长度
    jint length = env->GetArrayLength(names);

    // 创建一个新的数组,用于存储修改后的数据
    jobjectArray newArray = env->NewObjectArray(length, env->FindClass("java/lang/String"), nullptr);

    // 获取原始int数组的指针
    jint *originalArray = env->GetIntArrayElements(ages, nullptr);

    // 遍历原始数组
    for (int i = 0; i < length; i++) {
        // 获取原始数组元素
        jstring element = (jstring) env->GetObjectArrayElement(names, i);
        jint elementInt = originalArray[i] + 2;
        std::string numStr = std::to_string(elementInt);

        // 将原始字符串转换为新的字符串
        const char *c_str = env->GetStringUTFChars(element, nullptr);
        std::string modifiedStr = "Modified: ";
        modifiedStr += c_str;
        modifiedStr += ",age =";
        modifiedStr += numStr;
        //释放String
        env->ReleaseStringUTFChars(element, c_str);
        LOGE("listStringAndListIntToJNI modifiedStr = %s", modifiedStr.c_str());
        // 创建一个新的字符串对象,并将其设置到新的数组中
        jstring newElement = env->NewStringUTF(modifiedStr.c_str());
        env->SetObjectArrayElement(newArray, i, newElement);
    }

    // 释放原始int数组的指针
    env->ReleaseIntArrayElements(ages, originalArray, 0);

    // 返回修改后的数组
    return newArray;

}

里面的代码是不难,但是没写过的,就不知道用哪些api,字符串怎么修改,怎么拼接,打印;

并且这些知识在网上还是比较片段的,很多是待验证的。

3、Demo代码运行后日志打印

2024-03-04 16:24:44.773  I/MainActivity.java: onCreate
2024-03-04 16:24:44.773  I/native-lib.cpp: stringFromJNI hello = Hello from C++
2024-03-04 16:24:44.773  I/MainActivity.java: onCreate jniString = Hello from C++
2024-03-04 16:24:44.773  I/MainActivity.java: onCreate intToJNIBack = 26
2024-03-04 16:24:44.773  I/MainActivity.java: onCreate stringToJNIBack = liwenzhi
2024-03-04 16:24:44.773  D/native-lib.cpp: stringAndIntToJNI name = 陈wang
2024-03-04 16:24:44.773  I/MainActivity.java: onCreate stringAndIntToJNIBack = 陈wangcpp 拼接 age = 18

2024-03-04 16:24:44.773  E/native-lib.cpp: listStringAndListIntToJNI modifiedStr = Modified: 姚pengtao,age =22
2024-03-04 16:24:44.773 E/native-lib.cpp: listStringAndListIntToJNI modifiedStr = Modified: 朱dejiu,age =23
2024-03-04 16:24:44.773 E/native-lib.cpp: listStringAndListIntToJNI modifiedStr = Modified: 周fuping,age =32

2024-03-04 16:24:44.773 I/MainActivity.java: onCreate listStringAndListIntToJNIBack = [Modified: 姚pengtao,age =22, Modified: 朱dejiu,age =23, Modified: 周fuping,age =32]

2024-03-04 16:24:44.773 I/MainActivity.java: onCreate End jniString = Hello from C++

上面日志不仅包含Java代码的日志,同时也包含了cpp文件的日志。

三、其他

1、示例中cpp代码调用的主要api函数

(1)const char* c_str1 = env->GetStringUTFChars(name, nullptr);
把jstring name 数据转换成指针对象。

(2)jint length = env->GetArrayLength(names);
获取 jobjectArray names 数值数据的长度,其他类型的数值数据也是使用这个api就行

(3)jobjectArray newArray = env->NewObjectArray(length, env->FindClass("java/lang/String"),nullptr);
创建一个length长度的,string数组对象
其他类型的数值数据创建就一定不是用NewObjectArray这个函数,
比如创建int数组数据类型就用 jintArray newArray = env->NewIntArray(length);
其他的基础类型数据把Int换成其他类型就行了。

(4)env->SetObjectArrayElement(newArray, i, newElement);
给数组 newArray的序号位置 i,设置string数值 std::string modifiedStr
其他类型的数值数据创建就一定不是用SetObjectArrayElement这个函数,
比如,设置int的数值类型的数据:
jint *originalArray; originalArray[1] = 100; //直接就可以替换
 
(5)jint *originalArray = env->GetIntArrayElements(ages, nullptr);
获取 jintArray ages的指针对象。
直接使用下标就可以获取到对应的值,比如;int a = originalArray[i]

这里只是举例说明了某些函数api 的使用,如果会使用这些,那么其他的基础类型那些是没有问题的。

至于其他一些复杂类型,比如自定义的类操作处理,这些都是不常用的,有需要的自己摸索看看吧。

2、string 类型的转换和打印

在jni cpp代码中,基础类型是不用转换就可以直接使用的,但是对象类型的数据要一定要转换的。

比如下面的简单赋值示例:

//基础类型,其他类型比如jlong,jchar也是一样的
jint number;
int num = number + 10;//是没有问题的

jstring name;
std::string hello = name; //未转换,直接赋值是会报错的
const char *c_str = name;//类型是不对应的,会报错
jstring打印示例代码:
extern "C" JNIEXPORT jstring JNICALL
Java_com_demo_jniobject_MainActivity_stringToJNI(JNIEnv *env, jobject thiz, jstring name) {
    // 将jstring转换为C字符串, 打印指针数据
    const char *c_str1 = env->GetStringUTFChars(name, nullptr);
    LOGD("stringToJNI name = %s", c_str1);

    //打印int转换后的string数据
    int age = 20;
    LOGD("stringToJNI age = %d", age );
    std::string numStr = "age = " + std::to_string(age);
    LOGD("stringToJNI numStr = %s", numStr.c_str());

    return name;
}

cpp中 string类型数据是可以直接使用"+"进行拼接的,拼接后可以直接打印出来;

int或者其他基本数据类型,如果要和string拼接就要先转换成string类型;

numStr.c_str()其实也是转换成了指针类型数据,

所以"%s"打印,打印的是指针数据,知道了指针的第一个位置,字符串后面的数据就都知道了。

如果要char *类型的数据拼接,就没那么简单了,需要自己对指针位置进行控制,比如:

指针数据合并示例
//合并两个指针数据的实现函数
char* concatenateStrings(const char* a, const char* b) {
    // 获取两个字符串的长度
    size_t lenA = strlen(a);
    size_t lenB = strlen(b);

    // 分配足够的内存来存储合并后的字符串
    char* result = new char[lenA + lenB + 1];

    // 将第一个字符串复制到合并后的字符串中
    strcpy(result, a);

    // 将第二个字符串追加到合并后的字符串的末尾
    strcat(result, b);

    return result;
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_demo_jniobject_MainActivity_stringToJNI(JNIEnv *env, jobject thiz, jstring name) {
    // 将jstring转换为C字符串, 打印指针数据
    const char *c_str1 = env->GetStringUTFChars(name, nullptr);
    LOGD("stringToJNI name = %s", c_str1);
    
    //测试两个指针数据的合并和打印
    const char* strA = "Hello, ";
    const char* strB = "world!";
    // 合并两个字符串
    char* mergedString = concatenateStrings(strA, strB);
    LOGD("stringToJNI mergedString = %s", mergedString);
}

上面可以看到,指针数据处理需要用到一下api函数。

3、 Android jni cpp代码中接收jintArray并返回jintArray数据示例代码

#include <jni.h>

extern "C"  JNIEXPORT jintArray JNICALL Java_com_example_example_MyClass_modifyIntArray(JNIEnv *env, jobject obj, jintArray array) {
    // 获取数组长度
    jint length = env->GetArrayLength(array);

    // 创建一个新的int数组,用于存储修改后的数据
    jintArray newArray = env->NewIntArray(length);

    // 获取原始int数组的指针
    jint *originalArray = env->GetIntArrayElements(array, nullptr);

    // 遍历原始数组
    for (int i = 0; i < length; i++) {
        // 修改原始数组中的元素
        originalArray[i] = originalArray[i] * 2;
    }

    // 将修改后的数据设置到新的int数组中
    env->SetIntArrayRegion(newArray, 0, length, originalArray);

    // 释放原始int数组的指针
    env->ReleaseIntArrayElements(array, originalArray, 0);

    // 返回修改后的int数组
    return newArray;
}

上面的创建int类型数值列表的函数和设置数值数据的函数与string类型数组的数据的创建和设置函数是不一样的。

4、ArrayList<String> 数据类型处理

比如,一个包含 ArrayList<String> 类型的jni方法:

//返回的也是 ArrayList<String> 数据类型
public native ArrayList<String> modifyArrayList(ArrayList<String> name);

实际代码中,我就不会这样做了,一般用String[] 就比较好处理,返回再转换成ArrayList会简单很多。

但是如果一定要实现也也是可以的,就是要写反射代码。

其实不管是,String还是ArrayList 或者 ArrayList<String>数据,对于cpp代码来说都是object对象。

对象里面的操作是要通过反射进行的,相对来说比较麻烦,

比如要先获取对象调用某个api获取里面的某个数据,再调用某个api设置某个数据。

cpp代码实现:

#include <jni.h>

extern "C" JNIEXPORT jobject JNICALL Java_com_example_example_MyClass_modifyArrayList(JNIEnv *env, jobject obj, jobject arrayList) {
    // 获取ArrayList类
    jclass arrayListClass = env->GetObjectClass(arrayList);

    // 获取ArrayList的size()方法
    jmethodID sizeMethod = env->GetMethodID(arrayListClass, "size", "()I");

    // 调用size()方法获取ArrayList的大小
    jint size = env->CallIntMethod(arrayList, sizeMethod);

    // 获取ArrayList的get()方法
    jmethodID getMethod = env->GetMethodID(arrayListClass, "get", "(I)Ljava/lang/Object;");

    // 获取ArrayList的add()方法
    jmethodID addMethod = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");

    // 创建一个新的ArrayList用于存储修改后的数据
    jobject newArray = env->NewObject(arrayListClass, env->GetMethodID(arrayListClass, "<init>", "()V"));

    // 遍历原始ArrayList
    for (int i = 0; i < size; i++) {
        // 调用get()方法获取ArrayList的元素
        jstring element = (jstring) env->CallObjectMethod(arrayList, getMethod, i);

        // 将原始字符串转换为C字符串
        const char *c_str = env->GetStringUTFChars(element, nullptr);
        std::string modifiedStr = "Modified: ";
        modifiedStr += c_str;
        env->ReleaseStringUTFChars(element, c_str);

        // 创建一个新的String对象
        jstring newElement = env->NewStringUTF(modifiedStr.c_str());

        // 调用add()方法将新的String对象添加到新的ArrayList中
        env->CallBooleanMethod(newArray, addMethod, newElement);
    }

    // 返回修改后的ArrayList
    return newArray;
}

上述代码定义了一个名为modifyArrayList的JNI函数,该函数接收一个ArrayList参数,

并对其中的String元素进行修改(在每个元素前添加"Modified: "),然后返回修改后的ArrayList。

在代码中,使用JNI函数来获取ArrayList类和其中的方法,通过调用方法来操作ArrayList和其中的元素。

你可以将上述代码添加到你的CPP文件中,并根据你的需求进行适当修改和调整。

记得使用JNI提供的函数来获取和操作ArrayList中的元素,并创建新的ArrayList来存储修改后的数据。

最后,记得返回新的ArrayList对象。

虽然看完本文不一定能掌握上面的全部知识,但是对于复杂数据处理会有一些概念和认识。

可以先记录、收藏,到时候不明白的时候再看看,就可以大大提高效率了。