本文主要介绍android中NDK的使用,用简单NDK Demo作为演示。
NDK环境的配置这里不在详细介绍,可以参考:
- Windows下配置
- Mac 下配置
在android studio中配置NDK环境比较简单,创建工程之后,使用快捷键(Ctrl + Alt + Shift + S)会跳出Project Structure对话框。点选SDK Location即可看到Android NDK location配置。可直接download即可,可以在官网下载NDK包,配置好路径就行。
本文运行环境
- android studio : 2.3.1
- NDK : android-ndk-r14b
主要内容
- 工程示例
- NDK使用的方式(gradle自动编译和android.mk编译)
- NDK使用时错误及解决方法
- NDK中使用C和C++中的区别
工程示例
创建工程前请确保NDK环境已配置成功。
- 创建Android工程,工程名称为NDKDemo(包名org.demo.ndk)。工程创建完成后,先编译运行,确保工程没有问题。
- 检查NDK配置情况。
- 检查Projec Sturcture(Ctrl + Alt + Shift + S),查看是否设置好了NDK。也可以打开local.properties文件,检查是否设置了ndk.dir路径。我的NDK在
D:\Android\android-ndk-r13b
所以设置为ndk.dir=D:\\Android\\android-ndk-r13b
。 - 打开gradle.properties文件,在最后一行添加。
android.useDeprecatedNdk=true
- 在org.demo.ndk包中新建类,命名为NavHelper,作为测试示例DNK。NDK使用非常简单,只要将函数使用native修饰即可。这里定义了一个helloWorld的测试函数。
- 使用android studio左下角命令行工具,先定位到上图的java目录,在执行java命令:
javah org.demo.ndk.NavHelper
(包名 + 类名)
至此,生成C头文件,即得到了在C/C++中需要实现的函数原型声明。 - 创建jni工作目录。这个目录可由android studio来生成。
在模块app上单击右键—>New—>Folder—>JNI Folder —> Finish,则在src/main/目录下,创建了jni文件夹。 - 将第4步生成的C头文件移动到jni文件夹,并创建hello.cpp(名字可任取)的C++源文件,作为helloWorld函数的实现。在hello.cpp文件中添加内容如下:
#include <string.h>
#include <jni.h>
#include "org_demo_ndk_NavHelper.h"
/**
*/
JNIEXPORT jstring JNICALL Java_org_demo_ndk_NavHelper_helloWorld(JNIEnv *env, jobject thiz){
return env->NewStringUTF("Hello NDK !");
}
在gradle中配置ndk(配置模块app中的gradle)。
在defaultConfig节点中放置ndk,设置moduleName(必须设置)作为NDK加载的名称和abiFilters(可选)输出指定三种abi体系结构下的so库。
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "org.demo.ndk"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk{
moduleName 'NDKDemoName'//作为NDK加载的名称,和System.loadLibrary("NDKDemoName");中名字一致
abiFilters "armeabi", "armeabi-v7a", "x86" //输出指定三种abi体系结构下的so库,可忽略
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
至此,NDK示例的准备工作已经完成,在android studio上方点击Build—>rebuild可在模块 app/build/intermediates/ndk/debug/lib
下看到编译生成的库文件。接下来,修改MainActivity和NavHelper来测试。
- NavHelper中由static加载库文件,可就是在NavHelper创建对象时,就加载库文件。
package org.demo.ndk;
/**
* Created by Dell on 2017/5/5.
*/
public class NavHelper {
static {
System.loadLibrary("NDKDemoName");//加载库文件,名称和app.gradle文件中设置的moduleName一直
}
public native String helloWorld();
}
- 在MainActivity中调用NavHelper中定义的helloWorld函数。
package org.demo.ndk;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_main);
TextView tv = new TextView(this);
NavHelper navHelper = new NavHelper();
tv.setText(navHelper.helloWorld());
setContentView(tv);
}
}
编译运行,结果如图:
NDK使用的2种方式(gradle自动编译和Android.mk编译)
在使用NDK的时候,经常看到其他工程中出现Android.mk、Application.mk这样的文件,像示例工程中生存的库文件中app/build/intermediates/ndk/
目录下也存在一个Android.mk的文件。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := NDKDemoName
LOCAL_LDFLAGS := -Wl,--build-id
LOCAL_SRC_FILES := \
D:\AndroidStudioProjects\NDK\NDKDemo\app\src\main\jni\hello.cpp \
LOCAL_C_INCLUDES += D:\AndroidStudioProjects\NDK\NDKDemo\app\src\main\jni
LOCAL_C_INCLUDES += D:\AndroidStudioProjects\NDK\NDKDemo\app\src\debug\jni
include $(BUILD_SHARED_LIBRARY)
那么这个文件的作用是什么。
大致可描述为:Android.mk文件用来告知NDK Build 系统关于Source的信息。告知编译系统,在编译时如何处理NDK相关文件及生成库文件放置等。
最简单的一个Android.mk例子:
LOCAL_PATH := $(call my-dir) #当前 Android.mk文件所在的目录的路径
include $(CLEAR_VARS) #负责清理LOCAL_xxx文件
LOCAL_MODULE := NDKDemoName
LOCAL_SRC_FILES := hello.cpp
include $(BUILD_SHARED_LIBRARY)
因此可以看出,Android.mk文件决定了NDK如何编译C/C++文件。
- gradle自动编译
我们的示例工程就是gradle自动编译完成的,可大致分以下步骤:
- 定义native函数,并由javah命令生成,C/C++函数头文件。
- 创建jni目录,编写C/C++实现。
- 在模块gradle中配置ndk节点,主要设置moduleName。
在gradle中由于许多的,参数都是默认的,所以配置起来比较方便。如果想要修改其中的部分参数可自行配制。比如在android节点下配置sourceSets:
```
sourceSets.main {
// 你的源码目录, 如果设定为jni.srcDir=[] 表示禁用通过Gradle来编译本地c/c++代码
//jni.srcDir 'src/main/otherDir'
// 你的.so库的实际路径。jniLib.srcDirs定义了编译打包时Gradle在哪里寻找生成的so库文件
//jniLibs.srcDir 'src/main/libs'
}
```
- Android.mk编译
手动编译方式:
- 在jni文件中编辑Android.mk文件
- 在android studio中选中Android.mk文件,右键—>Link C++ Project with Gradle—>选ndk-build—>Project Path选择刚编辑Android.mk文件路径—》OK。
- 完成之后,会在app.gradle中android节点中,生成如下代码:
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
表明Android.mk文件和NDK 编译连接起来了。
NDK使用时错误及解决方法
1. 错误一:
编译时错误消息提示如下:
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:compileDebugNdk'.
> Error: Your project contains C++ files but it is not using a supported native build system.
Consider using CMake or ndk-build integration with the stable Android Gradle plugin:
https://developer.android.com/studio/projects/add-native-code.html
or use the experimental plugin:
https://developer.android.com/studio/build/experimental-plugin.html.
解决方法:
在工程根目录gradle.properties文件最后一行添加:android.useDeprecatedNdk=true
2.错误二:
程序执行时,直接在模拟器中显示 xxx has stop。Android Monitor中错误提示为:
AndroidRuntime: FATAL EXCEPTION: main
Process: org.demo.ndk, PID: 8654
java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader ....
..................................................................
... .... couldn't find "libNDKDemoName.so"
at java.lang.Runtime.loadLibrary0(Runtime.java:984)
at java.lang.System.loadLibrary(System.java:1530)
at org.demo.ndk.NavHelper.<clinit>(NavHelper.java:10)
at org.demo.ndk.MainActivity.onCreate(MainActivity.java:15)
at android.app.Activity.performCreate(Activity.java:6679)
可能解决方法:
在app.gradle(模块gradle文件中)检查defaultConfig节点下是否配置了ndk{…},如果没有则可能会出现这个问题。
3.错误三:
执行程序是Android Monitor提示:
No implementation found for java.lang.String demo.jni.org.jnidemo.NavHelper.printHelloJNI(java.lang.String)
表明没有正确加载库文件,因此,
- 检查是否有加载库文件语句(System.loadlibrary(“库文件名”)),并库文件名是否正确。
- 检查库文件是否正确生成。(生成库文件在app/build/intermediates/ndk/debug/lib中)
- 如果写的是C++源文件,需要添加该函数实现的头文件(头文件即javah命令产生的头文件)
4.错误四
JNI调用错误: No implementation found for native
1、JNIEnv *env参数的使用
所有JNI接口的第一个参数是JNIEnv *env, 在C中,使用方法是
(*env)->NewStringUTF(env, “Hello from JNI!”);
但在C++中,其调用方法是
env->NewStringUTF(“Hello from JNI!”);
为什么有这种区别呢,看看jni.h中关于JNIEnv的定义就可以知道了:
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
#else
typedef const struct JNINativeInterface* JNIEnv;
#endif
可以看到,对于C和C++,定义有所不同,主要原因是C不支持类,所以采用了一种变通的方法。
2、接口找不到
在Java中调用JNI接口时,出现异常,察看日志,发现有如下错误:
WARN/dalvikvm(422): No implementation found for native Lcom/whty/wcity/HelixPlayer;.setDllPath (Ljava/lang/String;)V
检查了几遍代码,Cpp中确实定义了这个接口,而且仔细对照了Java的包名、类名,确实没有错误,那为什么会出现这种问题呢。后来突然想到,JNI接口 都是以C的方式定义的,现在使用C++实现,函数定义前是否需要加上extern “C”呢?为此定义了一个头文件,在CPP文件中include该头文件,头文件加上如下代码片断:
#ifdef __cplusplus
extern "C" {
#endif
#endif
...
#ifdef __cplusplus
}
5.其他错误
参考链接:
NDK中使用C和C++中的区别
想要记录NDK中使用C和C++中的区别,主要原因是在写NDKDemo时,我创建的是C++的源文件,在源文件中返回一个字符串时一直报错(文中的helloWorld函数的实现)。故此,对比了下C源文件和C++源文件的实现。这部分可以和错误4联系起来看。
C源文件实现:
#include <string.h>
#include <jni.h>
JNIEXPORT jstring JNICALL Java_org_demo_ndk_NavHelper_helloWorld(JNIEnv *env, jobject thiz){
return (*env)->NewStringUTF(env, "Hello NDK !");
}
C++源文件实现:
#include <string.h>
#include <jni.h>
#include "org_demo_ndk_NavHelper.h"
JNIEXPORT jstring JNICALL Java_org_demo_ndk_NavHelper_helloWorld(JNIEnv *env, jobject thiz){
return env->NewStringUTF("Hello NDK !");
}