浅析Android NDK开发

浅析Android NDK开发

    本文主要介绍JNI、NDK相关知识,使用两个例子来阐述如何进行JNI和Android NDK开发。

一、 JNI和NDK简介

JNI全称Java Native Interface,Java本地接口,这个接口提供了Java与C/C++互相调用的能力。在Java中用native关键字标识该方法为本地方法,方法具体实现由C/C++代码实现。Java是跨平台的编程语言,而使用JNI会破坏该特性,在实际工作中使用JNI需要针对各系统平台生成不同的动态和静态库。

NDK全称Native Development Kit,原生开发工具包,提供了在Android应用中利用C/C++代码的工具。

二、 Java调用C代码实现时间格式化

1. 编写native方法

```

package com.example.jni;

import java.util.Date;

public class HelloJni {

    /**

    * 格式化时间

    */

    public static native String formatDate(Date date, String format);

}

```

2. native方法生成头文件

    进入src目录,编译javac com.example.jni.HelloJni.java,然后执行javah com.example.jni.HelloJni,可以看到在生成了com_example_jni_HelloJni.h,文件内容如下:

```

#include

#ifndef _Included_com_example_jni_HelloJni

#define _Included_com_example_jni_HelloJni

#ifdef __cplusplus

extern "C" {

#endif

/*

* Class:    com_example_jni_HelloJni

* Method:    formatDate

* Signature: (Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;

*/

JNIEXPORT jstring JNICALL Java_com_example_jni_HelloJni_formatDate

  (JNIEnv *, jclass, jobject, jstring);

#ifdef __cplusplus

}

#endif

#endif

```

