blog.csdn.net/u011418943/
关于 JNI 的基础就不多说了, 这篇文章主要讲解如何在 AS 中用 ndk-build 和 用 cmake 去构建我们的 JNI 工程, 并总结他们的特点以及优缺点
本文代码链接: github.com/LillteZheng
通过这篇文章, 你讲学习到:
用 AS 构建自己的 JNI 工程
学会使用 mk 去加载自己的 so 文件
学会调用第三方 so 或 .a 的方法 (工程提供测试的 so)
学会使用 camke, 体验丝般顺滑的 C/C++ 编写体验
1ndk-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 的学习, 可以参考这篇文章 (写得还不错), 这里就不多说了:
blog.csdn.net/mynameishua
回到 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
- 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 的构建方式去编写
官方中文文档如下
developer.android.google.cn/studio/proj
首先, 在新建工程的时候, 勾选上 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 的方法, 这里就不深入了, 这里就当做个 入门介绍吧
来源: https://juejin.im/entry/5a9d4ad05188255569187646