美文网首页Java知识Android NDKAndroid-NDK/JNI
Android JNI 篇 - JNI回调的三种方法(精华篇)

Android JNI 篇 - JNI回调的三种方法(精华篇)

作者: trycatchx | 来源:发表于2017-03-23 18:55 被阅读4021次
    开门见山, 不废话上效果, 上代码: c层回调进度
    device-2017-03-23-184023.gif

    第一种方法

    在当前函数(同一个线程)里面回调,直接用findClass或者GetObjectClass,进行回调(国内各大博客介绍的普遍方法):
    java 层代码:
    /**
     * Created by jiong103 on 2017/3/23.
     */
    
    public class Sdk {
        private Sdk() {
        }
    
        //单例
        private static class SdkHodler {
            static Sdk instance = new Sdk();
        }
    
        public static Sdk getInstance() {
            return SdkHodler.instance;
        }
    
        //调到C层的方法
        private native void nativeDownload();
    
        //c层回调上来的方法
        private int onProgressCallBack(long total, long already) {
            //自行执行回调后的操作
            System.out.println("total:"+total);
            System.out.println("already:"+already);
            return 1;
        }
    }
    
    c层代码:
    JNIEXPORT void JNICALL Java_xxx_nativeDownload(JNIEnv *env, jobject thiz) {
     
        //直接用GetObjectClass找到Class, 也就是Sdk.class.
        jcalss jSdkClass =(*env)->GetObjectClass(env,thiz);
        if (jSdkClass == 0) {
            LOG("Unable to find class");
            return;
        }
        //找到需要调用的方法ID
        jmethodID javaCallback = (*env)->GetMethodID(env, jSdkClass,
                                                     "onProgressCallBack", "(JJ)I");
                                                     
        //进行回调,ret是java层的返回值(这个有些场景很好用)
        jint ret = (*env)->CallIntMethod(env, thiz, javaCallback,1,1);
         
        return ;
    }
    
    或者是:
    
    JNIEXPORT void JNICALL Java_xxx_nativeDownload(JNIEnv *env, jobject thiz) {
     
        //直接用findClass找到Class, 也就是Sdk.class.
        jcalss jSdkClass =(*env)->FindClass(env,"your/package/name/Sdk");
        if (jSdkClass == 0) {
            LOG("Unable to find class");
            return;
        }
        //找到需要调用的方法ID
        jmethodID javaCallback = (*env)->GetMethodID(env, jSdkClass,
                                                     "onProgressCallBack", "(JJ)I");
                                                     
       //这时候要回调还没有jobject,那就new 一个
        jmethodID sdkInit = (*env)->GetMethodID(env, jSdkClass,"<init>","()V");
        jobject jSdkObject = (*env)->NewObject(env,jSdkClass,sdkInit); 
        
        //进行回调,ret是java层的返回值(这个有些场景很好用)
        jint ret = (*env)->CallIntMethod(env, jSdkObject, javaCallback,1,1);
         
        return ;
    }
    
    
    
    
    
    好了运行函数:
    Sdk.getInstance().nativeDownload();
    
    结果就出来了:
    total:1
    already:1
    

    好了第一种讲述完毕,有些人肯定会说,这尼玛坑爹, 写了一大堆东西就实现一个这么鸡肋的功能, 还在当前的函数回调。 那我还不如直接return一个值更加方便, 是的没错, 这就是网上最普遍的一种回调方法, 压根没法投入项目用。
    好了兄弟别激动

    2.png

    我再介绍一种你看看:

    第二种

    在其他线程里面回调到java层,通过NewGlobalRef,保存全局变量(Stack Overflow 介绍的方法):
    java层代码:
    /**
     * Created by jiong103 on 2017/3/23.
     */
    
    public class Sdk {
        private Sdk() {
        }
    
        //单例
        private static class SdkHodler {
            static Sdk instance = new Sdk();
        }
    
        public static Sdk getInstance() {
            return SdkHodler.instance;
        }
    
        //调到C层的方法
        private native void nativeDownload();
    
        //c层回调上来的方法
        private int onProgressCallBack(long total, long already) {
            //自行执行回调后的操作
            System.out.println("total:"+total);
            System.out.println("already:"+already);
            return 1;
        }
    }
    
    c层代码:
    JavaVM *g_VM;
    jobject g_obj;
    JNIEXPORT void JNICALL Java_xxx_nativeDownload(JNIEnv *env, jobject thiz) {
     
        //JavaVM是虚拟机在JNI中的表示,等下再其他线程回调java层需要用到
        (*env)->GetJavaVM(env, &g_VM);
      // 生成一个全局引用保留下来,以便回调
        g_obj = (*env)->NewGlobalRef(env, thiz);
        
        // 此处使用c语言开启一个线程,进行回调,这时候java层就不会阻塞,只是在等待回调
        pthread_create(xxx,xxx, download,NULL);
        return ;
    }
    
    //在此处跑在子线程中,并回调到java层
    void download(void *p) {
        JNIEnv *env;
     
        //获取当前native线程是否有没有被附加到jvm环境中
       int getEnvStat = (*g_VM)->GetEnv(g_VM, (void **) &env,JNI_VERSION_1_6);
        if (getEnvStat == JNI_EDETACHED) {
            //如果没有, 主动附加到jvm环境中,获取到env
            if ((*g_VM)->AttachCurrentThread(g_VM, &env, NULL) != 0) {
                return;
            }
            mNeedDetach = JNI_TRUE;
        }
    
        //通过全局变量g_obj 获取到要回调的类
        jclass javaClass = (*env)->GetObjectClass(env, g_obj);
    
        if (javaClass == 0) {
            LOG("Unable to find class");
            (*g_VM)->DetachCurrentThread(g_VM);
            return;
        }
    
       //获取要回调的方法ID
        jmethodID javaCallbackId = (*env)->GetMethodID(env, jSdkClass,
                                                     "onProgressCallBack", "(JJ)I");
        if (javaCallbackId == NULL) {
            LOGD("Unable to find method:onProgressCallBack");
            return;
        }
       //执行回调
        (*env)->CallIntMethod(env, g_obj, javaCallbackId,1,1);
        
        //释放当前线程
       if(mNeedDetach) {
            (*g_VM)->DetachCurrentThread(g_VM);
        }
        env = NULL;
    }
    
    
    好了运行函数:
    Sdk.getInstance().nativeDownload();
    
    结果又出来了:
    total:1
    already:1
    
    好了第二种讲述完毕, 是不是感觉第二种还真有点靠谱了,在和C语言同事开发的时候这东西,还真能派上用场。
    那么有同学问了我在新线程里的void download(void *p), 直接用findClass,直接找到类进行回调不就行了吗,干嘛要保存为一个全局变量。 我只能说jni不允许这么干, 你这么干是find到的class直接为空, 从而无法回调!!!
    可是又有同学问了,如果我的需求场景是这样子呢:

    多线程任务下载,然后需要回调进度,那么多的线程都一并回调到
    onProgressCallBack
    这一个函数,我怎么区分数据是属于哪一个线程任务的?

    怎么玩:
    3.gif

    其实很简单:在Java层的Sdk.class类里面 创建一个Map, 通过一个long型的Uid作为key, 去区分线程任务, 回调接口存到value, 这样子key-value保存在Map里面。 当你调用C层方法的时候传相应的uid下去, 处理完毕后, 再把uid作为参数回调到java层的Sdk.class类的onProgressCallBack, 通过Map.get(uid),取出之前存好的对应回调接口, 进行分发回调。 搞定, 上代码:

    java层代码:
    /**
     * Created by jiong103 on 2017/3/23.
     */
    
    public class Sdk {
    
    
        public Sdk() {
        }
    
        //单例
        private static class SdkHodler {
            static Sdk instance = new Sdk();
        }
    
        public static Sdk getInstance() {
            return SdkHodler.instance;
        }
        //回调分发接口
        public interface OnSubProgressListener {
    
            public int onProgressChange(long total, long already);
        };
    
        private Map<Long, OnSubProgressListener> mMap = new HashMap<>();
    
    
    
        //调到C层的方法
        private native int nativeDownload(long uid,String downloadPath);
    
    
        //回调的方法
        private int onProgressCallBack(long uid, long total, long already) {
            OnSubProgressListener listener =  mMap.get(uid);
            if(listener != null) {
                if(already >= total) {
                    //下载完成,取消回调
                    mMap.remove(uid);
                } else {
                    //回调到指定任务去,通过uid辨别
                    listener.onProgressChange(total,already);
                }
            }
            return 0;
        }
    
        public void download(long uid,String downloadPath,OnSubProgressListener l) {
            mMap.put(uid,l);
            nativeDownload(uid,downloadPath);
        }
    
    }
    
    C层代码:
    带着uid 去执行任务,回调时候,把uid 回传到java层上面的
    private int onProgressCallBack(long uid, long total, long already);
    就可以区分是哪一个任务,并且取出Map里面存好的OnSubProgressListener接口进行回调
    (这部分就不写了, 比较简单, 后面有读者要求我再补上)
    
    好了运行函数:

    开启两个下载任务

     Sdk.getInstance().download(1,"xxx.jpg",new OnSubProgressListener(){
    
                @Override
                public int onProgressChange(long total, long already) {
                
                    return 0;
                }
            });
            
    Sdk.getInstance().download(2,"xxx.png",new OnSubProgressListener(){
    
                @Override
                public int onProgressChange(long total, long already) {
                    return 0;
                }
            });
    
    

    完毕!这样子就会回调到不同的接口中去了, 当然还有更牛逼的方法, 请看第三种。

    第三种方法:

    通过把接口jobject 传递到c层下面去,然后在c层里面进行回调 ( 和公司写c的同事共同研究出来的方法 ) :

    java层代码:

    public class Sdk {
    
    
        public Sdk() {
        }
    
        //单例
        private static class SdkHodler {
            static Sdk instance = new Sdk();
        }
    
        public static Sdk getInstance() {
            return SdkHodler.instance;
        }
        //回调到各个线程
        public interface OnSubProgressListener {
    
            public int onProgressChange(long total, long already);
        };
        
        //调到C层的方法
        private native int nativeDownload(String downloadPath,OnSubProgressListener l);
    
    }
    

    c层代码:

    JavaVM *g_VM;
    JNIEXPORT void JNICALL Java_xxx_nativeDownload(JNIEnv *env, jobject thiz,jstring jpath,jobject jcallback) {
     
        //JavaVM是虚拟机在JNI中的表示,等下再其他线程回调java层需要用到
        (*env)->GetJavaVM(env, &g_VM);
        
        //生成一个全局引用,回调的时候findclass才不会为null
        jobject callback = (*env)->NewGlobalRef(env, jcallback)
        
        // 把接口传进去,或者保存在一个结构体里面的属性, 进行传递也可以
        pthread_create(xxx,xxx, download,callback);
        return ;
    }
    
    //在此处跑在子线程中,并回调到java层
    void download(void *p) {
    
       if(p == NULL) return ;
      
        JNIEnv *env;
           //获取当前native线程是否有没有被附加到jvm环境中
        int getEnvStat = (*g_VM)->GetEnv(g_VM, (void **)   &env,JNI_VERSION_1_6);
        if (getEnvStat == JNI_EDETACHED) {
            //如果没有, 主动附加到jvm环境中,获取到env
            if ((*g_VM)->AttachCurrentThread(g_VM, &env, NULL) != 0) {
                return;
            }
            mNeedDetach = JNI_TRUE;
        }
        //强转回来
        jobject jcallback = (jobject)p;
        
        //通过强转后的jcallback 获取到要回调的类
        jclass javaClass = (*env)->GetObjectClass(env, jcallback);
    
        if (javaClass == 0) {
            LOG("Unable to find class");
            (*g_VM)->DetachCurrentThread(g_VM);
            return;
        }
    
       //获取要回调的方法ID
        jmethodID javaCallbackId = (*env)->GetMethodID(env, javaClass,
                                                     "onProgressChange", "(JJ)I");
        if (javaCallbackId == NULL) {
            LOGD("Unable to find method:onProgressCallBack");
            return;
        }
       //执行回调
        (*env)->CallIntMethod(env, jcallback, javaCallbackId,1,1);
        
        //释放当前线程
       if(mNeedDetach) {
            (*g_VM)->DetachCurrentThread(g_VM);
        }
        env = NULL;
        
        //释放你的全局引用的接口,生命周期自己把控
         (*env)->DeleteGlobalRef(env, jcallback);
        jcallback = NULL;
    }
    
    
    好了运行函数:
    Sdk.getInstance().nativeDownload("xx.jpg",new OnSubProgressListener(){
    
                @Override
                public int onProgressChange(long total, long already) {
                    return 0;
                }
            });
            
    Sdk.getInstance().nativeDownload("xx.png",new OnSubProgressListener(){
    
                @Override
                public int onProgressChange(long total, long already) {
                    return 0;
                }
            });        
    

    完毕!是不是少了uid这个参数, 而且少了map去保存你的接口, 优化了好多内存,啊哈哈! 这个是直接把接口传到jni层, 对应的类型是jobject, 在c层传递的这个接口的时候需(*env)->NewGlobalRef(env, jcallback) 生成全局引用进行传递,匹配C语言的void *类型, 那么在与c层交互人员联调的时候,如果使用到回调,需要在c开发人员那边程序代码 预留一个 void *变量进行存放回调接口。

    上面的gif图是用了第二种方案的map,jni回调所有的方法基本都在这里了。
    转发请注明链接地址。谢幕!

    相关文章

      网友评论

      • yu_yue:想请教一下,回调的接口是不是在子线程。是不是还得使用handler才能操作UI
        trycatchx:C 语言是没有主线程的概念的,建议在 java 层处理。
      • d9dc2aa471c4:请问能把源码发给我作为参考吗,初学者~~~~多谢了
        1969316569@qq.com
        d9dc2aa471c4:@Overried 那就麻烦了,看了你的代码很有帮助,之前网上清一色的当前函数回调都把我看无语了:smile:
        trycatchx:并没有 demo 代码。后续我可以整理一个,第一时间发给你。:blush:
      • c8507b890eb0:楼主,我最近用到跟你第二种方法差不多回调函数,但是呢在Android5.0之前用的好好的,5.0之后就报错,这个怎么解?
        trycatchx:请问报什么错误?
      • gfson:李菊福😁
        trycatchx:@gfson :cry::cry:
      • df6f5d021bae:很好啊,兄弟
        trycatchx:@自由人188 啊哈哈,共勉!
      • 64d5c7978734:说得很详细 看完对我帮助好大,不错 好文章
        trycatchx:@weiru 哈哈,谢谢支持
      • trycatchx:JNI 这部分,已经研究了三年了,后续会多发一些JNI 的 技巧上去!多谢各位支持!@TryCatch
        trycatchx:@举头望明月泣 如果想要往底层发展例如: 搞rom , jni 和c是需要学习。如果只是写应用, 了解jni 基本用法就可以了。
        举头望明月泣: @Overried 前辈三年,似乎很难摸清jni的头脑和发展方向啊,应届生求指导

      本文标题:Android JNI 篇 - JNI回调的三种方法(精华篇)

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