JNI——项目实战

224 阅读5分钟

下面以一个在界面上绘制三角形的例子来讲解在实际项目中如何使用 JNI。

上层代码

新建一个data class,里面是三角形的 3 个顶点的坐标:

data class RulerInfo(var first: PointF = PointF(),
                     var second: PointF = PointF(),
                     var third: PointF = PointF()
    ) {
    external fun initId()
}

这里有一个 Native 方法 initId(),我们后面再讲它的作用。

我们使用一个自定义 View 来绘制三角形,名为 TriangleView,其代码如下:

class TriangleView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val paint = Paint().apply {
        color = Color.RED       // 颜色
        style = Paint.Style.FILL // 填充模式
        isAntiAlias = true      // 抗锯齿
    }

    private var path = Path()

    private var rulerInfo: RulerInfo? = null

    fun update(rulerInfo: RulerInfo){
        this.rulerInfo = rulerInfo
        invalidate()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        path.apply {
            rulerInfo?.let {
                moveTo(it.first.x, it.first.y)
                lineTo(it.second.x, it.second.y)
                lineTo(it.third.x, it.third.y)
            }
            close()
            canvas.drawPath(path, paint)
        }
    }
}

在外面调用 update() 方法并传入 rulerInfo 即可绘制三角形。

activity_main.xml 代码如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_init"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="init"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.example.jnitest.TriangleView
        android:id="@+id/view_triangle"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

里面包含一个按钮和一个 TriangleView。

MainActivity 代码如下:

class MainActivity : AppCompatActivity() {

    init {
        System.loadLibrary("jnitest")
        RulerInfo().initId()
    }

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.btnInit.setOnClickListener{
            initData()
        }
    }

    external fun initData()

    private fun drawTriangle(rulerInfo: RulerInfo){
        binding.viewTriangle.update(rulerInfo)
    }
}

这里在 init{} 中调用了 RulerInfo().initId(),在点击按钮的时候调用了 Native 方法 initData(),此外还有一个 drawTriangle() 方法,drawTriangle() 方法会调用 TriangleView 的 update() 方法。

C++ 代码

新建一个 Clz_RulerInfo.h:

#include "Common.h"

#ifndef JNITEST_CLZ_RULERINFO_H
#define JNITEST_CLZ_RULERINFO_H

class RulerInfoJniData {
    public:
        RulerInfoJniData();
        ~RulerInfoJniData();

        static jclass clz;
        static jmethodID constructorMethodId;
        static jfieldID firstFieldId;
        static jfieldID secondFieldId;
        static jfieldID thirdFieldId;

        static jfieldID xFieldId;
        static jfieldID yFieldId;

        static jobject SetAllField(RulerInfo rulerInfo);
};

#endif //JNITEST_CLZ_RULERINFO_H

这里声明了一些全局变量,对应的 Clz_RulerInfo.cpp 代码如下:

#include <jni.h>
#include "Clz_RulerInfo.h"
#include "Sdk.h"

jclass RulerInfoJniData::clz = nullptr;
jmethodID RulerInfoJniData::constructorMethodId = nullptr;
jfieldID RulerInfoJniData::firstFieldId  = nullptr;
jfieldID RulerInfoJniData::secondFieldId = nullptr;
jfieldID RulerInfoJniData::thirdFieldId  = nullptr;
jfieldID RulerInfoJniData::xFieldId = nullptr;
jfieldID RulerInfoJniData::yFieldId = nullptr;

extern "C"
JNIEXPORT void JNICALL
Java_com_example_jnitest_RulerInfo_initId(JNIEnv *env, jobject thiz) {
    jclass clzRulerInfo = env->FindClass("com/example/jnitest/RulerInfo");
    // clzRulerInfo 是局部引用,只在当前的 JNI 函数调用期间有效,使用 NewGlobalRef 创建全局引用
    RulerInfoJniData::clz = static_cast<jclass>(env->NewGlobalRef(clzRulerInfo));
    // 获取 RulerInfo 的构造函数的 MethodId,<init> 是 JVM 中构造函数的固定方法名。
    RulerInfoJniData::constructorMethodId = env->GetMethodID(clzRulerInfo, "<init>", "()V");
    // 获取三角形三个点的 FieldId
    RulerInfoJniData::firstFieldId = env->GetFieldID(clzRulerInfo, "first", "Landroid/graphics/PointF;");
    RulerInfoJniData::secondFieldId = env->GetFieldID(clzRulerInfo, "second", "Landroid/graphics/PointF;");
    RulerInfoJniData::thirdFieldId = env->GetFieldID(clzRulerInfo, "third", "Landroid/graphics/PointF;");

    jclass pointFClz = env->FindClass("android/graphics/PointF");
    // 获取 x 的 FieldId
    RulerInfoJniData::xFieldId = env->GetFieldID(pointFClz, "x", "F");
    RulerInfoJniData::yFieldId = env->GetFieldID(pointFClz, "y", "F");
    env->DeleteLocalRef(pointFClz);
}

