在標(biāo)準(zhǔn)庫(kù)開發(fā)中很少遇到回調(diào)函數(shù),但是在HAL庫(kù)開發(fā)中經(jīng)常要使用到回調(diào)函數(shù),不知你是不是也有這樣的疑惑,我們?yōu)槭裁葱枰卣{(diào)函數(shù)這個(gè)概念呢?直接調(diào)用函數(shù)不就可以了?回調(diào)函數(shù)到底有什么作用?程序員到底該如何理解回調(diào)函數(shù)?

這篇文章就來(lái)為你解答這些問題,讀完這篇文章后你的武器庫(kù)將新增一件功能強(qiáng)大的利器。
其實(shí)回調(diào)函數(shù)和普通函數(shù)沒有本質(zhì)的區(qū)別。首先讓我們來(lái)看看普通的函數(shù)調(diào)用,假設(shè)我們?cè)趂un函數(shù)中調(diào)用函數(shù)fun1:
void?fun(void?)?
{
???...
???fun1();
???...
}
想一想,你怎么知道可以調(diào)用func呢?哦,原來(lái)fun1是你自己定義的:
void?fun1(void?)?
{
??blablabla;
}
這很簡(jiǎn)單吧,現(xiàn)在假設(shè)你編寫的這段代碼無(wú)比之牛逼,全世界的程序員都無(wú)比瘋狂的想引入到自己的項(xiàng)目中,這時(shí)你會(huì)把A函數(shù)編寫成一個(gè)庫(kù)供全世界的碼農(nóng)使用。
但此時(shí)所有人都發(fā)現(xiàn)一個(gè)問題,那就是他們都想在fun函數(shù)中的某個(gè)特定點(diǎn)上執(zhí)行一段自己的代碼,作為這個(gè)庫(kù)的創(chuàng)作者你可能會(huì)這樣實(shí)現(xiàn):
void?fun()?{
???...???
???if?(張三)?
???{
?????func1();
???}?else?if?(李四)?
???{
?????func2();
???}
???...
}
假設(shè)全世界有一千萬(wàn)碼農(nóng),那你是不是要有一千萬(wàn)個(gè)if else。。。想想這樣的代碼就很刺激有沒有!
更好的辦法是什么呢?把函數(shù)也當(dāng)變量!你可以這樣定義fun函數(shù):
void?fun?(fun1?f)?{
???...
???f();
???...
}
任何一個(gè)碼農(nóng)在調(diào)用你的fun函數(shù)時(shí)傳入一個(gè)函數(shù)變量,fun函數(shù)在合適的時(shí)機(jī)調(diào)用你傳入的函數(shù)變量,從而節(jié)省了一千萬(wàn)行代碼。為了讓這個(gè)過(guò)程更加難懂一些,這個(gè)函數(shù)變量起了一個(gè)無(wú)比難懂的稱呼:回調(diào)函數(shù)。
現(xiàn)在你應(yīng)該明白了回調(diào)函數(shù)是怎么一回事了吧,相比回調(diào)函數(shù)來(lái)說(shuō)我更愿意將其看做函數(shù)變量。
什么?你說(shuō)你還是不理解?那我下面這樣說(shuō)你明白了嗎?
一般解釋:回調(diào)函數(shù)就是一個(gè)通過(guò)函數(shù)指針調(diào)用的函數(shù)。如果你把函數(shù)的地址傳遞給中間函數(shù)的形參,中間函數(shù)通過(guò)函數(shù)指針調(diào)用其所指向的函數(shù)時(shí),我們就說(shuō)這是回調(diào)函數(shù)。
通俗解釋:函數(shù)Fun1調(diào)用函數(shù)Fun2,同時(shí)將函數(shù)Fun3作為形參傳遞給Fun2,此時(shí),Fun1為應(yīng)用層函數(shù),Fun2為中間層函數(shù),Fun3為回調(diào)函數(shù),回調(diào)函數(shù)是一種說(shuō)法而已。
fun1()
{
fun2(fun3);
}
意義何在:
利于代碼結(jié)構(gòu),將代碼分為應(yīng)用層,中間層,硬件驅(qū)動(dòng)層,彼此獨(dú)立,方便程序的編輯,閱讀,修改與移植;
結(jié)構(gòu)化編程時(shí),結(jié)構(gòu)體只需要定義中間函數(shù),減小內(nèi)存的開銷。
為什么不在Fun2里面直接調(diào)用Fun3:
Fun1()
{
?Fun2()
?{
??Fun3_1();
??Fun3_2();
?}
}
通過(guò)函數(shù)指針的好處是,Fun2與Fun3實(shí)現(xiàn)隔離,比如Fun3具有多個(gè)功能函數(shù),增加或減少時(shí),不需要修改Fun2的代碼。
這會(huì)應(yīng)該明白了吧?什么還是不太清楚,那下面就來(lái)詳細(xì)說(shuō)一說(shuō)回調(diào)函數(shù)的吧!

