最近因为一个项目要在Android上使用OpenCV,所以补了一下Android NDK开发的知识,网上关于在Eclipse里进行NDK开发的文档比较多,但是很难找到在Android Studio或者IntelliJ里使用Gradle进行NDK开发的教程,这篇文章将会对两种情况都进行介绍。
这里我只是记录了一下基本的配置过程,并举了一个简单的小例子,并没有对其中的一些细节和原理进行介绍,毕竟我也是刚刚接触NDK。如果以后对NDK开发更加深入了解,我会再次进行补充。
基本环境配置
2、在Eclipse中安装Android Native Development Tools
在install new software中使用 Android Development Tools - https://dl-ssl.google.com/android/eclipse/,进行下载安装Android Native Development Tools。如下图:
3、设置NDK根目录:
Eclipse -> Window -> Preferences -> Android -> NDK,设置为NDK根目录。
代码编写
4、创建接口类:
首先新建JNI的接口类, 包含使用的静态方法。 位置: 项目->src->[package]->JniClient.java
public class JniClient {
static public native String sayName();
}
5、编译接口类:
进入项目文件夹, 生成JNI的头文件, 使用如下cmd命令:
javah -classpath bin/classes -d jni com.hellomyjni.JniClient
命令解析:
javah 生成头文件;
-classpath 使用类的位置(bin/classes), 都是.class文件;
-d jni 指定生成文件的存储位置,这里是在jni文件夹下;
[classname] 需要生成JNI的类(com.example.hellomyjni.JniClient), 包括[package]。
6、生成头文件:
按F5刷新项目, 项目会自动生成jni文件夹, 并包含一个头文件 “com_example_hellomyjni_JniClient.h”。文件内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_example_hellomyjni_JniClient */
#ifndef _Included_com_example_hellomyjni_JniClient
#define _Included_com_example_hellomyjni_JniClient
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_hellomyjni_JniClient
* Method: sayName
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_hellomyjni_JniClient_sayName
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
注意:
此时头文件可能会报错,主要是JNICALL这类符号不能被解析(type JNICALL can’t be resolved)
解决方案:
Project Properties -> C/C++ General -> Path and Symbols
选择include标签,Add -> $Android_NDK_HOME/platforms/android-19/arch-arm/usr/include
(其中”android-19”替换为当前使用的android api版本)
选中All languages,最后Apply -> OK。
7、给Android项目添加NativeSupport:
右键点击项目在Android Tools里面点击”Add NativeSupport”, 就会在jni文件夹下自动生成: HelloMyJni.cpp和Android.mk.
这一步可能会报错:
Unable to launch cygpath. Is Cygwin on the path?] java.io.IOException: Cannot run program “cygpath”: CreateProcess error=2, The system cannot find the file specified
解决方法:
A、右键点击项目->Properties->C/C++ Build, 修改Build Command为 “${NDK_ROOT}/ndk-build.cmd”,其中NDK_ROOT为NDK根目录。
B、右键点击项目->Properties->C/C++ Build ->Environment, 添加变量名为NDK_ROOT的环境变量。
8、修改生成的代码:
Android.mk不需要修改:LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := HelloMyJni
LOCAL_SRC_FILES := HelloMyJni.cpp
include $(BUILD_SHARED_LIBRARY)
HelloMyJni.cpp需要修改,添加头文件并编写实现函数。
A、添加头文件, 即JNI生成的头文件:
#include "com_example_hellomyjni_JniClient.h";
此时头文件的报错消失.
B、复制JNIEXPORT函数, 并填写参数名称。 完成函数编写,可以调用其他C++程序。
#include
#include "com_example_hellomyjni_JniClient.h"
JNIEXPORT jstring JNICALL Java_com_example_hellomyjni_JniClient_sayName
(JNIEnv *env, jclass) {
return env->NewStringUTF("I'm Spike!");
}
9、 调用JniClient:
在需要调用JniClient的类中添加:
static {
System.loadLibrary("HelloMyJni");
}
需要调用时使用如下代码就可以了:
JniClient.sayName();
调试
10、 jni代码调试:
最后、我们来看一下怎样来调试我们的NDK C/C++代码,打开jni/HelloMyJni.cpp文件,找地方打上断点,debug android Native Application,截图如下:
发现没有Debug起来,控制台的错误如下:[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] Android
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] NDK:
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] WARNING:
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] APP_PLATFORM
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] android-9
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] is
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] larger
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] than
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] android:minSdkVersion
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] 3
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] in
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni] ./AndroidManifest.xml
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni]
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni]
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni]
[2015-01-28 21:06:01 - HelloJni] Unknown Application ABI:
[2015-01-28 21:06:01 - HelloJni]
all
[2015-01-28 21:06:01 - HelloJni] Unable to detect application ABI's
这是由于android:minSdkVersion和项目的编译版本不一致,造成的,我们改成一致,在Application.mk文件中加入APP_PLATFORM := android-8 // android-8和android:minSdkVersion一致
再次运行,发现又报了一个错误:[2015-01-28 21:13:32 - HelloJni] gdbserver output:
[2015-01-28 21:13:32 - HelloJni] run-as: exec failed for lib/gdbserver Error:No such file or directory
[2015-01-28 21:13:32 - HelloJni] Verify if the application was built with NDK_DEBUG=1
这个需要我们再次修改一下C/C++ Build command:Properties–>C/C++ Build,在build command中输入:${NDK_ROOT}/ndk-build.cmd NDK_DEBUG=1,主要是增加了NDK_DEBUG=1,截图如下:
再次Debug我们的项目,发现没有进我们打的断点,但是我们发现了Android Native Application gdb运行起来了,如下:
但是控制台上打印出如上图的错误代码,这个错误信息对我们的影响不大,退出程序,再次进入,我们发现,进入断点了,可以调试了。
二、Gradle(IntelliJ/Android Studio)
基本构建
1、如上一节,创建接口类JniClient;
2、在src/main路径下创建jni文件夹;
3、使用External Tools代替手动编写javah命令生成头文件:
A、如下图找到External Tools,点击+添加javah工具;
B、按下图进行javah工具的编辑;
编辑完成,点击OK;
4、右击JniClient类,选择Android Tools -> javah,会自动在jni目录中生成com_example_hellomyjni_JniClient.h;
5、新建HelloMyJni.cpp文件,并添加和上一节相同的内容;
6、在build.gradle文件中配置ndk:
android {
defaultConfig {
...
ndk{
moduleName "hello"
}
}
}
7、在local.properties文件中配置ndk.dir:sdk.dir=D:\android\sdk
ndk.dir=D:\android\ndk
8、经过以上配置,就可以点击”Run”,在手机上看到效果了。
原理介绍
那么实际上发生了什么?
gradle默认将src/main/jni作为本地代码目录,当gradle构建app时,它会用一个自动生成的Android.mk(存在于app/build/intermediates/ndk/debug/Android.mk)自动运行ndk-build.cmd来编译本地代码,生成libs和obj文件夹,存放于app/build/intermediates/ndk/debug/路径。然后gradle会将libs文件夹打包到最终的apk中。
进阶
更近一步地,如果想和Eclipse中那样用自己写的Android.mk来控制编译,要怎么做呢?
1、在jni中新建Android.mk和Application.mk,并编写和Eclipse中开发相同的内容;
2、在build.gradle中添加以下代码:
android {
...
sourceSets.main.jni.srcDirs = []
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
def ndkDir = project.plugins.findPlugin('com.android.application').sdkHandler.getNdkFolder()
commandLine "$ndkDir/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'
}
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn ndkBuild
}
}
A、sourceSets.main.jni.srcDirs = [] 这句话禁止gradle去默认编译本地代码;
B、’NDK_LIBS_OUT=src/main/jniLibs’, 这句话将生成的so文件输出到jniLibs中,而jniLibs是gradle默认的本地类库文件夹,将会打包到apk;
这样就大工告成了!