上篇文章讲解了有关JNI知识,如果还未了解 JNI,详情请看Android JNI开发。那么到底如何生成一个“so”文件?带着这个疑问,开始我们的文章。
一、概述。
“so”文件是使用C/C++编写生成的,在Android 平台上快速编译、打包该文件,那么就要使用NDK了,当然不使用NDK,也可以生成“so”文件,读者如果感兴趣,可以自行查找文档。
NDK全称为native development kit本地语言(C&C++)开发包。简单来说利用NDK,可以开发纯C&C++的代码,然后编译成库,让Java程序调用。NDK开发的可以称之为底层开发或者jni(java native interface)层开发。
NDK是一系列工具的集合,NDK提供了一系列的工具,可以帮助开发者进行c/c++的开发,并能自动将.so打包成apk。NDK集成了交叉编译器,并提供了相应的mk文件可以做到隔离CPU,平台,ABI等差异,只需修改mk文件即可。开发人员只需要简单修改mk文件,就可以创建出“.so”文件。NDK还提供了一份稳定的功能有限的API头文件声明。
PS:NDK和JNI的区别是什么呢?
简单来说,NDK是工具,使用NDK可以更方便、快捷的开发JNI,NDK开发是基于JNI的。而JNI,是Java提出的协议,从Java1.1开始,JNI就已经是Java平台的一部分,它允许Java代码和其他语言写的代码进行交互,JNI开发便是基于此开发。
使用NDK开发JNI的步骤如下;
1.JNI接口的设计;
2.使用C/C++实现本地方法;
3.使用NDK生成JNI动态链接库.so文件;
4.将动态链接库复制到Java/Android工程,调用,运行即可。
二、NDK配置。
1.下载NDK。
2.配置环境变量。(配置环境变量,是为了方便使用命令ndk-build脚本进行NDK编译。)