假設(shè)你們公司要開發(fā)下一代國(guó)民App明日油條,一款主打解決國(guó)民早餐問題的App,為了加快開發(fā)進(jìn)度,這款應(yīng)用由A小組和B小組協(xié)同開發(fā)。
其中有一個(gè)核心模塊由A小組開發(fā)然后供B小組調(diào)用,這個(gè)核心模塊被封裝成了一個(gè)函數(shù),這個(gè)函數(shù)就叫make_youtiao()。
如果make_youtiao()這個(gè)函數(shù)執(zhí)行的很快并可以立即返回,那么B小組的同學(xué)只需要:
make_youtiao()從程序執(zhí)行的角度看這個(gè)過(guò)程是這樣的:
make_youtiao()這個(gè)函數(shù)make_youtiao()執(zhí)行完后,控制轉(zhuǎn)回到調(diào)用函數(shù)中
如果世界上所有的函數(shù)都像make_youtiao()這么簡(jiǎn)單,那么程序員大概率就要失業(yè)了,還好程序的世界是復(fù)雜的,這樣程序員才有了存在的價(jià)值。
現(xiàn)實(shí)中make_youtiao()這個(gè)函數(shù)需要處理的數(shù)據(jù)非常龐大,假設(shè)有10000個(gè),那么make_youtiao(10000)不會(huì)立刻返回,而是可能需要10分鐘才執(zhí)行完成并返回。
這時(shí)你該怎么辦呢?想一想這個(gè)問題。

可能有的同學(xué)會(huì)問,和剛才一樣直接調(diào)用不可以嗎,這樣多簡(jiǎn)單。
是的,這樣做沒有問題,但就像愛因斯坦說(shuō)的那樣“一切都應(yīng)該盡可能簡(jiǎn)單,但是不能過(guò)于簡(jiǎn)單”。
想一想直接調(diào)用會(huì)有什么問題?
顯然直接調(diào)用的話,那么調(diào)用線程會(huì)被阻塞暫停,在等待10分鐘后才能繼續(xù)運(yùn)行。在這10分鐘內(nèi)該線程不會(huì)被操作系統(tǒng)分配CPU,也就是說(shuō)該線程得不到任何推進(jìn)。
這并不是一種高效的做法。
沒有一個(gè)程序員想死盯著屏幕10分鐘后才能得到結(jié)果。
那么有沒有一種更加高效的做法呢?
我們知道了這種一直等待直到另一個(gè)任務(wù)完成的模式叫做同步。

如果你是老板的話你會(huì)什么都不干一直盯著員工寫代碼嗎?因此一種更好的做法是程序員在代碼的時(shí)候老板該干啥干啥,程序員寫完后自然會(huì)通知老板,這樣老板和程序員都不需要相互等待,這種模式被稱為異步。
回到我們的主題,這里一種更好的方式是調(diào)用make_youtiao()這個(gè)函數(shù)后不再等待這個(gè)函數(shù)執(zhí)行完成,而是直接返回繼續(xù)后續(xù)流程,這樣A小組的程序就可以和make_youtiao()這個(gè)函數(shù)同時(shí)進(jìn)行了,就像這樣:

在這種情況下,回調(diào)(callback)就必須出場(chǎng)了。
有的同學(xué)可能還沒有明白為什么在這種情況下需要回調(diào),別著急,我們慢慢講。假設(shè)我們明日油條App代碼第一版是這樣寫的:
make_youtiao(10000);
sell();
可以看到這是最簡(jiǎn)單的寫法,意思很簡(jiǎn)單,制作好油條后賣出去。

