Callback回调机制知识大全

来源:互联网 发布:nginx配置php域名访问 编辑:程序博客网 时间:2024/06/05 15:03

举个例子:你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。

另一种说法:

这个是程序上的概念。
本质上是叫别人做事,传进去的额外信息。

比如,A叫B做事,根据粒度不同,可以理解成A函数调用B函数,或者A类使用B类,或者A组件使用B组件等等。反正就是A叫B做事。

当B做这件事情的时候,自身的需要的信息不够,而A又有。就需要A从外面传进来,或者B做着做着主动向外面申请。对于B来说,一种被动得到信息,一种是主动去得到信息,有人给这两种方式术语,叫信息的push,和信息的pull。

接着,A调用B,A向B传参数。int max(int a, int b); 要使用这函数max, 得到两者最大的值, 外面就要传进来a, b。这个好理解。

再跟着,就来到计算机中比较诡异的地方。也就是代码(code)和数据(data)的统一,这是一个槛,如果不打通这个,很多概念就不清楚。我们常常说计算机程序分成code和data两部分。很多人就会觉得,code是会运行的,是动的,data是给code使用,是静态的,这是两种完全不同的东西。

其实code只是对行为的一种描述,比如有个机器人可以开灯,关灯,扫地。跟着我要机器人开灯,扫地,关灯。如果跟机器人约定好,0表示开灯,1表示关灯,2表示扫地。我发出指令,0 1 2 1 0。跟着就可以控制机器人开灯,扫地,关灯。再约定用二进制表示,两位一个指令,就有一个数字串,0001110100,这个时候0001110100这串数字就描述了机器人的一系列动作,这个就是从一方面理解是code,可以它可以控制机器人的行为。但另一方面,它可以传递,可以记录,可以修改,也就是数据。只要大家都协商好,code就可以编码成data, 将data解释运行的时候,也变成了code。

这个地方扯开了。我自己是不区分code和data的,统一称为信息。那既然int max(int a, int b)中int,double等表示普通data的东西可以传递进去,凭什么表示code的函数就不可以传进去了。有些语言确实是不区分的,它的function(表示code)跟int, double的地位是一样的。这种语言就为函数是第一类值。

但问题是,有些语言是不能存储函数的,不能动态创建函数的,不能动态销毁函数的。(这里函数是已经是广义的了,用来表示代码code)。只能存储一个指向函数的指针,这种语言称为函数是第二类值。

另外有些语言不单可以传递函数,函数里面又用到一些外部信息(包括code, data)。那些语言可以将函数跟函数所用到的信息一起传递存储。这种将函数和它所用的信息作为一个整体,就为闭包。

再次声明,将代码和数据打通,统一起来,是一个槛。过了这个槛,很多难以理解的概念就会清晰很多。比如一些修改自身的程序啊,数据驱动啊,先设计数据再写程序等等。过了这山,眼界就开阔了。

来到这里,其实已经没有什么好说的了。回调函数也就是是A让B做事,B做着做着,信息不够,不知道怎么做了,就再让外面处理。

比如排序,A让B排序,B是会做排序,但排序需要知道哪个比哪个大,这点B自己不知道,就需要A告诉它。而这种判断那个大,本身是一种动作,既然C语言中不可以传进第一值的函数,就设计成传递第二值的函数指针,这个函数指针就是A传向B的信息,用来表示一个行为。这里本来A调用B的,结果B又调用了A告诉它的信息,也就叫callback。

再比如A让B监听系统的某个消息,比如敲了哪个键。跟着B监听到了,但它不知道怎么去处理这个消息,就给外面关心这个消息,又知道怎么去处理这个消息的人去处理,这个处理过程本事是个行为,既然这个语言不可以传递函数,又只能传一个函数指针了。跟着有些人有会引申成,什么注册啊,通知啊等等等。假如B做监听,C, D, E, F, G, H告诉B自己有兴趣知道这消息,那B监听到了就去告诉C,D,E,F,G等人了,这样通知多人了,就叫广播。

其实你理解了,根本不用去关心术语的。术语是别人要告诉你啊,或者你去告诉人啊,使用的一套约定的词语。本质上就个东西,结果会有很多术语的。

跟着再将回调的概念进化,比如某人同时关心A,B,C,D,E,F事件,并且这些事件是一组的,比如敲键盘,鼠标移动,鼠标点击等一组。将一组事件结合起来。在有些语言就变成一个接口,接口有N个函数。有些语言就映射成一个结构,里面放着N个函数指针。跟着就不是将单个函数指针传进去,而是将接口,或者函数指针的结构传进去。这些根据不同的用途,有些人叫它为代理啊,监听者啊,观察者啊等等。


再简单点说:

回调函数的主要功能就是提供统一的接口以及事件的通知。如果你是从事middleware,框架,提供API编程的,那么你肯定少不了要使用回调函数。

所谓提供统一接口或者事件的通知即:当发生某一事件或者出现某一个状态之后必定会进行某种操作。但是这个操作又不能写死,不同的环境不同的程序会有不同的实现,也就是说提供接口的人不想把逻辑写死,而是由调用接口的人来实现逻辑。这也就是回调函数的主要功能。

 

 

