Android - JNI笔记

JNI开发流程

  1. 编写Java接口(native方法)
  2. (选中需要生成C代码的Java文件)生成.c接口文件(使用javah命令生成)
  3. 编写Android.mk文件
  4. 编写Application.mk文件
  5. 实现.c接口文件
  6. ndk-build编译
  7. so文件在src/main/libs目录下
  8. Java中使用System.loadLibrary(“Android.mk中LOCAL_MODULE的值”)加载动态链接库

Android Studio 配置External Tools实现快速生成.so文件

  1. External Tools 1: 根据Java文件生成.c接口文件
Name: javah
Description: JDK-javah tool
Program: javah
Arguments: -v -jni -d $ModuleFileDir$/src/main/jni $FileClass$
Working directory: $SourcepathEntry$
  1. External Tools 2: ndk-build生成动态链接库.so
Name: ndk build
Description: ndk build
Program: D:\android-ndk\android-ndk-r20b\ndk-build.cmd (ndk目录下的ndk-build.cmd文件地址)
Arguments:
Working directory: $ModuleFileDir$\src\main\

JNI接口与接口实现示例解释

接口:

#include <jni.h>

JNIEXPORT jstring JNICALL Java_com_example_myapplication_JNITest_getJniTestString
  (JNIEnv *, jclass);

接口实现:

#include "jni.h"
#include "com_example_myapplication_JNITest.h"
JNIEXPORT jstring JNICALL Java_com_example_myapplication_JNITest_getJniTestString
  (JNIEnv * env, jclass object) {
    return (*env)->NewStringUTF(env,"测试 jni");
  }
  • 接口命名规则: JNIEXPORT 返回值类型 JNICALL Java_包名_类名_方法名(JNIEnv *, jclass, 参数)
    • JNIEXPORT : 固定格式
    • jstring : 返回值为String类型
    • JNICALL : 固定格式
    • JNIEnv * :
      • 针对C : JNIEnv 为struct JNINativeInterface的一级指针, JNIEnv *为struct JNINativeInterface的二级指针, 通过该指针能调用大量操作java类型与c类型互转的函数(细节见头文件: jni.h), 调用时用 (*env)->方法
      • 针对C++ : JNIEnv为结构体struct JNINativeInterface的别名, JNIEnv *为struct JNINativeInterface的一级指针, 故在调用时, 直接使用 env->方法; (注意: C++函数应该需要先声明再使用,因此需要先把javap生成的接口文件include一下)
    • jclass/jobject:
      • 若Java中的方法为静态方法则为jclass,表示字节码对象
      • 若Java中的方法为非静态方法则为jobject,表示对象实例
    • JNINativeInterface : 该结构体位于jni.h头文件中, 该结构体中定义了大量函数指针, 这些函数在JNI开发中常用
    • 调用方式: (*env) -> 函数
  • 详细变量细节定义可见: jni.h
  • 注意点:
    1. 如果Java方法名中存在下划线(_), 那么JNI的C接口函数命名就应该用(_1)来代替 _ (使用javah命令可以避免这些问题, 让程序自动生成接口文件)

Android.mk文件介绍

  • 作用: 告诉ndk-build资源文件所在的位置
#找到jni目录
LOCAL_PATH := $(call my-dir)
#清除上一次编译的环境信息
include $(CLEAR_VARS)
#编译生成的文件的类库叫什么名字(JNITest生成出来的类库名为:libJNITest.so)
LOCAL_MODULE    := JNITest
#要编译的c源文件, 多个以空格隔开
LOCAL_SRC_FILES := JNITest.c
#指定要加载的链接库(如下为加载logcat支持库)
#LOCAL_LDLIBS += -llog  
#指定要生成的为动态链接库(.so文件)
include $(BUILD_SHARED_LIBRARY)

Application.mk文件介绍

  • 作用: 指定编译哪些架构的动态链接库(如: arm, x86等)
#编译指定CPU架构的动态链接库(全部用: "ALL", 单个: "armeabi", "armeabi-v7a", "x86", "x86_64"等)
#arm架构为Linux平台, x86为Intel平台
#多个平台可以用空格隔开
APP_ABI := ALL
#指定编译最小SDK版本
APP_PLATFORM := android-23

