Android NDK 使用简介

今天我们来简单说一下Android NDK的使用方法。众所周知,so文件在Android的开发过程中起到了很重要的作用,无论与底层设备打交道还是在Android安全领域。so文件都格外受人青睐。NDK就是Android发布的用于编译so文件的一套工具,

引用自百度百科的一段解释

Android NDK 是在SDK前面又加上了“原生”二字,即Native Development Kit,因此又被Google称为“NDK”。

众所周知,Android程序运行在Dalvik虚拟机中,NDK允许用户使用类似C / C++之类的原生代码语言执行部分程序。
NDK包括了:

  • 从C / C++生成原生代码库所需要的工具和build files。
  • 将一致的原生库嵌入可以在Android设备上部署的应用程序包文件(application packages files ,即.apk文件)中。
  • 支持所有未来Android平台的一系列原生系统头文件和库

为何要用到NDK?
概括来说主要分为以下几种情况:

  1. 代码的保护,由于apk的java层代码很容易被反编译,而C/C++库反汇难度较大。
  2. 在NDK中调用第三方C/C++库,因为大部分的开源库都是用C/C++代码编写的。
  3. 便于移植,用C/C++写的库可以方便在其他的嵌入式平台上再次使用。

本文从以下三个方面讲解NDK的使用

  • 直接在命令行中用NDK进行编译
  • Android Studio2.2以前对NDK的支持
  • Android Studio2.2及以后对NDK的支持

直接在命令行使用NDK

NDK本来就是一套编译工具,自然是在命令行中执行,其实后面两种方法都是对这种方法的自动化处理,万变不离其宗, 要理解后面两种方法,还是应该熟悉一下不借助任何工具时的操作。

SDK默认是不带NDK的,所以NDK需要额外下载,下载后还需要配置环境变量。具体方法可以查看百度,配置环境变量很简单,只需要把NDK根目录,也就是ndk-build所在的目录加入环境变量即可。

用NDK-BUILD构建一个NDK程序,我们知道就是将C文件编译成so文件,其实原理很简单,用gcc进行编译。哦,因为我是mac环境,所以自带GCC编译环境,如果是windows下的话,还需要安装Cygwin环境来模拟linux,不过听说最新的NDK自带Cygwin,所以不再需要额外安装,Windows的同学可以试一下,有问题可以在评论区提问, 有机会我会补充Win下的使用方法。

编译c程序需要makefile,其实简单说就是告诉GCC怎么编译,先编什么在编什么,需要哪些包等等。这个熟悉c的同学应该知道的。一个简单的so项目包含以下四个文件。

这里写图片描述

除了.h和.c文件,还有两个makefile,Application.mk是项目makefile,它会指定调用哪个子makefile,然后Android.mk是具体执行操作的makefile。Application.mk的名字不能变,因为NDK会默认去找这个文件,后面也会讲到,Android.mk的名字可以变,是配置在Application.mk中的。

然后NDK还有一些规定,看.h文件的名字,c文件中的方法与java中某个方法是一一对应的,出于安全考量,NDK要求C中的方法名应该以对应java文件的包名+类名+方法名来命名。

头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_zzw_helloworld_JniUtils */
#ifndef _Included_com_example_zzw_helloworld_JniUtils
#define _Included_com_example_zzw_helloworld_JniUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_zzw_helloworld_JniUtils
* Method: stringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_zzw_helloworld_JniUtils_stringFromJNI
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif

这个头文件很简单,就声明了一个方法,这两个参数是固定的,这个方法在java中的表现形式为

stringFromJNI() 

返回类型对应的是java中的string。然后创建C文件,实现该方法。

1
2
3
4
5
6
7
8
9
//
// Created by zzw on 16/10/11.
//
#include "com_example_zzw_helloworld_JniUtils.h"
JNIEXPORT jstring JNICALL Java_com_example_zzw_helloworld_JniUtils_stringFromJNI
(JNIEnv *env, jobject instance) {
return (*env)->NewStringUTF(env, "Hello from JNI !");
}

然后我们还需要两个make文件,一个是Application.mk另一个是Android.mk
Application.mk内容如下:

1
APP_BUILD_SCRIPT := /Users/zzw/Desktop/jni/Android.mk

其实就是声明Android.mk的位置
Android.mk如下:

1
2
3
4
5
6
7
8
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
include $(BUILD_SHARED_LIBRARY)

然后运行

1
./ndk-build NDK_PROJECT_PATH=/Users/zzw/Desktop

指定PROJECT的位置,然后NDK会自动寻找该路径下的jni文件夹中的Application.mk,然后去编译。

如果编译成功,则会在该文件夹下生成libs文件夹,里面就是各种架构下的so文件
这里写图片描述

生成的文件放在Android的libs中,对应的java文件调用方法为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.zzw.helloworld;
/**
* Created by zzw on 16/10/11.
*/
public class JniUtils {
static {
System.loadLibrary("hello-jni");
}
public static native String stringFromJNI();
public static String stringFromJNINative(){
return stringFromJNI();
}
}

先用loadLibrary引入so文件,然后用native声明底层方法,然后我们就可以在程序中调用方法了。

这种方法完全没有借助任何的IDE,不过你也能看出来,有些工作是可以简化的,下面我们就说下在IDE中的做法。

