当前位置: 首页 > news >正文

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);
}

编译 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<jni.h>

#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

实现 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)/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

其中 -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);
}

(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);
}

(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

切换到 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'
	}
}

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

  • 首先在 App 的 build.gradle 的 defaultConfig 区域内添加 NDK 选项,其中 moduleName 指定打包后的 so 库的文件名
android {
	...
	defaultConfig {
		...
		ndk {
			moduleName "jni-test"
		}
	}
}
  • 接着需要将 JNI 的代码放在 app/src/main/jni 目录下,也可以在 build.gradle 的 sourceSets.main 区域内指定 JNI 的代码路径
android {
	...
	sourceSets.main {
		jni.srcDirs 'src/main/jni_src'
	}
}
  • 同样可以指定产生某个平台的 so 库,修改 build.gradle 配置,然后在 Build Variants 面板中选择 armDebug 选项编辑即可
android {
	...
	productFlavors {
		arm {
			ndk {
				abiFilter "armeabi"
			}
		}
		x86 {
			ndk {
				abiFilter "x86"
			}
		}
	}
}

相关文章:

  • DOM 重点核心
  • 基于Python、wxpython的高校教务系统设计与实现
  • python的安装教程
  • DDD 洋葱架构才是 yyds!阿里大牛手记(DDD)领域驱动设计应对之道
  • Android 进阶——系统启动之SystemServer创建并启动PackageManagerService服务(十一)
  • JavaScript之document对象最常用相关知识总结
  • SpringBoot 临时属性、4种配置文件级别、自定义配置文件
  • 分布式事务(五)———可靠消息队列解决方案
  • Spring IOC概念与实现(注解方式)
  • springboot系列(二十四):如何实现Excel文件导出?这你得会 | 超级详细,建议收藏
  • 【C++】类和对象之六大默认成员函数上
  • 我在github上读清华北大|浙大计算机课程共享项目
  • 初识Cpp之 五、函数
  • 什么是Nginx服务器
  • 初识Cpp之 三、Cpp预处理器
  • JS 中的深拷贝与浅拷贝
  • 78. Subsets
  • Brief introduction of how to 'Call, Apply and Bind'
  • ES6 学习笔记(一)let,const和解构赋值
  • express.js的介绍及使用
  • Golang-长连接-状态推送
  • Leetcode 27 Remove Element
  • leetcode386. Lexicographical Numbers
  • Magento 1.x 中文订单打印乱码
  • maven工程打包jar以及java jar命令的classpath使用
  • MYSQL 的 IF 函数
  • Python连接Oracle
  • Sass Day-01
  • Spring Boot MyBatis配置多种数据库
  • SQLServer插入数据
  • Traffic-Sign Detection and Classification in the Wild 论文笔记
  • ⭐ Unity 开发bug —— 打包后shader失效或者bug (我这里用Shader做两张图片的合并发现了问题)
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • 电商搜索引擎的架构设计和性能优化
  • 多线程事务回滚
  • 类orAPI - 收藏集 - 掘金
  • 三分钟教你同步 Visual Studio Code 设置
  • 深入浅出Node.js
  • 手写双向链表LinkedList的几个常用功能
  • 腾讯优测优分享 | 你是否体验过Android手机插入耳机后仍外放的尴尬?
  • 协程
  • 移动端唤起键盘时取消position:fixed定位
  • 赢得Docker挑战最佳实践
  • Hibernate主键生成策略及选择
  • 移动端高清、多屏适配方案
  • ​一些不规范的GTID使用场景
  • #绘制圆心_R语言——绘制一个诚意满满的圆 祝你2021圆圆满满
  • (1)安装hadoop之虚拟机准备(配置IP与主机名)
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第5节(封闭类和Final方法)
  • (板子)A* astar算法,AcWing第k短路+八数码 带注释
  • (附表设计)不是我吹!超级全面的权限系统设计方案面世了
  • (附源码)springboot 校园学生兼职系统 毕业设计 742122
  • (附源码)计算机毕业设计ssm基于B_S的汽车售后服务管理系统
  • (详细版)Vary: Scaling up the Vision Vocabulary for Large Vision-Language Models
  • (一)appium-desktop定位元素原理