美文网首页JNI&NDK
JNI&NDK开发最佳实践(十):补充要点(持续更新)

JNI&NDK开发最佳实践(十):补充要点(持续更新)

作者: taoyyyy | 来源:发表于2019-07-22 21:57 被阅读24次

    一、在C中实现Java回调函数

    我们知道在C中通过传递函数指针可以轻易实现函数回调的效果,而在java中则一般是通过构造匿名内部类对象来间接实现函数回调。那么如何在C中构造一个具有回调函数功能的对象呢?
    例如在java中给一个Button设置点击事件回调

    mBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
    
                }
    });
    

    那倘若我们要在C中通过JNI的方式来实现给Button设置点击回调,该如何实现呢?

    1. java中声明本地方法
    public native void setOnclickListener();
    
    

    2.通过env反射调用View#setOnClickListener方法

    extern "C"
    JNIEXPORT void JNICALL
    Java_com_example_taoying_testndkapp_MainActivity_setOnclickListener(JNIEnv *env, jobject instance) {
    
        // TODO
        /*mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
    
            }
        });*/
        //获取java mBtn的引用
        jclass mainActivityClass = env->FindClass("com/example/taoying/testndkapp/MainActivity");
        jfieldID buttonField = env->GetFieldID(mainActivityClass, "mBtn", "Landroid/widget/Button;");
        jobject button = env->GetObjectField(instance, buttonField);
    
        //调用mBtn.setOnclickListener
        jclass buttonClass = env->FindClass("android/widget/Button");
    //    jclass viewClass = env->FindClass("android/view/View");
        jmethodID setOnClickListener = env->GetMethodID(buttonClass, "setOnClickListener",
                                                        "(Landroid/view/View$OnClickListener;)V");
    
        jobject listener = creatOnClickListener(env);
        env->CallVoidMethod(button, setOnClickListener, listener);
    }
    

    3.反射创建java层的NativeOnclickListener对象(继承自OnClickListener),将具体的回调实现通过构造方法把该函数指针传递到java层。

    static jobject creatOnClickListener(JNIEnv *env) {
        jclass nativeOnclickListenerClass = env->FindClass(
                "com/example/taoying/testndkapp/NativeOnclickListener");
        jmethodID init = env->GetMethodID(nativeOnclickListenerClass, "<init>", "(J)V");
        jlong lptr = reinterpret_cast<jlong>(onClick);
    //    LOGE("onClick: %p, long: %lld", ptr, lptr);
        jobject listener = env->NewObject(nativeOnclickListenerClass, init, lptr);
        return listener;
    }
    static void onClick(JNIEnv *env, jobject view) {
        LOGD("ZZZZZZZZZ");
    }
    

    4.java层实现继承自OnClickListener 的NativeOnclickListener,在其onClick回调中调用本地方法,最后调用到ptr所指向的本地函数。

    public class NativeOnclickListener implements View.OnClickListener {
        private static final String TAG = "NativeOnclickListener";
    
        private final long ptr;
    
        public NativeOnclickListener(long ptr) {
            this.ptr = ptr;
        }
    
        @Override
        public void onClick(View v) {
            Log.d(TAG, "onClick: ready to call nativeOnClick"+ Long.toHexString(ptr));
            MainActivity.nativeOnClick(ptr, v);//java层声明的本地方法
        }
    
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_example_taoying_testndkapp_MainActivity_nativeOnClick(JNIEnv *env, jclass type,
                                                                   jlong ptr, jobject view) {
        LOGD("do Java_com_example_taoying_testndkapp_MainActivity_nativeOnClick: %p", onClick);
        void (*func)(JNIEnv*, jobject) = reinterpret_cast<void (*)(JNIEnv *, jobject)>(ptr);
        if (!func){
            return;
        }
        func(env, view);
    }
    

    核心思想在于:将C中实现回调逻辑的函数的函数指针传给java。

    二、C调Java之泛型的处理

    在jni中反射调用java时遇到泛型该如何处理?例如如何反射调用java.util.ArrayList#add(E)???

    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    

    处理方式和在java中反射一样。事实上在java中由于泛型擦除的缘故,即在进入 JVM 之前,与泛型相关的信息会被擦除掉。举个例子

    • List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class
    • public class Erasure <T>中的T类型在jvm即为java.lang.Object
    • public class Erasure <T extends String>中的T类型在jvm即为java.lang.String

    三、C调Java之多态的处理

    先提几个问题?

    • c中拿到java中的子类对象后,获取父类的方法id,反射调用,此时调用的是父类方法还是子类方法?
    • c中拿到java中的子类对象后,获取子类的方法id,反射调用,此时调用的是父类方法还是子类方法?
    • c中拿到java中的子类对象后,如何调用其父类方法和子类方法?
    • env->CallObjectMethod与env->CallNonvirtualObjectMethod二者有何区别和联系?
      至此,我们需要先了解java与C中多态的区别
      在java中,由于父类引用指向子类对象,因此持有子类对象时调用的方法总是子类实现的方法。
      而在C++中,默认是调用的父类方法,若想执行子类方法,需要将父类该方法用virtual声明为虚函数。java中所有的函数都是虚函数。
      下面通过一段代码来演示如上所述问题。
      在java中声明成员变量tomson
    MainActivity.java
    ...
    Father tomson = new Son("sonname", 5);
    ...
    

    其各自实现如下所示

    public class Father{
        private final String name;
        private final int age;
        private String sex = "man";
    
        public Father(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getSex() {
            return sex;
        }
    
        public void setSex(String sex) {
            this.sex = sex;
        }
    
        public String speak(){
            return "I'm Father";
        }
    }
    
    
    public class Son extends Father {
        public Son(String name, int age) {
            super(name, age);
        }
    
        @Override
        public String speak() {
    //        return super.speak();
            return "I'm Son";
        }
    }
    

    在C中作如下测试

    extern "C"
    JNIEXPORT void JNICALL
    Java_com_example_taoying_testndkapp_MainActivity_printInC(JNIEnv *env, jobject instance,
                                                              jobject tomson1) {
        jclass pJclass = env->GetObjectClass(instance);
        //Father tomson时,能用Father.class找到,但无法用Son.class找到
        //Son tomson时,能用 Son.class找到,但无法用Father.class找到
        jfieldID tomsonField = env->GetFieldID(pJclass, "tomson", "Lcom/example/taoying/testndkapp/bean/Father;");//用父类class和子类class都能找到Field?
    //    jfieldID tomsonField = env->GetFieldID(pJclass, "tomson", "Lcom/example/taoying/testndkapp/bean/Son;");//
        jobject tomson = env->GetObjectField(instance, tomsonField);
    
        jclass FatherClass = env->FindClass("com/example/taoying/testndkapp/bean/Father");
        jclass SonClass = env->FindClass("com/example/taoying/testndkapp/bean/Son");
        jmethodID speak = env->GetMethodID(FatherClass, "speak", "()Ljava/lang/String;");
    
        jmethodID speakInSon = env->GetMethodID(SonClass, "speak", "()Ljava/lang/String;");
    
    
    
        jstring str1 = static_cast<jstring>(env->CallObjectMethod(tomson, speak));//调用的是子类的方法
        jstring str2 = static_cast<jstring>(env->CallNonvirtualObjectMethod(tomson, SonClass, speak));//调用的是父类的方法,将FatherClass换为SonClass,依然调用的是父类的方法
        LOGD("str1 = %s,str2 = %s",env->GetStringUTFChars(str1,JNI_FALSE),env->GetStringUTFChars(str2,JNI_FALSE));
    
        jstring str3 = static_cast<jstring>(env->CallObjectMethod(tomson, speakInSon));//调用的是子类的方法
        jstring str4 = static_cast<jstring>(env->CallNonvirtualObjectMethod(tomson, SonClass, speakInSon));//调用的是子类的方法,将SonClass换为FatherClass,报错
        LOGD("str3 = %s,str4 = %s",env->GetStringUTFChars(str3,JNI_FALSE),env->GetStringUTFChars(str4,JNI_FALSE));
    
    }
    

    测试结果如下


    测试结果.png
    • Q1:c中拿到java中的子类对象后,获取父类的方法id,用CallObjectMethod反射调用,此时调用的是父类方法还是子类方法?
      A:子类方法。
    • Q2:c中拿到java中的子类对象后,获取子类的方法id,用CallObjectMethod反射调用,此时调用的是父类方法还是子类方法?
      A:子类方法。
    • Q3:c中拿到java中的子类对象后,如何调用其父类方法和子类方法?
      A:env->CallObjectMethod调用子类方法,env->CallNonvirtualObjectMethod调用父类方法。
    • env->CallObjectMethod与env->CallNonvirtualObjectMethod二者有何区别和联系?
      A:如上。

    四、cmake配置之如何配置多个本地文件

    build.gradle.png
    工程目录结构.png

    方式一:
    CMakeLists.txt

    ...
    file(GLOB native_srcs "*.cpp")//此处配置源文件文件夹的路径
    
    add_library( # Sets the name of the library.
            native-lib
    
            # Sets the library as a shared library.
            SHARED
    
            # Provides a relative path to your source file(s).
    #        native-lib.cpp native-lib2.cpp
            ${native_srcs}
            )
    ...
    

    方式二:
    CMakeLists.txt

    add_library( # Sets the name of the library.
            native-lib
    
            # Sets the library as a shared library.
            SHARED
    
            # Provides a relative path to your source file(s).
            native-lib.cpp native-lib2.cpp
    #        ${native_srcs}
            )
    

    五、在C中如何打印LOG

    mylog.h

    //
    // Created by taoying on 2019/7/17.
    //
    
    #ifndef TESTNDKAPP_MYLOG_H
    #define TESTNDKAPP_MYLOG_H
    
    #endif //TESTNDKAPP_MYLOG_H
    #include <android/log.h>
    #define TAG "projectname" // 这个是自定义的LOG的标识
    #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型
    #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义LOGI类型
    #define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义LOGW类型
    #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义LOGE类型
    #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义LOGF类型
    

    在需要使用地方将上面的头文件include进来即可

    #include <jni.h>
    #include <string>
    #include <regex>
    #include "mylog.h"
    
    const char *
            KNOWN_DANGEROUS_APPS_PACKAGES[] = {"com.koushikdutta.rommanager",
                                               "com.dimonvideo.luckypatcher",
                                               "com.chelpus.lackypatch", "com.ramdroid.appquarantine"};
    ...
    

    相关文章

      网友评论

        本文标题:JNI&NDK开发最佳实践(十):补充要点(持续更新)

        本文链接:https://www.haomeiwen.com/subject/zllglctx.html