我們已經(jīng)知道了由于make_youtiao(10000)這個(gè)函數(shù)10分鐘才能返回,你不想一直死盯著屏幕10分鐘等待結(jié)果,那么一種更好的方法是讓make_youtiao()這個(gè)函數(shù)知道制作完油條后該干什么,即,更好的調(diào)用make_youtiao的方式是這樣的:“制作10000個(gè)油條,炸好后賣出去”,因此調(diào)用make_youtiao就變出這樣了:
make_youtiao(10000,?sell);
看到了吧,現(xiàn)在make_youtiao這個(gè)函數(shù)多了一個(gè)參數(shù),除了指定制作油條的數(shù)量外還可以指定制作好后該干什么,第二個(gè)被make_youtiao這個(gè)函數(shù)調(diào)用的函數(shù)就叫回調(diào),callback。
現(xiàn)在你應(yīng)該看出來(lái)了吧,雖然sell函數(shù)是你定義的,但是這個(gè)函數(shù)卻是被其它模塊調(diào)用執(zhí)行的,就像這樣:

make_youtiao這個(gè)函數(shù)是怎么實(shí)現(xiàn)的呢,很簡(jiǎn)單:
//make_youtiao這個(gè)函數(shù)是怎么實(shí)現(xiàn)的呢,很簡(jiǎn)單:
void?make_youtiao(int?num,?func?call_back)?
{
????//?制作油條
????call_back();?//執(zhí)行回調(diào)?
}
這樣你就不用死盯著屏幕了,因?yàn)槟惆?code style="font-size: 14px;word-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin: 0 2px;background-color: rgba(27,31,35,.05);font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(271,93,108);">make_youtiao這個(gè)函數(shù)執(zhí)行完后該做的任務(wù)交代給make_youtiao這個(gè)函數(shù)了,該函數(shù)制作完油條后知道該干些什么,這樣就解放了你的程序。
有的同學(xué)可能還是有疑問,為什么編寫make_youtiao這個(gè)小組不直接定義sell函數(shù)然后調(diào)用呢?
不要忘了明日油條這個(gè)App是由A小組和B小組同時(shí)開發(fā)的,A小組在編寫make_youtiao時(shí)怎么知道B小組要怎么用這個(gè)模塊,假設(shè)A小組真的自己定義sell函數(shù)就會(huì)這樣寫:
void?make_youtiao(int?num)
?{
????real_make_youtiao(num);
????sell();?//執(zhí)行回調(diào)?
}
同時(shí)A小組設(shè)計(jì)的模塊非常好用,這時(shí)C小組也想用這個(gè)模塊,然而C小組的需求是制作完油條后放到倉(cāng)庫(kù)而不是不是直接賣掉,要滿足這一需求那么A小組該怎么寫呢?
void?make_youtiao(int?num)?{
????real_make_youtiao(num);
????
????if?(Team_B)?{
???????sell();?//?執(zhí)行回調(diào)
????}?else?if?(Team_D)?{
???????store();?//?放到倉(cāng)庫(kù)
????}
}
故事還沒完,假設(shè)這時(shí)D小組又想使用呢,難道還要接著添加if else嗎?這樣的話A小組的同學(xué)只需要維護(hù)make_youtiao這個(gè)函數(shù)就能做到工作量飽滿了,顯然這是一種非常糟糕的設(shè)計(jì)。
所以你會(huì)看到,制作完油條后接下來(lái)該做什么不是實(shí)現(xiàn)make_youtiao的A小組該關(guān)心的事情,很明顯只有調(diào)用make_youtiao這個(gè)函數(shù)的使用方才知道。
因此make_youtiao的A小組完全可以通過(guò)回調(diào)函數(shù)將接下來(lái)該干什么交給調(diào)用方實(shí)現(xiàn),A小組的同學(xué)只需要針對(duì)回調(diào)函數(shù)這一抽象概念進(jìn)行編程就好了,這樣調(diào)用方在制作完油條后不管是賣掉、放到庫(kù)存還是自己吃掉等等想做什么都可以,A小組的make_youtiao函數(shù)根本不用做任何改動(dòng),因?yàn)锳小組是針對(duì)回調(diào)函數(shù)這一抽象概念來(lái)編程的。
以上就是回調(diào)函數(shù)的作用,當(dāng)然這也是針對(duì)抽象而不是具體實(shí)現(xiàn)進(jìn)行編程這一思想的威力所在。面向?qū)ο笾械亩鄳B(tài)本質(zhì)上就是讓你用來(lái)針對(duì)抽象而不是針對(duì)實(shí)現(xiàn)來(lái)編程的。
“在計(jì)算機(jī)科學(xué)中,回調(diào)函數(shù)是指一段以參數(shù)的形式傳遞給其它代碼的可執(zhí)行代碼。這就是回調(diào)函數(shù)的定義了。
”
回調(diào)函數(shù)就是一個(gè)函數(shù),和其它函數(shù)沒有任何區(qū)別。
注意,回調(diào)函數(shù)是一種軟件設(shè)計(jì)上的概念,和某個(gè)編程語(yǔ)言沒有關(guān)系,幾乎所有的編程語(yǔ)言都能實(shí)現(xiàn)回調(diào)函數(shù)。
對(duì)于一般的函數(shù)來(lái)說(shuō),我們自己編寫的函數(shù)會(huì)在自己的程序內(nèi)部調(diào)用,也就是說(shuō)函數(shù)的編寫方是我們自己,調(diào)用方也是我們自己。
但回調(diào)函數(shù)不是這樣的,雖然函數(shù)編寫方是我們自己,但是函數(shù)調(diào)用方不是我們,而是我們引用的其它模塊,也就是第三方庫(kù),我們調(diào)用第三方庫(kù)中的函數(shù),并把回調(diào)函數(shù)傳遞給第三方庫(kù),第三方庫(kù)中的函數(shù)調(diào)用我們編寫的回調(diào)函數(shù),如圖所示:

