本文主要是介绍这是一篇让你少走弯路的 JNI/NDK 实例教程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
作者: 夏至 欢迎转载,但保留这段申明
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++ 编写体验
- 3.5.1 as 之后的变化
一、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.cppinclude $(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 26buildToolsVersion "26.0.2"defaultConfig {applicationId "com.zhengsr.jnidemo"minSdkVersion 19targetSdkVersion 26versionCode 1versionName "1.0"testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}}sourceSets {main{jni.srcDirs=[]; //禁用as自动生成mkjniLibs.srcDirs 'src/main/jniLibs' //这里设置 so 生成的位置}}//设置编译任务,编译ndkBuildtasks.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 := \
vvwinclude $(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
效果如下;
二、使用 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 了 (as最新版的配置有所不同,参考2.3),具体如下:
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( vvwSHAREDIMPORTED )
#指明 so 库的路径,CMAKE_SOURCE_DIR 表示 CMakeLists.txt 的路径
set_target_properties( vvwPROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libvvw.so )#指明头文件路径,不然会提示找不到 so 的方法
include_directories(src/main/cpp/include/ )add_library(jniutils SHARED src/main/cpp/jniutils.cpp)target_link_libraries( # Specifies the target library.jniutils#关联第三方 sovvw${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);
}
效果如下:
2.3 3.5.1 之后的配置 (2020/06/08更新)
最新版的 CMakeLists.txt 放在了 cpp 的目录下,如果按照上面的路径去配置,肯定也会报 missing and no rules to make ,因为 so 的路径配置错了。
因此,我们把 jniLibs 放到 cpp 下,如:
后面,我们的 CMakeLists.txt 修改如下:
cmake_minimum_required(VERSION 3.4.1)# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.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).native-lib.cpp)# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.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)# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.#导入第三方so包,并声明为 IMPORTED 属性,指明只是想把 so 导入到项目中
add_library( vvwSHAREDIMPORTED )
#指明 so 库的路径,CMAKE_SOURCE_DIR 表示 CMakeLists.txt 的路径
set_target_properties( # Specifies the target library.vvw# Specifies the parameter you want to define.PROPERTIES IMPORTED_LOCATION# Provides the path to the library you want to import.${CMAKE_SOURCE_DIR}/jniLibs/${ANDROID_ABI}/libvvw.so )#指明头文件路径,不然会提示找不到 so 的方法
include_directories( ${CMAKE_SOURCE_DIR}/myInclude/ )target_link_libraries( # Specifies the target library.native-lib#关联自己的库vvw# Links the target library to the log library# included in the NDK.${log-lib})
三、总结
不管是 ndk-build 传统的方式,还是 cmake 的方式,都有一定的可取之处,当然,在我看来, cmake 无论在学习成本还是代码编写提示上都要优于 ndk-build。
如果是新建项目,我建议还是用 cmake 的方式,毕竟只 c/c++ 有提示这一点,我相信你也拒绝不了的。
当然,实际项目上,还有动态加载 so 的方法,这里就不深入了,这里就当做个 入门介绍吧。
这篇关于这是一篇让你少走弯路的 JNI/NDK 实例教程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!