话说,这个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的版别:

ndkVersion '25.1.8937393'

许多老的ndk 项目跑不起来,往往加上这个能够跑起来,可是得注意版别。

装备cmake:

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

这个阐明是经过cmake 编译静态库和动态库的。假如说这种编译形式,这个文件是必须的。

cpp

cmakeLists.txt 文件。

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

#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 层的代码

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

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:

class User(){
    var name:String="默许的用户"
    var age:Int=5
}

函数:

external fun userFromJNI(): User?

JNI函数:

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,这个主要是多个入参的结构函数:

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:

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;
}

改造一下,承受外部传入参数:

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 中的函数

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++的代码,有点打脑壳,后续再整。