Android Studio开发JNI示例
2018-03-24 23:46
337 查看
JNI和NDK介绍
JNI(Java Native Interface),是方便Java调用C、C++等Native代码所封装的一层接口,相当于一座桥梁。通过JNI可以操作一些Java无法完成的与系统相关的特性,尤其在图像和视频处理中大量用到。NDK(Native Development Kit)是Google提供的一套工具,其中一个特性是提供了交叉编译,即C或者C++不是跨平台的,但通过NDK配置生成的动态库却可以兼容各个平台。比如C在Windows平台编译后生成.exe文件,那么源码通过NDK编译后可以生成在安卓手机上运行的二进制文件.so
在AS中使用ndk-build开发JNI示例
Android Studio2.2之前对于JNI开发的支持不是很好,开发一般使用Eclipse+插件编写本地动态库。后面Google官方全面增强了对JNI的支持,包括内置NDK。1.在AS中新建一个项目
2.声明一个native方法
package com.mercury.jnidemo; public class JNITest { public native static String getStrFromJNI(); }
3.通过javah命令生成头文件
在AS的Terminal中,先进入要调用本地代码的类所在的目录,也就是在项目中的具体路径,比如这里是cd app\src\main\java。然后通过javah命令生成该类的头文件,注意包名+类名.这里是
javah -jni com.mercury.jnidemo.JNITest,生成头文件
com_mercury_jnidemo_JNITest.h
实际项目最终可以不包含此头文件,不熟悉C的语法的开发人员,借助于该头文件可以知道JNI的相关语法:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_mercury_jnidemo_JNITest */ #ifndef _Included_com_mercury_jnidemo_JNITest #define _Included_com_mercury_jnidemo_JNITest #ifdef __cplusplus extern "C" { #endif /* * Class: com_mercury_jnidemo_JNITest * Method: getStrFromJNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_mercury_jnidemo_JNITest_getStrFromJNI (JNIEnv *, jclass); #ifdef __cplusplus } #end 4000 if #endif
首先引入jni.h,里面包含了很多宏定义及调用本地方法的结构体。重点是方法名的格式。这里的JNIEXPORT和JNICALL都是jni.h中所定义的宏。JNIEnv *表示一个指向JNI环境的指针,可通过它来访问JNI提供的接口方法。jobject表示Java对象中的this.实际编写中一般只要遵循Java_包名类名方法名就好了。
4.实现JNI方法
像上面的头文件只是定义了方法,并没有实现,就像一个接口一样。这里就用C写一个简单的无参的JNI方法。先创建一个jni目录,我直接在src的父目录下创建的,也可以在其他目录创建,因为最终只需要要的编译好的动态库。在jni目录下创建Android.mk和demo.c文件。
Android.mk是一个makefile配置文件,安卓大量采用makefile进行自动化编译。LOCAL_MODULE定义的名称就是编译好的so库名称,比如这里是
jni-demo,最终生成的动态库名称就叫libjni-demo.so. LOCAL_SRC_FILES表示参与编译的源文件名称,那创建的源文件就是demo.c
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := jni-demo LOCAL_SRC_FILES := demo.c include $(BUILD_SHARED_LIBRARY)
这里的demo.c实现了一个很简单的方法,返回String类型。
#include<jni.h> jstring Java_com_mercury_jnidemo_JNITest_getStrFromJNI(JNIEnv *env,jobject thiz){ return (*env)->NewStringUTF(env,"I am Str from jni libs!"); }
这时候NDK编译生成的动态库会有四个CPU平台:arm64-v8a、armeabi-v7a、x86、x86_64。如果创建Application.mk就可以指定要生成的CPU平台,语法也很简单:
APP_ABI := all
这样就会生成各个CPU平台下的动态库。
5.使用ndk-build编程生成.so库
切回到jni目录的父目录下,在Terminal中运行ndk-build指令,就可以在和jni目录同级生成一个libs文件夹,里面存放相对应的平台的.so库。同时生成的还有一个中间临时的obj文件夹,和jni文件夹可以一起删除。需要注意,使用NDK一定要先在build.gradle下要配置ndk-build的相关路径,这样在编写本地代码时才会有相关的提示功能,并且可以关联到相关的头文件:
externalNativeBuild { ndkBuild { path 'jni/Android.mk' } }
还有一点,网上很多资料都在build.gradle中加入一下代码:
sourceSets{ main{ jniLibs.srcDirs=['libs'] } }
这样就指定了目标.so库的存放位置。但在实际使用中,就算不指定,运行时仍然可以加载正确的.so库文件,并且如果添加该代码后有时会报出以下错误:
Error:Execution failed for task ':usejava:transformNativeLibsWithMergeJniLibsForDebug'. > More than one file was found with OS independent path 'lib/x86/libjni-calljava.so' >
6.加载.so库并调用方法
在类初始化的时候要加载该.so库,一般会写在静态代码块里。名称就是前面的LOCAL_MODULE。static { System.loadLibrary("jni-demo"); }
需要注意的是如果是有参的JNI方法,那么直接在参数列表里补充在jni.h预先typedef好的数据类型就可以了,JNIEnv *是必须的,jobject则不一定。
JNI调用Java
不同于JNI调用C,JNI调用Java的过程不是单独存在的。还是先编写native方法,Java先通过JNI调用该方法,在方法内部再去回调类中相关的Java方法。步骤有些类似于Java中的反射。这里写定义三个点击事件,三个Native方法,三种Java的方法类型,根据相关的Log判断是否成功。public class MainActivity extends AppCompatActivity { public static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } static { System.loadLibrary("jni-calljava"); } public void noParamMethod() { Log.i(TAG, "无参的Java方法被调用了"); } public void paramMethod(int number) { Log.i(TAG, "有参的Java方法被调用了" + number + "次"); } public static void staticMethod() { Log.i(TAG, "静态的Java方法被调用了"); } public void click1(View view) { test1(); } public void click2(View view) { test2(); } public void click3(View view) { test3(); } public native void test1(); public native void test2(); public native void test3(); }
1.调用Java无参方法
JNI调用本地方法,根据类名找到类,注意类名用”/”分隔。找到类后,根据方法名找到方法。该函数GetMethodID最后一个形参是该形参列表的签名。不同于Java,C中是通过签名标识去找方法。
获取方法的签名:首先定位到该类的字节码文件所在的父目录,一般在
module\build\intermediates\classes\debug>,通过
javap -s com.mercury.usejava.MainActivity获取整个类所有的内部类型签名。无参方法test1()的签名是
()V。
通过JNIEnv对象的CallVoidMethod来完成方法的回调,最后一个形参是可变参数。
JNIEXPORT void JNICALL Java_com_mercury_usejava_MainActivity_test1 (JNIEnv * env, jobject obj){ //回调MainActivity中的noParamMethod jclass clazz = (*env)->FindClass(env, "com/mercury/usejava/MainActivity"); if (clazz == NULL) { printf("find class Error"); return; } jmethodID id = (*env)->GetMethodID(env, clazz, "noParamMethod", "()V"); if (id == NULL) { printf("find method Error"); } (*env)->CallVoidMethod(env, obj, id); }
2.调用Java有参方法
类似于无参方法,只是参数签名和可变参数的不同3.调用Java静态方法
注意获取方法名的方法是GetStaticMethodID,调用方法的函数名是
CallStaticVoidMethod,并且由于是静态方法,不应该传入jobject参数,而直接是jclass.
JNIEXPORT void JNICALL Java_com_mercury_usejava_MainActivity_test3 (JNIEnv * env, jobject obj){ jclass clazz = (*env)->FindClass(env, "com/mercury/usejava/MainActivity"); if (clazz == NULL) { printf("find class Error"); return; } jmethodID id = (*env)->GetStaticMethodID(env, clazz, "staticMethod", "()V"); if (id == NULL) { printf("find method Error"); } (*env)->CallStaticVoidMethod(env, clazz, id); }
相应日志
使用CMake开发JNI
CMake是一个跨平台的安装(编译)工具,通过编写CMakeLists.txt,可以生成对应的makefile或project文件,再调用底层的编译。AS 2.2之后工具中增加了对CMake的支持,官方也推荐用CMake+CMakeLists.txt的方式,代替ndk-build+Android.mk+Application.mk的方式去构建JNI项目.1.创建使用CMake构建的项目
开始前AS要先在SDK Manager中安装SDK Tools->CMake只要勾选
Include C++ Support。其中会提示配置C++支持的功能.
一般默认就可以了,各个选项的具体含义:
* C++ Standard:指定编译库的环境。
* Exception Support:当前项目支持C++异常处理
* Runtime Type Information Support:除异常处理外,还支持动态转类型(dynamic casting) 、模块集成、以及对象I/O
2.工程的目录结构
创建好的工程主Module下直接就有.externalNativeBuild,多出一个CMakeLists.txt,相当于以前的配置文件。并且在src/main目录下多了一个cpp文件夹,里面存放的是C++文件,相当于以前的jni文件夹。这个是工程创建后AS生成的示例JNI方法,返回了一个字符串。后面开发JNI就可以按照这个目录结构。
相应的,build.gradle下也增加了一些配置。
android { ... defaultConfig { ... externalNativeBuild { cmake { cppFlags "-std=c++14 -frtti -fexceptions" } } } buildTypes { ... } externalNativeBuild { cmake { path "CMakeLists.txt" } } }
defaultConfig中的externalNativeBuild各项属性和前面创建项目时的选项配置有关,外部的externalNativeBuild则定义了CMakeLists.txt的存放路径。
如果只是在项目中使用自己和本地的一些交互,在打包APK的时候会将工程中的本地代码一并打包,只有在提供给外部使用时才需要编译成.so文件。
Make Project,之后在
build/intermediates/cmake/debug/obj目录下就可以看到生成的.so文件。
CMakeLists.txt
CMakeLists.txt可以自定义命令、查找文件、头文件包含、设置变量,具体可见 官方文档。项目默认生成的CMakeLists.txt核心内容如下:# 编译本地库时我们需要的最小的cmake版本 cmake_minimum_required(VERSION 3.4.1) # 相当于Android.mk add_library( # Sets the name of the library.设置编译生成本地库的名字 native-lib # Sets the library as a shared library.库的类型 SHARED # Provides a relative path to your source file(s).编译文件的路径 c917 src/main/cpp/native-lib.cpp ) # 添加一些我们在编译我们的本地库的时候需要依赖的一些库,这里是用来打log的库 find_library( # Sets the name of the path variable. log-lib # Specifies the name of the NDK library that # you want CMake to locate. log ) # 关联自己生成的库和一些第三方库或者系统库 target_link_libraries( # Specifies the target library. native-lib # Links the target library to the log library # included in the NDK. ${log-lib} )
使用CMakeLists.txt同样可以指定so库的输出路径,但一定要在add_library之前设置,否则不会生效:
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}) #指定路径 #生成的so库在和CMakeLists.txt同级目录下的libs文件夹下
如果想要配置so库的目标CPU平台,可以在build.gradle中设置
android { ... defaultConfig { ... ndk{ abiFilters "x86","armeabi","armeabi-v7a" } } ... }
需要注意的是,如果是多次使用add_library,则会生成多个so库。如果想将多个本地文件编译到一个so库中,只要最后一个参数添加多个C/C++文件的相对路径就可以
用C语言实现字符串加密
Java中实现字符串加密的一种比较简单的方法是异或,将字符串转换为字符数组,遍历对其中的每个字符用密钥(可以是字符)进行一次异或运算,生成新的字符串。如果用JNI+C实现,大致步骤如下(jstring是要加密的字符串):1 获取jstring的长度
2 动态开辟一个跟data长度一样的char*
3 将 jstring类型转换为char数组(用char*接收)
4 遍历char数组,进行异或运算
5 将char*转换为jstring类型返回
6 释放动态开辟的堆内存空间
效果图我是用的是5.0的模拟器,有时会闪退,查看系统日志,会报出一下错误:
JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8
网上查了一下,JNI在调用NewStringUTF方法时,遇到不认识的字符就会退出,因为虚拟机dalvik/vm/CheckJni.cpp里面的checkUTFString会对字符类型进行检查。替代方案是在开始转换前,先检查char*中是否含有非UTF-8字符,有的话返回空字符串。完整代码如下:
#include<jni.h> #include <stdlib.h> jboolean checkUtfBytes(const char* bytes, const char** errorKind) ; jstring Java_com_mercury_cmakedemo_MainActivity_encryptStr (JNIEnv *env, jobject object, jstring data){ if(data==NULL){ return (*env)->NewStringUTF(env, ""); } jsize len = (*env)->GetStringLength(env, data); char *buffer = (char *) malloc(len * sizeof(char)); (*env)->GetStringUTFRegion(env, data, 0, len, buffer); int i=0; for (; i <len ; i++) { buffer[i] = (char) (buffer[i] ^ 2); } const char *errorKind = NULL; checkUtfBytes(buffer, &errorKind); free(buffer); if (errorKind == NULL) { return (*env)->NewStringUTF(env, buffer); } else { return (*env)->NewStringUTF(env, ""); } } //把char*和errorKind传入,如果errorKind不为NULL说明含有非utf-8字符,做相应处理 jboolean checkUtfBytes(const char* bytes, const char** errorKind) { while (*bytes != '\0') { jboolean utf8 = *(bytes++); // Switch on the high four bits. switch (utf8 >> 4) { case 0x00: case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: case 0x06: case 0x07: // Bit pattern 0xxx. No need for any extra bytes. break; case 0x08: case 0x09: case 0x0a: case 0x0b: case 0x0f: /* * Bit pattern 10xx or 1111, which are illegal start bytes. * Note: 1111 is valid for normal UTF-8, but not the * modified UTF-8 used here. */ *errorKind = "start"; return utf8; case 0x0e: // Bit pattern 1110, so there are two additional bytes. utf8 = *(bytes++); if ((utf8 & 0xc0) != 0x80) { *errorKind = "continuation"; return utf8; } // Fall through to take care of the final byte. case 0x0c: case 0x0d: // Bit pattern 110x, so there is one additional byte. utf8 = *(bytes++); if ((utf8 & 0xc0) != 0x80) { *errorKind = "continuation"; return utf8; } break; } } return 0; }
相关文章推荐
- Android Studio开发之 JNI 篇的简单示例
- android studio下 jni开发(java调cpp文件)
- Android Studio进行JNI开发的操作过程
- 基于NDK开发完JNI代码后,Android studio中引入对应的so文件的方法
- Android-NDK开发示例--通过JNI获取MD5码
- Android Studio进行Jni应用开发
- Android Eclipse JNI开发示例
- android studio 2.0下 jni开发环境的搭建
- 安卓 jni 开发 —— Android Studio 打包 so 的坑
- Android Studio2.3上JNI开发
- 安卓 jni 开发 —— Android Studio 打包 so 的坑
- Android Studio开发NDK JNI
- Android Studio进行JNI开发
- Android JNI 开发系列(三)Android Studio中C与Cpp文件构建脚本
- 使用Android Studio进行JNI开发 - Mac篇
- 基于CMake 在Android studio2.2+ 中开发JNI
- NDK 开发之 Android Studio 中使用 JNI
- Android studio 使用ndk开发JNI
- 使用Android Studio进行JNI开发 - Mac篇
- Android Studio建立jni开发环境