实用技巧

  • Android Studio NDK开发, 设置代码提示补全功能
    1. file -> Project Structure -> SDK Location -> Android NDK location 设置本地NDK路径
    2. 鼠标右击项目
    3. 选中Link C++ Project with Gradle
    4. Build System 选中 ndk-build
    5. Project Path 填入 Android.mk的文件路径
    6. 点击Apply
  • Android Studio 中C代码字体颜色不好看,可以进入 file -> setting -> Editor -> Color Scheme -> C/C++ 进行设置
  • JNI中, 在C代码中打日志方法(使用logcat)
    1. 包含头文件: #include <android/log.h>
    2. 配置Android.mk: 在Android.mk中添加: LOCAL_LDLIBS := -llog
    3. 定义LOGI,LOGE等宏
    #include <android/log.h>
    
    #define LOG_TAG "System.out"
    #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
    #define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
    
    1. 使用方法跟printf类似: LOGI("%d", i)
  • Android Studio 3.x以上查看logcat方法
    1. 进入AndroidSDK/tools目录
    2. 运行monitor.bat文件 (查不到文件可以使用Listary或everthing搜索)

NDK的DEBUG调试方法

  • 预配置说明:
    1. build.gradle中的android节点下添加配置:
      android {
          ...
          externalNativeBuild {
              ndkBuild {
                  path file('src/main/jni/Android.mk')
              }
          }
      }
      
    2. src\main目录下不能有jniLibs目录
  • 方法1: 直接点击DEBUG按钮(但有时C代码有一些隐式声明, 导致直接DEBUG时编译不通过, 但是使用ndk-build进行手动编译却能通过, 那么此时就采用方法2)
  • 方法2: 使用ndk-build后, 将src\main\obj\local下的文件拷贝到build\intermediates\ndkBuild\debug\obj\local目录下, 拷贝成功后, 将后者下面的每个架构文件夹下的objs文件夹改为objs-debug即可;
    • 文件拷问bat脚本
        @echo off
    
        set SOURCES_PATH=项目路径\src\main\obj\local
        set TARGET_PATH=项目路径\build\intermediates\ndkBuild\debug\obj\local
        
        echo 正在删除%TARGET_PATH%
        rmdir /s /q %TARGET_PATH%\
        
        echo 正在从%SOURCES_PATH%拷贝到%TARGET_PATH%
        xcopy %SOURCES_PATH% %TARGET_PATH%\ /s
        
        ren %TARGET_PATH%\arm64-v8a\objs objs-debug
        ren %TARGET_PATH%\x86\objs objs-debug
        
        echo OK
        pause
    
    

示例

1. Java向C传递int类型数据

  • 需求:传递两个int类型的数据给C, C进行相加后返回

Java代码

public class JNI {

    static {
        System.loadLibrary("JNITest");
    }

    public static native int add(int a, int b);

}

C代码

#include <jni.h>

JNIEXPORT jint JNICALL Java_com_cnostar_myapplication_JNI_add
        (JNIEnv *env, jclass clazz, jint a, jint b) {
    return a + b;
}

2. Java向C传递String类型数据

  • 需求:Java传递一个String数据到C, C做一些加工后返回
  • 流程:
    1. 调用工具方法把 java中的string 类型 转换成 C 语言中的 char*
    2. 调用strlen 可获取 cstr 字符串的长度

将JavaString转换成char*的工具类

/**
 * 把一个jstring转换成一个c语言的char* 类型.
 */
char* _JString2CStr(JNIEnv* env, jstring jstr) {
    // 定义一个char指针变量
    char* rtn = NULL;
    // 获取String类型字节码对象
    jclass clsstring = (*env)->FindClass(env, "java/lang/String");
    // 将"GB2312"字符编码字符串转换为jstring类型
    jstring strencode = (*env)->NewStringUTF(env,"GB2312");
    // 获取getBytes方法
    jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
    // 执行String的getByte("GB2312");
    jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode);
    // 获取数组长度
    jsize alen = (*env)->GetArrayLength(env, barr);
    // 获取数组首地址
    jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
    // alen>0表示数组有内容
    if(alen > 0) {
        // 在堆内存中申请alen+1大小的空间, +1是因为字符串数组有结束符"\0"
        rtn = (char*)malloc(alen+1); //"\0"
        // 将ba拷贝到rtn
        memcpy(rtn, ba, alen);
        // 将结束符给加上
        rtn[alen]=0;
    }
    // 释放
    (*env)->ReleaseByteArrayElements(env, barr, ba,0);
    return rtn;
}

