您的位置:首页 > 其它

这是一篇让你少走弯路的 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++ 编写体验

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 的方法,这里就不深入了,这里就当做个 入门介绍吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  jni cmake ndk