本文主要介绍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);
    }
}

编译运行,结果如图:

android ndk断点进不去 android ndk location_android ndk断点进不去


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自动编译完成的,可大致分以下步骤:

  1. 定义native函数,并由javah命令生成,C/C++函数头文件。
  2. 创建jni目录,编写C/C++实现。
  3. 在模块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)

表明没有正确加载库文件,因此,

  1. 检查是否有加载库文件语句(System.loadlibrary(“库文件名”)),并库文件名是否正确。
  2. 检查库文件是否正确生成。(生成库文件在app/build/intermediates/ndk/debug/lib中)
  3. 如果写的是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 !");
}