JNI—NDK开发流程(ndk-build与CMake)

版权声明:本文出自PPLLiang的博客,转载必须注明出处 https://blog.csdn.net/a469516684/article/details/86506411

注意,例子使用的各版本信息如下:
AS 3.3
Gradle Tool:3.3.0
Gradle Version:4.10.1

1. 概述

最近在阅读Android 源码的过程中发现大量的Native方法,在没有系统掌握JNI与NDK知识的情况下寸步难行,所以有必要系统地了解相关知识。
在学习之前,我常常有有如下几个疑问:

NDK的开发流程?
C/C++ 与 Java如何进行通信的?
如何阅读Android Native 源码?

希望经过一系列的学习和总结,以上的问题能得到解决。 今天首先解决“如何进行NDK开发?”

1.1. JNI:

  • JNI :
    • Java Native Interface,Java本地编程接口,是一套编程规范,提供了本地语言(C/C++)与Java 语言相互通信的一套API接口;
    • Java是跨平台语言,因为其背后依赖Java虚拟机,Java源码编译的.class字节码文件运行在虚拟机上,而虚拟机往往由C/C++编写,那么两者是如何通信的呢?——JNI;
    • 通过JNI, Java可以调用C/C++的代码,反过来亦然;
    • JNI 是Java 语言的一部分,而非Android 平台特有;
  • 作用:
    • 提高效率:加密算法、图像处理、机器视觉等需要大量计算的逻辑,可以在C/C++中实现;
    • 扩展JVM:扩展JVM的功能;
    • 代码复用:复用C/C++平台上的代码,避免重复在Java上重复造轮子;

1.2. NDK:

  • NDK:

    • Native Develop Kit,本地开发工具,是Google开发的一套方便开发者在Android 平台上开发Native 代码的工具;
    • NDK 是Android 的一部分,与Java无直接关系;
  • 作用:

    • 方便快速:使用NDK自带的工具,快速对C/C++代码进行构建、编译和打包;
    • 交叉编译:快速生成不同CPU平台的动态库;

2. NDK开发流程

在NDK的开发过程中,有两种形式——ndk-build和CMake,其实两种方式最终的目的都是一样,将C 或 C++(“原生代码”)嵌入到 Android 应用。只是CMake使用起来更加方便,同时Android官方也推荐在Android Studio 2.2及以上使用CMake。工具的本质就是方便人们进行日常活动,对比一下两者流程,方便开发过程进行技术选型。

