话说,这个NDK,其实学习了无数遍,之前项目上也没有什么地方用,就导致了一个问题,那就是学了忘记,忘记了学,我总结了下,那就是没有写笔记,没有写笔记,就没有所谓的回头看笔记的概念了。
但是呢,已经2024年了,是吧,鸿蒙的出现,就导致了这个C和C++ 不得不学了,但是还是有倾向性的,我的目的是学完后,看得懂github 上的一些简单代码就行,毕竟现在的我也不大可能进得了全手写C和C++的项目,所以核心还是在UI业务层,但是这个玩意不可不会,鸿蒙出来了,Android 不可能就死了,感觉就业机会的变多了(这个玩意是相对的,如果本身需求都到不到我头上,反而是减少了就业机会),当然对我这种技术不上不下的人而言,既然某个领域精不了,那么就得多会,现在恰恰是学习这个玩意的好时机,鸿蒙出来了,很多C或C++ 工程在Android上可用的,那么就会被搬到上面去,我们的目的就是等大佬搬完了,起码看得懂一些,不至于像现在Android,大佬各种设计模式一包,看的打脑壳,现在还在初中期,有很多大佬开始整blog,那么就可以趁机学习一波。至于说为什么不看之前的Android blog,很多demo运行不起来也是一个原因,编译模式也不一样,很多细节,不经历就完全搞不懂。
既然明确了目标,我们核心还是巩固Android,然后学习NDK,那么最好的方式,除了一个老师手把手教学,那就是自己看Google 提供的Demo了,最近看鸿蒙的Demo 发现的,原来Android 官方也提供了很多Demo,哭死,英语也得补。
OK,那就开整,我们从第一个Demo,尝试去逐行理解。
资料
正文
这个工程很简单,就是通过调用C或者C++提供的函数返回一个字符串。
配置
build.gradle
配置NDK的版本:
arduino
ndkVersion '25.1.8937393'
很多老的ndk 项目跑不起来,往往加上这个可以跑起来,但是得注意版本。
配置cmake:
css
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
这个说明是通过cmake 编译静态库和动态库的。如果说这种编译模式,这个文件是必须的。
cpp
cmakeLists.txt 文件。
scss
cmake_minimum_required(VERSION 3.18.1)
project("hello-jni")
add_library(hello-jni SHARED
hello-jni.cpp)
target_link_libraries(hello-jni
android
log)
- cmake_minimum_required 的版本
- project 项目名称。
- add_library 导入的c或c++的lib,hello-jni 表示这个是库的名称,SHARED 指定库的类型,表示是共享库,hello-jni.cpp 表示这个是lib 的源文件。
- target_link_libraries 表示:这行代码的意思是:"将
android
和log
这两个库链接到hello-jni
目标。" 这两个库,反正是需要的,我注释掉会报错。
源码
hello-jni.cpp
c
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from JNI.";
return env->NewStringUTF(hello.c_str());
}
- include 导入库,jni和string
- extern "C" JNIEXPORT jstring JNICALL ,JNI是C代码,但是是C++工程,所以需要添加 extern "C",如果不添加这个调调会出现函数找不到。JINEXPORT 和JNICALL 是JNI的宏,用于定义本地方法如何从Java调用,JNIEXPORT是用来定义导出函数的关键字,JNICALL则用于指定函数调用约定。jstring 表示这个函数返回了一个Java 的字符串对象。
- std::string hello = "Hello from JNI."; 定义了一个c++的 string 对象。
- env->NewStringUTF(hello.c_str()) 创建一个Java 的字符串对象然后返回。 env->NewStringUTF("Hello from JNI. 直接赋值") 这么也可以。
- jobject还是jclass是合JNI函数类型有关,如果说静态函数则是jclass。
从上面的代码 我们可以看出,jstring 用于定义函数的返回值,先定义了一个C++的字符串,然后通过env 创建了一个Java 对象。JNIEnv 可以对Java 对象进行如下操作:创建Java 对象,调用Java 对象方法,获取Java对象属性,所以NewStringUTF是创建了一个Java 的字符串对象。
kotlin 层的代码
kotlin
class HelloJni : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityHelloJniBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.helloTextview.text = stringFromJNI()
}
external fun stringFromJNI(): String?
companion object {
init {
System.loadLibrary("hello-jni")
}
}
}
- System.loadLibrary("hello-jni") 导入我们创建的hello-jni 的lib,这种属于静态导入。
- external fun stringFromJNI(): String? 这种是kotlin 中定义的external 定义native 函数。这个地方可以看出jobject 其实就是HelloJni 这个对象。
扩展
env 常用函数
- NewObject 创建Java 类对象
- NewString 创建字符串对象
- NewArray 创建type的数组对象
- GetField 获取 字段
- SetField 设置 字段
- get/setStaticField 获取或设置静态字段不能
- callMethod 调用返回值为type的方法。
- callStaticMethod 调用返回值为type 的静态方法
基于NewObject创建Int
ini
extern "C" JNIEXPORT jobject JNICALL
Java_com_example_hellojni_HelloJni_intFromJNI(JNIEnv *env, jobject thiz) {
// 获取到class
jclass intClass = env->FindClass("java/lang/Integer");
// 构造函数
jmethodID constructor = env->GetMethodID(intClass, "<init>", "(I)V");
jobject intObj = env->NewObject(intClass, constructor, 5);
return intObj;
}
通过这个例子,我们可以掌握几个知识点:
- 首先需要class 的包明
- 默认的构造函数,例如int 的是:(I)V,I表示入参类型,V表示没有返回值。如果括号里面填了值,那么这个玩意需要写默认值。
基于NewObject创建User
User的class:
kotlin
class User(){
var name:String="默认的用户"
var age:Int=5
}
函数:
kotlin
external fun userFromJNI(): User?
JNI函数:
ini
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_hellojni_HelloJni_userFromJNI(JNIEnv *env, jobject thiz) {
// 创建Java 对象
jclass userClass = env->FindClass("com/example/hellojni/User");
jmethodID constructor = env->GetMethodID(userClass, "<init>", "()V");
jobject intObj = env->NewObject(userClass, constructor);
// 直接调用属性 设置name
jfieldID nameFieldId = env->GetFieldID(userClass, "name", "Ljava/lang/String;");
env->SetObjectField(intObj, nameFieldId, env->NewStringUTF("Hello from JNI. 直接赋值"));
// 调用set 设置年龄
jmethodID ageMid = env->GetMethodID(userClass, "setAge", "(I)V");
env->CallVoidMethod(intObj,ageMid,55);
return intObj;
}
基于NewObject创建Student
class,这个主要是多个入参的构造函数:
arduino
public class Student {
public String name;
public int age;
public void setAge(int age) {
this.age = age;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
native:
ini
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_hellojni_HelloJni_studentFromJNI(JNIEnv *env, jobject thiz) {
jclass userClass = env->FindClass("com/example/hellojni/Student");
jmethodID constructor = env->GetMethodID(userClass, "<init>", "(Ljava/lang/String;I)V");
jobject intObj = env->NewObject(userClass, constructor,env->NewStringUTF("张3"),5);
return intObj;
}
改造一下,接受外部传入参数:
ini
extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_hellojni_HelloJni_studentFromJNI(JNIEnv *env, jobject thiz, jstring name,
jint age) {
jclass userClass = env->FindClass("com/example/hellojni/Student");
jmethodID constructor = env->GetMethodID(userClass, "<init>", "(Ljava/lang/String;I)V");
jobject intObj = env->NewObject(userClass, constructor,name,age);
return intObj;
}
我记得这个地方有问题,就是这个age 的类型是jint,但是我们NewObject 传入的不能是jint,所以会报错,抽时间补一下。
直接调用jobject 中的函数
ini
extern "C"
JNIEXPORT void JNICALL
Java_com_example_hellojni_HelloJni_setNativeCall(JNIEnv *env, jobject thiz) {
jclass callClass = env->FindClass("com/example/hellojni/HelloJni");
jmethodID nativeCall = env->GetMethodID(callClass, "nativeCall", "()V");
env->CallVoidMethod(thiz,nativeCall);
}
总结
通过这次的demo,我可以看到Java 对象和C和C++的对象是不能直接互通的,需要通过JNI 转换一层。Java 传入进入到需要通过JNIEnv 转换一次,传入的已经自己转换了。所以习惯反射的调用还是很重要的。无论是方法还是属性,都需要先获取到class,然后获取到方法属性,最后才是调用,静态函数则不需要创建对象,昨天请教了一个C大佬,大佬提点我说,通信应该走socket,很好的设计,很好的高度,直接调用确实是实现上简单了,就是写socket双端的代码量就多了,感觉还是得看情况吧。
当然了。这里面还有一些问题没有解决回答,比如说,我们学习C的时候,有一个概念,那就是C没有JAVA这种GC机制,但是我们上面写的一趴啦代码,却没有任何一句代码是用于GC的,但是这又是一个C++的代码,有点打脑壳,后续再整。