jobject RulerInfoJniData::SetAllField(RulerInfo rulerInfo){
    JNIEnv *pEnv;
    bool attach = getOrAttachEnv(&pEnv);
    // 创建 RulerInfo 的实例,这里创建的是 Java 中的 RulerInfo 的实例
    jobject rulerInfoObj = pEnv->NewObject(RulerInfoJniData::clz, RulerInfoJniData::constructorMethodId);

    // 拿到第一个点的实例
    jobject firstObj = pEnv->GetObjectField(rulerInfoObj, RulerInfoJniData::firstFieldId);
    // 配置第一个点的实例中的 x 和 y
    pEnv->SetFloatField(firstObj, RulerInfoJniData::xFieldId, rulerInfo.first.x);
    pEnv->SetFloatField(firstObj, RulerInfoJniData::yFieldId, rulerInfo.first.y);
    pEnv->DeleteLocalRef(firstObj);

    jobject secondObj = pEnv->GetObjectField(rulerInfoObj, RulerInfoJniData::secondFieldId);
    pEnv->SetFloatField(secondObj, RulerInfoJniData::xFieldId, rulerInfo.second.x);
    pEnv->SetFloatField(secondObj, RulerInfoJniData::yFieldId, rulerInfo.second.y);
    pEnv->DeleteLocalRef(secondObj);

    jobject thirdObj = pEnv->GetObjectField(rulerInfoObj, RulerInfoJniData::thirdFieldId);
    pEnv->SetFloatField(thirdObj, RulerInfoJniData::xFieldId, rulerInfo.third.x);
    pEnv->SetFloatField(thirdObj, RulerInfoJniData::yFieldId, rulerInfo.third.y);
    pEnv->DeleteLocalRef(thirdObj);

    if (attach) {
        detachEnv();
    }
    // 返回 Java 中的 RulerInfo 的实例
    return rulerInfoObj;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_example_jnitest_MainActivity_initData(JNIEnv *env, jobject thiz) {
    JNIEnv *pEnv;
    bool attach = getOrAttachEnv(&pEnv);

    // 初始化 RulerInfo 实例的三个点坐标,这里是 C++ 中的 RulerInfo
    RulerInfo rulerInfo;
    rulerInfo.first  = {100, 100};
    rulerInfo.second = {500, 100};
    rulerInfo.third = {300, 400};

    jobject rulerInfoObj = RulerInfoJniData::SetAllField(rulerInfo);

    jclass mainJClass = pEnv->GetObjectClass(thiz);
    jmethodID drawTriangleMethodId = pEnv->GetMethodID(mainJClass, "drawTriangle", "(Lcom/example/jnitest/RulerInfo;)V");
    /// 调用 drawTriangle() 方法并传入 rulerInfoObj
    pEnv->CallVoidMethod(thiz, drawTriangleMethodId, rulerInfoObj);

    pEnv->DeleteLocalRef(rulerInfoObj);
    pEnv->DeleteLocalRef(mainJClass);

    if (attach) {
        detachEnv();
    }
}

可以看到 initId() 函数中就是这些全局变量的初始化,这样做的好处是方便后面直接拿来用,并且可以重复使用。

当我们点击按钮的时候会调用 initData() 方法,这里先初始化 RulerInfo 实例的三个点坐标,然后调用 SetAllField() 给 Java 中的三角形的 3 个点赋值,最后调用 drawTriangle() 函数触发绘制。

这里直接在上层通过点击事件触发 ,实际项目中可能是 Native 代码来触发的。

这里的 RulerInfo 是 C++ 中的 RulerInfo,其代码如下:

// Common.h
struct TransformPointF{
    float x;
    float y;
};

struct RulerInfo{
    TransformPointF first;
    TransformPointF second;
    TransformPointF third;
};

Sdk.h 代码如下:

#include <jni.h>

extern "C" bool getOrAttachEnv(JNIEnv** pEnv);

extern "C" void detachEnv();

Sdk.cpp 代码如下:

#include <jni.h>
#include "Clz_RulerInfo.h"

JavaVM* jvm;
const jint version = JNI_VERSION_1_6;

extern "C"
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    jvm = vm;
    JNIEnv *pEnv = nullptr;
    if(vm->GetEnv(reinterpret_cast<void **>(&pEnv), version) != JNI_OK){
        return JNI_ERR;
    }
    return version;
}

extern "C"
JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) {
    JNIEnv *pEnv = nullptr;
    if (vm->GetEnv((void **) &pEnv, version) != JNI_OK) {
        return;
    }
    // 清理全局引用
    if (RulerInfoJniData::clz ){
        pEnv->DeleteGlobalRef(RulerInfoJniData::clz);
        RulerInfoJniData::clz = nullptr;
    }
}

extern "C" bool getOrAttachEnv(JNIEnv** pEnv){
    // 如果是主线程,直接拿到已有的 JNIEnv*,无需 detach
    jint result = jvm->GetEnv(reinterpret_cast<void **>(pEnv), version);
    if(result != JNI_OK){
        // 如果是非主线程,调用 AttachCurrentThread() 绑定一个新的 JNIEnv*, 需要 detach
        if(jvm->AttachCurrentThread(pEnv, nullptr) == JNI_OK){
            return true;
        }
    }
    return false;
}

extern "C" void detachEnv(){
    jvm->DetachCurrentThread();
}

JNI 规范规定,JNIEnv是线程局部存储 (Thread-Local Storage, TLS)的,这意味着每个线程都有自己独立的 JNIEnv 指针。当一个 native 方法被 Java 调用时,JVM 会自动为该线程绑定一个 JNIEnv*。如果你自己在 C++ 层 std::thread 或 pthread_create 创建了一个新线程,这个新线程对 JVM 来说是“陌生”的,它没有绑定的 JNIEnv*。

在主线程中,JVM 会自动为其绑定 JNIEnv* ,可以直接使用。在非主线中,默认没有 JNIEnv*,必须通过 AttachCurrentThread() 函数显式绑定后才能使用 JNIEnv*,并且需要后续调用 detachEnv()。所以它们俩是成对出现的:

bool attach = getOrAttachEnv(&pEnv);

...

if (attach) { 
    detachEnv(); 
}

这里 initData() 是由 Java 主线程调用的(此时 JNIEnv* 已存在),但在实际项目中,如果其他线程调用此函数,必须正确处理 JNIEnv*,最好始终通过 getOrAttachEnv() 函数来获取 JNIEnv*。

点击按钮后显示如下所示:

image.png
这样就把三角形画出来了。