这是一篇让你少走弯路的 JNI/NDK 实例教程
2018-03-05 18:06
246 查看
作者: 夏至 欢迎转载,但保留这段申明
http://blog.csdn.net/u011418943/article/details/79449108
关于 JNI 的基础就不多说了,这篇文章主要讲解如何在 AS 中用 ndk-build 和 用 cmake 去构建我们的 JNI 工程,并总结他们的特点以及优缺点。
本文代码链接:https://github.com/LillteZheng/JniDemo.git
通过这篇文章,你讲学习到:
用 AS 构建自己的 JNI 工程
学会使用 mk 去加载自己的 so 文件
学会调用第三方 so 或 .a 的方法 (工程提供测试的 so )
学会使用 camke,体验丝般顺滑的 C/C++ 编写体验
首先,新建一个工程,配置 ndk 的环境:
然后,新建一个工程,在 gradle.properties 中,添加如下:
android.useDeprecatedNdk=true
接着,先使用 AS 自带的功能,在 module 中的 build.gradle 添加 so 库的名字:
新建一个类,用来生成 native 方法:
接着,就是生成 class 文件了,先 build module 一下
(如果嫌麻烦,可以跳到快捷设置,不用写这么麻烦,不过我建议你还是操作一遍)
打开 cmd,或者用 as 的 Terminal ,这里用cmd演示,去到你的工程路径下,生成我们需要的 .h 文件 :
首先,我们需要设置 src 的根路径 ,如果不先设置根路径,一般会提示找不到类,用 set classpath 的命令,指向你的 java 文件:
然后,再使用 javah 去生成 .h 文件,即上面的 JniUtils:
就可以看到生成了 .h 文件,如下图:
接着,我们新建一个 jni 的文件夹:
把 .h 文件复制过去,然后复制多一份 .h 文件,后缀名改为.cpp ,如下:
make module 一下,会发现,已经生成了 so 库:
最后再 MainActivity 中调用即可看到效果。
去到 Setting 选择 external tools ,新建一个 ,命名为 javah,(忽略我配置的 ndk_build,后面会用到):
配置以下参数:
program 为要执行的命令
parameters ,先设置路径,然后就是把命令敲一遍,注意是 /src/main/jni ,如果你的路径不一样,记得修改
working directory 是 .h 的生成路径
然后在你的 jni 类中,按住右键:
之后会弹出一个弹窗,可以自己输入 .h 的名字 (ps:先把以前的去掉):
效果如下:
接下来的步骤,就跟上面的差不多了,这里就不赘述了。
关于 mk 的学习,可以参考这篇文章 (写得还不错),这里就不多说了:
http://blog.csdn.net/mynameishuangshuai/article/details/52577228
回到 build.gradle ,先把上面的 ndk 的属性去掉,然后添加:
在 jni 路径,添加 Android.mk 和 application.mk :
首先,先编写 Android.mk :
可以看到,我们把 jni 的 so 的名字改成了 jniutils,用于区别,记得改 JniUtils 中 loadLibrary 的名字,不然报错了,别怪我没提醒;
Application.mk 则如下:
指定生成所有平台下的 so。
由于我们使用了 mk 编译了,as 并不知道,我们要像刚才配置 javah 那样,配置一下 ndk-build ,配置信息如下:
参数已经解释过了,然后在 jni 的文件夹上右键,编译一下:
可以看到,生成的 so 包如下:
这样,我们就完成了我们的编译了,run 一下,就可以看到你想要的结果了。
所以我们可以在 build.gradle 中,添加任务,在每次 run 的时候,自动编译。
build 应该这样配置:
完整 build.gradle 文件如下:
接下来,我们在 jniutils.cpp 中,把返回的字符串改一下:
直接run,可以看到效果:
首先,我们需要有个第三方的 so 库,这里我从网上下载了一个,下载地址在 github 的demo 中;目录如下:
在引入第三方 so 库的时候,需要特别注意的是,这个 so 你要选择好版本,如果你的 so 是32的,而你在 appliaction.mk 的API版本中,选择了 all 或者 arm64-v8a等,那么编译肯定是报错的;
一般手机是 armeabi ,模拟器是 x86 ,机顶盒等板子是 arm64-v8a 的, 我的模拟器刚好是 x86_64 的,所以,这里引入的 so 库是 x86_64 下的,导入之后,目录如下:
重新编写 mk 文件:
与前面相比,多了一个第三方模块的引入。接着,我们要指定 application.mk 的 API:
如果导入的工程报错,可以试着 APP_ABI 为 x86 ,替换相应的 so 。
接着,我们在 java 类这里,添加一个 调用 so 方法的 java 方法 getIntValue :
JniUtils.cpp 的代码如下:
修改一下 MainActivity.java
效果如下;
官方中文文档如下
https://developer.android.google.cn/studio/projects/add-native-code.html
首先,在新建工程的时候,勾选上 c++ support ( 3.0 往下拉才有)
一路 next ,然后有两个提示框:
这两个也勾选上,解释如下:
Exceptions Support:如果您希望启用对 C++ 异常处理的支持,请选中此复选框。如果启用此复选框,Android Studio 会将 -fexceptions 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。
Runtime Type Information Support:如果您希望支持 RTTI,请选中此复选框。如果启用此复选框,Android Studio 会将 -frtti 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。
工程已经给了我们一个 jni 的例子,而它的编译方式就是通过 CMakeLists.txt 来构建的。
下面是对 CMakeLists.txt 的解释,由于篇幅,这里会删掉一些注释:
如果要添加库,则使用 add_library,括号以空格区分,如果要使用第三方库,比如打印的 log 这个库,就通过 find_library 的方式添加,最后通过 target_link_libraries 把源文件的库,和第三方的库变量名引进来,注意第三方库是个路径变量名,所以 ${}的方式引用。
相较传统配置,如果对 mk 不熟悉的小伙伴,估计会很喜欢 cmake 的方式.
然后编写,jniutils.cpp,你会惊喜地发现,竟然有提示!!
接下来就是 用 add_library 的方式,我们把 jniutils 加进来:
同步一下即可,修改一下 mainactivity,运行,效果如下:
可以看到,使用 cmake 的方式,除了有代码提示,在添加类上,简直不能太方便了。
接着,我们添加一下第三方so,还是上面的 libvvw.so ,目录如下:
接着,我们需要制定一下 ndk 编译时的 类型,不然会增加一个 mips 的类型,这个是编不过的。
接着,则是配置最重要的 CMakeLists.txt 了,具体如下:
注释已经写得很清楚了,关键是要写对 so 的路径,不然会提示 missing and no rules to make 等错误;
jniutils.cpp 的代码如下:
效果如下:
如果是新建项目,我建议还是用 cmake 的方式,毕竟只 c/c++ 有提示这一点,我相信你也拒绝不了的。
当然,实际项目上,还有动态加载 so 的方法,这里就不深入了,这里就当做个 入门介绍吧。
http://blog.csdn.net/u011418943/article/details/79449108
关于 JNI 的基础就不多说了,这篇文章主要讲解如何在 AS 中用 ndk-build 和 用 cmake 去构建我们的 JNI 工程,并总结他们的特点以及优缺点。
本文代码链接:https://github.com/LillteZheng/JniDemo.git
通过这篇文章,你讲学习到:
用 AS 构建自己的 JNI 工程
学会使用 mk 去加载自己的 so 文件
学会调用第三方 so 或 .a 的方法 (工程提供测试的 so )
学会使用 camke,体验丝般顺滑的 C/C++ 编写体验
1、ndk-build
先用传统的方式,即 ndk-build 的方式首先,新建一个工程,配置 ndk 的环境:
然后,新建一个工程,在 gradle.properties 中,添加如下:
android.useDeprecatedNdk=true
接着,先使用 AS 自带的功能,在 module 中的 build.gradle 添加 so 库的名字:
新建一个类,用来生成 native 方法:
public class JniUtils { static { System.loadLibrary("JNIDemo"); } public static native String getName(); }
接着,就是生成 class 文件了,先 build module 一下
(如果嫌麻烦,可以跳到快捷设置,不用写这么麻烦,不过我建议你还是操作一遍)
打开 cmd,或者用 as 的 Terminal ,这里用cmd演示,去到你的工程路径下,生成我们需要的 .h 文件 :
首先,我们需要设置 src 的根路径 ,如果不先设置根路径,一般会提示找不到类,用 set classpath 的命令,指向你的 java 文件:
然后,再使用 javah 去生成 .h 文件,即上面的 JniUtils:
就可以看到生成了 .h 文件,如下图:
接着,我们新建一个 jni 的文件夹:
把 .h 文件复制过去,然后复制多一份 .h 文件,后缀名改为.cpp ,如下:
#ifdef __cplusplus #endif #include <jni.h> extern "C" JNIEXPORT jstring JNICALL Java_com_zhengsr_jnidemo_JniUtils_getName (JNIEnv *env, jobject obj) { return env->NewStringUTF("这是个 jni 测试"); }
make module 一下,会发现,已经生成了 so 库:
最后再 MainActivity 中调用即可看到效果。
1.1、配置快捷方式
如果每次都这样,想想都觉得崩溃,这个时候,我们就可以配置快捷方式,这样就不用每次都开终端去输入,怎么配置呢?去到 Setting 选择 external tools ,新建一个 ,命名为 javah,(忽略我配置的 ndk_build,后面会用到):
配置以下参数:
program 为要执行的命令
parameters ,先设置路径,然后就是把命令敲一遍,注意是 /src/main/jni ,如果你的路径不一样,记得修改
working directory 是 .h 的生成路径
然后在你的 jni 类中,按住右键:
之后会弹出一个弹窗,可以自己输入 .h 的名字 (ps:先把以前的去掉):
效果如下:
接下来的步骤,就跟上面的差不多了,这里就不赘述了。
1.2、编写自己的 mk
上面已经说过,我们并没有 mk 的文件,这是因为 as 用了自身的mk,如果我们需要引入第三方的so或者.a,或者需要特殊配置时,就需要编写自己的 mk 文件了。关于 mk 的学习,可以参考这篇文章 (写得还不错),这里就不多说了:
http://blog.csdn.net/mynameishuangshuai/article/details/52577228
回到 build.gradle ,先把上面的 ndk 的属性去掉,然后添加:
在 jni 路径,添加 Android.mk 和 application.mk :
首先,先编写 Android.mk :
#设置路径 LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := jniutils LOCAL_SRC_FILES := jniutils.cpp include $(BUILD_SHARED_LIBRARY)
可以看到,我们把 jni 的 so 的名字改成了 jniutils,用于区别,记得改 JniUtils 中 loadLibrary 的名字,不然报错了,别怪我没提醒;
Application.mk 则如下:
APP_ABI:=all
指定生成所有平台下的 so。
由于我们使用了 mk 编译了,as 并不知道,我们要像刚才配置 javah 那样,配置一下 ndk-build ,配置信息如下:
参数已经解释过了,然后在 jni 的文件夹上右键,编译一下:
可以看到,生成的 so 包如下:
这样,我们就完成了我们的编译了,run 一下,就可以看到你想要的结果了。
1.3、在 build.gradle 中配置编译
从上面中,我们可以看到,如果改动了 .cpp 的方法,每次都要 ndk-build 一下,其实是很烦的;所以我们可以在 build.gradle 中,添加任务,在每次 run 的时候,自动编译。
build 应该这样配置:
完整 build.gradle 文件如下:
apply plugin: 'com.android.application' android { compileSdkVersion 26 buildToolsVersion "26.0.2" defaultConfig { applicationId "com.zhengsr.jnidemo" minSdkVersion 19 targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } sourceSets { main{ jni.srcDirs=[]; //禁用as自动生成mk jniLibs.srcDirs 'src/main/jniLibs' //这里设置 so 生成的位置 } } //设置编译任务,编译ndkBuild tasks.withType(JavaCompile) { compileTask -> compileTask.dependsOn 'ndkBuild' } } task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') { //应该都看得明白,就不解释了 commandLine "C:\\Users\\Administrator\\AppData\\Local\\Android\\Sdk\\ndk-bundle\\ndk-build.cmd", 'NDK_PROJECT_PATH=build/intermediates/ndk', 'NDK_LIBS_OUT=src/main/jniLibs', 'APP_BUILD_SCRIPT=src/main/jni/Android.mk', 'NDK_APPLICATION_MK=src/main/jni/Application.mk' } ....
接下来,我们在 jniutils.cpp 中,把返回的字符串改一下:
直接run,可以看到效果:
1.4、引入第三方 so,.a 包
很多时候,像一些比较涉及加密或者核心代码,都是用 so 库来实现,java 只要编写对应的 jni 即可,这里就涉及到引入第三方包的问题,怎么写呢?首先,我们需要有个第三方的 so 库,这里我从网上下载了一个,下载地址在 github 的demo 中;目录如下:
在引入第三方 so 库的时候,需要特别注意的是,这个 so 你要选择好版本,如果你的 so 是32的,而你在 appliaction.mk 的API版本中,选择了 all 或者 arm64-v8a等,那么编译肯定是报错的;
一般手机是 armeabi ,模拟器是 x86 ,机顶盒等板子是 arm64-v8a 的, 我的模拟器刚好是 x86_64 的,所以,这里引入的 so 库是 x86_64 下的,导入之后,目录如下:
重新编写 mk 文件:
LOCAL_PATH := $(call my-dir) #引入第三方 so include $(CLEAR_VARS) LOCAL_MODULE := vvw #这里的so名字叫做 vvw,规则是lib 与 so 之间的名字,在加载时使用 vvw,如果是 # libvvw1.0.so,则在 loadlibaray 用 "vvw1.0",module 名字只是给下面加载的 LOCAL_SRC_FILES := libvvw.so LOCAL_EXPORT_C_INCLUDES := include include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := jniutils LOCAL_SRC_FILES := jniutils.cpp LOCAL_LDLIBS :=-llog #引入第三方编译模块 LOCAL_SHARED_LIBRARIES := \ vvw include $(BUILD_SHARED_LIBRARY)
与前面相比,多了一个第三方模块的引入。接着,我们要指定 application.mk 的 API:
#模拟器是 x86_64 的 APP_ABI := x86_64
如果导入的工程报错,可以试着 APP_ABI 为 x86 ,替换相应的 so 。
接着,我们在 java 类这里,添加一个 调用 so 方法的 java 方法 getIntValue :
public class JniUtils { static { System.loadLibrary("jniutils"); System.loadLibrary("vvw"); } public static native String getName(); public static native int getIntValue(int a,int b); }
JniUtils.cpp 的代码如下:
#include <jni.h> #include <string> #include "include/vvwUtils.h" extern "C" jstring Java_com_zhengsr_jnidemo_JniUtils_getName( JNIEnv* env, jobject /* this */) { return env->NewStringUTF("获取两数字之和:"); } extern "C" jint Java_com_zhengsr_jnidemo_getIntValue( JNIEnv* env, jobject obj,jint a,jint b) { # addMethod 为 libvvw.so 的方法 return addMethod(a,b); }
修改一下 MainActivity.java
效果如下;
2、使用 cmake 的方式
上面的 demo 中,写 c/c++ 的时候,并没有任何提示,这真的是让人崩溃啊,写了都不知道写对了没有。所以,在 as 2.2.2 之后,as 就支持用 cmake 的方式去编写 jni 了,而使用 camke,除了 c/c++ 有提示之外,在 jni 的配置上,也更加的人性化,如果是新建项目,我是推荐你用 camke 的构建方式去编写。官方中文文档如下
https://developer.android.google.cn/studio/projects/add-native-code.html
首先,在新建工程的时候,勾选上 c++ support ( 3.0 往下拉才有)
一路 next ,然后有两个提示框:
这两个也勾选上,解释如下:
Exceptions Support:如果您希望启用对 C++ 异常处理的支持,请选中此复选框。如果启用此复选框,Android Studio 会将 -fexceptions 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。
Runtime Type Information Support:如果您希望支持 RTTI,请选中此复选框。如果启用此复选框,Android Studio 会将 -frtti 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。
工程已经给了我们一个 jni 的例子,而它的编译方式就是通过 CMakeLists.txt 来构建的。
下面是对 CMakeLists.txt 的解释,由于篇幅,这里会删掉一些注释:
cmake_minimum_required(VERSION 3.4.1) #这里会把 native-lib.cpp 转换成共享库,并命名为 native-lib add_library( # 库的名字 native-lib # 设置成共享库 SHARED # 库的原文件 src/main/cpp/native-lib.cpp ) #如果需要使用第三方库,则可以使用 find_library 来找到,比如这里的 log 这个库 find_library( # so库的变量路径名字,在关联的时候是使用 log-lib #你需要关联的so名字 log ) #因为使用了第三方库,所以,这里我们通过 link 这这个库添加进来 target_link_libraries( # 关联的so的路径变量名 native-lib #把上面的 log 中的关联的变量名 log-lib 添加进来即可 ${log-lib} )
如果要添加库,则使用 add_library,括号以空格区分,如果要使用第三方库,比如打印的 log 这个库,就通过 find_library 的方式添加,最后通过 target_link_libraries 把源文件的库,和第三方的库变量名引进来,注意第三方库是个路径变量名,所以 ${}的方式引用。
相较传统配置,如果对 mk 不熟悉的小伙伴,估计会很喜欢 cmake 的方式.
2.1 用 cmake 写 jni
按照上面的方式,新建 JniUtils.java 这个类:public class JniUtils { static { System.loadLibrary("jniutils"); } public static native String getName(); }
然后编写,jniutils.cpp,你会惊喜地发现,竟然有提示!!
#include <jni.h> #include <string> extern "C" jstring Java_com_zhengsr_jnidemo_camke_JniUtils_getName( JNIEnv* env, jobject /* this */) { std::string hello = "这是使用 camke 的编译方式啦"; return env->NewStringUTF(hello.c_str()); }
接下来就是 用 add_library 的方式,我们把 jniutils 加进来:
同步一下即可,修改一下 mainactivity,运行,效果如下:
可以看到,使用 cmake 的方式,除了有代码提示,在添加类上,简直不能太方便了。
2.2、引入第三方 so 库
官方推荐,每次库变动之前,先 clean project 一下,所以,先clean 一下,免得出现找不到 so 的情况;接着,我们添加一下第三方so,还是上面的 libvvw.so ,目录如下:
接着,我们需要制定一下 ndk 编译时的 类型,不然会增加一个 mips 的类型,这个是编不过的。
接着,则是配置最重要的 CMakeLists.txt 了,具体如下:
cmake_minimum_required(VERSION 3.4.1) 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 ) #导入第三方so包,并声明为 IMPORTED 属性,指明只是想把 so 导入到项目中 add_library( vvw SHARED IMPORTED ) #指明 so 库的路径,CMAKE_SOURCE_DIR 表示 CMakeLists.txt 的路径 set_target_properties( vvw PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libvvw.so ) #指明头文件路径,不然会提示找不到 so 的方法 include_directories(scr/main/cpp/include/ ) add_library(jniutils SHARED src/main/cpp/jniutils.cpp) target_link_libraries( # Specifies the target library. jniutils #关联第三方 so vvw ${log-lib} )
注释已经写得很清楚了,关键是要写对 so 的路径,不然会提示 missing and no rules to make 等错误;
jniutils.cpp 的代码如下:
#include <jni.h> #include <string> #include "include/vvwUtils.h" extern "C" jstring Java_com_zhengsr_jnidemo_1camke_JniUtils_getName( JNIEnv* env, jobject /* this */) { std::string hello = "这是使用 camke 的编译方式啦,还获取到两数之和啦: "; return env->NewStringUTF(hello.c_str()); } extern "C" jint Java_com_zhengsr_jnidemo_1camke_JniUtils_getIntValue( JNIEnv* env, jobject obj,jint a,jint b) { return addMethod(a,b); }
效果如下:
3、总结
不管是 ndk-build 传统的方式,还是 cmake 的方式,都有一定的可取之处,当然,在我看来, cmake 无论在学习成本还是代码编写提示上都要优于 ndk-build。如果是新建项目,我建议还是用 cmake 的方式,毕竟只 c/c++ 有提示这一点,我相信你也拒绝不了的。
当然,实际项目上,还有动态加载 so 的方法,这里就不深入了,这里就当做个 入门介绍吧。
相关文章推荐
- NDK-JNI实战教程(一) 在Android Studio运行第一个NDK程序
- JNI实例教程,附详细步骤
- NDK-JNI实战教程(一) 在Android Studio运行第一个NDK程序
- 测试框架 Mocha 实例教程(转载:来自阮一峰的一篇文章)
- 转一篇比较好的NDK编程实例
- ndk配置教程和导入jni的include(可查看c源码)
- NDK-JNI实战教程(二) JNI官方中文资料
- NDK-JNI实战教程(四)再谈新工具及NDK开发调试
- OSG for Android新手教程系列(四)——JNI与NDK的使用
- Android-本地方法C调用Java中的方法/NDK-JNI开发实例(六)
- NDK配置+第一个JNI实例+ndk-build常见问题
- NDK-JNI实战教程(二) JNI官方中文资料
- Android Studio 定制快速生成Jni 头文件工具 Ndk教程
- android Jni NDK开发环境搭建及其简单实例的编写
- android之一篇史上最适合最全面的JNI入门教程
- NDK-JNI实战教程(三) 从比Hello World稍复杂点儿的NDK例子说说模板
- Android Studio NDK 入门教程--JNI签名验证防止恶意调用
- Android-调用本地方法实现将C进程分支出来即生成系统进程/NDK-JNI开发实例(九)
- NDK-JNI实战教程(三) 从比Hello World稍复杂点儿的NDK例子说说模板