2.1. ndk-build

  • 下载和配置NDK
    ndk-build只是NDK工具包里面的一个脚本工具,帮忙调用NDK里面正确的构建脚本,路径在sdk/ndk-bundle目录下。
    需要在Studio中下载与配置NDK:Settings–Appearance & Behavior–System Settings–Android SDK–SDK Tools。
    image

  • Java中创建本地方法
    在Java类HelloWorldJNI中创建本地方法:

    public class HelloWorldJNI {
    
        // 加载Native动态库(so库),动态库的名称后面在mk文件中会使用到
        static {
            System.loadLibrary("helloworld_jni");
        }
    
        // 定义Native方法
        public native String getString();
    
        public native String setString(String text);
    }
    
  • 创建JNI文件目录并链接项目
    在src\main 目录下创建 jni文件夹,JNI相关的编码都在讲在该文件下进行,有两种创建方式:
    a. Android Studio UI下右击main文件下创建:
    image
    b. 手动创建jni文件:
    在main文件夹手动创建jni文件夹,然后在项目的gradle中配置

    android {
        ......
        externalNativeBuild {
            ndkBuild {
                path 'src/main/jni/Android.mk'
            }
        }
    }
    

    或者
    image
    两者效果一样,推荐使用方法a,简单快捷。

  • 生成本地方法头文件
    在命令终端Terminal中进入到项目的java文件目录中,如本例中的 DemoProjects\JNITest\src\main\java,执行命令:

    javah -jni -encoding UTF-8 -d <你的工程目录>\DemoProjects\JNITest\src\main\jni com.wuzl.jnitest.HelloWorldJNI
    

    -jni:表示生成jni格式的头文件;
    -encoding:编码格式;
    -d:头文件的输出路径;

    javah 命令导出HelloWorldJNI的头文件(com_wuzl_jnitest_HelloWorldJNI.h)至jni文件目录中,重点来了!
    头文件的命名规则为:
    包名(.变成下划线)
    其函数命名遵循如下规则:
    Java_包名_类名_方法名

        /* DO NOT EDIT THIS FILE - it is machine generated */
        #include <jni.h>
        /* Header for class com_wuzl_jnitest_HelloWorldJNI */
        
        #ifndef _Included_com_wuzl_jnitest_HelloWorldJNI
        #define _Included_com_wuzl_jnitest_HelloWorldJNI
        #ifdef __cplusplus
        extern "C" {
        #endif
        /*
         * Class:     com_wuzl_jnitest_HelloWorldJNI
         * Method:    getString
         * Signature: ()Ljava/lang/String;
         */
        JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_getString
          (JNIEnv *, jobject);
        
        /*
         * Class:     com_wuzl_jnitest_HelloWorldJNI
         * Method:    setString
         * Signature: (Ljava/lang/String;)Ljava/lang/String;
         */
        JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_setString
          (JNIEnv *, jobject, jstring);
        
        #ifdef __cplusplus
        }
        #endif
        #endif
    
  • 创建C/C++ 代码文件
    在jni目录下创建helloworld.cpp,然后导入上面生成的头文件,并实现头文件声明的两个本地方法

    # include <jni.h>
    # include <stdio.h>
    #include <cstring>
    # include "com_wuzl_jnitest_HelloWorldJNI.h"
    
    # ifdef _cplusplus
    extern "C"
    {
    # endif
    
        JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_getString(JNIEnv *env, jobject obj){
            return env -> NewStringUTF("Get Hello world JNI!");
        }
    
        JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_setString(JNIEnv *env, jobject obj, jstring str){
            char* jnistr = (char *) env->GetStringUTFChars(str, NULL);
            strcat(jnistr,": I am JNI");
            return env -> NewStringUTF(jnistr);
        }
    # ifdef _cplusplus
    }
    # endif
    
  • 创建配置文件
    在jni文件目录下配置Android.mk和Application.mk配置文件
    Android.mk

    # 当前模块路径,即Android.mk所在的文件目录
    LOCAL_PATH       :=  $(call my-dir)
    
    # 开始清除LOCAL_XXX变量
    include              $(CLEAR_VARS)
    
    # 动态库模块名称,Java代码中中加载动态库中使用System.loadLibrary("helloworld_jni");
    LOCAL_MODULE     :=  helloworld_jni
    
    # 编译的C/C++源文件,可多个,以空格隔开
    LOCAL_SRC_FILES  :=  helloworld.cpp
    
    # 开始动态编译生成动态库
    include $(BUILD_SHARED_LIBRARY)
    

    Application.mk

    # 需要编译的ABI
    APP_ABI :=armeabi-v7a x86 x86_64
    

    配置文件的详细选项可查看Google官网:
    Android.mk, Application.mk

  • 直接运行
    Alt

  • 可利用ndk-build命令工具生成so库
    在命令终端Terminal中进入到jni目录的父目录,直接执行ndk-build命令(需要将ndk目录添加到环境变量中)将会在当前目录下生成libs和obj文件目录,其中libs就是我们想要so库
    image


小技巧:
ndk-build NDK开发流程讲得差不多了,但是有个小技巧可以分享给大家,在上述流程中,每次都需要输入命令来生成头文件和so库,而且有时候忘记命令了,其实AS提供了相关技巧来优化开发流程:
Settings–External Tools–“+”
image
其中 Program:javah所在的路径;
Arguments: 命令参数,此处为-jni -encoding UTF-8 -d M o d u l e F i l e D i r ModuleFileDir \src\main\jni F i l e C l a s s FileClass
Wroking directory: 工作目录,此处为 M o d u l e F i l e D i r ModuleFileDir \src\main\java;
使用(右击定义Native方法的Java文件,选择External Tools-javah-jni,然后在jni文件下直接生成对应的头文件):
image

2.2. CMake

CMake并不是什么新鲜的内容,脱离Android来说,CMake是一个跨平台的安装编译工具,通过简单的脚本语句描述不同平台的编译过程,已经运用在UNIX和WindowW系统中。

