惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
CXSECURITY Database RSS Feed - CXSecurity.com
Google Online Security Blog
Google Online Security Blog
The Last Watchdog
The Last Watchdog
S
Security @ Cisco Blogs
Help Net Security
Help Net Security
Security Archives - TechRepublic
Security Archives - TechRepublic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
W
WeLiveSecurity
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
H
Hacker News: Front Page
人人都是产品经理
人人都是产品经理
aimingoo的专栏
aimingoo的专栏
Vercel News
Vercel News
Microsoft Azure Blog
Microsoft Azure Blog
小众软件
小众软件
Project Zero
Project Zero
T
Tailwind CSS Blog
V
Vulnerabilities – Threatpost
P
Privacy & Cybersecurity Law Blog
Know Your Adversary
Know Your Adversary
Last Week in AI
Last Week in AI
腾讯CDC
Schneier on Security
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
WordPress大学
WordPress大学
量子位
L
Lohrmann on Cybersecurity
J
Java Code Geeks
Cyberwarzone
Cyberwarzone
Recent Announcements
Recent Announcements
IT之家
IT之家
博客园_首页
罗磊的独立博客
博客园 - 聂微东
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Application and Cybersecurity Blog
Application and Cybersecurity Blog
F
Fortinet All Blogs
博客园 - Franky
P
Palo Alto Networks Blog
V2EX - 技术
V2EX - 技术
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Martin Fowler
Martin Fowler
N
News and Events Feed by Topic
C
Cybersecurity and Infrastructure Security Agency CISA
B
Blog RSS Feed
Cisco Talos Blog
Cisco Talos Blog
TaoSecurity Blog
TaoSecurity Blog
H
Heimdal Security Blog
G
GRAHAM CLULEY
Cloudbric
Cloudbric

音视频开发进阶

