超详细的安卓ndk编译的两种方式(ndk-build和cmake)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/you__are_my_sunshine/article/details/83150722

一、概述

        搞安卓的工作中难免需要使用native的方法,高效,安全。。。优点就不说了。以前使用到native方法的时候,都是临时抓起来一种方式就用了,也没详细整理两种方式的差别和详细的使用方式,虽然不复杂,但是中间还是有很多小细节需要注意的。虽然ndk-build的方式谷歌官方已经不支持使用了,新版的studio和ndk中工具中已经将其移除了,但是还是有必要了解怎么使用的,以备不时之需,cmake是官方推荐的方式,使用起来也很方便,基本studio把需要的步骤都给你创建好了,当离开studio,手写的时候你还能知道怎么写吗?第一步写什么,第二部写什么来着?今天就主要总结了下两种编译方式的详细步骤和方法,以免日后忘记了。

二、ndk-build方式

1.新建Java类,声明native方法和参数

eg:

public class NdkJniUtils { 
    public native String getCLanguageString(); 
}

2.使用的地方引用加入的native工具类

eg:

NdkTest ndkTest = new NdkTest();
TextView tv2 = (TextView) findViewById(R.id.tv_2);
tv2.setText(ndkTest.getStringFromC());

3.编译一下工程,将native工具类编译为class文件

找到指定目录:

projectname\app\build\intermediates\classes\debug

输入命令行:

javac HelloJNI.java

或者makeproject进行编译

注:命令行编译后的class文件会生成在当前路径下

4.找到对应的class文件

利用Android Studio的Terminal,进入你自己的Android工程文件的对应的class目录,

在Terminal中输入命令

cd \app\build\intermediates\classes\debug

或者在进入指定目录后再打开控制台即可找到编译成功的class文件

5.利用javah生成对应的 .h头文件

androidstudio的Terminal中cd到~/workspace/projectname/app/src/main/java目录

在Terminal中输入命令

  • 方式一:

输入

javah -classpath . -jni com.demo.testc.MyTestC  (-jni为默认值可省略)

注意:classpath后面有个 "." 前后都有空格

com.demo.testc.MyTestC 是自己要转换.h文件的类的全路径名;

可以加入-d 参数来指定生成的.h头文件位置,如 -d ../jni

mac系统下一定要加入-classpath 参数,否则编译时会报找不到com.xxx类的错误

  • 方式二:

javah  -classpath   ~\workspace\TestC\app\src\main\java com.demo.testc.MyTestC

 ~\workspace\TestC\app\src\main\java 要生成.h文件的类的全路径  com.demo.testc.MyTestC 就是包名+类名 ,注意指令之间的空格

指令用法说明:

javah -d jni -jni -classpath ..\..\build\intermediates\classes\debug com.demo.JniTest

使用以上命令需要先对native类进行编译为class文件

Javah命令的参数说明如下:

-d<dir> 输出目录,后面跟上要生成的目录名

-jni 生成Jni样式的标头文件

-classpath<path> 指定加载类的路径,后面跟上你要生成头文件的这个类的路径,例如:

..\..\..\build\intermediates\classes\debug(这个是类所在的路径)

com.demo.JniTest(类的包名)

用法:

javah [options] <classes> 其中, [options] 包括:

-o <file> 输出文件 (只能使用 -d 或 -o 之一)

-d <dir> 输出目录

-v -verbose 启用详细输出

-h --help -? 输出此消息

-version 输出版本信息

-jni 生成 JNI 样式的标头文件 (默认值)

-force 始终写入输出文件

-classpath <path> 从中加载类的路径

-bootclasspath <path> 从中加载引导类的路径

<classes> 是使用其全限定名称指定的 (例如, java.lang.Object)。

在我们正常使用的时候只需要简单的几个参数即可,我们以Hello这个类来举例说明:

javah -d ~\workspace\TestC com.demo.testc.NdkTest

6.编写C/C++代码

建立jni目录,编写对应的.c或者.cpp文件,引入生成的.h头文件,以及从.h中拷贝方法体,实现方法,实现函数时注意参数和头文件中定义的有所不同,定义时只定义参数类型,没有定义参数名