代码解析:   1. 头文件中引入了jni.h、jni_md.h(jni.h内引入)文件,两个文件位与$JAVA_HOME/include和$JAVA_HOME/include/darwin(与操作系统有关)目录下,文件中定义了JNI所支持的类型、接口和方法,在代码中我们可以看到java数据类型与C/C++的映射关系和结构体实的对象, 映射表格可见下表,大部分情况java基础数据类型在前加"j"即为对应的C/C++类型。

                        字符(方法签名、返回类型用)  C类型          JAVA类型

                        V                    void          void

                        Z                    jboolean      boolean

                        I                    jint          int

                        J                    jlong        long

                        D                    jdouble      double

                        F                    jfloat        float

                        B                    jbyte        byte

                        C                    jchar        char

                        S                    jshort        short

                        [I                    jintArray    int[]

                        Ljava/lang/String;    jstring      char*

                        L${以/连接的全路径类名}; jobject      ${类}

2. 方法解析: 方法名由JAVA_${package}_${class}_${method}组成,在Java调用native方法JVM会时会自动定位到该库文件的该方法,

        该方法第一个参数类型JNIEnv表示Java运行环境,全局唯一,是JVM有JNI层的体现,该类型实现在jni.h中的struct, 在jni.h中定义了一系列方法操作获取Java的类和方法、创建对象、执行方法等,通过其可以获取到java中的类jclass、方法jmethodID和执行方法等,第二个参数是jclass,表示Java native方法为类方法,指向Java类,若Java方法为对象方法,该参数为jobject, 表示当前调用native方法的对象。

            下面将举出JNIEnv该类型常用的方法

                1. FindClass(env,${全路径类名}), 该方法获取Java类,返回结果为jclass;

                2. GetMethodID(env,jclass,${方法名},${方法签名}) 获取Java类的方法类型jmethodID,其中方法签名生成"$(参数标识列表)"+返回标识字符,在刚生成的头文件可以看到签名为"(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String",还可使用javap -s com.${全路径类名} 中的descriptor,具体参数签名规则为”L+全限定类名+;”三部分组成,其中全限定类名以”/”分隔,而不是用”.”或”_”分隔。如Java方法:long fun(int n, String str, int[] arr);其方法签名为(ILjava/lang/String;[I)J括号里的内容分成三部分,之间没有空格,即”I”,”Ljava/lang/String;”和”[I”,分别代表int,String,int[]。括号后面是返回值类型签名,J代表long型。特列而言,构造函数也是方法,方法名为init;

                3. NewObject,该方法是生成JAVA对象,需传入构造函数的JMethodId和参数;

                4. CallObjectMethod、CallStaticObjectMethod、CallStaticBooleanMethod等,这些方法是执行Java的对象方法和类方法。

3. 实现头文件, com_example_jni_HelloJni.c

```

#include "com_example_jni_HelloJni.h"

JNIEXPORT jstring JNICALL

Java_com_example_jni_HelloJni_formatDate(JNIEnv *env, jclass clazz, jobject date, jstring format)

{

    jclass formatClazz = (*env)->FindClass(env, "java/text/SimpleDateFormat"); //获取SimpleDateFormat.class

    jmethodID constructorMethod =

        (*env)->GetMethodID(env, formatClazz, "", "(Ljava/lang/String;)V");              //获取构造函数方法

    jobject simpleDateFormat = (*env)->NewObject(env, formatClazz, constructorMethod, format); //生成对象

    jmethodID formatMethod =

        (*env)->GetMethodID(env, formatClazz, "format", "(Ljava/util/Date;)Ljava/lang/String;"); //获取format(Date)方法

    jstring result = (*env)->CallObjectMethod(env, simpleDateFormat, formatMethod, date);        //执行方法

    return result;

}

```

4. 编译C代码生成动态链接库

        1. 编译生成链接文件      gcc -I $JAVA_HOME/include/ -I $JAVA_HOME/include/darwin/ -c -fPIC com_example_jni_HelloJni.c

        2. 生成动态链接库文件    gcc -shared -fPIC -o libhelloJni.dylib com_example_jni_HelloJni.o

        注意:  C/C++非跨平台,生成的so文件无法在所在系统平台使用,需要单独编译或使用交叉编译,本文件使用gcc编译,也可使用g++等编译。动态链接库文件后缀名windows下为dll文件,Mac为dylib,Linux为so文件。

5. JAVA调用

        使用System.loadLibrary(${去除lib前缀和扩展名的库名})或System.load(${绝对路径}),使用前者引入文件必须位与环境变量path目录下。

```

package com.example.jni;

import java.util.Date;

public class Main {

    static {

        System.load("/xx/xx/libhelloJni.dylib");

      //System.loadLibrary("helloJni");

}

    public static void main(String[] args) {

        System.out.println(HelloJni.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));

    }

}

```

三、 NDK方式实现BsDiff(Cmake方式)

1. 环境下载

AndroidStudio中配置下载NDK,SDK-Manager里下载CMake编译工具

2. Android Studio NDK环境集成

配置build.gradle

```

android{

    defaultConfig {

    ...

            externalNativeBuild {

                cmake {

                    abiFilters "armeabi","arm64-v8a","armeabi-v7a","x86","x86_64"  //指定生成的so文件支持的CPU和系统架构

                }

            }

    }

    externalNativeBuild {

        cmake {

            path "src/main/cpp/CMakeLists.txt" // CMakeLists.txt 路径

        }

    }

    ...

}

```

3. CMakeLists.txt配置

CMake语法规则详见CMake官网

```

#该指令的主要作用就是将指定的源文件生成链接文件,生右apk会自动添加到libs中

add_library{

    bsdiff

    SHARED #STATIC静态链接库等

    #{源文件1}

    #{其它源文件}

}

#引入其它动态、静态库并命名,示例假入了android log库(打印Log.x)

find_library{

    log-lib

    log

}

#链接库

target_link_libraries{

    bsdiff

    ${log-lib}

    ${其它find_libary引入的库}

}

```

4. bsdiff源文件下载

下载bsdiff-4.3、bzip2-1.0.6,解压后把.c和.h文件放入项目新建好的cpp目录下,修改bsdiff和bspatch中引入了头文件为"bzlib.h",重命名所有的的文件中main函数(多个main函数会打包失败),本例中bsdiff.c的main修改为diff_main,bspatch.c的main方法名修改为patch_main。

5. 配置源文件至CMake

添加bsdiff和bzip下的所有C文件名至Cmake的add_libray下,当文件较多时,可使用Cmake 的File块简写。

6. JNI代码编写

新建源文件main.cpp并加入到cmake下,文件内容如下

```

#include

#ifndef _Included_com_example_jni_HelloJni

#define _Included_com_example_jni_HelloJni

//外部实现,本文件为C++文件,extern C 必须,表示按C语言方式编译,若按C++方法编译则会重命名方法导致查找方法失败

extern "C" {

    int bspatch_main(int i,  char *pString[4]);

    int bsdiff_main(int i,  char *pString[4]);

}

//注意实现native方法的方法名为JAVA_${用_全路径类名}_${方法},若想使用自己定义的方法名,则需使用动态注册方法(实现JNI_OnLoad)

extern "C"

JNIEXPORT jboolean JNICALL

Java_com_example_jni_BsDiff_diffNative(JNIEnv *env, jclass clazz, jstring path1, jstring path2,

                                  jstring output) {

    //定义参数

    const char *argv[4];

    argv[0] = "";

    argv[1] = env->GetStringUTFChars(path1, 0);

    argv[2] = env->GetStringUTFChars(path2, 0);

    argv[3] = env->GetStringUTFChars(output, 0);

    int success = diff_main(4, const_cast(argv));  //调用diff库的差分main函数,第一个参数表示参数个数,第二个参数为具体的参数字符串数组,可从diff_main中看参数的判断

    //释放char*

    env->ReleaseStringUTFChars(path1, argv[1]);

    env->ReleaseStringUTFChars(path2, argv[2]);

    env->ReleaseStringUTFChars(output, argv[3]);


    return 0 == success ? JNI_TRUE : JNI_FALSE;

}

extern "C"

JNIEXPORT jboolean JNICALL

Java_com_example_jni_BsDiff_patchNative(JNIEnv *env, jclass clazz, jstring path1, jstring patch,

                                  jstring output) {

    const char *argv[4];

    argv[0] = "";

    argv[1] = env->GetStringUTFChars(path1, 0);

    argv[2] = env->GetStringUTFChars(output, 0);

    argv[3] = env->GetStringUTFChars(patch, 0);

    int success = patch_main(4, const_cast(argv)); //调用合并库的差分main函数

    env->ReleaseStringUTFChars(path1, argv[1]);

    env->ReleaseStringUTFChars(output, argv[2]);

    env->ReleaseStringUTFChars(patch, argv[3]);

    return 0 == success ? JNI_TRUE : JNI_FALSE;

}

#endif

```

7. 执行

Java直接调用native方法即可。

注: 正常而言,差分包的生成即diff方法在服务端调用,相应的移动代码至服务端项目,根据部署平台和系统编译成相应的动态链接库格式即可。

你可能感兴趣的:(浅析Android NDK开发)