Android NDK/JIN 从入门到精通

·  阅读 1000

1.1 JNI(Java Native Interface)

提供一种Java字节码调用C/C++的解决方案,JNI描述的是一种技术 调用顺序

1.2 NDK(Native Development Kit)

Android NDK 是一组允许您将 C 或 C++(“原生代码”)嵌入到 Android 应用中的工具,NDK描述的是工具集。 能够在 Android 应用中使用原生代码对于想执行以下一项或多项操作的开发者特别有用:

  • 在平台之间移植其应用。
  • 重复使用现有库,或者提供其自己的库供重复使用。
  • 在某些情况下提高性能,特别是像游戏这种计算密集型应用。

1.3 JNI注册

1.3.1 环境配置

这里已有现成的文章就引用一下别人的了 ,讲的很仔细 关于JNI环境配置

1.3.2 静态注册

当Java层调用navtie函数时,会在JNI库中根据函数名查找对应的JNI函数。如果没找到,会报错。如果找到了,则会在native函数与JNI函数之间建立关联关系,其实就是保存JNI函数的函数指针。下次再调用native函数,就可以直接使用这个函数指针。

JNI函数名格式(包名里面的”.”需要改为”_”): Java_ + 包名(com_example_auto_jnitest)+ 类名(_MainActivity) + 函数名(_stringFromJNI)

静态注册缺点:

  • 要求JNI函数的名字必须遵循JNI规范的命名格式;
  • 名字冗长,容易出错;
  • 初次调用会根据函数名去搜索JNI中对应的函数,会影响执行效率;
  • 需要编译所有声明了native函数的Java类,每个所生成的class文件都要用javah工具生成一个头文件;

静态注册例子

类 JNITest 包名:com.hqk.jnitestone

package com.hqk.jnitestone;

public class JNITest {

static {
    System.loadLibrary("native-lib");
}

    public static native String sayHello();
} 
复制代码

对应c++代码 ,cpp 类名为:native-lib.cpp

#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring JNICALL
Java_com_hqk_jnitestone_JNITest_sayHello(JNIEnv *env, jclass clazz) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
} 
复制代码

注:

  • cpp 类名为:native-lib.cpp,则对应java交互类 需要加入
 static {
        System.loadLibrary("native-lib");
    } 
复制代码
  • java 对应交互类的包名:com.hqk.jnitestone,则cpp对应方法需要将对应[.]转化成[_];(参照上面格式)

1.3.2 动态注册

通过提供一个函数映射表,注册给JVM虚拟机,这样JVM就可以用函数映射表来调用相应的函数,就不必通过函数名来查找需要调用的函数。

Java与JNI通过JNINativeMethod的结构来建立函数映射表,它在jni.h头文件中定义,其结构内容如下:

typedef struct {
    const char* name; // 对应交互java 类对应的方法名
    const char* signature;	//对应交互方法的函数签名 (参考本文1.4.3)
    void*       fnPtr;	//对应交互cpp方法的 指针函数 (指向对应函数)
} JNINativeMethod; 
复制代码

1、创建映射表后,调用RegisterNatives函数将映射表注册给JVM; 2、当Java层通过System.loadLibrary加载JNI库时,会在库中查JNI_OnLoad函数。可将JNI_OnLoad视为JNI库的入口函数,需要在这里完成所有函数映射和动态注册工作,及其他一些初始化工作。

概念说完了,实际操作一下

JAVA类 JNITest 包名:com.hqk.jnitestone

package com.hqk.jnitestone;

public class JNITest {
    static {
        System.loadLibrary("native-lib");
    }

    public static native String sayHello();

    public static native String sayHello2();
} 
复制代码

注意:这里多了一个 sayHello2 方法,方法返回值为: String

CPP类名 native-lib.cpp 对应代码

#include <jni.h>
#include <string>
#include <android/log.h>

extern "C"
JNIEXPORT jstring JNICALL
Java_com_hqk_jnitestone_JNITest_sayHello(JNIEnv *env, jclass clazz) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

//cpp 交互方法
jstring sayHello2(JNIEnv *env, jobject thiz) {
    std::string hello = "Hello,我是动态注册成功的!";
    return env->NewStringUTF(hello.c_str());
}

// 动态注册 函数 结构数组
static const JNINativeMethod gMethods[] = { 
        {"sayHello2",	//对应java交互类的方法名
        "()Ljava/lang/String;", //对应方法名的函数签名 (参考本文1.4.3)
         (jstring *) sayHello2 //对应 cpp交互类的指针函数
         } 
};

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    __android_log_print(ANDROID_LOG_INFO, "native", "Jni_OnLoad");
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) //从JavaVM获取JNIEnv,一般使用1.4的版本
        return -1;
        //注意:这里 FindClass 必须要和交互类的 包名对应上,并换成[/]符号
    jclass clazz = env->FindClass("com/hqk/jnitestone/JNITest");
    if (!clazz) {
        __android_log_print(ANDROID_LOG_INFO, "native",
                            "cannot get class: com/hqk/jnitestone/JNITest");
        return -1;
    }
    if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0]))) {
        __android_log_print(ANDROID_LOG_INFO, "native", "register native method failed!\n");
        return -1;
    }
    return JNI_VERSION_1_4;
} 
复制代码