//
// Created by wangjp on 2018/10/18.
//
#include <jni.h>
#include <string>
#include <com_demo_testc_NdkTest.h>

using namespace std;

/*
 * Class:     com_demo_testc_NdkTest
 * Method:    getStringFromC
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_demo_testc_NdkTest_getStringFromC
        (JNIEnv *env, jobject instance) {
    return env->NewStringUTF("自己编写的c文件");
}

/*
 * Class:     com_demo_testc_NdkTest
 * Method:    getStringWithParams
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_demo_testc_NdkTest_getStringWithParams
        (JNIEnv *env, jobject instance, jstring paras_) {
    const char *paras = env->GetStringUTFChars(paras_, 0);
    string s = string(paras);
    string s1 = "我的c文件";
    s = s + s1;

    env->ReleaseStringUTFChars(paras_, paras);

    return env->NewStringUTF(s.c_str());
}

7.编写编译的配置文件

  • 编辑Android.mk文件:

#每个Android.mk文件必须以LOCAL_PATH开头,在整个开发中,它通常别用做定位资源文件,例如,功能宏my-dir提供给编译系统当前的路径。

LOCAL_PATH := $(call my-dir)

#CLEAR_VARS指编译系统提供一个特殊的GUN MakeFile来为你清除所有的LOCAL_XXX变量,LOCAL_PATH不会被清除。使用这个变量是因为在编译系统时,所有的控制文件都会在一个GUN Make上下文进行执行,而在此上下文中所有的LOCAL_XXX都是全局的。

include $(CLEAR_VARS)

#LOCAL_MODULE变量是为了确定模块名,并且必须要定义。这个名字必须是唯一的同时不能含有空格。会自动的为文件添加适当的前缀或后缀,模块名为“foo”它将会生成一个名为“libfoo.so”文件。

LOCAL_MODULE := myjni

#包含一系列被编译进模块的C 或C++资源文件

LOCAL_SRC_FILES := JNI_C++.cpp

#指明一个GUN Makefile脚本,并且收集从最近“include$(CLEAR_VARS)”下的所有LOCALL_XXX变量的信息,最后告诉编译系统如何正确的进行编译。将会生成一个静态库hello-jni.a文件或者动态库libhello-jni.so。

include $(BUILD_SHARED_LIBRARY)

总结:

LOCAL_PATH即为调用命令的所在目录,你在哪个目录下使用cmd命令,这里就会返回它的路径地址

LOCAL_MODULE你生成的文件名称是什么,输出之后会自动在名称的前后加上lib和.so

LOCAL_SRC_FILES要对哪个文件进行编译

  • 编辑Application.mk

可没有,主要指定so调用库名以及编译的so对应CPU平台

若没有,则可在build.gradle中设置,此时使用系统进行编译,而非使用ndk-build

APP_MODULES := MyJni

APP_ABI := all all代表全平台

8.编译方式选择

  • 使用gradle进行ndk编译

在app module目录下的build.gradle中设置库文件名(生成的so文件名)。找到gradle文件的defaultConfig这项,在里面添加如下内容:

defaultConfig { 

    ...... 

    ndk{ 
        moduleName "JniLibName" //生成的so名字

        ldLibs "log", "z", "m" //添加依赖库文件,如果有log打印等 
        abiFilters "armeabi", "armeabi-v7a", "x86" //输出指定cpu体系结构下的so库。 
    } 
}

externalNativeBuild { 

    ndkBuild { path file("src/main/java/jni/Android.mk") 

    } 
}

sourceSets {

    main {

        jni.srcDirs('src/main/java/jni')

    }

}

此种生成的so文件在app/build/intermediates/ndkBuild/debug下

  • 直接使用指令ndk-build在c的源文件目录进行编译
defaultConfig { 
    ......

    sourceSets.main{

        jni.srcDirs = []

        jniLibs.srcDirs "src/main/java/libs"

    }

}

9.在native方法的申明中引用so库

static { 
    System.loadLibrary("myjni"); //defaultConfig.ndk.moduleName 
}

10.可能遇到的问题

  • Error:Execution failed for task ':jnilib:compileReleaseNdk'.

> Error: Your project contains C++ files but it is not using a supported native build system.

解决:

意思是项目中没有使用NDK的配置,解决方法是在gradle.properties文件中添加如下配置:

android.useDeprecatedNdk=true

需要注意的是你的jni所在module 的gradle需要如下配置,一般来说在创建jni folder时就已经自动创建了:

sourceSets {

    main {

        jni.srcDirs = ['src/main/java/jni', 'src/main/java/cpp']

    }

}
  • 在C++头文件中加入#include <string>

ndk-build后报错

fatal error: 'string' file not found

#include <string>

^~~~~~~~

1 error generated.

解决:

在网上搜索了一大圈, 原来是需要让Android NDK支持STL(Standard Template Library)

Import STL libraries to the Android NDK code

将Application.mk放在jni目录下,添加以下内容:

APP_STL := stlport_static

三:CMake方式

1.创建项目时勾选包含c/c++文件,或者手动创建cpp目录并创建.c/.cpp文件,创建CMakeLists.txt文件

2.在对应的类中定义对应的native方法,并实现.c/.cpp中对应的native方法

3.配置CMakeLists.txt文件,主要关注以下几个方法

  • 设置构建本地库所需要的CMake的最低版本

cmake_minimum_required(VERSION 3.4.1)

  • 设置本地库的名称,类型,路径

创建并命名一个库,将其设置为静态或共享动态(安卓只支持加载动态so库,但是动态库依赖库可以是静态库.a),并提供其源代码的相对路径。您可以定义多个库,并为您构建CMake。Gradle会自动将共享库与APK打包。

add_library( # Sets the name of the library.

native-lib

# Sets the library as a shared library.

SHARED

# Provides a relative path to your source file(s).

src/main/cpp/native-lib.cpp )

  • 找到需要添加的其他本地依赖库,配置其名称,路径

搜索指定的预构建库并将路径存储为变量。因为CMake在默认情况下在搜索路径中包含系统库,所以您只需要指定要添加的公共NDK库的名称

find_library( # Sets the name of the path variable.

log-lib

# Specifies the name of the NDK library that

# you want CMake to locate.

log )

  • 将需要依赖的三方库引入本地库

指定CMake应该链接到目标库的库。您可以链接多个库,例如在这个构建脚本中定义的库、预构建的第三方库或系统库。

target_link_libraries( # Specifies the target library.

native-lib

# Links the target library to the log library

# included in the NDK.

${log-lib} )

4.配置build.gradle文件

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.demo.testc"
        minSdkVersion 15
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }

        // 配置需要的cpu平台
        ndk {
            abiFilters  "armeabi", "armeabi-v7a","arm64-v8a", "x86"
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
//        cmake方式编译时再打开,不能和ndk-build方式共存
//        cmake {
//            path "CMakeLists.txt"
//        }

//        使用gradle编译ndk时打开,指定编译方式
        ndkBuild {
            path file("src/main/java/jni/Android.mk")
        }
    }

//此种为使用ndk-build命令行编译成so后指定资源
//    sourceSets {
//        main {
//            jniLibs.srcDirs('src/main/java/libs')
//        }
//    }

//    此种为使用gradle编译ndk,指定编译的c源文件
    sourceSets {
        main {
            jni.srcDirs('src/main/java/jni')
        }
    }

}

5.在对应的类中调用native方法

首先使用静态代码块加载动态库

static { 
    System.loadLibrary("myjni"); //defaultConfig.ndk.moduleName 
}

然后即可调用动态库中的函数

6.可能遇到的问题

  • 部分平台的so无法编译出来,如armeabi,mips,mips64等,如果在gradle中强制指定这些平台,studio将会报错

解决:由于当前使用的ndk的版本高于r16,目前最新的是18

   

16.1.4479499

Update Available: 18.1.5063045

,新版本的ndk已经移除了对这几个平台的支持,如果还需要使用,只能降低ndk的版本(<=16),具体原因可以看官方说明https://developer.android.com/studio/build/configure-apk-splits

 

总结:

好了能力有限,就先是这么多,详细的可以看谷歌官方文档,https://developer.android.com/ndk/guides/大部分还是中文的,真是大大的方便了我等英语渣渣啊,最后附上demo的下载地址https://download.csdn.net/download/you__are_my_sunshine/10730266

 

没有更多推荐了,返回首页