Java代码

public class JNI {

    static {
        System.loadLibrary("JNITest");
    }
    
    public static native String stringToC(String str);
}

C代码

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

JNIEXPORT jstring JNICALL
Java_com_cnostar_myapplication_JNI_stringToC(JNIEnv *env, jclass clazz, jstring str) {
    char *cstr = _JString2CStr(env, str);
    char *append = "say ";
    char *newstr = (char *) malloc(strlen(cstr) + strlen(append));
    sprintf(newstr, "%s %s", append, cstr);
    return (*env)->NewStringUTF(env, newstr);
}

/**
 * 把一个jstring转换成一个c语言的char* 类型.
 */
char* _JString2CStr(JNIEnv* env, jstring jstr) {
    char* rtn = NULL;
    jclass clsstring = (*env)->FindClass(env, "java/lang/String");
    jstring strencode = (*env)->NewStringUTF(env,"GB2312");
    jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
    jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode); // String .getByte("GB2312");
    jsize alen = (*env)->GetArrayLength(env, barr);
    jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
    if(alen > 0) {
        rtn = (char*)malloc(alen+1); //"\0"
        memcpy(rtn, ba, alen);
        rtn[alen]=0;
    }
    (*env)->ReleaseByteArrayElements(env, barr, ba, 0);
    return rtn;
}

3. Java代码传递一个int[]到C

  • 获取数组首地址: 以int[]数组为例(其他的类型方法名规则类似): jint *arr_pointer = (*env)->GetIntArrayElements(env, int_arr, NULL);
    • 参数1: JNIEnv*
    • 参数2: 要取首地址的数组
    • 参数3: 可以定义一个jboolean*指针, 该方法执行会赋值jboolean*,告诉开发者系统是否生成了一份数组拷贝,不想传可以为NULL
  • 获取数组长度: jsize len = (*env)->GetArrayLength(env, int_arr);
    • 参数1: JNIEnv*
    • 参数2: 要获取长度的数组
  • 将int*数组数据拷贝到jintArray = (*env)->ReleaseIntArrayElements(env, int_arr, arr_pointer, 0);
    • 参数1: JNIEnv*
    • 参数2: 目标数组
    • 参数3: 源指针
    • 参数4:

Java代码

public class JNI {

    static {
        System.loadLibrary("JNITest");
    }

    public static native int[] intArrToC(int[] intArr);
}

C代码

#include <jni.h>
#include <android/log.h>

// 日志宏定义,可在logcat中输出
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

JNIEXPORT jintArray JNICALL
Java_com_cnostar_myapplication_JNI_intArrToC(JNIEnv *env, jclass clazz, jintArray int_arr) {
    jsize len = (*env)->GetArrayLength(env, int_arr);
    jint *arr_pointer = (*env)->GetIntArrayElements(env, int_arr, NULL);
    for (int i = 0; i < len; ++i) {
        *(arr_pointer + i) *= 10;
        LOGI("%d", *(arr_pointer + i));
    }
    (*env)->ReleaseIntArrayElements(env, int_arr, arr_pointer, 0);
    return int_arr;
}

4. C回调Java的方法

  • 关键流程: 在JNI/C中使用反射获取到Java的方法, 然后调用
  • 步骤:
    1. 反射获取字节码对象
      • 方法: FindClass(env, “目标Java类的全路径”)
      • 返回值: jclass
    2. 获取Method方法对象
      • 获取普通方法: GetMethodID(evn, 第一步获取的字节码对象jclass, “方法名”, “参数签名,如(II)I”)
      • 获取静态方法: GetStaticMethodID(evn, 第一步获取的字节码对象jclass, “方法名”, “参数签名,如(II)I”)
      • 返回值: jmethodID
      • 参数签名: 因为Java中的方法会有重载的情况, 所以需要传入签名来找到对应的方法, 如(II)I: 表示两个形参为int类型,返回值为int类型的方法;
      • 获取参数签名的方法:
        1. 如果JNI接口是采用javah命令生成的, 那么方法上面的注释会标注签名,如: Signature: (Ljava/lang/String;)Ljava/lang/String;
        2. 也可以自行进入class文件所在目录, 然后打开cmd使用命令 javap -s class文件的全限定名 来查看
    3. 通过字节码对象创建实例(若对象实例已存在, 则直接使用, 无需创建)
      • 方法: AllocObject(env, jclass)
      • 返回值: jobject
    4. 通过方法对象执行方法
      • 方法
        1. Call返回值类型Method(env, 调用方法的对象, jmethodID, …参数) 如: CallBooleanMethod
        2. Call返回值类型MethodV(env, 调用方法的对象, jmethodID, 不知道意思) 如: CallBooleanMethodV
        3. Call返回值类型MethodA(env, 调用方法的对象, jmethodID, 不知道意思) 如: CallBooleanMethodA
        4. 如果为静态方法, 那么方法名为 CallStatic返回值类型Method

