NDK(01)运行一次hello-jni项目能学到什么

话说,这个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 表示:这行代码的意思是:"将 androidlog 这两个库链接到 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++的代码,有点打脑壳,后续再整。

相关推荐
MavenTalk9 分钟前
前端技术选型之uniapp
android·前端·flutter·ios·uni-app·前端开发
坚定信念,勇往无前16 分钟前
uni-app运行 安卓模拟器 MuMu模拟器
android·uni-app
吾即是光3 小时前
[SWPUCTF 2021 新生赛]error
android
大耳猫3 小时前
Android 基于Camera2 API进行摄像机图像预览
android·kotlin·相机·camera
MYBOYER3 小时前
Kotlin DSL Gradle 指南
android·开发语言·kotlin
Mr_Xuhhh4 小时前
程序地址空间
android·java·开发语言·数据库
呆呆小雅6 小时前
C# 结构体
android·java·c#
ᥬ 小月亮8 小时前
Layui表格的分页下拉框新增“全部”选项
android·javascript·layui
sunly_17 小时前
Flutter:启动屏逻辑处理02:启动页
android·javascript·flutter
Sgq丶18 小时前
Android Studio 配置 proto
android·ide·android studio