Android Studio开发JNI示例

JNI和NDK介绍

JNI(Java Native Interface),是方便Java调用C、C++等Native代码所封装的一层接口,相当于一座桥梁。通过JNI可以操作一些Java无法完成的与系统相关的特性,尤其在图像和视频处理中大量用到。

NDK(Native Development Kit)是Google提供的一套工具,其中一个特性是提供了交叉编译,即C或者C++不是跨平台的,但通过NDK配置生成的动态库却可以兼容各个平台。比如C在Windows平台编译后生成.exe文件,那么源码通过NDK编译后可以生成在安卓手机上运行的二进制文件.so

在AS中使用ndk-build开发JNI示例

Android Studio2.2之前对于JNI开发的支持不是很好,开发一般使用Eclipse+插件编写本地动态库。后面Google官方全面增强了对JNI的支持,包括内置NDK。

1.在AS中新建一个项目

2.声明一个native方法

package com.mercury.jnidemo;

public class JNITest {

    public native static String getStrFromJNI();

}

3.通过javah命令生成头文件

在AS的Terminal中,先进入要调用本地代码的类所在的目录,也就是在项目中的具体路径,比如这里是cd app\src\main\java。然后通过javah命令生成该类的头文件,注意包名+类名.这里是javah -jni com.mercury.jnidemo.JNITest,生成头文件com_mercury_jnidemo_JNITest.h

实际项目最终可以不包含此头文件,不熟悉C的语法的开发人员,借助于该头文件可以知道JNI的相关语法:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_mercury_jnidemo_JNITest */

#ifndef _Included_com_mercury_jnidemo_JNITest
#define _Included_com_mercury_jnidemo_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_mercury_jnidemo_JNITest
 * Method:    getStrFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_mercury_jnidemo_JNITest_getStrFromJNI
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

首先引入jni.h,里面包含了很多宏定义及调用本地方法的结构体。重点是方法名的格式。这里的JNIEXPORT和JNICALL都是jni.h中所定义的宏。JNIEnv *表示一个指向JNI环境的指针,可通过它来访问JNI提供的接口方法。jobject表示Java对象中的this.实际编写中一般只要遵循Java_包名类名方法名就好了。

4.实现JNI方法

像上面的头文件只是定义了方法,并没有实现,就像一个接口一样。这里就用C写一个简单的无参的JNI方法。
先创建一个jni目录,我直接在src的父目录下创建的,也可以在其他目录创建,因为最终只需要要的编译好的动态库。在jni目录下创建Android.mk和demo.c文件。

AndroidStudio开发JNI示例_1.png

Android.mk是一个makefile配置文件,安卓大量采用makefile进行自动化编译。LOCAL_MODULE定义的名称就是编译好的so库名称,比如这里是jni-demo,最终生成的动态库名称就叫libjni-demo.so. LOCAL_SRC_FILES表示参与编译的源文件名称,那创建的源文件就是demo.c

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := jni-demo
LOCAL_SRC_FILES := demo.c

include $(BUILD_SHARED_LIBRARY)

这里的demo.c实现了一个很简单的方法,返回String类型。

#include<jni.h>

jstring Java_com_mercury_jnidemo_JNITest_getStrFromJNI(JNIEnv *env,jobject thiz){
    return (*env)->NewStringUTF(env,"I am Str from jni libs!");
}

这时候NDK编译生成的动态库会有四个CPU平台:arm64-v8a、armeabi-v7a、x86、x86_64。如果创建Application.mk就可以指定要生成的CPU平台,语法也很简单:

APP_ABI := all

这样就会生成各个CPU平台下的动态库。

5.使用ndk-build编程生成.so库

切回到jni目录的父目录下,在Terminal中运行ndk-build指令,就可以在和jni目录同级生成一个libs文件夹,里面存放相对应的平台的.so库。同时生成的还有一个中间临时的obj文件夹,和jni文件夹可以一起删除。
需要注意,使用NDK一定要先在build.gradle下要配置ndk-build的相关路径,这样在编写本地代码时才会有相关的提示功能,并且可以关联到相关的头文件

externalNativeBuild {
        ndkBuild {
            path 'jni/Android.mk'
        }
    }

还有一点,网上很多资料都在build.gradle中加入一下代码:

sourceSets{
        main{
            jniLibs.srcDirs=['libs']
        }
    }

这样就指定了目标.so库的存放位置。但在实际使用中,就算不指定,运行时仍然可以加载正确的.so库文件,并且如果添加该代码后有时会报出以下错误:

 Error:Execution failed for task ':usejava:transformNativeLibsWithMergeJniLibsForDebug'.
    > More than one file was found with OS independent path 'lib/x86/libjni-calljava.so'
    > 

6.加载.so库并调用方法

在类初始化的时候要加载该.so库,一般会写在静态代码块里。名称就是前面的LOCAL_MODULE。

    static {
        System.loadLibrary("jni-demo");
    }

需要注意的是如果是有参的JNI方法,那么直接在参数列表里补充在jni.h预先typedef好的数据类型就可以了,JNIEnv *是必须的,jobject则不一定。
AndroidStudio开发JNI示例_jni-1.gif

