Android:JNI开发介绍与小结

目录

一 概念介绍

1.1 JNI

1.2 NDK

1.3 交叉编译

1.4 指令集

1.5 微架构

1.6 ABI

二 工程实践

2.1新建工程

2.2 已有工程

三 生成So库

四 动态加载 so 库

4.1 何为动态加载

4.2 动态加载优点

4.3 实现步骤

4.4 总结

参考文章:


一 概念介绍

某些计算密集型的运算,我们大多不会在 java 层来实现,一般使用 native 层处理,比如一些视频的解码等,最终会通过 C/C++ 来实现,然后编译成 so(共享)库供 Android 端使用。

1.1 JNI

JNI 是指 Java 原生接口。它定义了 Android 从受管理代码(使用 Java 或 Kotlin 编程语言编写)编译的字节码与原生代码(使用 C/C++ 编写)互动的方式。

从 JVM 角度,存在两种类型的代码:“Java”和“native”, native 一般指的是 c/c++,为了使 java 和 native 端能够进行交互,java 设计了 JNI(java native interface)。

JNI 允许java虚拟机(VM)内运行的java代码与C++、C++和汇编等其他编程语言编写的应用程序和库进行互操作。

虽然大部分情况下我们的软件完全可以由 java 来实现,但是某些场景下使用 native 代码更加适合,比如:

  • 代码效率:使用 native 代码的性能更高
  • 跨平台特性:标准Java类库不支持应用程序所需的依赖于平台的特性,或者希望用较低级别的语言(如汇编语言)实现一小部分时间关键型代码。

native 层使用 JNI 主要可以做到:

  • 创建、检查和更新Java对象(包括数组和字符串)。
  • 调用Java方法。
  • 加载类并获取类信息。

1.2 NDK

原生开发套件 (NDK) 是Android Studio提供的一套工具,使您能够在 Android 应用中使用 C 和 C++ 代码。Android Studio 编译原生库的默认构建工具是CMake。

1.3 交叉编译

交叉编译:在A平台上编译出B平台可以执行的二级制文件的过程。比如在 MacOS 或者Windows上编译出 android 上可用的so文件。

交叉编译android 平台的库文件通过Android Studio内提供的ndk实现。

1.4 指令集

CPU 执行计算任务时都需要遵从一定的规范,程序在被执行前都需要先翻译为 CPU 可以理解的语言。这种规范或语言就是指令集(ISA,Instruction Set Architecture)。

程序被按照某种指令集的规范翻译为 CPU 可识别的底层代码的过程叫做编译(compile)。常见的指令集有 x86、ARM、MIPS 等,其中指令集可以扩展,比如 x86 指令集可以增加 64 位支持,就变成 x86_64,同理 ARM 可以扩展成 arm64-v8a,MIPS 可以扩展为 MIPS64。

1.5 微架构

“微架构”就是“指令集”的具体“实现”。例如:AMD和英特尔同样都是采用x86指令集的处理器,但是他们处理器具体微架构是不同的,这就是典型的“实现”问题。

指令集是一套公开的规范,它的实现(微架构)是一个极具技术含量的工作,而且即便是你有这个技术想设计某套指令集的微架构,还需要得到该指令集的授权,否则会吃官司。而微架构的设计直接影响了核心可以达到的最高频率、核心在一定频率下能执行的运算量、一定工艺水平下核心的能耗水平等等,也就是微架构设计技术的好坏,决定了设计出核心性能的高低。

值得注意的是,一款 CPU 使用了 ARM 指令集不等于它就使用了 ARM 研发的微架构,因为 Intel、高通、苹果、Nvidia等厂商都自行开发了兼容 ARM 指令集的微架构,这也就是前面讲到的指令集授权。

1.6 ABI

abi是应用程序二进制接口(Application Binary Interface)。不同 Android 手机使用不同的 CPU,因此支持不同的指令集。

目前 Android 平台主流的有:armeabiarmeabi-v7ax86mipsarm64-v8amips64x86_64:

x86/x86_6432/64位电脑使用的CPU架构,x86_64兼容 x86_64、x86
armeabi与 ARMv5、v6 设备不兼容。兼容 armeabi-v7a、armeabi
armeabi-v7第7代及以上的 ARM 处理器,2011年15月以后的生产的大部分Android设备都使用它
arm64-v8a兼容arm64-v8a、armeabi-v7a、armeabi,最主要的区别在于支持64位(谷歌商店强制要求从2019年8月起使用arm64-v8a)

Android Studio默认是会编译全平台的链接库的,如果你不需要兼容某些架构,你可以在app的Gradle脚本中指定需要编译的架构,如此AS在编译的时候,就不会再编译没有被声明的平台了。

android {
 
    ...
 
    defaultConfig {
        
        ...
 
        ndk {
            //设置支持的SO库架构(开发者可以根据需要,选择一个或多个平台的so)
            abiFilters "armeabi-v7a", "arm64-v8a"
        }
    }
}