Google在AS2.2版本上开始支持CMake,并提倡在AS 2.2及以上版本使用CMake进行JNI开发,在上述使用ndk-build开发过程中发现,需要的步骤特别多,比如生成头文件、配置Android.mk, Application.mk文件,并需要了解其中mk各种繁琐的配置项,而使用CMake进行开发可以省略这些步骤。

官网已经给出非常详细的使用过程:向您的项目添加 C 和 C++ 代码

如果是新建项目使用CMake进行JNI开发,非常简单,在创建新项目的时向导面板中选中 Include C++ Support 复选框即可,AS会创建一个简单的DEMO供使用参考。
但是我们大多数都是需要在已存在的项目中进行JNI的开发,下面将以此作为讲解。

  • 创建CMakeLists.txt配置文件
    在项目根目录下创建CMakeLists.txt配置文件

    # CMake 最小工具版本要求,可通过SDK Manager 查看CMake的本地版本
    # 该版本对Gradle有要求,比如3.10 版本需要Gragle 4.10+version才行
    cmake_minimum_required(VERSION 3.6)
    
    add_library( # Sets the name of the library.
                 # 动态库的名称,与ndk-build中Android.mk 的模块名一样
                 # 最终编译出来的动态库名称为:lib+library name
                 helloworld
    
                 # Sets the library as a shared library.
                 # 编译库的类型,有静态库、动态库、和模块库
                 SHARED
    
                 # Provides a relative path to your source file(s).
                 # C/C++ 源文件
                 src/main/cpp/helloworld.cpp)
    
    # Searches for a specified prebuilt library and stores the path as a
    # variable. Because CMake includes system libraries in the search path by
    # default, you only need to specify the name of the public NDK library
    # you want to add. CMake verifies that the library exists before
    # completing its build.
    # 找到依赖库,比如下面将系统log-lib链接到helloworld动态库
    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 )
    
    # Specifies libraries CMake should link to your target library. You
    # can link multiple libraries, such as libraries you define in this
    # build script, prebuilt third-party libraries, or system libraries.
    # 配置库的链接
    target_link_libraries( # Specifies the target library.
            helloworld
    
            # Links the target library to the log library
            # included in the NDK.
            ${log-lib} )
    

    这个是最最基本的CMake的配置,CMake还可以做很多强大的配置选项,详情可查看CMake的官方文档(https://cmake.org/documentation/)

  • 创建C/C++源文件
    在main目录(当然目录可以自己选择)下创建cpp文件夹,然后在该文件夹下创建helloworld.cpp源文件,与ndk-build一致:

    # include <jni.h>
    # include <stdio.h>
    # include <cstring>
    
    extern "C" {
        JNIEXPORT jstring JNICALL Java_com_wuzl_cmake_HelloWorldJNI_getString(JNIEnv *env, jobject obj){
            return env -> NewStringUTF("Get Hello world JNI");
        }
    
        JNIEXPORT jstring JNICALL Java_com_wuzl_cmake_HelloWorldJNI_setString(JNIEnv *env, jobject obj, jstring str){
            char* jnistr = (char *) env->GetStringUTFChars(str, NULL);
            strcat(jnistr,": I am JNI by CMake building");
            return env -> NewStringUTF(jnistr);
        }
    }
    
  • 链接项目
    右击项目,选择Lick C++ Project with Gradle,然后在弹出来的选项里选择CMake并选择上面配置好的CMakeLists.txt
    image

  • 运行查看结果
    image

3. 总结

NDK开发的两种流程:

  1. ndk-build+Android.mk+Application.mk
  2. CMake+CMakeLists.txt

两者只是构建的脚本和命令不同而已,但是个人感觉CMake更加方便和简洁
Google推荐使用CMake进行JNI的开发,而且CMake的配置更加强大,其官方文档可查阅使用的细节(https://cmake.org/documentation/)。

最后上DEMO:https://github.com/PlepleLiang/JNIDemo


参考:
https://blog.csdn.net/quwei3930921/article/details/78820991
https://juejin.im/post/5a67dcdb518825732c53b338
https://developer.android.com/studio/projects/add-native-code?hl=zh-cn

猜你喜欢

转载自blog.csdn.net/a469516684/article/details/86506411