AndroidNDK使用简介

Posted 轩辕223

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AndroidNDK使用简介相关的知识,希望对你有一定的参考价值。

例行推广一下我的博客http://zwgeek.com

今天我们来简单说一下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文件的包名+类名+方法名来命名。

头文件

/* 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文件,实现该方法。

//
// 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内容如下:

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

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

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES := hello-jni.c

include $(BUILD_SHARED_LIBRARY)

然后运行

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

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

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

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

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文件并且声明底层方法,这个和前面一样,如下

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文件夹一层,然后执行如下命令

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

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

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

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

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

文件内容如下

#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

然后点击运行。

遇到的问题,
报错

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里面加一句话,但经过我测试,那句话是不对的,应该加如下一句:

android.useDeprecatedNdk=true

然后运行,正常。

Android Studio2.2之后使用NDK

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

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

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

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配置如下

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文件如下:

#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文件中调用了,代码如下:

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中声明的位置,如下:

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+类名命名文件,如下:

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

喜欢这篇文章的朋友,可以关注我的博客http://zwgeek.com

以上是关于AndroidNDK使用简介的主要内容,如果未能解决你的问题,请参考以下文章

添加新对象时运行时覆盖列表元素[重复]

如何创建没有任何窗口的任务栏按钮?

C++ 中的运行时运算符

使用ActiveSheet.Range时运行时错误13“类型不匹配”

为啥 Kubernetes 的容器在 runsc (gVisor) 上作为 Docker 中的运行时运行时会失败?

应用程序在调试时运行时如何禁用 Firebase 崩溃报告?