在项目中添加第三方依赖的时候,经常会使用到它的so文件,可一直没有去想过so文件是怎么编译出来的,编译出来的so文件又是怎么供别的项目使用的,今天趁着闲余时间,准备查查资料好好研究一下这块。因为我现在使用的是android studio2.3.3,ndk-r16b,所以在编写过程中,可能会存在差异,不过应该问题不大。
在正式开始之前,如果你的电脑还没有配置NDK环境,那么请先去下载NDK吧,或者你也可以使用AS在Setting=>System Settings=>Android SDK=>SDK Tools中勾选NDK下载,下载之后解压文件,例如我将解压后的文件放在了D盘的ndk目录之下
D:\ndk\android-ndk-r16b
随后配置一下环境变量,我的电脑=>属性=>高级系统设置=>环境变量,在PATH中添加ndk路径
D:\ndk\android-ndk-r16b;
在我的File=>Project Structure=>SDK Location=>Android NDK Location,填上我们刚才的ndk路径,之后点击ok
D:\ndk\android-ndk-r16b
在项目中的local.properties文件中添加
ndk.dir=D\:\\ndk\\android-ndk-r16b
在gradle.properties该文件中添加过时ndk版本支持
Android.useDeprecatedNdk=true
这是我们编写的java类,并添加了native方法
public class MyJniUtils {
//获取一个最简单的helloWord字符串
public static native String getHelloWorld();
}
我们 Build=>Make Project,并找到MyJniUtils编译出来的class文件,本地所在目录如下:
我们View=>Tool Windows=>Terminal打开终端,并cd到上图目录结构中debug目录之下:
E:\MyJniTest>cd app/build/intermediates/classes/debug
使用javah命令生成MyJniUtils.class的JNI的头文件
javah zmj.myjnitest.MyJniUtils
上面两步的图例如下
你会发现在debug目录下生成了zmj_myjnitest_MyJniUtils.h头文件,我们将该头文件复制到当前项目下的jni目录,如果没有则新建,jni所在目录如下
在jni目录下编写一个.c文件实现该JNI头文件中的函数,c文件名字自己随意定义,本例中为MyJniTest.c,也可以使用cpp的实现文件,但两者在代码编写上有差异
#include <zmj_myjnitest_MyJniUtils.h>
JNIEXPORT jstring JNICALL Java_zmj_myjnitest_MyJniUtils_getHelloWorld (JNIEnv *env, jobject jObj){
return (*env)->NewStringUTF(env, "Hello World");
}
编写Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := myJniTest
LOCAL_SRC_FILES := MyJniTest.c
include $(BUILD_SHARED_LIBRARY)
编写Application.mk文件
APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
APP_ABI := all
APP_PLATFORM := android-14
jni目录结构图如下
利用ndk-build生成动态链接库,将终端cd 到jni目录之下,执行ndk-build指令,编译出该动态so库
编译完成之后,你会发现,libs目录下已经自动生成了.so文件,并在main目录下自动生成了obj文件夹
在build.gradle中,android{ }内添加
sourceSets {
main() {
jniLibs.srcDirs = ['src/main/libs']
jni.srcDirs = [] //屏蔽掉默认的jni编译生成过程
}
}
在我们的HomePageActivity类中加载刚刚编译好的so库
public class HomePageActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home_page);
Log.i("Tag",MyJniUtils.getHelloWorld());
}
static {
System.loadLibrary("myJniTest");
}
}
到这里我们就已经介绍完了生成so文件的过程,也知道了怎么去实现java层调用Native代码,运行下我们的项目,结果如下
新建一个新项目,将我们so文件放置到该项目的相应libs目录下,其实最好放在项目main目录下的jniLibs文件夹之下,这两者在build.gradle的内容稍有不同,前者需要指定jniLibs.srcDirs = [‘libs’]
在java目录下新建zmj.myjnitest包(zmj.myjnitest为我们编译so文件所在的项目包名),之后将我们的MyJniUtils类复制到zmj.myjnitest目录下(记得要跟so文件所在项目内的MyJniUtils类路径一致),目录结果图如下
在我们需要的地方调用
public class MyJniTestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_jni_test);
Log.i("Tag", MyJniUtils.getHelloWorld());
}
static {
System.loadLibrary("myJniTest");
}
}
在build.gradle的android{ }内添加
sourceSets {
main() {
jni.srcDirs = []
jniLibs.srcDirs = ['libs']//如果将libs文件下的一系列arm文件夹,放在项目main目录下的jniLibs文件夹里,则不需要
}
}
最后运行一下我们的程序,结果如下
到这里我们就已经介绍完了最简单的JNI的使用,其实JNI的调用方式主要有两种,一种是Java代码调用Native的代码,这也是最常见的,另一种则恰好相反,就是在Native层调用Java代码,这种方式如果以后有机会会另行介绍。还有在本文中没有对.c和.mk文件中的每一行进行含义说明,也没有介绍JNI和NDK的相关概念,我会在后面博客中详细解释,本文最重要的是把该项目运行起来。