好久沒有更新了真竖,前段時間因為簽證的問題一直很鬧心所以沒有寫東西糙及。
今天雖然依然沒有好消息详幽,而且按照往年的數(shù)據(jù),現(xiàn)在還抽不中H1b的估計都沒戲了,也可能我的硅谷夢就會就此破滅唇聘。版姑。。
但是想了想迟郎,生活還得繼續(xù)剥险,學習不能停下。我還是要按照正常的節(jié)奏來宪肖。
這一期就主要給大家介紹在安卓應用或者輪子中最常見的一個設計表制,就是消息隊列。
我這次會以一個簡單的例子來一步步的展示消息隊列這種設計的應用控乾,最后會借鑒Java和安卓源碼中對消息隊列實現(xiàn)的實例來做一個簡化版的代碼么介,希望大家在看完這篇文章之后在自己今后的app開發(fā),或者輪子開發(fā)中能利用消息隊列設計來優(yōu)化代碼結構蜕衡,讓代碼更加可讀壤短。
1.網(wǎng)絡請求(Volley)
相信大部分安卓開發(fā)者都有用過這個叫Volley的網(wǎng)絡請求庫,底層的網(wǎng)絡請求實際上是用HttpUrlConnection類或者HttpClient這個庫做的慨仿。Volley在這些基礎庫上做了封裝久脯,例如線程的控制,緩存和回調(diào)镶骗。這里我們詳細說說大部分網(wǎng)絡請求隊列的處理桶现。
一個最基本最簡單的設計是,使用一個線程(非主線程)鼎姊,不停的從一個隊列中獲取請求,處理完畢之后從隊列拋出并且發(fā)射回調(diào)相赁,回調(diào)確保在主線程運行相寇。
實現(xiàn)起來非常簡單,這里借鑒Volley源碼的設計钮科,簡化一下:
/**
簡化版本的請求類唤衫,包含請求的Url和一個Runnable 回調(diào)
**/
class Request{
public String requestUrl;
public Runnable callback;
public Request(String url, Runnable callback)
{
this.requestUrl = url;
this.callback = callback;
}
}
//消息隊列
Queue<Request> requestQueue = new LinkedList<Request>();
new Thread( new Runnable(){
public void run(){
//啟動一個新的線程,用一個True的while循環(huán)不停的從隊列里面獲取第一個request并且處理
while(true){
if( !requestQueue.isEmpty() ){
Request request = requestQueue.poll();
String response = // 處理request 的 url绵脯,這一步將是耗時的操作佳励,省略細節(jié)
new Handler( Looper.getMainLooper() ).post( request.callback )
}
}
}
}).start();
上面這一系列代碼就把我們的準備工作做好了。那么往這個傻瓜版輪子里面添加一個請求就非常簡單了蛆挫。
requestQueue.add( new Request("http.....", new Runnable( -> //do something )) );
就這樣赃承,一個簡化版的網(wǎng)絡請求的輪子就完成了,是不是很簡單悴侵,雖然我們沒有考慮同步瞧剖,緩存等問題,但其實看過Volley源碼的朋友也應該清楚,Volley的核心就是這樣的隊列抓于,只不過不是一個隊列做粤,而是兩種隊列(一個隊列真正的進行網(wǎng)絡請求,一個是嘗試從緩存中找對應request的返回內(nèi)容)
代碼的核心也就是用while循環(huán)不停的彈出請求捉撮,再處理而已怕品。
2.發(fā)送延遲消息
消息隊列的還有一種玩法就是發(fā)送延遲消息,比如說我想控制當前發(fā)送的消息在三秒之后處理巾遭,那這樣應該怎么寫我們的代碼呢堵泽,畢竟在網(wǎng)絡請求的例子里面,我們完全不在乎消息的執(zhí)行順序恢总,把請求丟進隊列之后就就開始等待回調(diào)了迎罗。
這個時候我們可以采用鏈表這個數(shù)據(jù)結構來取代隊列(當然Java里面鏈表可以作為隊列的實例),按照每個請求或者消息的執(zhí)行時間進行排序片仿。
廢話不多說纹安,先上簡版代碼。
//一個消息的類結構砂豌,除了runnable厢岂,還有一個該Message需要被執(zhí)行的時間execTime,兩個引用阳距,指向該Message在鏈表中的前任節(jié)點和后繼節(jié)點塔粒。
public class Message{
public long execTime = -1;
public Runnable task;
public Message prev;
public Message next;
public Message(Runnable runnable, long milliSec){
this.task = runnable;
this.execTime = milliSec;
}
}
public class MessageQueue{
//維持兩個dummy的頭和尾作為我們消息鏈表的頭和尾,這樣做的好處是當我們插入新Message時筐摘,不需要考慮頭尾為Null的情況卒茬,這樣代碼寫起來更加簡潔,也是一個小技巧咖熟。
//頭的執(zhí)行時間設置為-1圃酵,尾是Long的最大值,這樣可以保證其他正常的Message肯定會落在這兩個點之間馍管。
private Message head = new Message(null,-1);
private Message tail = new Message(null,Long.MAX_VALUE);
public MessageQueue(){
head.next = tail;
tail.prev = next;
}
public void run(){
new Thread( new Runnable(){
public void run(){
//用死循環(huán)來不停處理消息
while(true){
//這里是關鍵郭赐,當頭不是dummy頭,并且當前時間是大于或者等于頭節(jié)點的執(zhí)行時間的時候确沸,我們可以執(zhí)行頭節(jié)點的任務task捌锭。
if( head.next != tail && System.currentTimeMillis()>= head.next.execTime ){
//執(zhí)行的過程需要把頭結點拿出來并且從鏈表結構中刪除
Message current = head.next;
Message next = current.next;
current.task.run();
current.next = null;
current.prev =null;
head.next = next;
next.prev = head;
}
}
}
}).start();
}
public void post(Runnable task){
//如果是純post,那么把消息放在最尾部
Message message = new Message( task, System.currentMilliSec() );
Message prev = tail.prev;
prev.next = message;
message.prev = prev;
message.next = tail;
tail.prev = message;
}
public void postDelay(Runnable task, long milliSec){
//如果是延遲消息罗捎,生成的Message的執(zhí)行時間是當前時間+延遲的秒數(shù)观谦。
Message message = new Message( task, System.currentMilliSec()+milliSec);
//這里使用一個while循環(huán)去找第一個執(zhí)行時間在新創(chuàng)建的Message之前的Message,新創(chuàng)建的Message就要插在它后面宛逗。
Message target = tail;
while(target.execTime>= message.execTime){
target = target.prev;
}
Message next = target.next;
message.prev = target;
target.next = message;
message.next = next;
next.prev = message;
}
}
上述代碼有幾個比較關鍵的點坎匿。
- 消息采用鏈表的方式存儲,為的是方便插入新的消息,每次插入尾部的時間復雜度為O(1),插入中間的復雜度為O(n),大家可以想想如果換成數(shù)組會是什么復雜度替蔬。
- 代碼中可以用兩個Dummy node作為頭和尾告私,這樣我們每次插入新消息的時候不需要檢查空指針, 如果頭為空承桥,我們插入Message還需要做
if(head == null){ head = message } else if( tail == null ){head.next = message; tail = message}
這樣的檢查驻粟。
3.每次發(fā)送延遲消息的時候,遍歷循環(huán)找到第一個時間比當前要插入的消息的時間小凶异。以下面這個圖為例子蜀撑。
當前插入Message時間為3的時候,它需要插入在1和5中間剩彬,那么1節(jié)點就是我們上面代碼循環(huán)中的最后的Target了酷麦。
這樣,我們就完成了一個延遲消息的輪子了喉恋!哈哈沃饶,調(diào)用代碼非常簡單。
MessageQueue queue = new MessageQueue();
//開啟queue的while循環(huán)
queue.run();
queue.post( new Runnable(....) )
//三秒之后執(zhí)行
queue.postDelay( new Runnable(...) , 3*1000 )
大家可能覺得post轻黑,和postDelay看起來非常眼熟糊肤,沒錯,這個就是安卓里面Handler的經(jīng)典方法
在安卓系統(tǒng)中的源代碼里面氓鄙,postDelay就是運用上述的原理馆揉,只不過安卓系統(tǒng)對回收Message還有額外的處理。但是對于延遲消息的發(fā)送抖拦,安卓的Handler就是對其對應的Looper里面的消息鏈表進行處理升酣,比較執(zhí)行時間從而實現(xiàn)延遲消息發(fā)送的。
最后大家再思考一下蟋座,像上述代碼的例子里面拗踢,延遲三秒,是不是精確的做到了在當前時間的三秒后運行
答案當然是NO!
在這個設計下向臀,我們只能保證:
假如消息A延遲的秒數(shù)為X,當前時間為Y诸狭,系統(tǒng)能保證A不會在X+Y之前執(zhí)行券膀。 這樣其實很好理解,因為如果使用隊列來執(zhí)行代碼的話驯遇,你永遠不知道你前面那個Message的執(zhí)行時間是多少芹彬,假如前面的Message執(zhí)行時間異常的長。叉庐。舒帮。。那么輪到當前Message執(zhí)行的時候,肯定會比它自己的execTime偏后玩郊。但是這是可接受的肢执。
如果我們需要嚴格讓每個Message按照設計的時間執(zhí)行,那就需要Alarm译红,類似鬧鐘的設計了预茄。大家有興趣可以想想看怎么用最基本的數(shù)據(jù)結構實現(xiàn)。
3.線程池的實現(xiàn)
說到線程池侦厚,我一直有很多疑惑耻陕,網(wǎng)上很多文章都會以線程池最全解析,或者史上最詳細Java線程池原理諸如此類的Title為標題刨沦,但卻主要以怎么操作Java線程池的API為內(nèi)容诗宣。
在我看來這類文章都是耍流氓,對于一個合格的Java開發(fā)來說想诅,如果連API都不會查召庞,那干脆別干了,還需要你專門寫一篇文章來介紹API怎么用嘛侧蘸。裁眯。。讳癌。穿稳。我也一直在問我自己,為啥大家都對源代碼沒有興趣晌坤。逢艘。。骤菠。
這個章節(jié)我就會用簡單版本的代碼把線程池的實現(xiàn)給展示一下它改。
其實線程池的實現(xiàn)很簡單,就是使用一個隊列若干Thread就行了商乎。
public class ThreadPool{
//用一個Set或者其他數(shù)據(jù)結構把創(chuàng)建的線程保存起來央拖,為的是方便以后獲取線程的handle,做其他操作鹉戚。
Set<WorkerThread> set = null;
private Queue<Runnable> queue;
//初始化線程池鲜戒,創(chuàng)建內(nèi)部類WorkerThread并且啟動它
public ThreadPool(int size){
for( int i = 0 ;i < size ;i++ ){
WorkerThread t = new WorkerThread();
t.start();
set.add( t );
}
queue = new LinkedList<Runnable>();
}
//submit一個runnable進線程池
public void submit(Runnable runnable){
synchronized (queue){
queue.add(runnable);
}
}
//WorkerThread用一個死循環(huán)不停的去向Runnable隊列拿Runnable執(zhí)行。
public class WorkerThread extends Thread{
@Override
public void run() {
super.run();
while(true){
synchronized (queue){
if( !queue.isEmpty() ){
Runnable current = queue.poll();
current.run();
}
}
}
}
}
}
這樣抹凳,一個簡單版本的線程池就完成了遏餐。。赢底。失都。使用一組Thread柏蘑,不停的向Runnable隊列去拿Runnable執(zhí)行就好了。粹庞。咳焚。看起來完全沒有技術含量信粮。但是這卻是Java的線程池的基本原理黔攒。大家抽空可以去看看源碼。還有很多細節(jié)我都沒有寫出來强缘,比如說怎么shutdown線程池督惰,或者線程池內(nèi)部的WorkerThread怎么處理異常。怎么設置最大線程數(shù)量等等旅掂。
注意點不多赏胚,就是要使用synchronized對并發(fā)部分的代碼做好同步就可以了。
調(diào)用代碼簡單
ThreadPool pool = new ThreadPool(5);
pool.submit(new Runnable(...))
華麗麗的分割線
后記
這一期的分享結束啦商虐,其實上面三個例子都是大部分安卓開發(fā)者會接觸到的觉阅,如果稍微有點興趣和耐心就可以明白其原理,都是用最簡單的數(shù)據(jù)結構加最“幼稚”的設計完成的秘车。
最后我還想說典勇,希望每個安卓開發(fā)者都能有一顆疑問的心, 比如線程池叮趴,基于Java的Thread這個類割笙,怎么去完成一個線程池的實現(xiàn),如果每次在使用這些API之后都能問問自己眯亦,為什么伤溉,保持一顆愿意提問的心,這些都能學會妻率。愿大家都能有且保持這種熱忱乱顾。
我也需要時刻提醒自己,無論能不能去硅谷都好宫静,都要一直有這種熱情走净,一刻也不能懈怠。如果我的熱情因為不能去硅谷而破滅孤里,那我的堅持也太脆弱了温技。