NDK在验签加密项目中的应用

基本概念介绍

  • JNI 全称 Java Native Interface,Java 本地化接口,可以通过 JNI 调用系统提供的 API。操作系统,无论是 Linux,Windows 还是 Mac OS,或者一些汇编语言写的底层硬件驱动都是 C/C++ 写的。Java和C/C++不同 ,它不会直接编译成平台机器码,而是编译成虚拟机可以运行的Java字节码的.class文件,通过JIT技术即时编译成本地机器码,所以有效率就比不上C/C++代码,JNI技术就解决了这一痛点,JNI 可以说是 C 语言和 Java 语言交流的适配器、中间件

  • NDK(Native Development Kit) : 原生开发工具包,即帮助开发原生代码的一系列工具,包括但不限于编译工具、一些公共库、开发IDE等。

  • NDK 工具包中提供了完整的一套将 c/c++ 代码编译成静态/动态库的工具,而 Android.mk 和 Application.mk 你可以认为是描述编译参数和一些配置的文件。比如指定使用c++11还是c++14编译,会引用哪些共享库,并描述关系等,还会指定编译的 abi。只有有了这些 NDK 中的编译工具才能准确的编译 c/c++ 代码。

  • CMake 则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt)生成 对应 makefile 或 project 文件,然后再调用底层的编译。

JNI 与 NDK 区别

  • JNI:JNI是一套编程接口,用来实现Java代码与本地的C/C++代码进行交互;
  • NDK: NDK是Google开发的一套开发和编译工具集,可以生成动态链接库,主要用于Android的JNI开发。

JNI 作用

  • 扩展:JNI扩展了JVM能力,驱动开发,例如开发一个wifi驱动,可以将手机设置为无限路由;
  • 高效: 本地代码效率高,游戏渲染,音频视频处理等方面使用JNI调用本地代码,C语言可以灵活操作内存;
  • 复用: 在文件压缩算法 7zip开源代码库,机器视觉 OpenCV开放算法库等方面可以复用C平台上的代码,不必在开发一套完整的Java体系,避免重复发明轮子;
  • 特殊: 产品的核心技术一般也采用JNI开发,不易破解。

JNI的简要流程

最近项目中用到数据验签加密功能,就是对指定请求的数据进行加密处理。

数据加密过程是增长部门实现的一个c项目,接下来的工作就是使用增长部门开发人员提供的c项目进行JNI封装,期间遇到一些坑。

接下来讲解的知识点:

  • 将c项目编译生成一个.so文件;
  • JNI静态注册;
  • 生成供业务方使用的.java文件。

目录文件介绍

新建一个Native C++ 项目

默认moduel下的build.gradle文件部分内容

android {
    ...
    defaultConfig {
        ...
        // CMake的命令参数 可以在 .externalNativeBuild/cmake/debug/{abi}/cmake_build_command.txt 中查到
        externalNativeBuild {
            cmake {
                cppFlags "-fvisibility=hidden"
            }
        }
    }

    // 指明CMakeList.txt的路径
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }

    ndk {
        // Specifies the ABI configurations of your native
        // libraries Gradle should build and package with your APK.
        abiFilters 'armeabi-v7a' // 指定NDK需要兼容的架构
    }
}

文件目录结构

NDK在验签加密项目中的应用_第1张图片

业务方只需要将EncryptUtils.java文件和build/intermediates/cmake/debug/obj/armeabi-v7a下的.so文件拷贝到项目中就可以使用.so中提供的功能。

JNI开发流程的步骤

  • 第1步:在Java中先声明一个native方法
  • 第2步:编译Java源文件javac得到.class文件
  • 第3步:通过javah -jni命令导出JNI的.h头文件(可以直接在java代码中编写一个native方法, 会在方法上方有个红色, 点击弹框提示 Create jni function for XXX即可, 或者快捷键开发工具自动修复)
  • 第4步:使用Java需要交互的本地代码,实现在Java中声明的Native方法(如果Java需要与C++交互,那么就用C++实现Java的Native方法。)
  • 第5步:将本地代码编译成动态库(Windows系统下是.dll文件,如果是Linux系统下是.so文件,如果是Mac系统下是.jnilib)
  • 第6步:通过Java命令执行Java程序,最终实现Java调用本地代码。

JNI的命名规则

extern "C" JNIEXPORT jstring JNICALL Java_com_ke_inspectionsign_EncryptUtils_encrypt
        (JNIEnv *jniEnv, jclass clazz)

jstring 是返回值类型
Java_com_ke_inspectionsign 是包名
EncryptUtils 是类名
encrypt 是方法名

其中JNIExportJNICALL是不固定保留的关键字不要修改