定節(jié)點(diǎn),比如我們舉的例子油條制作完成、接收到網(wǎng)絡(luò)數(shù)據(jù)、文件讀取完成等之后該做什么,這些只有庫(kù)的使用方才知道,因此第三方庫(kù)的編寫者無(wú)法針對(duì)具體的實(shí)現(xiàn)來(lái)寫代碼,而只能對(duì)外提供一個(gè)回調(diào)函數(shù),庫(kù)的使用方來(lái)實(shí)現(xiàn)該函數(shù),第三方庫(kù)在特定的節(jié)點(diǎn)調(diào)用該回調(diào)函數(shù)就可以了。
另一點(diǎn)值得注意的是,從圖中我們可以看出回調(diào)函數(shù)和我們的主程序位于同一層中,我們只負(fù)責(zé)編寫該回調(diào)函數(shù),但并不是我們來(lái)調(diào)用的。
最后值得注意的一點(diǎn)就是回調(diào)函數(shù)被調(diào)用的時(shí)間節(jié)點(diǎn),回調(diào)函數(shù)只在某些特定的節(jié)點(diǎn)被調(diào)用,就像上面說(shuō)的油條制作完成、接收到網(wǎng)絡(luò)數(shù)據(jù)、文件讀取完成等,這些都是事件,也就是event,本質(zhì)上我們編寫的回調(diào)函數(shù)就是用來(lái)處理event的,因此從這個(gè)角度看回調(diào)函數(shù)不過(guò)就是event handler,因此回調(diào)函數(shù)天然適用于事件驅(qū)動(dòng)編程event-driven。
在HAL庫(kù)中我們的中斷服務(wù)函數(shù)里我們不需要清楚中斷標(biāo)志位。在標(biāo)準(zhǔn)庫(kù)中,串口中斷了以后,我們要先在中斷中判斷是否是接收中斷,然后讀出數(shù)據(jù),順便清除中斷標(biāo)志位,然后再是對(duì)數(shù)據(jù)的處理,這樣如果我們?cè)谝粋€(gè)中斷函數(shù)中寫這么多代碼,就會(huì)顯得很混亂:
標(biāo)準(zhǔn)庫(kù)寫法
//中斷服務(wù)程序
void?TIM3_IRQHandler(void)
{
?if?(TIM_GetITStatus(TIM3,?TIM_IT_Update)?!=?RESET)?
??{
??TIM_ClearITPendingBit(TIM3,?TIM_IT_Update??);??
??LED1=!LED1;
??}
}
HAL庫(kù)回調(diào)函數(shù)寫法
//中斷服務(wù)函數(shù)
void?TIM3_IRQHandler(void)
{
????HAL_TIM_IRQHandler(&TIM3_Handler);
}
//回調(diào)函數(shù)
void?HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef?*htim)
{
????if(htim==(&TIM3_Handler))
????{
????????LED1=!LED1;?
????}
}
現(xiàn)在應(yīng)該明白回調(diào)函數(shù)是什么了吧!

關(guān)注【一起學(xué)嵌入式】,回復(fù)“加群”進(jìn)技術(shù)交流群。
覺得文章不錯(cuò),點(diǎn)擊“分享”、“贊”、“在看” 唄!