[Android]使用函数指针实现native层异步回调

1. 前言

在上篇关于lambda表达式实现方式的文章中,有提到一个概念叫做MethodHandle,当时的解释是类似于C/C++的函数指针,但是文章发出后咨询友人的意见,发现很多人并不清楚函数指针是怎么用的,其实我本人也是只是知道这个概念,但是并没有实际使用过。仿佛冥冥中自有天意,前几天公司的项目正好用到了函数指针来做native层的事件回调,也让我理解了函数指针的妙用。但是关于C/C++我并不是特别熟练,于是将实现过程写了个DEMO,一是为了做个记录熟悉过程,二是以备后续使用。

2. 概念

如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

那么这个指针变量怎么定义呢?虽然同样是指向一个地址,但指向函数的指针变量同我们之前讲的指向变量的指针变量的定义方式是不同的。例如:

int(*p)(int, int);

这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“”,即(p);其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为 int(*)(int,int)。

所以函数指针的定义方式为:

函数返回值类型 (* 指针变量名) (函数参数列表);

“函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;“函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。

我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(指针变量名)”。但是这里需要注意的是:“(指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。

那么怎么判断一个指针变量是指向变量的指针变量还是指向函数的指针变量呢?首先看变量名前面有没有“”,如果有“”说明是指针变量;其次看变量名的后面有没有带有形参类型的圆括号,如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。

3. 定义函数指针和枚举

假设native层有个耗时操作需要异步调用,我们在异步调用结束后通过回调通知业务层完成事件,那么这个时候就可以使用函数指针作为回调方法。

定义方式:

  1. 首先定义事件枚举:
enum EventEnum {
    eeSleepWake,
};
  1. 其次,定义一个函数指针:
typedef void (*onSleepWake)(int code, void* sender);

这个函数指针可以指向一个返回值为void 参数分别为 int 和void型指针的函数,其中void型指针表示调用方的指针

  1. 定义一个结构体,包含函数指针和调用方的指针
struct EventData {
    void* eventPointer;
    void* sender;
};
  1. 注册事件持有类,使其成为单例

这个操作的部分代码:

class EventManager {
public:
    static EventManager& singleton()
    {
        static EventManager sl;
        return sl;
    }
    static EventManager& getInstance()
    {
        return singleton();
    }

    //注册事件
    void addEvent(EventEnum eventEnum, void* event, void* sender);

    EventData getEventData(EventEnum eventEnum);

private:
    std::map<EventEnum, EventData> eventMap;
    EventManager(){};
    ~EventManager(){};
};
  1. 实现事件注册函数
void EventManager::addEvent(EventEnum eventEnum, void* event, void* sender) {
    if(event == nullptr || sender == nullptr) {
        return;
    }
    EventData eventData;
    eventData.eventPointer = event;
    eventData.sender = sender;

    eventMap.insert(std::pair<EventEnum, EventData>(eventEnum, eventData));
}
  1. 编写函数指针对应函数的具体实现
void eeSleepWakeCallback(int result, void* sender) {
    JniTester *tester = (JniTester *) sender;
    tester->onResultCallback(result);
}
  1. 在入口类中注册事件及其对应的枚举和函数
JniTester::JniTester() {
    EventManager::getInstance().addEvent(eeSleepWake, (void*)eeSleepWakeCallback, this);
}
  1. 编写异步函数调用
    ···
    void JniTester::getThreadResult() {
    ThreadTest *test = new ThreadTest();
    test->sleepThread();
    }
    ···
    耗时函数的具体实现:
void ThreadTest::sleepThread() {
    std::thread cal_task(&ThreadTest::makeSleep, this);
    cal_task.detach();
}

void ThreadTest::makeSleep() {
    sleep(2);
}

这一步我们是通过新建一个线程,并让其等待2S来模拟异步耗时操作

4. 异步回调的实现

  1. 在java层编写java的回调方法
private OnResultCallback callback;

    public void setOnResultCallback(OnResultCallback callback) {
        this.callback = callback;
    }

    public interface OnResultCallback {
        void onResult(int result);
    }
  1. 在java曾编写java层回调的触发:
    public void onResult(int result) {
        if (this.callback != null) {
            callback.onResult(result);
        }
    }
  1. native层异步动作完成的通知

通过向单例的事件持有类获取对应的事件枚举,获取到其对应的函数指针,并调用该函数指针实现:

void ThreadTest::makeSleep() {
    sleep(2);
    EventData eventData = EventManager::singleton().getEventData(eeSleepWake);
    onSleepWake wake = (onSleepWake)eventData.eventPointer;
    if(wake) {
        wake(12345, eventData.sender);
    }
}

因为我们在第三章节第7步注册的函数指针是eeSleepWakeCallback, 因此,这里会调用到这个函数:

void eeSleepWakeCallback(int result, void* sender) {
    JniTester *tester = (JniTester *) sender;
    tester->onResultCallback(result);
}

通过sender确定具体的对象,调用其onResultCallback函数

  1. onResultCallback函数的实现
void JniTester::onResultCallback(int result) {
    JNIEnv *env = NULL;
    int status = f_jvm->GetEnv((void **) &env, JNI_VERSION_1_4);

    bool isInThread = false;
    if (status < 0) {
        isInThread = true;
        f_jvm->AttachCurrentThread(&env, NULL);
    }

    if (f_cls != NULL) {
        jmethodID id = env->GetMethodID(f_cls, "onResult", "(I)V");
        if (id != NULL) {
            env->CallVoidMethod(f_obj, id, result);
        }
    }

    if (isInThread) {
        f_jvm->DetachCurrentThread();
    }
}

这里因为缺少java环境,因此我们需要将该线程挂载到jvm上执行,并获取对应的JNIEnv ,通过jnienv获取java层的回调触发方法onResult并执行。

5.效果

编写测试代码:

        JniTester tester = new JniTester();
        Log.d("zyl", "startTime = " + System.currentTimeMillis());
        tester.setOnResultCallback(result -> {
            Log.d("zyl", "endTime = " + System.currentTimeMillis());
            Log.d("zyl", "result = " + result);
        });
        tester.requestData();

执行结果:


image.png

和预期一致,完美。

完整版代码

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

推荐阅读更多精彩内容