回调函数,唯一途径就是传递函数指针。通过这个函数指针来调用真正的函数。

 

 

下面使用代码说明究竟如何实现回调函数,并通过这个例子来说明回调函数的作用:

 

 

[cpp] view plaincopyprint?
  1. /** 
  2. filename: mytimer.h 
  3. description: 
  4. 这里我用一个闹钟静态库mytimer.a来演示回调函数的实现方法 
  5. */  
  6. #ifndef MYTIMER_H  
  7. #define MYTIMER_H  
  8. typedef void (*tmfun)(tumfun );  
  9. typedef struct{  
  10.       
  11.     tmfun t_fun;  
  12. }tmhandle;  
  13. static tmhandle tmstruct;  
  14. extern void starttimer(int intercal);  
  15. extern void registtimer(tumfun f);  

 

 

[cpp] view plaincopyprint?
  1. /** 
  2. filename: mytimer.c 
  3. description:  
  4. */  
  5. /** 
  6. name: registtimer 
  7. description: 
  8. 注册闹钟处理函数。由于闹钟时间到之后,不同的人处理函数不同 
  9. 所以,这里只提供接口,并不实现处理函数逻辑。 
  10. 调用接口者需要自己实现处理函数,并注册启动闹钟 
  11. */  
  12. void registtimer(tmfun f){  
  13.     tmstruct.t fun = f;  
  14. }  
  15. /** 
  16. name; starttimer 
  17. description: 
  18. 根据参数设定时间并启动定时器 
  19. */  
  20. void starttimer(int interval){  
  21.     int tmpi = 0;  
  22.     int fixtim = 500000000;  
  23.     int tmpFix = 0;  
  24.    
  25.     for(tmpi;tmpi<=interval;tmpi++)  
  26.         for(tmpFix;tmpFix<=fixtim*2;tmpFix++);  
  27.     (*tmstruct.t_fun)();  
  28. }  

 

以上是静态库的全部代码

Linux下输入以下命令制作动态库:

[cpp] view plaincopyprint?
  1. $ gcc -c mytimer.c  
  2. $ ar -rc libmytimer.a mytimer.o  

 

 

 

测试程序:

[cpp] view plaincopyprint?
  1. /** 
  2. filename: testtimer.c 
  3. description: 
  4. 对接口进行测试,调用静态库 
  5. */  
  6. #include <stdio.h>  
  7. #include <unistd.h>  
  8. #include "mytimer.h"  
  9. /** 
  10. name:handler 
  11. description:闹钟处理函数,即回调函数 
  12. */  
  13. void handle(){  
  14.     printf("Hi Legend, I am a callback/n");  
  15. }  
  16. int main(int argc, char **argv){  
  17.     registtimer(&handle);//对闹钟处理函数进行注册  
  18.     starttimer(5);  
  19.     return 0;  
  20. }  




所谓回调, 定义是一个方法的指针传递给事件源,当某一事件发生时用来调用这个方法。

比如客户程序C调用服务程序S中的某个函数A,然后S又在某个时候反过来调用C中的某个函数B,对于C来说,这个B便叫做回调函数。例如Win32下的窗口过程函数就是一个典型的回调函数。一般说来,C不会自己调用BC提供B的目的就是让S来调用它,而且是C不得不提供。由于S并不知道C提供的B姓甚名谁,所以S会约定B的接口规范(函数原型),然后由C提前通过S的一个函数A告诉S自己将要使用B函数,这个过程称为回调函数的注册,A称为注册函数。

总的来说,回调函数就是那些自己写的,但是不是自己来调,而是给别人来调的函数。

 

回调一般用于层间协作,上层将本层函数安装在下层,这个函数就是回调函数,而下层在一定条件下触发回调,例如作为一个驱动,是一个底层,他在收到一个数据时,除了完成本层的处理工作外,还将进行回调,将这个数据交给上层应用层来做进一步处理,这在分层的数据通信中很普遍。 
   
 其实回调和API非常接近,他们的共性都是跨层调用的函数。但区别是API是低层提供给高层的调用,一般这个函数对高层都是已知的;而回调正好相反,他是高层提供给底层的调用,对于低层他是未知的,必须由高层进行安装,这个安装函数其实就是一个低层提供的API,安装后低层不知道这个回调的名字,但它通过一个函数指针来保存这个回调,在需要调用时,只需引用这个函数指针和相关的参数指针。

消息响应函数就可以看成是回调函数,因为是让系统在合适的时候去调用。只不过消息响应函数就是为了处理消息的,所以就拿出来单做一类了。其实本质上就是回调函数。但是回调函数不是只有消息响应函数一种,比如在内核编程中,驱动程序就要提供一些回调函数,当一个设备的数据读写完成后,让系统调用这些回调函数来执行一些后续工作。

   
 其实:回调就是该函数写在高层,低层通过一个函数指针保存这个函数,在某个事件的触发下,低层通过该函数指针调用高层那个函数

 

