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

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

[TOC]

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-20180827082921362

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

    image-20180827083307623

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

    image-20180827084048635

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

    image-20180827084256925

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

    image-20180827172434885

  4. 编写Java代码调用native代码

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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){},编译运行生成动态库

    1
    2
    3
    4
    5
    6
    7
    8
    #StudyNdk.cpp

    #include <jni.h>

    extern "C"
    void Java_com_guidongyuan_jnilib_MyClass_test(JNIEnv *env, jobject){
    printf("接收到Java的调用");
    }

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

    1
    2
    3
    4
    5
    6
    7
    # 在包名的上一个路径
    ➜ 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文件,则可以拷贝其方法名字

    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_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

dylib动态库

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

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

1
2
➜  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

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

image-20180827202438268

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

image-20180906130657422

目录介绍

CMakeList.txt

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

builde.gradle

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

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

image-20180907140137140

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

image-20180907140305906

参考资料

公众号:亦袁非猿

欢迎关注,交流学习