PS:javah 是JDK自带的一个命令,-jni参数表示将class 中用到native 声明的函数生成JNI 规则的函数

将jni文件所在的包拖到Terminal中,执行javac EncryptUtils.java

将java文件拖到Terminal中,执行javah -jni com.xxx.xxx.EncryptUtils

获取class文件的方法属性签名 javap -s EncryptUtils

CMakeLists.txt文件的分析

# 设置头文件搜索路径
include_directories(src/main/cpp/thirdparty/sds)
include_directories(src/main/cpp/thirdparty/cJSON)

 

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
        encryptsign # 指定产物.so的名称

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s). 
        # 资源路径是相对路径,相对于本CMakeLists.txt所在目录
        src/main/cpp/encrypt.c
        src/main/cpp/native-lib.cpp
        src/main/cpp/thirdparty/sds/sds.c
        src/main/cpp/thirdparty/cJSON/cJSON.c
        src/main/cpp/thirdparty/cJSON/cJSON_Utils.c
        )

add_library:创建一个静态或者动态库,并提供其关联的源文件路径,开发者可以定义多个库,CMake会自动去构建它们。

Gradle可以自动将它们打包进APK中。

第一个参数——encryptsign:是产物.so的的名称
第二个参数——SHARED:是库的类别,是动态的还是静态的
第三个参数——src/main/cpp/native-lib.cpp ...:是库的源文件的路径

参数二的取值
STATIC:静态链接库,当生成可执行程序时进行链接。在Linux下为.a文件
SHARED:动态链接库,可执行程序运行时动态加载并链接,源文件由.c(pp)提供。在Linux下为.so文件
MODULE:模块,可执行程序运行时动态加载,但不链接到可执行程序中。
BUILD_SHARED_LIBS变量决定了默认值,如果为on则为动态的,否则为静态的
可以通过设置CMAKE_LIBRARY_OUTPUT_DIRECTORY变量指定输出的路径

# 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.
# 依赖NDK中的库 使用CMake默认的搜索路径中的本地系统库
find_library( # Sets the name of the path variable
        # android系统每个类型的库会存放一个特定的位置,而log库存放在log-lib中
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        # android系统在c环境下打log到logcat的库
        log)

find_library( # Sets the name of the path variable. 
        # 在C语言中使用了AndroidBitmap,需要依赖Bitmap库
        jnigraphics-lib

        # Specifies the name of the NDK library that 
        # you want CMake to locate.
        jnigraphics
        )


find_library:找到一个预编译的库,并作为一个变量保存起来。由于CMake在搜索库路径的时候会包含系统库,并且CMake会检查它自己之前编译的库的名字,所以开发者需要保证开发者自行添加的库的名字的独特性。

第一个参数——log-lib:设置路径变量的名称
第一个参数——log:指定NDK库的名称,这样CMake就可以找到这个库

 

# 加载自定义库或者第三方库或者.a静态库等目标
# 如果是.a静态库 STATIC  如果是.so库 SHARED
# add_library(crypto STATIC IMPORTED)
# 指定该库文件的路径
#set_target_properties(crypto
        PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/lib/libcrypto.a)

# add_library(ssl STATIC IMPORTED)
# set_target_properties(ssl
        PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/lib/libssl.a)

# add_library(curl STATIC IMPORTED)
# set_target_properties(curl
        PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/lib/libcurl.a)

 

# 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.
# 为该动态库添加要链接的库 库文件的顺序符合gcc链接顺序的规则,即被依赖的库放在依赖它的库的后面
target_link_libraries( # Specifies the target library.
        encryptsign # 输出.so名称
 
        # Links the target library to the log library
        # included in the NDK.
        ssl
        crypto
        ${log-lib}
        z)

target_link_libraries( [item1] [item2] [...]
                      [[debug|optimized|general] ] ...)
target_link_libraries:指定CMake链接到目标库。开发者可以链接多个库,比如开发者可以在此定义库的构建脚本,并且预编译第三方库或者系统库。

第一个参数——encryptsign:指定的目标库
第一个参数——${log-lib}:将目标库链接到NDK中的日志库,

是指通过add_executable()和add_library()指令生成已经创建的目标文件target_link_libraries(hello A B.a C.so)在上面的命令中,libA.so可能依赖于libB.a和libC.so,如果顺序有错,链接时会报错。B.a会告诉CMake优先使用静态链接库libB.a,C.so会告诉CMake优先使用动态链接库libC.so,也可直接使用库文件的相对路径或绝对路径。使用绝对路径的好处在于,当依赖的库被更新时,make的时候也会重新链接。

 

package com.ke.inspectionsign;

// 业务方需要拷贝的.java文件
public class EncryptUtils {