二 工程实践

2.1新建工程

首先要确认一下你的Android Studio版本是4.0以上,过低版本需要手写Android.mk 文件来配置 makefile,手动编译SO库。

新版Android Studio,在创建工程的时选择最后的Native C++,然后C++ Standard保持默认,Android Studio会自动创建好JNI环境。

2.2 已有工程

注意:前提配置好NDK

在app/src/main 下创建cpp 目录,并在该目录下创建CMakeLists.txt文件和native-lib.cpp(名字可以随意,注意与下面一致即可),如下图(下图的lame文件夹无须在意,这是我自己工程里需要加的一个):

CMakeLists.txt文件复制编辑内容如下:

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

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).
        native-lib.cpp
  
        )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

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)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        native-lib

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

在 app/build.gradle 中指定cmake 编译文件路径

android {

    .......

    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}

native-lib.cpp文件编辑内容,实现一个简单的C++方法,返回一个Hello from C++的字符串:

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

static lame_global_flags *glf = NULL;

extern "C"
JNIEXPORT jstring JNICALL
Java_com_lucas_audioSample_view_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
    // TODO: implement stringFromJNI()
    std::string hello = "Hello from C++";
//    return env->NewStringUTF(hello.c_str());
    return env->NewStringUTF(get_lame_version());
}

现在可以在MainActivity中调用native方法了:

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

点击构建并运行,Gradle 会以依赖项的形式添加 CMake 或 ndk-build 进程,用于编译和构建原生库并将其随 APK 一起打包。


三 生成So库

项目结束后可以将C\C++中的方法保存起来做成SDK,此刻需要生成So库。

生成So库听起来很唬人,其实在章节2中的demo运行完后,So库就已经自动生成了。打开项目intermediates->cmake,可以看到生成的so库文件。

在项目中建立src的同级目录libs,并把上面的so库文件复制进去。 

在build.gradle下添加:



android {

    .......

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

此时如果你不想自己的cpp代码被别人看到,可以直接删除man下的cpp目录,直接通过编译好的so库进行编码。


四 动态加载 so 库

静态加载so库会有一些局限:

  • 增加 apk 体积大小
  • 多个 sdk so文件同时加载可能会出现冲突
  • 针对不同的 sdk 版本,单个 so 的兼容性不是很好
  • 不能动态的对 so 库进行更新,除非发布新版本

针对以上问题,我们发现,静态加载 so 不够灵活,所有的 so 文件都是安装 软件时候被焊死在 apk 包中,不易扩展,那有没有什么方法可以解决或者说优化这些问题呢?有,那就是动态加载共享链接库。

4.1 何为动态加载

不将所需 so 库直接拷贝到 apk 包里面,而是放在云端,待用户安装完 apk 使用时,先判断用户的手机 CPU 架构,再通过某种策略选择合适的时间去云端下载该用户手机最优 ABI 架构的 so 库文件到本地,然后当用户实际需要使用 native 功能时,手动System.load(lib),最后使用 native 方法或者数据结构,至此动态加载完成。

4.2 动态加载优点

  • 大大减小了 apk 体积,只需要当前 ABI 的so库
  • 动态更新 so 库

4.3 实现步骤

  • 生成所需的各种ABI 版本的 so 库文件,保存在云端
  • 设计某种策略,在特定时间触发初始化任务
  • 初始化任务中,首先通过Build.CPU_ABI获取当前设备最优 ABI 架构,除了Build.CPU_ABI还有第二 ABI,通过Build.CPU_ABI2获取,sdk21 之后推荐使用``Build.SUPPORTED_ABIS`获取,此为一个数组,数组元素越靠前,越是当前设备最优的ABI
  • 去云端下载对应 ABI 类型的 so 文件到本地 app 的用户空间,如:getDir("libs", Context.MODE_PRIVATE).getAbsolutePath(),对应的路径为data\data\package\app_libs\
  • 使用 native 功能时,调用 System.load(lib)方法去加载刚下载下来的 so 库文件,lib为刚下载的库文件结对路径
  • 调用 native 方法验证

4.4 总结

动态加载虽然可以使加载 so 库变得更加灵活,定制性更强,但亦有弊端:

  • 何时去下载 so 文件?需要制定的策略
  • 下载耗时时间,需要考虑网络异常等问题,
  • 策略考虑的场景必须完备:除网络因素外,各种情况需要考虑
  • 降低用户体验:用户初次使用功能时,等待时间下载so库

所以实际开发中,最好是静态加载和动态加载一起配合使用,动态加载只用于那些业务层级较深、使用频率较低的场景。

参考文章:

https://developer.android.com/ndk/guides

Neural Networks API  |  Android NDK  |  Android Developers

Android - JNI 开发你所需要知道的基础

https://hymane.itscoder.com/dynamic-load-jnilib-file-by-android/

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许进进

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值