音视频教程-第三节 音视频教程-第二节 真的,AI 可能就是新时代的信息差 充值 Cursor 之后,工作有了哪些变化?🤔 个人'蒸馏'大模型能做哪些有意思的事情 DeepSeek 大模型在 Mac 上的部署和运行 音视频教程-第一节 【WebRTC 专栏】-- Android 开发集成 WebRTC 库的几种方式 【WebRTC 专栏】-- 在 Mac M1 等系列芯片编译和开发 WebRTC-Android 库 Meta Llama3 大模型在 Mac 上的部署和运行 iOS VideoToolBox 解码 HEVC Open-GOP 视频的问题排查 Flutter 状态管理之 InheritedWidget 使用和分析 用 ChatGPT 回答技术问题怎么样 ? 音视频开发系统入门大致路线 UE 4.27 添加自定义 ShadingModel 用 UE4 虚幻引擎做个捏脸小功能~~ UE4 材质练习 之 凹凸贴图偏移的使用 UE4 材质练习系列基础 OpenGL上下文创建以及共享机制 007 | 播放器系列专栏-解析 MP4 文件读取信息 006 | 播放器系列专栏-在 Mac 上查看 MP4 格式信息 干货 | 快速抽取缩略图是怎么练成的? 关于直播、WebRTC、FFmpeg 的那些事 005 | 播放器系列专栏-在 Windows 上查看 MP4 格式信息 将音视频中的花屏、绿屏、黑屏问题一网打尽 关于音视频里面的解码帧率和渲染帧率 004 | 播放器系列专栏-认识MP4视频(下) 003 | 播放器系列专栏-认识MP4视频(上) 入门或者转行音视频,应该要怎么做? H264视频文件如何缩放分辨率 002 | 播放器系列专栏-FFmpeg依赖库的配置 001 | 播放器系列专栏-关于播放器项目的一个小实践 Seek策略以及在有B帧情况下的处理 目前流媒体开发工程师工作内容主要是什么? 一个音视频领域专业问答的小圈子 干货收藏 || Vulkan Game Engine 视频教程 音视频春节假期内卷指南(实操) Vulkan 在 FFmpeg 中的支持 Windows 下 FFmpeg 和 LibX264 的编译和配置 Metal 开发 | 使用 C++ 进行接口调用 音视频开发工作经验分享 || 视频版 FFmpeg 调用 MediaCodec 硬解码到 Surface 上 代码吸猫 | 用 OpenGL 图像渲染的养猫计划 百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!! 老生常谈-FFmpeg 的编译问题轻松搞定 FFmpeg 调用 Android MediaCodec 进行硬解码(附源码) 【WebRTC 专栏】--创建相机预览 Unity Shader 光照基础之 Half Lambert 光照模型 Unity Shader 光照基础之Lambert光照模型 Unity Shader 光照基础内容 Unity Shader 显示一张图片纹理 UnityShader 的基本概念 Unity 物体的基本操作 C++ 模板系列小结07-尾置返回类型 C++ 模板系列小结06-可变参数模板特性 C++ 中的多线程的使用和线程池建设 C++ 模板系列小结05-模板类型作为模板参数 C++ 模板系列小结04-类模板中的成员模板 C++ 模板系列小结03-在模板中指定变量类型 C++ 模板系列小结02-非类型模板参数 C++ 模板系列小结01-函数模板和类模板 从零打造渲染引擎系列01-什么是渲染引擎 iOS开发 - 在 Swift 中去调用 C/C++ 代码 2021 技术新番 - 从零打造渲染引擎系列 iOS 音视频开发的一些基础准备工作 音视频交流群又来啦~~~ 【WebRTC 专栏】WebRTC & Android 开发学习环境搭建~ 【喜大普奔】域名终于备案通过啦 Shader 优化 | OpenGL 绘制网格效果 【音视频连载-011】第二季 FFmpeg 一层一层获取文件信息 KodeLife | Shader 实时编辑预览的强大工具使用实践 推荐几个堪称教科书级别的 Android 音视频入门项目 【音视频连载-010】第二季 FFmpeg 日志打印 【音视频连载-008】基础学习篇-SDL 播放 PCM 音频文件(下) 【音视频连载-007】基础学习篇-SDL 播放 PCM 音频文件(上) 【音视频连载-006】基础学习篇-SDL 播放 YUV 视频文件 【音视频连载-005】基础学习篇-SDL 加载 YUV 文件并显示 【音视频连载-004】基础学习篇-SDL 加载图片并显示 【音视频连载-003】基础学习篇-SDL 消息循环和事件响应 【音视频连载-002】基础学习篇-SDL 创建窗口并显示颜色 【音视频连载-001】基础学习篇- SDL 介绍以及工程配置 LearnOpenGL 源码在 MAC 上的编译与调试 2019 年终总结与回顾 Android NDK 开发的免费技术视频来啦~~ OpenGL 实现视频编辑中的转场效果 OpenGL 实践之贝塞尔曲线绘制 图像库 libjpeg-turbo 编译与实践 图像库 libpng 编译与实践 rust 开发编译 Android 动态库实践 Android NDK 开发 —— 从 Assets 文件夹加载图片并上传纹理 简单易用的图像解码库介绍 —— stb_image 博客图床迁移记 进击的 Vulkan 移动开发之 SwapChain 进击的 Vulkan 移动开发之 Command Buffer 进击的 Vulkan 移动开发之 Instance & Device & Queue 进击的 Vulkan 移动开发(一)之今生前世 Java 显式锁 Lock 与条件队列 C++ 标准容器库小结 一文读懂 YUV 的采样与格式 《OpenGL ES 3.x 游戏开发》碰撞检测之 AABB 包围盒
Android JNI 基本操作
音视频开发进阶 · 2018-05-07 · via 音视频开发进阶

一个专注音视频领域的小圈子

自从 Android Studio 升级到 2.3 版本以后,使用 CMake 进行编译就方便多了,不需要再写 Android.mk 了,也不需要用 javah 来生成头文件了,直接写好 native 方法,快捷方式就可以生成对应的 C++ 方法,只要专注写好 C++ 代码,CMake 就可以指定的 CPU 架构生成对应的 SO 库。

JNI 和 NDK 的区别

NDK 开发难免会搞不清 JNI 和 NDK 的区别。