Android Studio2.2 之前使用NDK

老的Android Studio支持NDK的方式可以根据java类帮我们生成头文件,然后编译过程可以写在gradle中,而不需要先编出so,再编android这样,具体过程如下

先写一个Java类,因为我们可以用jni工具根据java类来生成头文件,java类载入so文件并且声明底层方法,这个和前面一样,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.zzw.helloworld;
/**
* Created by zzw on 16/10/11.
*/
public class JniUtils {
static {
System.loadLibrary("hello-jni");
}
public static native String stringFromJNI();
public static String stringFromJNINative(){
return stringFromJNI();
}
}

然后编译程序,注意只编译不运行,因为此时运行会报错,编译后,在build的文件夹下面能看到这个class文件

这里写图片描述

接下来我们可以用jni工具来根据这个class文件自动生成头文件了。命令行cd到build文件夹一层,然后执行如下命令

1
2
cd app/build/intermediates/classes/debug/
javah -jni com.example.zzw.helloworld.JniUtils

完整路径的类名,如果这一步报错,可以检查一下NDK有没有配到环境变量里。方法可以自行百度。

如果成功的话,你应该能看到在build文件夹下生成了一个.h文件,如下

这里写图片描述

把这个文件拷贝到main下面jni文件夹下,这个文件夹也不是固定的,可以配置。但是我们一般习惯于放在这个地方。

复制过来之后这就是我们的头文件,然后我们可以创建一个c文件,实现头文件的方法

文件内容如下

1
2
3
4
5
6
#include "com_example_zzw_helloworld_JniUtils.h"
JNIEXPORT jstring JNICALL Java_com_example_zzw_helloworld_JniUtils_stringFromJNI
(JNIEnv *env, jobject instance) {
return (*env)->NewStringUTF(env, "Hello from JNI !");
}

build.gradle中需要配置ndk
这里写图片描述

然后点击运行。

遇到的问题,
报错

1
2
Error:Execution failed for task ':app:compileDebugNdk'.
> Error: NDK integration is deprecated in the current plugin. Consider trying the new experimental plugin. For details, see http://tools.android.com/tech-docs/new-build-system/gradle-experimental. Set "$USE_DEPRECATED_NDK=true" in gradle.properties to continue using the current NDK integration.

这是因为gradle插件版本太高,已经不支持这个方法了,它提示我们在gradle.properties里面加一句话,但经过我测试,那句话是不对的,应该加如下一句:

1
android.useDeprecatedNdk=true

然后运行,正常。

Android Studio2.2之后使用NDK

前面我们看到其实比起第一种方法也没有简化多少,所以在Studio2.2的时候google又尝试简化了做法,Android Studio 2.2开始支持用內建的方法来执行复杂的NDK编译,这意味着开发者只需要写好c文件,其他所有的编译,链接都可以交给系统去做。

注意:gradle版本需要2.2及以上

这个特性的实现要依赖于一个build标签,叫externalNativeBuild。标签配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
defaultConfig {
externalNativeBuild {
ndkBuild {
arguments "NDK_LIBS_OUT=$jniLibsDir", "-j$numProcs", "all"
abiFilters "armeabi-v7a"
}
}
}
externalNativeBuild{
ndkBuild{
path "src/main/jni/Android.mk"
}
}

arguments指定编译的参数,abiFilters指定编译的平台,这些参数都可以省略以使用默认参数。下面path指定make文件的位置。

之后,NDK执行的task配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
task ndkBuild(type: Exec) {
commandLine getNdkBuildCmd(),
'-C', file('src/main/jni').absolutePath,
'-j', Runtime.runtime.availableProcessors(),
"NDK_LIBS_OUT=$jniLibsDir",
'all',
'NDK_DEBUG=1'
dependsOn 'generateLuaBytecodes'
doFirst {
println '== ndkBuild =='
}
}

接下来让我们来看一下,具体的实现步骤是什么样的,不需要像以前一样自己写头文件,然后再编译,现在只需要关注c文件即可。以hello world为例,我们写一个C文件如下:

1
2
3
4
5
6
7
8
#include <string.h>
#include <jni.h>
JNIEXPORT jstring JNICALL
Java_com_example_zzw_helloworld_MainActivity_stringFromJNI(JNIEnv *env, jobject instance) {
return (*env)->NewStringUTF(env, "Hello from JNI !");
}

注意方法名要以调用它的JAVA文件的包名+类名+方法名命名。
这样写完之后,我们就可以在相应的JAVA文件中调用了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.zzw.helloworld;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("hello-jni");
}
public native String stringFromJNI();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
System.out.println(stringFromJNI());
}
}

先用loadLibrary引入so文件,然后用native声明底层方法,然后我们就可以在程序中调用方法了。
当然前面提到了make文件,我们要创建一个Android.mk文件在externalNativeBuild中声明的位置,如下:

1
2
3
4
5
6
7
8
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
include $(BUILD_SHARED_LIBRARY)

之后,make project,运行程序应该就能看的效果了,通过这种方法生成的so文件放在app/.externalNativeBuild/debug/obj/local/下,并以lib+类名命名文件,如下:

这里写图片描述

感觉现在应该很简单了,只需要关注方法实现就可以了,其他基本都不要开发者关心了。

坚持原创技术分享,您的支持将鼓励我继续创作!