  // Used to load the 'encryptsign' library on application startup.
  static {
    System.loadLibrary("encryptsign"); // encryptsigin
  }

  /**
   * A native method that is implemented by the ' encrypt_lib' native library,
   * which is packaged with this application.
   */

  public static native String encrypt(String app_version);
}

 

这里生成jni文件的步骤:

1) 将jni文件所在的包拖到Terminal中,执行javac EncryptUtils.java

2) 将java文件拖到Terminal中,执行javah -jni com.xxx.xxx.EncryptUtils

3) 获取class文件的方法属性签名 javap -s EncryptUtils

4) 由于采用的是静态注册JNI,要求JNI中的java文件路径和业务方调用的java文件路径一致。

 

JNI 的两种注册方式

静态注册JNI的原理:根据函数名建立 Java 方法和 JNI 函数的一一对应关系。流程如下:

  • 先编写 Java 的 native 方法;
  • 然后用 javah 工具生成对应的头文件,执行命令 javah packagename.classname可以生成由包名加类名命名的 jni 层头文件,或执行命名javah -o custom.h packagename.classname,其中 custom.h 为自定义的文件名;
  • 实现 JNI 里面的函数,再在Java中通过System.loadLibrary加载 so 库即可;

静态注册的方式有两个重要的关键词 JNIEXPORT 和 JNICALL,这两个关键词是宏定义,主要是注明该函数式 JNI 函数,当虚拟机加载 so 库时,如果发现函数含有这两个宏定义时,就会链接到对应的 Java 层的 native 方法。