JNI 全称是 Java Native Interface,即 Java 本地接口。它是用来使得 Java 语言和 C/C++ 语言相互调用的。它本身和 Android 并无关系,只是在 Android 开发中会用到,在其他地方也会用到的。

而 NDK 的全称是 Native Development Kit,和 SDK 的全称是 Software Development Kit 一样,都是开发工具包。NDK 是 Android 开发的工具包,主要用作 C/C++ 开发,提供了相关的动态库。

在 Android 上进行 NDK 开发还是得先学会 JNI 相关技能,先可以从 Java 层到 C/C++ 层的相互调用,然后再学习 NDK 开发的那些技巧。

简单实例

在 AS 新建工程时若选择了 Include C++ Support,就会自带配置好的 C++ 开发环境。

在声明 native 方法时还是用 Java 来写比较好,比 Kotlin 的 external 关键字要友好多了,可以直接快捷键生成对用的 C++ 方法。

声明 native 方法如下:

public static native int plus(int a, int b);

快捷键便会生成对应的 C++ 方法

extern "C"
JNIEXPORT jint JNICALL
Java_com_glumes_myapplication_NativeClass_plus(JNIEnv *env, jobject instance, jint a, jint b) {
    jint sum = a + b;
    return sum;
}

这是一个简单的计算 a+b 的 native 方法,但却包含了许多基本内容,在 C++ 层接收来自 Java 层的参数,并转换成 C++ 层的数据类型,计算之后再返回成 Java 层的数据类型。

在 Java 层中只有两个参数,而在 C++ 代码就有四个参数了,至少都会包含前面两个参数,下面讲解这些参数意义。

其中:

  • env变量是 JNIEnv 类型的对象,该对象是一个 Java 虚拟机所运行的环境,通过它可以访问到 Java 虚拟机内部的各种对象。

JNIEnv 类型对象参数 env

JNIEnv* 是定义任意 native 函数的第一个参数,它是一个指针,通过它可以访问虚拟机内部的各种数据结构,同时它还指向 JVM 函数表的指针,函数表中的每一个入口指向一个 JNI 函数,每个函数用于访问 JVM 中特定的数据结构。

结构如下图所示:

可以看到这里面涉及了三类指针,JNIEnv * 本身就是指针,而它指向的也是指针,在 JVM 函数表里面的每一项又都是指针。

jobject 参数

jobject 是 native 函数里的第二个参数类型,但却不是一定的。

如果该 native 方法是一个静态 static 方法,那么第二个参数就是 jobject 类型,指的是调用该函数的对象;

如果是一个实例方法,那么第二个参数就是 jclass 类型,指的是调用该函数的类。

基本数据类型转换

在 Java 中传递的参数类型是 int,而在 JNI 中就成了 jint,这就涉及到 Java 到 JNI 的数据类型转换。

如下表所示:

Java 类型 Native 类型 符号属性 字长
boolean jboolean 无符号 8位
byte jbyte 无符号 8位
char jchar 无符号 16位
short jshort 有符号 16位
int jnit 有符号 32位
long jlong 有符号 64位
float jfloat 有符号 32位
double jdouble 有符号 64位

我们传递的基本数据类型在 JNI 中都有相对的数据类型。

引用数据类型转换

除了基本数据类型之外,引用数据类型也有着一一对应。

Java 引用类型 Native 类型 Java 引用类型 Native 类型
All objects jobject char$[]$ jcharArray
java.lang.Class jclass short$[]$ jshortArray
java.lang.String jstring int$[]$ jintArray
Object$[]$ jobjectArray long$[]$ jlongArray
boolean$[]$ jbooleanArray float$[]$ jfloatArray
byte$[]$ jbyteArray double$[]$ jdoubleArray
java.lang.Throwable jthrowable

可以看到,除了 Java 中基本数据类型的数组、Class、String 和 Throwable 外,其余所有 Java 对象的数据类型在 JNI 中都用 jobject 表示。

明白了参数类型之后,接下来就是按照正常写代码一样,完成函数的返回值了。

String 字符串操作

对于基本数据类型的操作,比如 boolean、int、float 等都大同小异,无非是在原来的数据类型前面加了一个 j来表示 JNI 数据类型。