JNI调用Java

不同于JNI调用C,JNI调用Java的过程不是单独存在的。还是先编写native方法,Java先通过JNI调用该方法,在方法内部再去回调类中相关的Java方法。步骤有些类似于Java中的反射。这里写定义三个点击事件,三个Native方法,三种Java的方法类型,根据相关的Log判断是否成功。

public class MainActivity extends AppCompatActivity {

    public static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    static {
        System.loadLibrary("jni-calljava");
    }

    public void noParamMethod() {
        Log.i(TAG, "无参的Java方法被调用了");
    }

    public void paramMethod(int number) {
        Log.i(TAG, "有参的Java方法被调用了" + number + "次");
    }

    public static void staticMethod() {
        Log.i(TAG, "静态的Java方法被调用了");
    }

    public void click1(View view) {
        test1();
    }

    public void click2(View view) {
        test2();
    }

    public void click3(View view) {
        test3();
    }

    public native void test1();

    public native void test2();

    public native void test3();

}

1.调用Java无参方法

  • JNI调用本地方法,根据类名找到类,注意类名用”/”分隔。
  • 找到类后,根据方法名找到方法。该函数GetMethodID最后一个形参是该形参列表的签名。不同于Java,C中是通过签名标识去找方法。
  • 获取方法的签名:首先定位到该类的字节码文件所在的父目录,一般在module\build\intermediates\classes\debug>,通过javap -s com.mercury.usejava.MainActivity获取整个类所有的内部类型签名。无参方法test1()的签名是()V
  • 通过JNIEnv对象的CallVoidMethod来完成方法的回调,最后一个形参是可变参数。
JNIEXPORT void JNICALL Java_com_mercury_usejava_MainActivity_test1
  (JNIEnv * env, jobject obj){
       //回调MainActivity中的noParamMethod
    jclass clazz = (*env)->FindClass(env, "com/mercury/usejava/MainActivity");
    if (clazz == NULL) {
        printf("find class Error");
        return;
    }
    jmethodID id = (*env)->GetMethodID(env, clazz, "noParamMethod", "()V");
    if (id == NULL) {
        printf("find method Error");
    }
    (*env)->CallVoidMethod(env, obj, id);
  }

2.调用Java有参方法

类似于无参方法,只是参数签名和可变参数的不同

3.调用Java静态方法

注意获取方法名的方法是GetStaticMethodID,调用方法的函数名是CallStaticVoidMethod,并且由于是静态方法,不应该传入jobject参数,而直接是jclass.

JNIEXPORT void JNICALL Java_com_mercury_usejava_MainActivity_test3
  (JNIEnv * env, jobject obj){
    jclass clazz = (*env)->FindClass(env, "com/mercury/usejava/MainActivity");
    if (clazz == NULL) {
        printf("find class Error");
        return;
    }
    jmethodID id = (*env)->GetStaticMethodID(env, clazz, "staticMethod", "()V");
    if (id == NULL) {
        printf("find method Error");
    }

    (*env)->CallStaticVoidMethod(env, clazz, id);
  }

相应日志

AndroidStudio开发JNI示例_jni-2.gif

使用CMake开发JNI

CMake是一个跨平台的安装(编译)工具,通过编写CMakeLists.txt,可以生成对应的makefile或project文件,再调用底层的编译。AS 2.2之后工具中增加了对CMake的支持,官方也推荐用CMake+CMakeLists.txt的方式,代替ndk-build+Android.mk+Application.mk的方式去构建JNI项目.

1.创建使用CMake构建的项目

开始前AS要先在SDK Manager中安装SDK Tools->CMake
AndroidStudio开发JNI示例_2.png
只要勾选Include C++ Support。其中会提示配置C++支持的功能.
AndroidStudio开发JNI示例_3.png
一般默认就可以了,各个选项的具体含义:
* C++ Standard:指定编译库的环境。
* Exception Support:当前项目支持C++异常处理
* Runtime Type Information Support:除异常处理外,还支持动态转类型(dynamic casting) 、模块集成、以及对象I/O

2.工程的目录结构

AndroidStudio开发JNI示例_4.png
创建好的工程主Module下直接就有.externalNativeBuild,多出一个CMakeLists.txt,相当于以前的配置文件。并且在src/main目录下多了一个cpp文件夹,里面存放的是C++文件,相当于以前的jni文件夹。这个是工程创建后AS生成的示例JNI方法,返回了一个字符串。后面开发JNI就可以按照这个目录结构。