例如,Windows系统配置如下:
环境变量 PATH 下追加 :F:\android-ndk-r13b-windows-x86_64\android-ndk-r13b
配置完成后。打开cmd,输入ndk-build,出现如下如所示,则配置成功。
3.配置NDK路径。
我使用的是Android Studio。
点菜单栏的File->ProjectStructure…->在打开的窗口中左侧选中SDKLocation->在右侧Android NDK Location中填入NDK目录所在路径,如下图所示:
三、生成so库。
1.新建一个Module。
2.新建CalculateUtils类定义一个native方法。例如,
public class CalculateUtils {
static {
System.loadLibrary("calculate_jni");
}
public native String getStringFromNative(String str);//本地方法
}
3.生成jni目录及对应的头文件。
打开Terminal,cd到工程的“src/main/java”目录;
(1).输入指令
javah -jni cn.xinxing.jnitest.CalculateUtils
此时,刷新 Module,便会出现一个名为“cn_xinxing_jnitest_CalculateUtils.h”的文件,
在“src/main”下,新建一个jni目录,将生成的“cn_xinxing_jnitest_CalculateUtils.h”的文件复制到该目录中。还有另一种更加简单的实现方式。
(2).在“src/main/java”路径下,输入
javah -d ../jni 你的包名.引用本地方法的类的名称
javah意思是生成一个.h头文件,-d ../jni的意思是生成一个名字叫做jni的文件夹(directory),位置是在当前目录(src/main/java)的上一级目录(即src/main目录)。
例如:
javah -d ../jni cn.xinxing.jnitest.CalculateUtils
这种方式同样也生成jni目录及对应的头文件,并且更加简单,值得推荐!
4.实现本地方法。
在jni目录下新建一个 c或者cpp文件。来实现头文件里面声明的方法。例如“cn_xinxing_jnitest_CalculateUtils”,具体实现如下,
#include <cn_xinxing_jnitest_CalculateUtils.h>
JNIEXPORT jstring JNICALL Java_cn_xinxing_jnitest_CalculateUtils_getStringFromNative
(JNIEnv * env, jobject obj, jstring str){
return str;
}
5.生成“so”文件。
生成“so”文件有两种方式,一种是直接使用 ndk-build 命令,另外一种是使用Android Studio的gradle来自动编译生成。下面分别用这两种方式实现。
(1).使用gradle脚本。
需要配置build.gradle文件。
这里的build.gradle是指该Module模块下的build.gradle,不是整个工程的build.gradle文件。在模块的build.gradle的defaultConfig下加入以下配置:
...
ndk {
moduleName "calculate_jni"
abiFilters "armeabi", "armeabi-v7a", "x86"
}
点击同步按钮,如果没有报错,此时 点击Build->Make Module ‘jnitest’进行编译,生成.so库文件,如果没有报错,那么最终可以在路径:’jnitest‘->build->intermediates->ndk->debug->lib下:
如果在编译时可能报错,例如
那么需要在gradle.properties文件末尾添加android.useDeprecatedNdk=true就ok了!
其他错误,请根据提示,解决就行!
(2).使用 ndk-build 命令。
使用ndk-build 生成“so”文件,我们还需要在“jni”目录中创建两个文件,分别是Android.mk和Application.mk。下面分别看看他们的具体实现!
Application.mk,这个文件用来配置编译平台相关内容,我们最常用的估计只是APP_ABI字段,它用来指定我们需要基于哪些CPU架构的.so文件,当然你可以配置多个平台。下面指定了两个平台。
APP_ABI := armeabi armeabi-v7a
Android.mk,文件用来指定源码编译的配置信息,例如工作目录,编译模块的名称,参与编译的文件等。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := calculate_jni
LOCAL_SRC_FILES := cn_xinxing_jnitest_CalculateUtils.cpp
include $(BUILD_SHARED_LIBRARY)
LOCAL_PATH:设置工作目录,而my-dir则会返回Android.mk文件所在的目录。
CLEAR——VARS:清除几乎所有以LOCAL开头的变量(不包括LOCAL_PATH)。
LOCAL_MODULE:指定生成动态库名。
LOCAL_SRC_FILES:用来指定生成动态库的源文件 。
BUILD_SHARED_LIBRARY:作用是指定生成的静态库或者共享库在运行时依赖的共享库模块列表。
按照上面的步骤,创建并输入相应的内容后,我们就可以生成“so”文件了。 打开Terminal,cd到工程的“src/main/jni”目录,输入“ndk-build”命令后,回车,如果你看到下面的截图,那就编译成功了!
然后在Module目录结构中(刷新或者点击目录),就可以看到新多了“libs”目录,
可以看到生成了两个平台的“so”文件。
5.验证“so”文件中的方法。
创建一个Module,将上面生成的所有平台中的“so”文件复制到该Module的src/main目录下的jniLibs目录,如果没有该目录,则手动传建一个,然后将CalculateUtils类复制到该Module中,一定要记得类名和包名必须和原CalculateUtils类(生成“so”文件的CalculateUtils类)保持一致。否则可能会报错“java.lang.UnsatisfiedLinkError: Native method not found:...”。
下面我们就可以调用jni方法了。在Activity中调用jni方法,
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CalculateUtils calculateUtils=new CalculateUtils();
Log.e(TAG, calculateUtils.getStringFromNative("zhangsan------------>"));
}
}
运行后截图,
打印出我们传递的字符串。说明生成“so”文件以及实现JNI成功!
ps:在"so"文件中打印log。
(1).配置build.gradle文件。
...
ndk {
moduleName "calculate_jni"
ldLibs "log"
abiFilters "armeabi", "armeabi-v7a"
}
(2).修改cn_xinxing_jnitest_CalculateUtils.cpp文件,
#include <cn_xinxing_jnitest_CalculateUtils.h>
#include <android/log.h>
#define TAG "jni"
#define LOGV(...)__android_log_print(ANDROID_LOG_VERBOSE,TAG,__VA_ARGS__)
JNIEXPORT jstring JNICALL Java_cn_xinxing_jnitest_CalculateUtils_getStringFromNative
(JNIEnv * env, jobject obj, jstring str){
LOGV("log from native");
return str;
}
编译生成“so”文件。然后调用该“so”文件,截图如下:
四、总结。
1.在配置build.gradle时,如果我们的jni源码没有在‘jni’目录中,编译的话会报错,此时,我们需要在build.gradle文件中指定jni源码的路径,如下配置即可,
android {
//
//...
sourceSets {
main {
//你的源码目录
jniLibs.srcDirs 'src/main/jnis'
}
}
}
2.为何生成一个“libxxx.so”文件呢?
可能有的人会问,为何定义的“so”文件与生成的“so”文件名不一样呢?一个是“calculate_jni”,生成的“so”文件是“libcalculate_jni.so”。libxxx.so的命名规则,沿袭Linux传统,lib<something>.so是类库文件名称的格式,在Java的System.loadLibrary(" something ")方法中指定库名称时,不能包括 前缀—— lib,以及后缀——.so。所以在加载“so”文件时,需要注意文件名。