从上面代码可以看出

1、CPP类里面 创建了 JNI_OnLoad 方法--》 该方法是在 初始化的时候执行,类似于 activity 的onCreate, 核心代码,就是通过Find找到对应Java类,最后通过RegisterNatives 实现动态注册 2、CPP类里面 新增了JNINativeMethod 类型的 结构数组 ; 3、CPP类里面 新增了 sayHello2 交互方法 4、Java类里面通用新增了 sayHello2方法

1.4 数据类型转换

1.4.1 基本数据转换

基本数据转换

1.4.2 引用数据类型转换

除了Class、String、Throwable和基本数据类型的数组外,其余所有Java对象的数据类型在JNI中都用jobject表示。Java中的String也是引用类型,但是由于使用频率较高,所以在JNI中单独创建了一个jstring类型。

引用数据类型转换

  • 引用类型不能直接在 Native 层使用,需要根据 JNI 函数进行类型的转化后,才能使用;
    • 多维数组(含二维数组)都是引用类型,需要使用 jobjectArray 类型存取其值;

例如,二维整型数组就是指向一位数组的数组,其声明使用方式如下:

 //获得一维数组的类引用,即jintArray类型 
    jclass intArrayClass = env->FindClass("[I");   
    //构造一个指向jintArray类一维数组的对象数组,该对象数组初始大小为length,类型为 jsize
    jobjectArray obejctIntArray  =  env->NewObjectArray(length ,intArrayClass , NULL); 
复制代码

1.4.3 JNI函数签名信息

由于Java支持函数重载,因此仅仅根据函数名是没法找到对应的JNI函数。为了解决这个问题,JNI将参数类型和返回值类型作为函数的签名信息。

  • JNI规范定义的函数签名信息格式:

(参数1类型字符…)返回值类型字符

  • 函数签名例子:

函数签名例子 3.JNI常用的数据类型及对应字符: JNI常用的数据类型及对应字符

1.4.4 JNIEnv介绍

  • JNIEnv概念 : JNIEnv是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境。通过JNIEnv可以调用到一系列JNI系统函数。
  • JNIEnv线程相关性: 每个线程中都有一个 JNIEnv 指针。JNIEnv只在其所在线程有效, 它不能在线程之间进行传递。

注意: 在C++创建的子线程中获取JNIEnv,要通过调用JavaVM的AttachCurrentThread函数获得。在子线程退出时,要调用JavaVM的DetachCurrentThread函数来释放对应的资源,否则会出错。

JNIEnv 作用:

  • 访问Java成员变量和成员方法;
  • 调用Java构造方法创建Java对象等。

1.5 JNI编译

1.5.1 Cmake编译

CMake 则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt)生成 对应 makefile 或 project 文件,然后再调用底层的编译, 在Android Studio 2.2 之后支持Cmake编译。

 cmake_minimum_required(VERSION 3.4.1)

add_library(
        native-lib
        SHARED
        native-lib.cpp)
find_library(
        log-lib
        log)
target_link_libraries(
        native-lib
        ${log-lib}) 
复制代码

1.5.1.1 add_library 指令

语法:add_library(libname SHARED | STATIC | MODULE [source])

将一组源文件 source 编译出一个库文件,并保存为 libname.so (lib 前缀是生成文件时 CMake自动添加上去的)。其中有三种库文件类型,不写的话,默认为 STATIC;

  • SHARED: 表示动态库,可以在(Java)代码中使用 System.loadLibrary(name) 动态调用;
  • STATIC: 表示静态库,集成到代码中会在编译时调用;
  • MODULE: 只有在使用 dyId 的系统有效,如果不支持 dyId,则被当作 SHARED 对待;
  • EXCLUDE_FROM_ALL: 表示这个库不被默认构建,除非其他组件依赖或手工构建;
#将compress.c 编译成 libcompress.so 的共享库
add_library(compress SHARED compress.c) 
复制代码

1.5.1.2 target_link_libraries 指令

语法:target_link_libraries(target library <debug | optimized> library2…)

这个指令可以用来为 target 添加需要的链接的共享库,同样也可以用于为自己编写的共享库添加共享库链接。如:

#指定 compress 工程需要用到 libjpeg 库和 log 库
target_link_libraries(compress libjpeg ${log-lib}) 
复制代码

1.5.1.3 find_library 指令

语法:find_library( name1 path1 path2 ...)

VAR 变量表示找到的库全路径,包含库文件名 。例如:

find_library(libX  X11 /usr/lib)
find_library(log-lib log)  #路径为空,应该是查找系统环境变量路径 
复制代码

1.5.1.4 更多详细使用

参考文献:更多关于Cmake的详细使用

1.5.2 Abi架构