而对于 String 类型,必须要使用合适的 JNI 函数来将 jstring 转变成 C/C++ 字符串。

对于下面的 Native 方法,传入一个字符串,并要求返回一个字符串。

    public static native String getNativeString(String str);

生成的对应的 C++ 代码如下:

extern "C"
JNIEXPORT jstring JNICALL
Java_com_glumes_cppso_SampleNativeMethod_getNativeString(JNIEnv *env, jclass type, jstring str_) {
    
    // 生成 jstring 类型的字符串
    jstring returnValue = env->NewStringUTF("hello native string");
    // 将 jstring 类型的字符串转换为 C 风格的字符串,会额外申请内存
    const char *str = env->GetStringUTFChars(str_, 0);
    // 释放掉申请的 C 风格字符串的内存
    env->ReleaseStringUTFChars(str_, str);
    // 返回 jstring 类型字符串
    return returnValue;
}

Java 层的字符串到了 JNI 就成了 jstring 类型的,但 jstring 指向的是 JVM 内部的一个字符串,它不是 C 风格的字符串 char*,所以不能像使用 C 风格字符串一样来使用 jstring 。

JNI 支持将 jstring 转换成 UTF 编码和 Unicode 编码两种。因为 Java 默认使用 Unicode 编码,而 C/C++ 默认使用 UTF 编码。

  • GetStringUTFChars(jstring string, jboolean* isCopy)

将 jstring 转换成 UTF 编码的字符串

  • GetStringChars(jstring string, jboolean* isCopy)

将 jstring 转换成 Unicode 编码的字符串,由于 Native 层是 C/C++ 编码,默认使用 UTF 格式,所以 GetStringChars 并不常用。

其中,jstring 类型参数就是我们需要转换的字符串,而 isCopy 参数的值为 JNI_TRUE 或者 JNI_FALSE ,代表是否返回 JVM 源字符串的一份拷贝。如果为JNI_TRUE 则返回拷贝,并且要为产生的字符串拷贝分配内存空间;如果为JNI_FALSE 就直接返回了 JVM 源字符串的指针,意味着可以通过指针修改源字符串的内容,但这就违反了 Java 中字符串不能修改的规定,在实际开发中,直接填 NULL 就好了。

当调用完 GetStringUTFChars 方法时别忘了做完全检查。因为 JVM 需要为产生的新字符串分配内存空间,如果分配失败就会返回 NULL,并且会抛出 OutOfMemoryError 异常,所以要对 GetStringUTFChars 结果进行判断。

当使用完 UTF 编码的字符串时,还不能忘了释放所申请的内存空间。调用 ReleaseStringUTFChars 方法进行释放。

完整地转换字符串的代码如下:

    // 申请分配内存空间,jstring 转换为 C 风格字符串
    const char *utfStr = env->GetStringUTFChars(str_,NULL);
    // 做检查判断
    if (utfStr == NULL){
        return NULL;
    }
    // 实际操作
    printf("%s",utfStr);
    // 操作结束后,释放内存
    env->ReleaseStringUTFChars(str_,utfStr);

除了将 jstring 转换为 C 风格字符串,JNI 还提供了将 C 风格字符串转换为 jstring 类型。

通过 NewStringUTF 函数可以将 UTF 编码的 C 风格字符串转换为 jstring 类型,通过 NewString 函数可以将 Unicode 编码的 C 风格字符串转换为 jstring 类型。这个 jstring 类型会自动转换成 Java 支持的 Unicode 编码格式。

除了 jstring 和 C 风格字符串的相互转换之外,JNI 还提供了其他的函数。

获得源字符串的指针

在某些情况下,我们只需要获得 Java 字符串的直接指针,而不需要把它转换成 C 风格的字符串。

比如,一个字符串内容很大,有 1 M 多,而我们只是需要读取字符串内容,这种情况下再把它转换为 C 风格字符串,不仅多此一举(通过直接字符串指针也可以读取内容),而且还需要为 C 风格字符串分配内存。

