文章主要介绍了如何给 Jetpack Compose 添加 Native 支持,包括在新项目和现有项目中添加的方法,如创建 C++项目、添加 JNI 支持的步骤、JNI 注册方式、线程处理、对象连接、库添加、版本指定、内存管理以及使用 JNI 的正确姿势和能做的事等。
想要在Compose中使用native代码是完全可行的,这是因为Compose是基于Kotlin的,而Kotlin本质上是JVM的字节码,也就是运行在虚拟机之上的语言。Java的Native接口,即JNI其实是虚拟机开出的口子,只要能在JVM上运行就可以用JNI,所以标准的Java JNI是完全可以用在Compose里面的。
注意: native代码(原生代码)在不同的语境有不同的意思,它通常指操作系统直接支持的可执行程序。Java(字节码)是运行在虚拟机上的,操作系统被虚拟机给隔离了,对Java是透明的,这时像可以编译为直接在操作系统上运行的代码(如C/C++)称为native代码;假如换个语境,如运行在WebView中的Web前端,则可以直接运行在Android上的或者iOS上的原生SDK代码则称为native代码。
先来看一下如何在Compose项目中添加native支持。
新项目
新的项目在创建项目的时候可以选择C++,无论是Kotlin的类,以及C++的实现,以及配置文件都会有模板。但除了demo以外,一般不会有新建项目的机会,极少项目是从0开始。绝大多数情况都是在现有项目中添加native支持,所以我们重点看看如何在现有项目中添加native支持。
现有项目添加JNI支持
现在的Android Studio已经对JNI有了很好的支持,AGP中也提供了支持,所以可以不用NDK中命令行式的ndk-build了。对于现有项目想添加JNI支持也不麻烦,有两种方式:一种是添加一个native的Module,新建Module时选择native library就可以了,这个Module里面与新建的Native项目是差不多的。这种方式适合于比较独立的一个新的需要native支持的模块,然后此模块再作为主模块的依赖,比较合适的场景是一个独立的功能模块;
第二种方式就是,像新建 的native项目那样,直接添加native支持:
Step 1 添加C/C++源码目录
先在对应的module如app中添加cpp源码目录,要放在与java或者kotlin同级别的目录,如app/src/main/下面,之后所有native层的东西都在app/src/main/cpp下面。
Step 2 设置CMake
在建 好的目录下面添加源码LocalJNI.cpp和编译文件CMakeLists.txt。
cmake_minimum_required(VERSION 3.22.1)
project("effectivejni")
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
LocalJNI.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log)
CMake是一个跨平台的C/C++编译系统,可以参考 其官文档了解详细信息。
Step 3 在Gradle脚本中添加native build关联
在android块中加入externalNativeBuild:
android {
// ...
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
}
Step 4 添加带有native方法的类
这一步要特别注意,因为JNI是Java Native Interface,所以必须要严格符合Java的方式,native方法的声明必须是某个类的方法;另外,JNI调用Java时也必须先找到某个类,然后再调用它的方法。所以必须 要有一个Java的public类:
package net.toughcoder.effectivejni
class LocalJNI {
external fun stringFromLocal(): String
companion object {
init {
System.loadLibrary("effectivejni")
}
}
}
当然,这个类可以放在任何文件中。因为Kotlin放宽了Java的限制,在Java中每一个public的类必须要放在一个名字一样的文件中,但Kotlin的文件与类没有对应的关系,所以可以把这个类放在任何文件中,当然了package要指明,因为在JNI中查找class时,要指定package name。
Step 5 实现native方法
具体native方法的实现就看具体要做什么了。这里只是演示所以简单返回一个字符串。
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_net_toughcoder_effectivejni_LocalJNI_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++ with static mapping";
return env->NewStringUTF(hello.c_str());
}
注意: 虽然Compose使用的编程语言是Kotlin,但毕竟Kotlin是JVM语言,也与Java可以相互替换。对于JNI来说,Kotlin与Java无区别,所以后面会以Java来统一当作native的另一端。
JNI注册
无论是用C/C++去实现native接口,还是复用现成的native方法,都需要要把native方法与Java层声明的方法进行关联映射,以让JVM能找到此方法的实现,这也即所谓的JNI注册。有两种方式进行JNI注册。
静态方式,其实就是Java默认支持的方式,它要求Native的实现函数是纯C的,要用『extern C』包裹起来,还有就是方法的名字要是Java_包名_类名_方法名,比较严格。前面的示例用的就是静态注册。
动态注册的原理是加载so的时候,当虚拟机在找到so以后,会查找里面一个叫做JNI_OnLoad的函数指针,然后执行此函数。那么,在so的实现中,写一个叫做JNI_OnLoad的函数,在里面手动进行Native方法注册,然后当so被加载时JNI_OnLoad就会被执行,JNI方法就注册好了。
#include <jni.h>
#include <string>
// Method declaration
jstring dynamicString(JNIEnv *env, jobject thiz);
// JNI wrapper
const char className[] = "net/toughcoder/effectivejni/LocalJNI";
const JNINativeMethod methods[] = {
{"stringFromJNI", "()Ljava/lang/String;", reinterpret_cast<void *>(dynamicString)}
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass clazz = env->FindClass(className);
env->RegisterNatives(clazz, methods, (int) (sizeof(methods) / sizeof(methods[0])));
return JNI_VERSION_1_6;
}
// The implementation
jstring dynamicString(JNIEnv *env, jobject thiz) {
std::string str = "String from JNI via dynamic mapping.";
return env->NewStringUTF(str.c_str());
}
这个JNI_OnLoad的方法的参数很有意思是一个JavaVM对象指针,JavaVM对象每个应用进程只有一个,可以认为就是应用的虚拟机。但每个JNI方法都有一个JNIEnv对象指针,它给native方法提供一个JNI上下文,这个则是每个线程都有一个。
推荐使用动态注册方式进行JNI注册,这是因为这种方式更为灵活,不必写繁琐的方法声明,也不必用extern C限制,可以是常规的C++函数。
JNI是一个接口层
JNI是一个口子,可以让Java调用native代码,也能让native代码调用Java代码,调用Java代码就相当于反射。JNI是一个传送门,虽然入口处有一些限制,但深入到native里面就是完全的C和C++世界了,只要是C和C++能实现的事情都可以做。
JNI线程
需要注意的是结构体JavaVM是所有线程共享,它代表着进程所在的虚拟机。但结构体JNIEnv则是代表着栈中的执行环境(因为JNI仅一个方法,而方法必然运行在某个线程之中),每个线程有一个。创建的局部引用也不能跨线程使用。
从JNIEnv获取JavaVM:env->GetJavaVM(&vm)
从JavaVM获得当前JNIENV:vm->AttachCurrentThread(&env, null)
最好都从Java层来管理线程,JNI只是某些方法的实现。
如果JNI的native代码也很复杂需要线程的话,也可以用pthread创建线程,但也应该维持在一定的作用域范围内,不应该再从此线程去调用Java。这样只会制造混乱。
两个世界的对象连接
需要注意JNI是纯C接口,没有对象的概念,入口处的native方法不属于任何C++对象。假如native深入层足够复杂也有一套对象,如何建立起 Java层对象和native对象的连接呢?可以参考Android frameworks的作法,它通常会给Java层的对象有一个整形域变量,用以存放native层对象指针,这样就能建立起来对象与对象的一一对应关系。
添加已编译好的native库
JNI是连接Java层与C/C++层的传送门,除了新写的native代码,也可以直接使用已编译好的C/C++的库,静态库libxxx.a和动态库libxxx.so。
预编译的库通常作为JNI的依赖,当然也可以直接加载,前提是so里面已包含了JNI接口。但需要特别注意的是静态的库.a是无法直接在Java中加载的,也即无法通过System.loadLibrary()来加载native的静态库。因此静态库只能作为依赖,要包一层,写一个Wrapper层编译为so,静态库作为so的依赖,然后把so加载为JNI。
通过CMake中的add_library指令来添加预编译好的库,具体可以 参考其文档。
NDK的版本
在项目的配置gradle文件中可以指定具体的NDK版本:
android {
ndkVersion = "28.0.12433566"
}
NDK的版本可以看官方发布历史,NDK主要是指Android提供的native API(C/C++ API),主要是一些系统提供的能力,如音频视频能力,图形图像能力等,可以看其接口说明文档,以及NDK开发文档。
C/C++的版本指定
C++语言自从其诞生,在Java和新一代编程语言出现后,曾一度长期停滞,在泛型,函数式编程,并发上面落后于其他语言,并被诟病。但从C++11开始,(C++语言的版本以年份的后两位来命名,如C++11是指2011年发布的版本,C++17指2017年发布的,以此类推)这门古老的语言焕然一新,增加了很多新时代编程语言的特性,其后的C++17继续前进,到现在的C++20已经完全是一个现代化的编程语言了,lambda,函数式,泛型和并发都有了非常好的支持,甚至已经超越了老对手Java。因此,C++11以后的版本也称为『现代C++(Modern C++)』。
都4202年了,肯定要用最新的C++20才行啊。CMake使用的是LLVM编译器,而LLVM已经完全支持C++20了,但默认的版本使用的是C++17,想要特别的版本,就需要在CMakefile.txt中进行指定,也即通过添加编译选项来指定C++的版本:
set(CMAKE_ANDROID_STL_TYPE "c++_shared")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20")
JNI内存管理
Java层是自动档(自动内存管理),但C/C++是手动档,因此穿过JNI后就需要特别小心内存管理。有一些注意事项:
- Java 层传过来的对象,不需要手动去释放。比如说传过来的数组或者字符串。
- 传回给Java层的对象,也不需要手动释放。比如像上面的例子新创建出来的字符串,尽管使用了New,但不需要管。GC会追踪。而且你也没法释放,创建完对象交给Java层了,不确定Java还在不在使用中呢,你咋delete?
- 只应该管理生命周期全都在native的new出来的对象,和引用。
- 需要特别注意方法运行的上下文,也即JNIEnv,这个东西每个线程有一个,且是不同的。要保证在同一个JNIEnv中管理内存,不同的JNIEnv无法共享创建出来的对象和引用,不能交叉使用,更不能交叉式的释放。
JNI能做什么
JNI是一个接口层,能够让Java进入C/C++世界,调用C/C++的代码,包括现有代码。所以只要编译出来了目标平台(ARM)的so,就可以在JNI中用。
当然了,为了兼容性,使用的so最好用NDK进行编译。
因为Android是Linux内核的,所以,理论上Linux系统调用支持的东西全都能在JNI里面搞。当然,使用native最为正确的体位是使用NDK来实现想要的功能,可以查看NDK的开发文档来明确可以做哪些事情。
使用JNI的正确姿式
JNI虽好,但不要滥用,不能单单以『C/C++语言性能高于Java(JVM)』为理由就去使用JNI。JNI本身是一个口子,单从方法调用角度讲,从Java层调用过来要有历经查询和数据转换,不见得会比Java方法高效到哪里去。而且JNI在线程调度,异常管理和内存管理上面都较Java层相比非常的不方便,那点看起来的性能优势的代价是很大的,所以说能不用JNI就别用。
使用JNI的正确理由:
- 做一些Java层无法做到的事情,比如一些底层的系统调用(System calls),Java层做不到,那自然得用C/C++
- 使用一些现有的C/C++代码,这个是最为正统的理由
- 基于安全角度考量,把一些关键的实现放在C/C++层,这个也合理,因为C/C++相较于Java字节码要略难破解一些
- 基于跨平台角度考虑,把一些与平台关联密切的,且独立的模块用C/C++实现,比如像通信协议,或者加密,或者压缩之类的非常独立的功能模块,用C/C++来实现,屏蔽名个平台的不同,这会让Java层更加的简单
除此之外,似乎没有理由使用JNI。另外,在使用的时候也要注意尽可能的单进单出,也就是说从Java层调用native方法,进去后一直在native运算,得到结果后返回给Java。而不应该频繁的有交互,比如说Java层调用进了native方法,但在native中又频繁 的调用Java层的方法。这明显是设计不合理,应该在Java层把需要的数据准备齐全后,再调用native层。
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22776,转载请注明出处。
评论0