ABI(Application binary interface)应用程序二进制接口。不同的CPU 与指令集的每种组合都有定义的 ABI (应用程序二进制接口),一段程序只有遵循这个接口规范才能在该 CPU 上运行,所以同样的程序代码为了兼容多个不同的CPU,需要为不同的 ABI 构建不同的库文件。当然对于CPU来说,不同的架构并不意味着一定互不兼容。

  • armeabi设备只兼容armeabi;
  • armeabi-v7a设备兼容armeabi-v7a、armeabi;
  • arm64-v8a设备兼容arm64-v8a、armeabi-v7a、armeabi;
  • X86设备兼容X86、armeabi;
  • X86_64设备兼容X86_64、X86、armeabi;
  • mips64设备兼容mips64、mips;
  • mips只兼容mips;

根据以上的兼容总结,我们还可以得到一些规律:

  • armeabi的SO文件基本上可以说是万金油,它能运行在除了mips和mips64的设备上,但在非armeabi设备上运行性能还是有所损耗;
  • 64位的CPU架构总能向下兼容其对应的32位指令集,如:x86_64兼容X86,arm64-v8a兼容armeabi-v7a,mips64兼容mips;

以上就是NDK/JNI的入门内容,那么要如何进阶学习呢?下面,高能的地方来了!有幸从一位字节跳动大神那里得到他本人吐血整理的“582页Android NDK七大模块学习宝典”,从原理到实战,一应俱全!

秉承好东西的当然要共享的原则,今天就来秀一把,试试这“582页Android NDK七大模块学习宝典”是否也能让你事半功倍!这份宝典主要涉及以下几个方面:

  • NDK 模块开发
  • JNI 模块
  • Native 开发工具
  • Linux 编程
  • 底层图片处理
  • 音视频开发
  • 机器学习

一、NDK 模块开发

主要内容:

  • C++与 C#数据类型总结
  • C 与 C++之内存结构与管理
  • C 与 C++之预处理命令与用 typedef 命名已有类型
  • C 与 C++之结构体、共用体
  • C 与 C++之指针
  • C/C++ 之多线程机制
  • C/C++ 之函数与初始化列表

二、JNI 模块

主要内容:

  • JNI 开发之 静态注册与动态注册

静态注册、动态注册、JNINativeMethod、数据类型映射、jni 函数默认参数

  • JNI 开发之方法签名与 Java 通信

Android NDK 开发 JNI 类型签名和方法签名、JNI 实现 java 与 c/c++相互通讯

  • JNI 开发之局部引用、全局引用和弱全局引用

三、Native 开发工具

主要内容:

  • 编译器、打包工具与分析器

十大最受欢迎的 React Native 应用开发编辑器、react-native 打包流程

  • 静态库与动态库

  • CPU 架构与注意事项

ABI 管理、处理 CPU 功能、NEON 支持

  • 构建脚本与构建工具

环境搭建、NDK 项目、Cmake、Makefile

  • 交叉编译移植

FFmpeg 编译、FFmpeg+LIBX264+FACC 交叉编译 实现 264 流录制、移植 FFmpeg 在 arm 交叉编译时遇到的问题、FFmpeg 交叉编译、X264 FAAC 交叉编译、解决所有移植问题

  • AS 构建 NDK 项目

配置 NDK 环境、建立 app 项目、生成.h 头文件、创建 C 文件,实现 native 方法、jni.h 文件

四、Linux 编程

  • Linux 环境搭建,系统管理,权限系统和工具使用(vim 等)

Linux 环境的搭建、Linux 系统管理操作(25 个命令)

  • Shell 脚本编程

Shell 脚本、编写简单 Shell 脚本、流程控制语句、计划任务服务程序

五、底层图片处理

  • PNG/JPEG/WEBP 图像处理与压缩

四种图片格式、推荐几种图片处理网站、squoosh 在线无损图片压缩工具,JPG/webP/PNG/ 互转

  • 微信图片压缩

计算原始宽高、计算近似宽高、第一次采样获取目标图片、循环逼近目标大小

  • GIF 合成原理与实现

GIF 图片的解析、GIF 图片的合成(序列图像合成 GIF 图像)

六、音视频开发

  • 多媒体系统

Camera 与手机屏幕采集、图像原始数据格式 YUV420(NV21 与 YV12 等)、音频采集与播放系统、编解码器 MediaCodec、MediaMuxer 复用与 MediaExtractor

  • FFmpeg

ffmpeg 模块介绍、音视频解码,音视频同步、I 帧,B 帧,P 帧解码原理、x264 视频编码与 faac 音频编码、OpenGL 绘制与 NativeWindow 绘制

  • 流媒体协议

RTMP 协议、、音视频通话 P2P WebRtc

  • OpenGL ES 滤镜开发之美颜效果

高斯模糊、高反差保留、强光处理、融合

  • 抖音视频效果分析与实现

流程列表、视频拍摄、视频编辑、视频导出

  • 音视频变速原理

变速入口分析、音频变速实现、视频变速实现

七、机器学习

  • Opencv

  • 图像预处理

灰度化和二值化、腐蚀与膨胀、人脸检测、身份证识别

本资料已开源,需要的朋友自行前往GitHub下载。

最后

由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

除了上面的之外我还自己整理了以下一系列的学习进阶资料:

《Android开发七大模块核心知识笔记》

《2246页最新Android大厂高频面试题解析大全》

本资料已开源,需要的朋友自行前往GitHub下载。

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改