为此,JNI 提供了 GetStringCritical 和 ReleaseStringCritical 函数来返回字符串的直接指针,这样只需要分配一个指针的内存空间就好了。

    const jchar *c_str = NULL;
    c_str = env->GetStringCritical(str_, NULL);
    
    if (c_str == NULL) {
        // error handle
    }
    env->ReleaseStringCritical(str_, c_str);

和 GetStringUTFChars 一样,在使用完之后,还需要将分配的指针内存空间给释放掉。

注意它的返回值指针类型是 const jchar *,而 GetStringUTFChars 函数的返回值就是 const char*,这就说明 GetStringUTFChars 返回的是 C 风格字符串的指针,而 GetStringCritical 返回的是源 Java 字符串的直接指针。

另外,GetStringCritical 还有额外的限制。

在 GetStringCritical 和 ReleaseStringCritical 两个函数之间的 Native 代码不能调用任何会让线程阻塞或者等待 JVM 中其他线程的 Native 函数或 JNI 函数。

因为通过 GetStringCritical 得到的是一个指向 JVM 内部字符串的直接指针,获取这个直接指针后会导致暂停 GC 线程,当 GC 线程被暂停后,如果其他线程触发 GC 继续运行的话,都会导致阻塞调用者。所以,GetStringCritical 和 ReleaseStringCritical 这对函数中间的任何本地代码都不可以执行导致阻塞的调用或为新对象在 JVM 中分配内存,否则,JVM 有可能死锁。

另外还是需要检查是否因为内存溢出而导致返回值为 NULL,因为 JVM 在执行 GetStringCritical 函数时,仍有发生数据复制的可能性,尤其是当 JVM 内部存储的数组不连续时,为了返回一个指向连续内存空间的指针,JVM 必须复制所有数据。

获得字符串的长度:

由于 UTF-8 编码的字符串以 \0 结尾,而 Unicode 字符串不是,所以对于两种编码获得字符串长度的函数也是不同的。

  • GetStringLength

获得 Unicode 编码的字符串的长度。

  • GetStringUTFLength

获得 UTF-8 编码的字符串的长度,或者使用 C 语言的 strlen 函数。

这里的字符串指的是 Java 层的字符串,传入的参数都是 jsting 类型,而 Java 层默认是 Unicode 编码,所以大多使用 GetStringLength 方法。

获得指定范围的字符串内容

JNI 提供了函数来获得字符串指定范围的内容,这里的字符串指的是 Java 层的字符串。函数会把源字符串复制到一个预先分配的缓冲区内。

  • GetStringRegion

获得 Unicode 编码的字符串指定内容。

  • GetStringUTFRegion

获得 UTF-8 编码的字符串指定内容。

    jchar outbuf[128],inbuf[128];
    int len = env->GetStringLength(str_);
    env->GetStringRegion(str_,0,len,outbuf);
    LOGD("%s",outbuf);

String 字符串函数操作总结

关于字符串的函数汇总

JNI 函数 描述
GetStringChars / ReleaseStringChars 获得或释放一个指向 Unicode 编码的字符串的指针(指 C/C++ 字符串)
GetStringUTFChars / ReleaseStringUTFChars 获得或释放一个指向 UTF-8 编码的字符串的指针(指 C/C++ 字符串)
GetStringLength 返回 Unicode 编码的字符串的长度
getStringUTFLength 返回 UTF-8 编码的字符串的长度
NewString 将 Unicode 编码的 C/C++ 字符串转换为 Java 字符串
NewStringUTF 将 UTF-8 编码的 C/C++ 字符串转换为 Java 字符串
GetStringCritical / ReleaseStringCritical 获得或释放一个指向字符串内容的指针(指 Java 字符串)
GetStringRegion 获取或者设置 Unicode 编码的字符串的指定范围的内容
GetStringUTFRegion 获取或者设置 UTF-8 编码的字符串的指定范围的内容

选择合适的 JNI 函数

对于 JNI String 操作,要选择合适的函数,上表可以作为参考。

参考

  1. 《The Java Native Interface》
  2. https://github.com/glumes/AndroidDevWithCpp

具体示例代码可参考我的 Github 项目 https://github.com/glumes/AndroidDevWithCpp,欢迎 Star。

原创文章,转载请注明来源:    Android JNI 基本操作