Android Studio下的JNI开发

前言

Android开发无法绕开JNI,因为通过JNI我们可以使用大量成熟的用C或者C++写的库,比如openssl等。这几天一直在研究JNI的开发过程,现记录如下。

开发环境:Android Studio 2.0 Preview 3b

1. 新建项目

新建项目,命名MyJniProject,选择Empty Activity。

2. 新建一个HelloWorld类

在HelloWorld类里新增一个native方法,命名为getJNIString,如下图所示。

然后Build整个工程,会自动生成class文件。先把目录结构从Android切为Project,看到class文件的目录如下图所示。

3. 生成.h文件

点击Android Studio底部的Terminal,会打开IDE内置的命令行工具。然后cd命令进入到debug目录下,然后输入命令:

set classpath=”debug目录的绝对路径”

比如在我的MAC上,绝对路径就是

/Users/lqynydyxf/Documents/MyJniProject/app/build/intermediates/classes/debug

所以我输入的就是:

1
set classpath=/Users/lqynydyxf/Documents/MyJniProject/app/build/intermediates/classes/debug

然后用javah -jni命令编译HelloWorld.class,生成.h文件。

1
javah -jni com.example.lqynydyxf.myjniproject.HelloWorld

执行上述操作后,在debug目录下,会出现一个文件名为
com_example_lqynydyxf_myjniproject_HelloWorld的.h文件。

.h文件内的代码其实就是对之前定义的getJNIString方法的声明,但是此时方法名会变成

Java_com_example_lqynydyxf_myjniproject_HelloWorld_getJNIString**。

.h文件的内容如下图所示:

4. 生成.c文件

进入到src目录下,新建一个名为jni的文件夹(New->Directory),然后把之前生成的.h文件剪切过来,放到jni目录下,同时在jni目录下新建一个.c文件,可随意命名,但是这里为了整齐,我将.c文件的文件名写的和.h文件一样。.c文件的内容是对.h文件中声明的方法进行具体实现。.c文件的内容如下所示:

1
2
3
4
5
#include "com_example_lqynydyxf_myjniproject_HelloWorld.h"
JNIEXPORT jstring JNICALL Java_com_example_lqynydyxf_myjniproject_HelloWorld_getJNIString
(JNIEnv *env, jobject object){
return (*env)->NewStringUTF(env, "Hello JNI!");
}

这里,我们直接返回了一个 Hello World! 字符串。

5. 生成Android.mk文件

此时,在jni目录下,我们已经拥有了一个.h和.c文件,然后我们需要再新建一个Android.mk文件,然后添加如下内容:

1
2
3
4
5
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := helloworld
LOCAL_SRC_FILES := com_example_lqynydyxf_myjniproject_HelloWorld.c
include $(BUILD_SHARED_LIBRARY)

添加后,结果如下图所示:

  • LOCAL_PATH: 这个变量用于给出当前文件的路径。 必须在 Android.mk 的开头定义,可以这样使用:LOCAL_PATH := $(call my-dir)。如当前目录下有个文件夹名称 src,则可以这样写 $(call src),那么就会得到 src 目录的完整路径。这个变量不会被$(CLEAR_VARS)清除,因此每个 Android.mk 只需要定义一次(即使在一个文件中定义了几个模块的情况下)。

my-dir是GNU Make函数宏,必须通过使用$(call ),返回当前Android.mk文件所在的目录的路径,相对于 NDK 编译系统的顶层。

  • LOCAL_MODULE: 这是模块的名字,它必须是唯一的,而且不能包含空格。 必须在包含任一的$(BUILD_XXXX)脚本之前定义它。模块的名字决定了生成文件的名字。

  • LOCAL_SRC_FILES: 这是要编译的源代码文件列表。 只要列出要传递给编译器的文件,因为编译系统自动计算依赖。

如果想要生成不同架构下的so文件,再新建一个Application.mk文件(默认是armeabi),添加想要支持的平台,比如:

Application.mk

1
APP_ABI := armeabi armeabi-v7a x86 mips

6. 其他配置

  • local.properties文件中指明ndk的目录:

  • 修改Module的build.gradle文件:在defaultConfig中加入如下代码:
1
2
3
4
ndk{
moduleName 'helloworld'
abiFilters 'armeabi', 'armeabi-v7a', 'x86'
}

这里的moduleName需要和Android.mk文件里的LOCAL_MODULE一致。

  • 在gradle.properties中加入以下代码:
1
android.useDeprecatedNdk=true

到这里,基本的配置结束,然后Build整个项目。Build通过后,就可以进行下一步,也是最重要的一步。

7. 生成so文件

打开Terminal,使用cd命令进入到jni目录下,执行ndk-build命令。

如图所示,代表执行成功,这时切换到Project目录结构下,我们可以发现,在libs文件夹下,成功生成了.so文件。

NDK编译系统会在moduleName前面加上lib,后面加上.so来作为so文件的文件名。

8. 调用so文件

回到HelloWorld类中,我们添加如下代码:

1
2
3
static {
System.loadLibrary("helloworld");
}

最后HelloWorld类的内部如下图所示:

然后修改MainActivity的onCreate代码如下:

1
2
3
4
5
6
7
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView textView = new TextView(this);
HelloWorld helloWorld = new HelloWorld();
textView.setText(helloWorld.getJNIString());
setContentView(textView);
}

结果如图所示:

9.运行

运行项目,模拟器界面如下:

这里输出的Hello World!就是我们通过JNI调用得到的。