回撥機制
所謂回撥,在實現具有通用性質的應用框架時非常常見:對於一個具有通用性質的程式框架來說,程式架構完成整個應用的通用功能、流程,但在某個特定的點上,需要一段業務相關的程式碼——通用程式架構無法完成這段程式碼,那麼程式架構會在這個點上留一個“空”。
對於Java程式來說,程式架構在某個點上留的”空“,有2種實現方式
以介面形式存在
以抽象方法的形式存在
回撥機制的第一種實現方式就是典型的命令者模式。
介面回撥
原文連結:http://blog。csdn。net/aigestudio/article/details/40869893
廢話不多說,像許多網上介紹回撥機制的文章一樣,我這裡也以一個現實的例子開頭:假設你公司的總經理出差前需要你幫他辦件事情,這件事情你需要花些時間去做,這時候總經理肯定不能守著你做完再出差吧,於是就他告訴你他的手機號碼叫你如果事情辦完了你就打電話告訴他一聲;這是一個現實生活中常能碰到的例子,我們用呢就用程式碼的方式來實現一個這個過程,看一下這個過程究竟是怎樣的。
首先在Eclipse中新建一個Java專案:CallBackDemoInJava
然後再新建三個類:Manager(該類用來模擬總經理),Personnel(該類用來模擬員工),Main(主類)
Manager的程式碼如下:
package com。aige。test;
/**
* @description 該類用來模擬總經理
*/
public class Manager {
/**
* @param personnel 傳入一個員工類的物件
*/
public Manager(Personnel personnel) {
// 想讓該讓員工做什麼
personnel。doSomething(this, “整理公司檔案”);
}
/**
* @description 當員工做完總經理讓他做的事後就透過該方法通知總經理
* @param result 事情結果
*/
public void phoneCall(String result) {
System。out。println(“事情” + result);
}
}
Manager類是個模擬總經理的類,當該類的物件被造出來後就會透過Personnel的物件去執行Personnel中的doSomething方法告訴員工做些什麼
Personnel的程式碼如下:
package com。aige。test;
/**
* @description 該類用來模擬員工
*/
public class Personnel {
public void doSomething(Manager manager, String task) {
// 總經理透過doSomething方法告訴員工要做什麼
System。out。println(“總經理要你做” + task);
String result = “做完了”;
// 當事情做完了我們就透過總經理公佈的phoneCall方法通知總經理結果
manager。phoneCall(result);
}
}
總經理透過呼叫Personnel中的doSomething方法告訴員工該做些什麼,當員工做完後就透過Manager中的phoneCall方法通知總經理結果
那麼好的!萬事俱備,我們在Main中測試執行下程式碼看看結果:
package com。aige。test;
public class Main {
public static void main(String[] args) {
// 首先我們需要一個員工
Personnel personnel = new Personnel();
// 其次把這個員工指派給總經理
new Manager(personnel);
}
}
回到我們剛才舉的那個現實中的例子:總經理出差前要你去辦件事情,這件事情透過doSomething告訴你了,事情要是辦完了就打總經理的電話phoneCall通知他一聲。這裡的phoneCall我們就稱為回撥方法,為什麼稱之為回撥呢?你問我我也不清楚哈,這你得問Sun公司了,不過我們從程式碼的執行過程可以看出資料的流向大致是Manager → Personnel → Manager,這不就是一個“回撥”的過程麼?現在我們來總結下滿足回撥的兩個基本條件:
Class A呼叫Class B中的X方法
ClassB中X方法執行的過程中呼叫Class A中的Y方法完成回撥
一切看上去都很完美,以上例子程式碼簡單通俗地描述了回撥,但是這裡我就會有這樣一個疑問:假設總經理出差前交了件事情給我去辦,不巧……副總經理也要給我件事去辦,更無恥的是……主管也發任務過來了,都要求說做完就打電話通知他們……這時我們就要定義更多類,什麼總經理類啦、經理類啦、主管類啦、雜七雜八的類,但是這些雜七雜八的大爺們都要求做完事情就電話通知,每個類都會有一個類似phoneCall的方法作為回撥方法,這時,我們利用面向物件的思想來看,是不是可以把這個回撥方法抽象出來作為一個獨立的抽象類或介面呢?多型的思想油然而生,鑑於JAVA介面的好處,我們就定義一個名為CallBack的介面作為回撥介面,再在該介面下定義一個名為backResult的抽象方法作為回撥方法,讓那些總經理類啦、經理類啦、主管類啦、什麼的都去實現該介面。
CallBack程式碼如下:
package com。aige。test;
/**
* @description 回撥介面
*/
public interface CallBack {
// 回撥方法
public void backResult(String result);
}
Manager程式碼改造後如下:實現CallBack介面重寫backResult方法
package com。aige。test;
/**
* @description 該類用來模擬總經理
*/
public class Manager implements CallBack {
/**
* @param personnel 傳入一個員工類的物件
*/
public Manager(Personnel personnel) {
// 想讓該讓員工做什麼
personnel。doSomething(this, “整理公司檔案”);
}
/**
* @description 當員工做完總經理讓他做的事後就透過該方法通知總經理
* @param result 事情結果
*/
public void backResult(String result) {
System。out。println(“事情” + result);
}
}
Personnel程式碼改造後如下:doSomething方法不再傳入一個Manager物件而是一個CallBack介面
package com。aige。test;
/**
* @description 該類用來模擬員工
*/
public class Personnel {
public void doSomething(CallBack callBack, String task) {
// 總經理透過doSomething方法告訴員工要做什麼
System。out。println(“總經理要你做” + task);
String result = “做完了”;
// 當事情做完了我們就透過總經理公佈的phoneCall方法通知總經理結果
callBack。backResult(result);
}
}
Main程式碼不變,執行結果也是一樣的。
至此,回撥的基本概念差不多就是這樣了~其實回撥真心不難理解,但是回撥在Android中相當重要,幾乎處處可見回撥機制,如果你能理解到回撥的奧秘~我相信對你的Android技術是一個不小的提升
介面回撥機制
介面回撥就是一個通知機制,作用:
單純的通知
通知+傳值
步驟:
定義介面以及介面方法
定義介面物件
在某一個地方,介面物件呼叫介面方法
暴露介面物件(構造方法,setter方法)
程式碼示例
// 1。定義介面,以及介面方法
public interface OnTeacherComeListener {
void onTeachCome(String teacherName);
void onTeachCome();
}
public class ClassMate {
// 2。定義介面物件
OnTeacherComeListener mOnTeacherComeListener;
// 方式1:透過構造方法賦值
public ClassMate(OnTeacherComeListener onTeacherComeListener) {
super();
mOnTeacherComeListener = onTeacherComeListener;
}
public ClassMate() {
super();
}
// 模擬老師來了
public void doTeacherCome(String teacherName) {
// 3。在某一個地方。介面物件呼叫介面方法——>老師來了的時候
mOnTeacherComeListener。onTeachCome(teacherName);
}
// 4。暴露介面物件(構造方法,setter方法)
// 方式2:透過setter賦值
public void setOnTeacherComeListener(OnTeacherComeListener onTeacherComeListener) {
mOnTeacherComeListener = onTeacherComeListener;
}
}
public class Me {
private Timer mTimer;
/**想睡覺*/
public void wantSleep() {
System。out。println(“昨晚敲程式碼敲到4點鐘,突然想睡覺。。。”);
}
/**開始睡覺*/
public void startSleep() {
mTimer = new Timer();
mTimer。schedule(new TimerTask() {
@Override
public void run() {
System。out。println(“開始呼呼大睡……。。。”);
}
}, 0, 1000);
}
/**停止睡覺*/
public void stopSleep() {
if (mTimer != null) {
mTimer。cancel();
mTimer = null;
System。out。println(“停止了睡覺”);
}
}
}
我想要睡覺,但是睡覺前我跟同桌說,伍老師來了把我叫醒,要是李老師來了不用管,繼續睡
public class Test {
public static void main(String[] args) {
final Me me = new Me();
// 我想睡覺
me。wantSleep();
// 找到一個同桌
ClassMate classMate = new ClassMate();
System。out。println(“我去和同桌協商……”);
// 和他商量好——>如果老師來了。你拍醒我。讓我停止睡覺
classMate。setOnTeacherComeListener(new OnTeacherComeListener() {
@Override
public void onTeachCome(String teacherName) {// 通知+傳值的效果
if (“伍老師”。equals(teacherName)) {// 回撥過程中。有傳值的效果
System。out。println(“伍老師不是班主任,我繼續睡覺”);
} else if (“李老師”。equals(teacherName)) {
me。stopSleep();//
}
}
@Override
public void onTeachCome() {// 通知
}
});
// 模擬商量了2s鍾
try {
Thread。sleep(2000);
} catch (InterruptedException e) {
e。printStackTrace();
}
// 開始睡覺
me。startSleep();
// 模擬伍老師來了。
try {
Thread。sleep(10000);
} catch (InterruptedException e) {
e。printStackTrace();
}
classMate。doTeacherCome(“伍老師”);
// 模擬伍老師來了。
try {
Thread。sleep(10000);
} catch (InterruptedException e) {
e。printStackTrace();
}
classMate。doTeacherCome(“李老師”);
}
}同步、非同步、回撥、輪詢
同步
好比你去麥當勞點餐,你說“來個漢堡”,服務員告訴你,對不起,漢堡要現做,需要等5分鐘,於是你站在收銀臺前面等了5分鐘,拿到漢堡再去逛商場,這是同步。
非同步
你說“來個漢堡”,服務員告訴你,漢堡需要等5分鐘,你可以先去逛商場,等做好了,我們再通知你,這樣你可以立刻去幹別的事情(逛商場),這是非同步
回撥、輪詢
很明顯,使用非同步來編寫程式效能會遠遠高於同步,但是非同步的缺點是程式設計模型複雜。想想看,你得知道什麼時候通知你“漢堡做好了”,而通知你的方法也各不相同。如果是服務員跑過來找到你,這是回撥模式,如果服務員發簡訊通知你,你就得不停地檢查手機,這是輪詢模式。總之,非同步的複雜度遠遠高於同步
回撥函式
你到一個商店買東西,剛好你要的東西沒有貨,於是你在店員那裡留下了你的電話,過了幾天店裡有貨了,店員就打了你的電話,然後你接到電話後就到店裡去取了貨。在這個例子裡,你的電話號碼就叫回調函式,你把電話留給店員就叫登記回撥函式,店裡後來有貨了叫做觸發了回撥關聯的事件,店員給你打電話叫做呼叫回撥函式,你到店裡去取貨叫做響應回撥事件。
回撥函式是你寫一個函式,讓預先寫好的系統來呼叫。你去呼叫系統的函式,是直調。讓系統呼叫你的函式,就是回撥。但假如滿足於這種一句話結論,是不會真正明白的。
回撥函式可以看成,讓別人做事,傳進去的額外資訊。
比如 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);
要使用這函式,得到兩者最大的值, 外面就要傳進來 a, b。這個很好理解。
void qsort(void *, size_t, size_t, int (*)(const void *, const void *));
而這個函式用於排序,最後一個引數就是回撥函式,似乎就比較難以理解了。這是因為人為割裂了程式碼和資料。
我們暫停一下,看看計算機中比較詭異的地方,也就是程式碼(code)和資料(data)的統一。這是一個檻,如果不跨過這檻,很多概念就不清楚。我們常常說計算機程式分成 code 和 data 兩部分。很多人會理解成,code 是會執行的,是動態的,data 是給 code 使用,是靜態的,這是兩種完全不同的東西。
其實 code 只是對行為的一種描述,比如有個機器人可以開燈,關燈,掃地。如果跟機器人約定好,0 表示開燈,1 表示關燈,2 表示掃地。我發出指令串,0 1 2,就可以控制機器人開燈,關燈,掃地。再約定用二進位制表示,兩位一個指令,就有一個數字串,000111,這個時候 000111 這串數字就描述了機器人的一系列動作,這個就是從一方面理解是 code,它可以控制機器人的行為。但另一方面,它可以傳遞,可以記錄,可以修改,也就是資料。只要大家都協商好,code 就可以編碼成 data, 將 data 解釋執行的時候,也變成了 code。
code 和 data 可以不用區分,統一稱為資訊。既然 int max(int a, int b) 中 int,double 等表示普通 data 的東西可以傳遞進去,自然表示 code 的函式也可以傳進去了。有些語言確實是不區分的,它的 function(表示code)跟 int, double 的地位是一樣的。這種語言就為函式是第一類值。
而有些語言是不能儲存函式,不能動態建立函式,不能動態銷燬函式。只能儲存一個指向函式的指標,這種語言稱為函式是第二類值。
另外有些語言不單可以傳遞函式,函數里面又用到一些外部資訊(包括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 個函式指標。跟著就不是將單個函式指標傳進去,而是將介面,或者函式指標的結構傳進去。根據不同的用途,有些人叫它為代理,監聽者,觀察者等等。
實際上也是將某種行為儲存下來,以後有需要再進行呼叫。跟回撥函式在更深層次是沒有區別的。