Java代码

public class JNI {

    static {
        System.loadLibrary("JNITest");
    }

    public native void requestC();

    public void callBack() {
        System.out.println("回调成功!");
    }

}

C代码

#include <jni.h>

JNIEXPORT void JNICALL
Java_com_cnostar_myapplication_JNI_requestC(JNIEnv *env, jobject thiz) {
    // 1. 获取字节码对象
    jclass clz = (*env)->FindClass(env, "com/xxx/myapplication/JNI");
    // 2. 获取方法
    jmethodID methodID = (*env)->GetMethodID(env, clz, "callBack", "()V");
    // 3. 执行方法
    (*env)->CallVoidMethod(env, thiz, methodID);
}

5. C回调Java静态方法,传入String,返回String

Java代码

public class JNI {

    static {
        System.loadLibrary("JNITest");
    }

    public static native String requestC();

    public static String callBack(String a) {
        return "back: " + a;
    }
}

C代码

#include <jni.h>
#include <string.h>
#include <malloc.h>
#include <android/log.h>

#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)


JNIEXPORT jstring JNICALL
Java_com_cnostar_myapplication_JNI_requestC(JNIEnv *env, jclass clazz) {
    // 获取静态方法
    jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, "callBack","(Ljava/lang/String;)Ljava/lang/String;");
    // 将hello字符串转换为jstring类型
    jstring param = (*env)->NewStringUTF(env, "hello");
    // 调用方法,并接收返回值
    jstring result = (*env)->CallStaticObjectMethod(env, clazz, methodID, param);
    // 将jstring转换为char*, 为了打印日志看看
    char *result_pointer = _JString2CStr(env, result);
    // 打印日志
    LOGI("===>>>%s", result_pointer);
    // 将char*转换为jstring并返回给java
    return (*env)->NewStringUTF(env, result_pointer);
}

/**
 * 把一个jstring转换成一个c语言的char* 类型.
 */
char* _JString2CStr(JNIEnv* env, jstring jstr) {
	 char* rtn = NULL;
	 jclass clsstring = (*env)->FindClass(env, "java/lang/String");
	 jstring strencode = (*env)->NewStringUTF(env,"GB2312");
	 jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
	 jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode); // String .getByte("GB2312");
	 jsize alen = (*env)->GetArrayLength(env, barr);
	 jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
	 if(alen > 0) {
		rtn = (char*)malloc(alen+1); //"\0"
		memcpy(rtn, ba, alen);
		rtn[alen]=0;
	 }
	 (*env)->ReleaseByteArrayElements(env, barr, ba,0);
	 return rtn;
}

6. C回调Java弹出Toast

  • 关键点: 弹Toast需要用到Context, 那么就在Java的JNI定义类时,传入Context即可

java代码

public class JNI {

    static {
        System.loadLibrary("JNITest");
    }

    private Context mContext;

    public JNI(Context mContext) {
        this.mContext = mContext;
    }

    public native void requestC();

    public void callBack(String a) {
        Toast.makeText(mContext, "=>" + a, Toast.LENGTH_LONG).show();
    }
}

C代码

#include <jni.h>

JNIEXPORT void JNICALL
Java_com_cnostar_myapplication_JNI_requestC(JNIEnv *env, jobject thiz) {
    jclass clazz = (*env)->FindClass(env, "com/cnostar/myapplication/JNI");
    // 获取方法
    jmethodID methodID = (*env)->GetMethodID(env, clazz, "callBack","(Ljava/lang/String;)V");
    jstring param = (*env)->NewStringUTF(env, "hello");
    // 执行方法
    (*env)->CallVoidMethod(env, thiz, methodID, param);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值