回调函数技术广泛运用在动态库开发(或者类库)中,是使软件模块化的重要手段。回调函数可以看作是一种通知和实现机制,用于控制反转,即模块A调用模块B时,模块B完成一定任务后反过头来调用模块A。在被调用方代码改变(功能变化)时,调用者代码保持不变。这种方式对应了一个经典的软件设计原则--开闭原则:软件模块对修改关闭,对添加新代码开放,也就是说,增加新功能时,增加新代码,但不修改老代码。由于可以动态加载dll,只要新的dll(接口与旧的dll相同)覆盖老dll,就实现了系统升级。
一般地,代码调用有三种方式,见图示:
同步调用最常见,它是单向调用,调用方A阻塞等待调用方B完成后返回。
回调是双向调用,被调用接口被调用时随后会调用调用方的接口。如果把调用方A称为高层,调用方B称为底层,回调就是高层调用底层,底层再回过头来调用高层的过程。这也就是回调得名的原因吧。从这个过程来看,回调接口由被调用方提供,调用方定义相同的接口原型和实现,并注册到被调用方提供的登记入口上,回调的真正实现在调用方。
异步调用:类似于消息或事件通知机制,在接口的服务收到被调用的消息或事件时,会主动调用调用者的接口,方向正好与同步调用相反。当然,实现异步调用的代码比同步调用要复杂的多。
当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function,中间函数)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。见下面的图示:
可以看到,回调函数由应用层提供,与应用处于同一抽象层。
中间函数与回调函数是回调的两个必要部分,不过人们常常忽略了回调里的第三位要角,就是中间函数的调用者。在一般简单的例子中,这个调用者可以和程序的主函数等同起来,但在模块化编程中,也许这个调用者是程序中的某个模块,模块中的某个函数调用了中间函数。为了表示区别,把它成为起始函数。
很多文章在解释回调概念时,都会提到这么一句话:“if you call me,i will call you back”。回调不是中间函数、回调函数两方的互动,而是起始函数、中间函数、回调函数的三方互动。给中间函数传入什么样的回调函数,是起始函数决定的。有了这层理解,在代码中实现回调时才不容易混淆出错。
回调技术最主要的用途是解耦,假设有两个模块A和B,如果模块A依赖模块B,在A中调用B,依赖是单方向的,即A依赖于B,如果B又要通知A,B就对A产生了依赖,而且是双向依赖。现在我们要解耦,让依赖只是单方向的,做法是A依赖B,B依赖一个函数指针,这个指针可以来自于任何地方。这样B对A的调用就变为隐式调用,B对A不依赖,只是依赖一个函数接口,这个接口就是回调。这样就做到了不依赖实现,依赖接口。
上面的表述过程太过抽象,用生活中的例子来比喻回调技术吧。
打个比方,有家酒店不仅提供住宿服务还提供叫醒服务,叫醒服务内容是客服在规定的时间打电话到客房。旅客即可以选择睡到自己醒来,也可以选择睡到客服打电话叫醒自己。前者是睡觉(高层调用底层实现)醒来(高层自身实现)两个步骤,后者是登记叫醒服务(高层调用底层注册回调的接口),睡觉(高层调用底层实现)呼叫(底层通知高层)醒来(高层自身实现)三个步骤。旅客要享受叫醒服务,需要先告诉酒店,这个告诉的动作,就叫登记回调函数。多出来的登记动作和通知动作就是回调与一般的函数调用最大的不同。
回调机制提供了巨大的灵活性,比如上边的叫醒服务,如果酒店不仅提供打客房电话,还可以是工作人员敲房门,登记了不同的服务内容,享受到的服务也不同,这就是灵活性的好处。举一个编程上的例子,Win32 SDK编程中,操作系统提供了注册窗口过程函数的接口,不同的软件实现自己不同的窗口过程,并注册到操作系统,软件运行的行为也就此不同了。
回调机制落地,代码实现
1.最简单的C语言实现
void callback(int a)
{
cout<<"callback called with para="<<a<<endl;
} typedef void (*pfunc)(int);
void caller(pfunc p)
{
(*p)(1);
} int main(int argc, char* argv[])
{
caller(&callback);
}
2. C++ 静态成员函数方式实现(公司实际项目中大量使用)
3. Sink方式
参考: