在我们的c++代码写好后,我们往往会需要打包成so文件给别人使用,那就要求我们知道如何把c++代码打包成so文件,这里介绍NDK打包的方式,想知道CMake的打包方式请前往我的下一篇。
我这里分五步走,咱一步步往下看。
-
1.创建android工程
-
2.关联NDK
-
3.声明native方法,并创建头文件和C++文件
-
4.创建Android.mk文件和Application.mk文件
-
5.打包so文件
-
1.创建android工程


不出意外,项目一会就建立完成。
-
2.关联NDK


选择好了之后,点击OK即可。
这里说一下,然后如果没有配置NDK环境变量的,需要给系统配置一下环境变量,我的是mac直接运行命令行:
vim ./.bash_profile
打开后,添加
export NDK_HOME=填写你的NDK安装路径
export PATH=$PATH:$NDK_HOME
-
3.声明native方法,并创建头文件和C++文件
首先声明一个方法,如下:
public class JNIUtils {
/**
* java调C中的方法都需要用native声明且方法名必须和c的方法名一样
* **/
public native static String stringFromJNI();
}
这里我们声明一个native方法,这就是我们待会在C++中会产生的方法。
我们打开terminal准备运行命令行,可以先输入pwd,可以查看当前命令行所在文件夹的位置,然后进入java文件夹下,具体命令如下:

然后执行命令行:
javac com/simple/ndktest/JNIUtils.java
注意这里的com/simple/ndktest是对应我的包名com.simple.ndktest,如果你们和我的不一样,需要替换,然后点击enter运行,会产生一个JNIUtils.class,可能不会刷新出来,我们点一下我们的包名文件夹就可以了,然后我们再执行命令行:
javah com.simple.ndktest.JNIUtils
然后我们就利用刚刚生成的class文件生成我们需要的C++头文件,名称为:com_simple_ndktest_JNIUtils.h,刚才的JNIUtils.class文件就可以删除了
内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_simple_ndktest_JNIUtils */
#ifndef _Included_com_simple_ndktest_JNIUtils
#define _Included_com_simple_ndktest_JNIUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_simple_ndktest_JNIUtils
* Method: stringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_simple_ndktest_JNIUtils_stringFromJNI
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
有了这个头文件我们就可以去建立我们的C++文件了,我们新建一个文件为:com_simple_ndktest_JNIUtils.cpp,操作如下:


这样就建立成功了,但是里面是空空如也的,我们需要导入头文件,并且复制头文件中的方法,最终效果如下:
#include "com_simple_ndktest_JNIUtils.h"
JNIEXPORT jstring JNICALL Java_com_simple_ndktest_JNIUtils_stringFromJNI
(JNIEnv *env, jclass){
return env->NewStringUTF("Hello from C++");
}
我这里返回了一个字符串"Hello from C++"
到此,我们的native方法,C++及其头文件就建立好了,开始下一步。
-
4.创建Android.mk文件和Application.mk文件
Android.mk文件是一个负责向NDK构建系统描述NDK项目的GNU Makefile片段,是每一个NDK项目的必备组件,如下。
Android.mk
# Android.mk必须以LOCAL_PATH开头,注释#除外
# 设置工作目录,而my-dir则会返回Android.mk文件所在的目录
LOCAL_PATH := $(call my-dir)
# 借助CLEAR_VARS变量清除除LOCAL_PATH外的所有LOCAL_<name>变量
include $(CLEAR_VARS)
# 设置模块的名称,即编译出来.so文件名
# 注,要和上述步骤中build.gradle中NDK节点设置的名字相同
LOCAL_MODULE := native-lib
# 指定参与模块编译的C/C++源文件列表,多文件用"\"隔开
LOCAL_SRC_FILES := com_simple_ndktest_JNIUtils.cpp
# 必须在文件结尾定义编译类型,指定生成的静态库或者共享库在运行时依赖的共享库模块列表。
# BUILD_SHARED_LIBRARY 共享库,供java或者其他共享库调用
# BUILD_STATIC_LIBRARY 静态库,供共享库调用,不能直接被java调用
include $(BUILD_SHARED_LIBRARY)
需要复制的同学只需要更改com_simple_ndktest_JNIUtils.cpp即可,其他不需要更改。
Application.mk
# 最常用的APP_ABI字段:指定需要基于哪些CPU平台的.so文件
# 常见的平台有armeabi x86 mips,其中移动设备主要是armeabi平台
# 默认情况下,Android平台会生成所有平台的.so文件,即同APP_ABI := armeabi x86 mips
# 指定CPU平台类型后,就只会生成该平台的.so文件,即上述语句只会生成armeabi平台的.so文件
# APP_ABI := armeabi armeabi-v7a mips x86
APP_ABI := all
APP_PLATFORM := android-23
好了,俩个文件创建完毕。
细心的同学可能注意到了Android.mk中有一句:
LOCAL_MODULE := native-lib
这个其实就是我们最终要生成的so文件的名称,那么我们一开始的JNIUtils如果想使用native方法,就必须要加载这个so库,更改后如下:
public class JNIUtils {
/**
* 加载生成的so库文件
* **/
static {
System.loadLibrary("native-lib");
}
/**
* java调C中的方法都需要用native声明且方法名必须和c的方法名一样
* **/
public native static String stringFromJNI();
}
需要加载的so文件和我们即将生成的so文件名称需要保持一致。
然后我们需要在app下的build.gradle中也声明一下,效果如下:
defaultConfig {
...
ndk{
//模块名称,即编译的.so文件名
moduleName "native-lib"
//"log"表示加入Android的调试日志,只要再导入#include <android/log.h>
//就可以使用__android_log_print方法打印日志到logcat中
ldLibs "log"
}
}
至此我们共生成了五个文件,除了JNIUtils,其余四个我们在他们放在一起,我这里是,在app下建了一个jni的包,然后全部放在里面,效果如下:

-
5.打包so文件
仍然进入命令行,这里说一下,cd xxx 是进入到xxx文件夹下,cd ./ 是退出所在文件夹,pwd是展示当前所在位置,ls是展示当前文件下所有文件名称,通过调整,我们进入到我们新建的jni文件夹下,执行:
ndk-build
执行后效果如下:

如不出意外,我们的so文件就可以生成了,在app的lib下,分为多个cpu架构,如图:

然后我们在我们的MainActivity中使用,如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("JNIUtils", JNIUtils.stringFromJNI());
}
}
等等!我们的gradle还需要配置一下,在app的build.gradle的android下直接添加
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
这样其实就是指定我们的so文件路径,我们才可以正常使用,有的朋友可能问了,为什么有时候需要配置,有时候不需要呢,这是因为我们把so文件放在了lib下,这个调用so文件的用法来自于Eclipse,所以android studio保留了,但是android studio原本也有调用so文件的方式,路径是main文件下jniLibs,如果没有,可以新建,所以如果我们把我们的so文件放在我们main文件下jniLibs中,上面的这一配置就可以不用写了,因为默认就是调用这个地方。
最后我们运行app,可以发现界面上会显示文字:
到此,ndk打包so文件的方式介绍完毕。
网友评论