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

开门见山, 不废话上效果, 上代码: 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回调所有的方法基本都在这里了。
转发请注明链接地址。谢幕!

推荐阅读:Android 编译速度优化黑科技 - RocketX

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容