相应的,build.gradle下也增加了一些配置。

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++14 -frtti -fexceptions"
            }
        }
    }
    buildTypes {
        ...
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

defaultConfig中的externalNativeBuild各项属性和前面创建项目时的选项配置有关,外部的externalNativeBuild则定义了CMakeLists.txt的存放路径。
如果只是在项目中使用自己和本地的一些交互,在打包APK的时候会将工程中的本地代码一并打包,只有在提供给外部使用时才需要编译成.so文件。Make Project,之后在build/intermediates/cmake/debug/obj目录下就可以看到生成的.so文件。

CMakeLists.txt

CMakeLists.txt可以自定义命令、查找文件、头文件包含、设置变量,具体可见 官方文档。项目默认生成的CMakeLists.txt核心内容如下:


# 编译本地库时我们需要的最小的cmake版本
cmake_minimum_required(VERSION 3.4.1)

# 相当于Android.mk
add_library( # Sets the name of the library.设置编译生成本地库的名字
             native-lib

             # Sets the library as a shared library.库的类型
             SHARED

             # Provides a relative path to your source file(s).编译文件的路径
             src/main/cpp/native-lib.cpp )

# 添加一些我们在编译我们的本地库的时候需要依赖的一些库,这里是用来打log的库
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 )

# 关联自己生成的库和一些第三方库或者系统库
target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

使用CMakeLists.txt同样可以指定so库的输出路径,但一定要在add_library之前设置,否则不会生效:


set(CMAKE_LIBRARY_OUTPUT_DIRECTORY 
    ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}) #指定路径
#生成的so库在和CMakeLists.txt同级目录下的libs文件夹下

如果想要配置so库的目标CPU平台,可以在build.gradle中设置

android {
    ...
    defaultConfig {
        ...
        ndk{
            abiFilters "x86","armeabi","armeabi-v7a"
        }
    }
    ...

}

需要注意的是,如果是多次使用add_library,则会生成多个so库。如果想将多个本地文件编译到一个so库中,只要最后一个参数添加多个C/C++文件的相对路径就可以

用C语言实现字符串加密

Java中实现字符串加密的一种比较简单的方法是异或,将字符串转换为字符数组,遍历对其中的每个字符用密钥(可以是字符)进行一次异或运算,生成新的字符串。如果用JNI+C实现,大致步骤如下(jstring是要加密的字符串):

1 获取jstring的长度

2 动态开辟一个跟data长度一样的char*

3 将 jstring类型转换为char数组(用char*接收)

4 遍历char数组,进行异或运算

5 将char*转换为jstring类型返回

6 释放动态开辟的堆内存空间

效果图
AndroidStudio开发JNI示例_jni-3.gif

我是用的是5.0的模拟器,有时会闪退,查看系统日志,会报出一下错误:

JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8

网上查了一下,JNI在调用NewStringUTF方法时,遇到不认识的字符就会退出,因为虚拟机dalvik/vm/CheckJni.cpp里面的checkUTFString会对字符类型进行检查。替代方案是在开始转换前,先检查char*中是否含有非UTF-8字符,有的话返回空字符串。完整代码如下:

#include<jni.h>
#include <stdlib.h>

jboolean checkUtfBytes(const char* bytes, const char** errorKind) ;

jstring Java_com_mercury_cmakedemo_MainActivity_encryptStr
        (JNIEnv *env, jobject object, jstring data){
    if(data==NULL){
        return (*env)->NewStringUTF(env, "");
    }
    jsize len = (*env)->GetStringLength(env, data);
    char *buffer = (char *) malloc(len * sizeof(char));
    (*env)->GetStringUTFRegion(env, data, 0, len, buffer);
    int i=0;
    for (; i <len ; i++) {
        buffer[i] = (char) (buffer[i] ^ 2);
    }

    const char *errorKind = NULL;
    checkUtfBytes(buffer, &errorKind);
    free(buffer);
    if (errorKind == NULL) {
        return (*env)->NewStringUTF(env, buffer);
    } else {
        return (*env)->NewStringUTF(env, "");
    }
}

//把char*和errorKind传入,如果errorKind不为NULL说明含有非utf-8字符,做相应处理
jboolean checkUtfBytes(const char* bytes, const char** errorKind) {
    while (*bytes != '\0') {
        jboolean utf8 = *(bytes++);
        // Switch on the high four bits.
        switch (utf8 >> 4) {
            case 0x00:
            case 0x01:
            case 0x02:
            case 0x03:
            case 0x04:
            case 0x05:
            case 0x06:
            case 0x07:
                // Bit pattern 0xxx. No need for any extra bytes.
                break;
            case 0x08:
            case 0x09:
            case 0x0a:
            case 0x0b:
            case 0x0f:
                /*
                 * Bit pattern 10xx or 1111, which are illegal start bytes.
                 * Note: 1111 is valid for normal UTF-8, but not the
                 * modified UTF-8 used here.
                 */
                *errorKind = "start";
                return utf8;
            case 0x0e:
                // Bit pattern 1110, so there are two additional bytes.
                utf8 = *(bytes++);
                if ((utf8 & 0xc0) != 0x80) {
                    *errorKind = "continuation";
                    return utf8;
                }
                // Fall through to take care of the final byte.
            case 0x0c:
            case 0x0d:
                // Bit pattern 110x, so there is one additional byte.
                utf8 = *(bytes++);
                if ((utf8 & 0xc0) != 0x80) {
                    *errorKind = "continuation";
                    return utf8;
                }
                break;
        }
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/wzhseu/article/details/79683045