• Connor学Android - JNI和NDK编程


    在这里插入图片描述

    Learn && Live

    虚度年华浮萍于世,勤学善思至死不渝

    前言

    Hey,欢迎阅读Connor学Android系列,这个系列记录了我的Android原理知识学习、复盘过程,欢迎各位大佬阅读斧正!原创不易,转载请注明出处:http://t.csdn.cn/YshKi,话不多说我们马上开始!

    1.JNI

    Java JNI 是指 Java Native Interface,即 Java 本地接口,它是为了方便 Java 调用 C、C++ 等本地代码所封装的一层接口

    1.1 JNI 的开发流程

    在 Java 中声明 native 方法

    在 Java 中声明 native 方法,其内部可以完成如下操作

    (1)调用 System.loadLibrary 方法加载 JNI 的动态库

    (2)声明 native 的 get、set 方法

    package com.connor;
    
    import java.lang.System;
    
    public class JniTest {
        static {
            System.loadLibrary("jni-test");
        }
        
        public static void main(String[] args) {
            JniTest jniTest = new JniTest();
            System.out.println(jniTest.get());
            jniTest.set("hello world");
        }
        
        public native String get();
        public native void set(String str);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    编译 Java 源文件得到 .class 文件,然后通过 javah 命令导出 JNI 的头文件

    (1)javah 命令会自动生成一个 com_connor_JniTest.h 头文件,其内声明了代码风格、get 和 set 方法的 C 语言版本

    (2)函数名的格式遵循如下规则:Java_包名_类名_方法名,以 set 方法为例,对应的命名是 JNIEXPORT void JNICALL Java_com_connor_JniTest_set(JNIEnv*, jobject, jstring)

    • com_connor 是包名

    • JniTest 是类名

    • jstring 代表 String 类型的参数

    • JNIEnv*:表示一个指向 JNI 环境的指针,可以通过它来访问 JNI 提供的接口方法

    • jobject:表示 Java 对象中的 this

    • JNIEXPORT:JNI 定义的宏,作用是保证在本动态库中声明的方法 , 能够在其他项目中、外部代码中可以被调用,根据不同平台替换成不同的声明

      • Windows 平台:#define JNIEXPORT __declspec(dllexport)
      • Linux 平台:#define JNIEXPORT _attribute_ ((visibility (“default”)))
        • 当-fvisibility=hidden时,动态库中的函数默认是被隐藏的即 hidden。
        • 当-fvisibility=default时,动态库中的函数默认是可见的。
    • JNICALL:JNI 定义的宏,在 Windows 中调用函数时 , 该函数的参数是以栈的形式保存的,而在 Linux 平台没有对其进行定义

    • extern C:用于声明内部的函数是采用 C 语言的命名风格来编译,如果 JNI 使用 C++ 来实现时,会导致 JNI 在链接时无法根据函数名查找到具体的函数,无法完成 JNI 调用

    #include
    
    #ifndef _Included_com_connor_JniTest
    #define _Included_com_connor_JniTest
    #ifdef __cplusplus
    extern "C" {
    #endif
    	/ *
      	* Class:		com_connor_JniTest
      	* Method:		get
      	* Signature:	()Ljava/lang/String;
      	*/
        JNIEXPORT jstring JNICALL Java_com_connor_JniTest_get(JniEnv*, jobject);
        
        / *
      	* Class:		com_connor_JniTest
      	* Method:		set
      	* Signature:	(Ljava/lang/String;)V
      	*/
        JNIEXPORT void JNICALL Java_com_connor_JniTest_set(JNIEnv*, jobject, jstring);
        
    #ifdef __cplusplus
    }
    #endif
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    实现 JNI 方法

    JNI 方法是指 Java 中声明的 native 方法,可以选择 C++ 或 C 来实现

    (1)首先,在工程的主目录下创建一个子目录,名称自定

    (2)然后将之前通过 javah 生成的头文件复制到这个目录下

    (3)接着创建 test.cpp 和 test.c 两个文件完成内部的实现

    编译 so 库并在 Java 中调用

    使用 gcc 编译实现 JNI 方法的 cpp、c 文件

    gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC test.c -o libjni-test.so
    
    • 1

    (1)/usr/lib/jvm/java-7-openjdk-amd64 为本地 jdk 的安装路径,其他环境编译时将其指向本机的 jdk 路径即可

    (2)libjni-test.so 是生成的 so 库的名字,可以根据这个名称在第一步中加载到 Java 中,其中 lib、.so 可以省略

    java 指令执行 Java 程序

    java -Djava.library.path=jni com.connor.JniTest
    
    • 1

    其中 -Djava.library.path=jni 用于指明 so 库的路径

    1.2 JNI 的数据类型

    基本类型

    JNI 类型Java 类型描述
    jbooleanboolean无符号 8 位整型
    jbytebyte有符号 8 位整型
    jcharchar无符号 16 位整型
    jshortshort有符号 16 位整型
    jintint32 位整型
    jlonglong64 位整型
    jfloatfloat32 位浮点型
    jdoubledouble64 位浮点型
    voidvoid无类型

    引用类型

    JNI 类型Java 类型描述
    jobjectObjectObject 类型
    jclassClassClass 类型
    jstringString字符串
    jobjectArrayObject[]对象数组
    jbooleanArrayboolean[]boolean 数组
    jbyteArraybyte[]byte 数组
    jcharArraychar[]char 数组
    jshortArrayshort[]short 数组
    jintArrayint[]int 数组
    jlongArraylong[]long 数组
    jfloatArrayfloat[]float 数组
    jdoubleArraydouble[]double 数组
    jthrowableThrowableThrowable
    1.3 JNI 的类型签名

    类的签名

    Java 类型签名Java 类型签名
    booleanZlongJ
    byteBfloatF
    charCdoubleD
    shortSvoidV
    intI

    对象的签名

    它的签名就是对象所属的类的签名,String → Ljava/lang/String

    数组的签名

    签名为 [ + 类型签名,如 int[] → [I、int[][] → [[I

    方法的签名

    签名为 (参数类型签名) + 返回值类型签名

    boolean fun1(int a, double b, int[] c) → (ILjava/lang/String;[I)Z

    1.4 JNI 调用 Java 方法的流程

    (1)首先定义一个静态方法

    public static void methodCalledByJni(String msgFromJni) {
        Log.d(TAG, "methodCalledByJni, mst:" + msgFromJni);
    }
    
    • 1
    • 2
    • 3

    (2)然后在 JNI 中调用上面定义的静态方法

    • 首先根据全类名找到声明 native 方法的类
    • 根据方法名、方法签名找到对应的方法
    • 接着通过 JNIEnv 对象的 CallStaticVoidMethod 方法来调用 Java 对应的方法
    void callJavaMethod(JNIEnv* env, jobject thiz) {
        jclass clazz = env -> FindClass("com/connor/JniTestApp/MainActivity");
        ...
        jmethodID id = env -> GetStaticMethodID(clazz, "methodCalledByJni", "(Ljava/lang/String;)V");
        ...
        jstring msg = env -> NextStringUTF("msg send by callJavaMethod in test.cpp");
        env -> CallStaticVoidMethod(clazz, id, msg);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    (3)最后在 Java_com_connor_JniTestApp_MainActivity_get 方法中调用 callJavaMethod 方法

    (4)JNI 调用 Java 的过程和 Java 中方法的定义有很大关联,针对不同类型的 Java 方法,JNIEnv 提供了不同的接口去调用

    2.NDK

    (1)NDK 是 Android 提供的一个工具集合,通过 NDK 可以在 Android 中更方便地通过 JNI 来访问本地代码

    (2)NDK 提供了交叉编译器,使用时只需要简单地修改 mk 文件就可以生成特定的 CPU 平台的动态库

    (3)在 Linux 环境中,JNI 和 NDK 开发所用到的动态库的格式是以 .so 为后缀的文件,简称 so 库

    (4)使用 NDK 有如下好处:

    • 提高代码的安全性。由于 so 库反编译比较困难,因此 NDK 提高了 Android 程序的安全性
    • 可以很方便地使用目前已有的 C/C++ 开源库
    • 便于平台间的移植。通过 C/C++ 实现的动态库可以很方便地在其他平台上使用
    • 提高程序在某些特定情形下的执行效率,但是并不能明显提升 Android 程序的性能
    NDK 的开发流程

    下载并配置 NDK

    创建 Android 项目,并声明所需要的 native 方法

    与 Java JNI 类似,比如可以在 Activity 中完成加载、声明

    实现 Android 项目中所声明的 native 方法

    在外部创建 jni 目录,创建三个文件:test.cpp、Android.mk、Application.mk

    (1)Android.mk 文件中会设置如下几个参数

    • LOCAL_MODULE:表示模块的名称
    • LOCAL_SRC_FILES:表示需要参与编译的源文件

    (2)Application.mk 文件中常用的配置项是 APP_ABI,表示 CPU 的架构平台的类型,如 armeabi、x86、mips、all

    (3)默认情况下 NDK 会编译产生各个 CPU 平台的 so 库(all),通过 APP_ABI 指定平台即可只编译该平台下的 so 库了

    // Android.mk
    LOCAL_PATH := $(call my-dir)
        
    include $(CLEAR_VARS)
    
    LOCAL_MODULE := jni-test
    LOCAL_SRC_FILES := test.cpp
    
    include $(BUILD_SHARED_LIBRARY)
    
    // Application.mk
    APP_ABI := armeabi
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    切换到 jni 目录的父目录,然后通过 ndk-build 命令编译产生 so 库

    (1)此时会创建一个和 jni 平级的 libs 目录,libs 内存放的就是 so 库的目录

    (2)需要注意,ndk-build 默认指定 jni 目录为本地源码的目录,如果源码不在这个目录下,则无法成功编译

    (3)之后会在 app/src/main 中创建一个 jniLibs 目录,将生成的 so 库复制到这个目录中,然后就可以通过 AndroidStudio 编译运行了

    • 这个文件的位置、命名可以通过 App 的 build.gradle 文件指定:sourceSets.main { jniLibs.srcDir ‘src/main/jni_libs’ }
    android {
    	...
    	sourceSets.main {
    		jniLibs.srcDir 'src/main/jni_libs'
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    (4)除了通过命令编译,还可以自动编译产生 so 库

    • 首先在 App 的 build.gradle 的 defaultConfig 区域内添加 NDK 选项,其中 moduleName 指定打包后的 so 库的文件名
    android {
    	...
    	defaultConfig {
    		...
    		ndk {
    			moduleName "jni-test"
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 接着需要将 JNI 的代码放在 app/src/main/jni 目录下,也可以在 build.gradle 的 sourceSets.main 区域内指定 JNI 的代码路径
    android {
    	...
    	sourceSets.main {
    		jni.srcDirs 'src/main/jni_src'
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 同样可以指定产生某个平台的 so 库,修改 build.gradle 配置,然后在 Build Variants 面板中选择 armDebug 选项编辑即可
    android {
    	...
    	productFlavors {
    		arm {
    			ndk {
    				abiFilter "armeabi"
    			}
    		}
    		x86 {
    			ndk {
    				abiFilter "x86"
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  • 相关阅读:
    国庆中秋宅家自省: Python在Excel中绘图尝鲜
    模拟实现C语言--memcpy函数和memmove函数
    1.QML Hello world
    docker容器
    四川易点慧电子商务抖音小店打造便捷生活新体验
    8月外贸新规
    UI组件Kendo UI for Angular R3 2022亮点——让应用程序体验更酷炫
    【力扣SQL】几个常见SQL题
    MySQL中创建partition表的几种方式
    电商评论文本情感分类(中文文本分类)(第二部分-Bert)
  • 原文地址:https://blog.csdn.net/scottb/article/details/126686931