需要注意几点特殊规则:

  • 包名或类名或方法名中含下划线 _ 要用 _1 连接;
  • 重载的本地方法命名要用双下划线 __ 连接;
  • 参数签名的斜杠 “/” 改为下划线 “_” 连接,分号 “;” 改为 “_2” 连接,左方括号 “[” 改为 “_3” 连接;
    另外,对于 Java 的 native 方法,static 和非 static 方法的区别在于第二个参数,static 的为 jclass,非 static 的 为 jobject;JNI 函数中是没有修饰符的。

优点:
实现比较简单,可以通过 javah 工具将 Java代码的 native 方法直接转化为对应的native层代码的函数;
缺点:

  • javah 生成的 native 层函数名特别长,可读性很差;
  • 后期修改文件名、类名或函数名时,头文件的函数将失效,需要重新生成或手动改,比较麻烦;
  • 程序运行效率低,首次调用 native 函数时,需要根据函数名在 JNI 层搜索对应的本地函数,建立对应关系,有点耗时。

动态注册 JNI 的原理:直接告诉 native 方法其在JNI 中对应函数的指针。通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关联关系,步骤:

  • 先编写 Java 的 native 方法;
  • 编写 JNI 函数的实现(函数名可以随便命名);
  • 利用结构体 JNINativeMethod 保存Java native方法和 JNI函数的对应关系;
  • 利用registerNatives(JNIEnv* env)注册类的所有本地方法;
  • 在 JNI_OnLoad 方法中调用注册方法;
  • 在Java中通过System.loadLibrary加载完JNI动态库之后,会自动调用JNI_OnLoad函数,完成动态注册。

       当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个JNI_OnLoad函数并调用该函数,这个函数的作用是告诉Dalvik虚拟机此C库使用的是哪一个JNI版本,如果你的库里面没有写明JNI_OnLoad()函数,VM会默认该库使用最老的JNI 1.1版本。由于最新版本的JNI做了很多扩充,也优化了一些内容,如果需要使用JNI新版本的功能,就必须在JNI_OnLoad()函数声明JNI的版本。同时也可以在该函数中做一些初始化的动作,其实这个函数有点类似于Android中的Activity中的onCreate()方法。该函数前面也有三个关键字分别是JNIEXPORTJNICALLjint。其中JNIEXPORTJNICALL是两个宏定义,用于指定该函数时JNI函数。jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,用于衔接Java层和JNI层。

PS:与JNI_OnLoad()函数相对应的有JNI_OnUnload()函数,当虚拟机释放的该C库的时候,则会调用JNI_OnUnload()函数来进行善后清除工作。

动态注册实现步骤:

1. 声明的本地方法

public static native List parse(ArrayList

address);

2. 编写 JNI 函数的实现

JNIEXPORT jobject JNICALL crash_parse(JNIEnv *env, jclass jcl, jobject jobj) {
    ljcrash_address_info info;
    //获取ArrayList 对象
    jclass jcs_alist = env->GetObjectClass(jobj);
    //获取Arraylist的methodid
    jmethodID list_get = env->GetMethodID(jcs_alist, "get", "(I)Ljava/lang/Object;");
    jmethodID list_size = env->GetMethodID(jcs_alist, "size", "()I");
    jint len = env->CallIntMethod(jobj, list_size);

    for (int i = 0; i < len; i++) {
        jobject addressObj = env->CallObjectMethod(jobj, list_get, i);
        //获取Address类
        jclass addressClazz = env->GetObjectClass(addressObj);
        jmethodID archId = env->GetMethodID(addressClazz, "getArch", "()Ljava/lang/String;");
        jstring archValue = (jstring) env->CallObjectMethod(addressObj, archId);
        const char * arch = env->GetStringUTFChars(archValue, 0);
        LOGV("arch = %s", arch);
        // ArrayList addresses; 打印该属性集合的数值
        jmethodID addressesMethodID = env->GetMethodID(addressClazz, "getAddresses", "()Ljava/util/ArrayList;");
        jobject addressesObj = env->CallObjectMethod(addressObj, addressesMethodID);
        jclass addressesClazz = env->GetObjectClass(addressesObj);
        //method in class ArrayList 获取集合大小
        jmethodID addresses_get = env->GetMethodID(addressesClazz,"get","(I)Ljava/lang/Object;");
        jmethodID addresses_size = env->GetMethodID(addressesClazz,"size","()I");
        len = env->CallIntMethod(addressesObj, addresses_size);
        for(int j = 0; j < len; j++){
            jstring addressStr = (jstring) (env->CallObjectMethod(addressesObj, addresses_get, j));
            const char * address = env->GetStringUTFChars(addressStr, 0);
            LOGV("address = %s", address);
        }
    }
    //获取ArrayList类引用
    jclass list_jcs = env->FindClass("java/util/ArrayList");
    if (list_jcs == NULL) {
        LOGV("ArrayList no  find!");
        return NULL;
    }
    //获取ArrayList构造函数id
    jmethodID list_init = env->GetMethodID(list_jcs, "", "()V");
    //创建一个ArrayList对象
    jobject list_obj = env->NewObject(list_jcs, list_init, "");
    //获取ArrayList对象的add()的methodID
    jmethodID list_add = env->GetMethodID(list_jcs, "add", "(Ljava/lang/Object;)Z");
    //获取Symbol类
    jclass symbol_cls = env->FindClass("com/ke/crash/ios/jni/Symbol");
    //获取Symbol的构造函数
    jmethodID symbol_init = env->GetMethodID(symbol_cls, "",
                                             "(Ljava/lang/String;Ljava/lang/String;)V");
    // 初始化结构体
    // Fatal signal 11 (SIGSEGV), code 2, fault addr 0x72df900500 in tid 19394 (om.ke.crash.ios)
    ljcrash_symbol_info  symbol_info = {};
    symbol_info.address = "上地";
    symbol_info.symbol = "com.ke.crash.ios";
    // 填充ArrayList
    for (int i = 0; i < len; i++) {
        jobject symbol_obj = env->NewObject(symbol_cls, symbol_init, env->NewStringUTF(symbol_info.address),
                                            env->NewStringUTF(symbol_info.symbol));
        env->CallBooleanMethod(list_obj, list_add, symbol_obj);
    }
    return list_obj;
}

3. 利用结构体 JNINativeMethod 保存Java native方法和 JNI函数的对应关系

static JNINativeMethod g_methods[] = {
        { "parse", "(Ljava/util/ArrayList;)Ljava/util/List;", (jobject)crash_parse}
};

4. 利用registerNatives(JNIEnv* env)注册类的所有本地方法并在 JNI_OnLoad 方法中调用注册方法;

JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm,void *reserved) {
    JNIEnv *env;
    if (vm->GetEnv((void **) &env,JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    jclass javaClass = env->FindClass(JNI_REG_CLASS);
    if (javaClass == NULL){
        return JNI_ERR;
    }

    int method_count = sizeof(g_methods) / sizeof(g_methods[0]);
    if (env->RegisterNatives(javaClass, g_methods, method_count) < 0) {
        return JNI_ERR;
    }

    return JNI_VERSION_1_6;
}

 

遇到的坑

SIMPLE: Error configuring

解决办法: 在工程目录下, 修改build.gradle

classpath 'com.android.tools.build:gradle:3.1.3' 版本号改为3.2.1 

 

默认生成的是.cpp文件,c项目提供的是c语言的.h文件,为了让c++兼容c语言的全局函数, 需要在.h文件的方法加上

extern "C" {
    unsigned char *app_encrypt(security_data data);
}

 

早期版本中的编译错误  undefined reference to `__android_log_print'

解决方法: target_link_libraries(

               ${log-lib}

               z )

z表示linux下的{log-lib}库, 最近写该文档时发现没有这个z配置也能生成so库。

 

你可能感兴趣的:(android知识点)