C语言中回调函数解释:

我们调用函数的方法有两种:

         直接调用:在函数A的函数体里通过书写函数B的函数名来调用之,使内存中对应函数B的代码得以执行。这里,A称为主叫函数Caller),B称为被叫函数Callee)。

       间接调用:在函数A的函数体里并不出现函数B的函数名,而是使用指向函数B的函数指针p来使内存中属于函数B的代码片断得以执行。

 

比起直接调用来,间接调用的确麻烦,那为什么还要使用间接调用呢?原因很简单——直接调用把函数名都写进函数体了,经过编译器那么一编译,板上钉钉,A注定调用的是B了,这样的程序只能按照程序员事先设计好的流程执行下去,太呆板了。此时,间接调用的巨大灵活性就显现出来了。想一想,如果p是函数A的一个参数(参数是变量,是变量就可以变吗!),那么程序的最终用户完全可以通过操作来改变p的指向——这样,A在通过p调用函数的时候就有机会调用到不同的函数,这样程序的实用性和扩展性就强多了。

 

WINDOWS中,程序员想让系统DLL调用自己编写的一个方法,于是利用DLL当中回调函数(CALLBACK)的接口来编写程序,使它调用,这个就称为回调。在调用接口时,需要严格的按照定义的参数和方法调用,并且需要处理函数的异步,否则会导致程序的崩溃。这样的解释似乎还是比较难懂,这里举个简单的例子,程序员A写了一段程序(程序a),其中预留有回调函数接口,并封装好了该程序。程序员B要让a调用自己的程序b中的一个方法,于是,他通过a中的接口回调自己b中的方法。目的达到。在C/C++中,要用回调函数,被调函数需要告诉调用者自己的指针地址,但在JAVA中没有指针,怎么办?我们可以通过接口(interface)来实现定义回调函数。

正常情况下开发人员使用已经定义好的API,这个过程叫Call。但是有时这样不能满足需求,就需要程序员注册自己的程序,然后让事先定义好API在合适的时候调用注册的方法,这叫CallBack

通常大家说的回调函数一般就是按照别人(李四)的定好的接口规范写,等待别人(张三)调用的函数,在C语言中,回调函数通常通过函数指针来传递;在Java中,通常就是编写另外一个类或类库的人(李四)规定一个接口,然后你(张三)来实现这个接口,然后把这个实现类的一个对象作为参数传给别人(例如王五)的程序,王五的程序必要时就会通过那个接口来调用你编写的函数。


    下面举个通俗的例子:
   
 某天,我打电话向你请教问题,当然是个难题,^_^,你一时想不出解决方法,我又不能拿着电话在那里傻等,于是我们约定:等你想出办法后打手机通知我,这样,我就挂掉电话办其它事情去了。过了XX分钟,我的手机响了,你兴高采烈的说问题已经搞定,应该如此这般处理。故事到此结束。这个例子说明了“异步+回调”的编程模式。其中,你后来打手机告诉我结果便是一个“回调”过程;我的手机号码必须在以前告诉你,这便是注册回调函数;我的手机号码应该有效并且手机能够接收到你的呼叫,这是回调函数必须符合接口规范。

java回调机制:

软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。

同步调用:一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;

      调:一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;

异步调用:一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。

回调和异步调用的关系非常紧密:使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。 

Java里的例子: 

public interface ICallBack //接口,规范需要有的函数

{

               public void postExec();//需要回调的方法

}

另外的一个类:

public class FooBar

            private  ICallBack callBack;

               public   void setCallBack(ICallBack callBack)

                               {

                                              this.callBack = callBack;

                                              doSth();

                               }

               public  void doSth()

                               {

                                              callBack.postExec(); //调用回调函数

                               }

}

第二个类在测试类里面,是一个匿名类:

public class Test

{

       public static void main(String[] args)

       {

              FooBar foo = new FooBar();

              foo.setCallBack(new ICallBack() {

public void postExec()

{

System.out.println("Test类中实现但由FooBar对象调用");

                     }实现了IcallBack接口的匿名类。

 });

 }

}

上诉的代码:

  1.两个类:匿名类和FooBar

  2.匿名类实现接口ICallBack(test测试的main方法中用匿名类的形式实现)

  3.FooBar 拥有一个参数为ICallBack接口类型的函数setCallBack(ICallBack   

  4.匿名类运行时调用FooBarsetCallBack函数,以自身传入参数  

  5.FooBar已取得匿名类,就可以随时回调匿名类中所实现的ICallBack接口中的方法


假设有接口名为 ICallBack 其中有方法名为postExec()。有类Myclass 实现了该接口,也就是一定实现了postExec()这个方法。现在有另一个类FooBar它有个方法 setCallBack(ICallBack callBack) ,并且setCallBack方法调用了callBackpostExec()方法。
如果现在,我们使用一个Myclass 的实例myClass,将它作为参数带入到setCallBack(ICallBack callBack)方法中,我们就说setCallBack(ICallBack callBack)方法回调了myClasspostExec()方法。 


参考:http://www.zhihu.com/question/19801131


0 0