NDK(一):编写第一个JNI项目

平时在开发中,或多或少都会用到JNI方面的技术,比如我们项目中,消息的加密和解密就是通过C来实现的,然后打包为.so动态库,并提供Java接口供应用层调用,这么做的目的主要就是为了提供应用的安全性,防止被反编译后被分析加密的逻辑。
接下来就要介绍JNI和NDK的区别,怎样创建一个项目开发JNI。

JNI与NDK

JNI

Java Native Interface,即Java本地接口,使用JNI可以使得Java与本地其他类型语言(如C、C++)交互。
JNI是 Java调用Native语言的一种特性,JNI是属于Java的,与 Android无直接关系。

NDK

Native Development Kit,是Android的一个工具开发包。
NDK是属于Android的,与Java并无直接关系,只是通过NDK可以快速方便的使用JNI,开发C、C++动态库。

开发JNI

开发JNI,一般都有两种途径:

  • Java项目直接引用生成好的动态库,动态库的具体C、C++代码在其他地方编写变生成动态库(常见的为在Window平台上的VS编写,在Mac平台上的Xcode编写,或者直接用命令行编写),引用第三方库大多就是采用这种方式
  • Android Studio项目中通过NDK实现JNI

Java项目调用Xcode编译动态库

  1. 创建Xcode项目,选择Library,选择为C++库

    image-20180827082713032
    image-20180827082713032
    image-20180827082921362
    image-20180827082921362
  2. 先删除多余文件和代码,引入jni.h头文件

    image-20180827083307623
    image-20180827083307623
  3. 设置jni.h头文件的路径,即Java环境的配置路径

    image-20180827084048635
    image-20180827084048635

    分别添加进include目录和include/darwin目录

    image-20180827084256925
    image-20180827084256925

    编译运行就不会报找不到头文件的错误了,同时,可以看到生成了动态库libStudyNdk.dylib,记录动态库的路径/Users/guidongyuan/Library/Developer/Xcode/DerivedData/StudyNdk-bwbgrzjykrevnaeyglsvxjelvnth/Build/Products/Debug/libStudyNdk.dylib,接下来会使用到。

    image-20180827172434885
    image-20180827172434885
  4. 编写Java代码调用native代码

    调用native代码,可以直接使用Test包中的测试类来进行测试,只要是Java类就可以了,可以在Android Studio中创建Java目录。上面介绍jni也说到,jni是与Java有关的,与Android没关系,只要是Java项目都可以使用到。

    在Android Studio创建Java项目jnilib,修改MyClass.java类,代码如下:

    package com.guidongyuan.jnilib;
    
    public class MyClass {
        
        static {
            // 加载动态库
            System.load("/Users/guidongyuan/Library/Developer/Xcode/DerivedData/StudyNdk-bwbgrzjykrevnaeyglsvxjelvnth/Build/Products/Debug/libStudyNdk.dylib");
        }
        
        public static void main(String[] args){
            new MyClass().test();
        }
    
        /**
         * native方法
         */
        native void test();
    }
    
    • System.load(String path)

      加载动态库,传入动态库的绝对路径,另外还有System.loadLibrary()方法,传入为动态库的名字,不需要动态库的后缀。

  5. 在XCode中完成native代码

    方法名格式为Java_包名_类名_方法名(JNIEnv,jobject){},编译运行生成动态库

    #StudyNdk.cpp
    
    #include <jni.h>
    
    extern "C"
    void Java_com_guidongyuan_jnilib_MyClass_test(JNIEnv *env, jobject){
        printf("接收到Java的调用");
    }
    

    方法太长的话,也可以通过javah命令生成.h文件,要注意当前所处路径,否则会报找不到类的错误

    # 在包名的上一个路径
    ➜  java pwd
    /Users/guidongyuan/code/Android/ndk/StudyNdk/jnilib/src/main/java
    ➜  java javah -o MyClass.h com.guidongyuan.jnilib.MyClass
    # 可以看到多了一个.h文件
    ➜  java ls
    MyClass.h com
    

    打开MyClass.h文件,则可以拷贝其方法名字

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_guidongyuan_jnilib_MyClass */
    
    #ifndef _Included_com_guidongyuan_jnilib_MyClass
    #define _Included_com_guidongyuan_jnilib_MyClass
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     com_guidongyuan_jnilib_MyClass
     * Method:    test
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_com_guidongyuan_jnilib_MyClass_test
      (JNIEnv *, jobject);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    
  6. 运行Java代码

    可以看到终端的输出,成功调用到native的代码

    image-20180827173444589
    image-20180827173444589

dylib动态库

Windows系统的动态库是DLL文件,Linux系统是so文件,macOS系统的动态库则使用dylib文件作为动态库。

可以通过file 文件查看生成的文件属性

➜  file libStudyNdk.dylib
libStudyNdk.dylib: Mach-O 64-bit dynamically linked shared library x86_64

要注意的是,Mac平台上的动态库,和Android平台上使用的.so平台上不一样的,就算是在Mac平台上,编译为后缀名为.so,不过不是生成arm架构编译的话,直接放在apk包中去调用,同样会出现异常,接下来的文章会讲到。

Android Studio编写NDK代码

向Android Studio的项目添加 C 和 C++ 代码实现JNI,需要先下载 NDK 和构建工具,然后,再创建支持C/C++的项目。

  • 如果是新创建的项目,非常简单,直接勾选Include C++ Support就可以了;

  • 旧项目的话,步骤比较麻烦,需要以下三个步骤
    向您的项目添加 C 和 C++ 代码

    详细步骤可以参考官方的链接

    • 创建新的原生源文件
    • 创建 CMake 构建脚本
    • 将 Gradle 关联到您的原生库

创建支持 C/C++ 的新项目

创建新项目后,编译运行,可以看到代码结构如下,多了两个文件,如果想添加新的jni方法,就可以在native-lib.cpp中增加

image-20180827202406568
image-20180827202406568

编译运行,界面如下,第一个hello world就完成了

image-20180827202438268
image-20180827202438268

通过解压应用包也可以看到,导入了libnative-lib.so

image-20180906130657422
image-20180906130657422

目录介绍

CMakeList.txt

在该文件中,采用Cmake语法规则,自动生成相关的makefile文件,并编译出动态库或者静态库。接下来的文章会具体介绍到。

builde.gradle

查看gradle文件可以看到多出来externalNativeBuild方法,第一个为设置编译的一些属性,另外一个设置CmakeLists.txt配置文件的路径。

android {
    defaultConfig {
        externalNativeBuild {
            cmake {
                cppFlags ""
                // 设置编译c/c++ 源文件的cpu类型,默认不设置的话,编译为armeabi-v7a
                // 如果不设置,连接不同的设备,gralde会自动根据连接的设备,如模拟器,armeabi-v7a架构的手机,编译出不同的架构
                abiFilters "armeabi-v7a","x86","arm64-v8a"
            }
        }
    }
    externalNativeBuild {
        cmake {
            // 在同个目录下,直接写文件名就可以了
            path "CMakeLists.txt"
        }
    }
}    

连接模拟器(x86架构)

image-20180906130657422
image-20180906130657422

连接小米4(armeabi-v7a架构)

image-20180907140137140
image-20180907140137140

连接小米6(arm64-v8a架构)

image-20180907140305906
image-20180907140305906

参考资料

推荐阅读更多精彩内容