Android程序員會遇到的算法(part 4 消息隊列的應用)

好久沒有更新了真竖,前段時間因為簽證的問題一直很鬧心所以沒有寫東西糙及。

今天雖然依然沒有好消息详幽,而且按照往年的數(shù)據(jù),現(xiàn)在還抽不中H1b的估計都沒戲了,也可能我的硅谷夢就會就此破滅唇聘。版姑。。

但是想了想迟郎,生活還得繼續(xù)剥险,學習不能停下。我還是要按照正常的節(jié)奏來宪肖。

這一期就主要給大家介紹在安卓應用或者輪子中最常見的一個設計表制,就是消息隊列

message-queue-small.png

我這次會以一個簡單的例子來一步步的展示消息隊列這種設計的應用控乾,最后會借鑒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;
           
    }
}

上述代碼有幾個比較關鍵的點坎匿。

  1. 消息采用鏈表的方式存儲,為的是方便插入新的消息,每次插入尾部的時間復雜度為O(1),插入中間的復雜度為O(n),大家可以想想如果換成數(shù)組會是什么復雜度替蔬。
  2. 代碼中可以用兩個Dummy node作為頭和尾告私,這樣我們每次插入新消息的時候不需要檢查空指針, 如果頭為空承桥,我們插入Message還需要做 if(head == null){ head = message } else if( tail == null ){head.next = message; tail = message} 這樣的檢查驻粟。
    3.每次發(fā)送延遲消息的時候,遍歷循環(huán)找到第一個時間比當前要插入的消息的時間小凶异。以下面這個圖為例子蜀撑。
Screen Shot 2018-05-01 at 10.34.06 PM.png

當前插入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)典方法

Screen Shot 2018-05-01 at 10.39.09 PM.png

在安卓系統(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怎么用嘛侧蘸。裁眯。。讳癌。穿稳。我也一直在問我自己,為啥大家都對源代碼沒有興趣晌坤。逢艘。。骤菠。

download.jpeg

這個章節(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之后都能問問自己眯亦,為什么伤溉,保持一顆愿意提問的心,這些都能學會妻率。愿大家都能有且保持這種熱忱乱顾。

我也需要時刻提醒自己,無論能不能去硅谷都好宫静,都要一直有這種熱情走净,一刻也不能懈怠。如果我的熱情因為不能去硅谷而破滅孤里,那我的堅持也太脆弱了温技。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市扭粱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌震檩,老刑警劉巖琢蛤,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜓堕,死亡現(xiàn)場離奇詭異,居然都是意外死亡博其,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扁凛,“玉大人病附,你說我怎么就攤上這事》逅瑁” “怎么了傻寂?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長携兵。 經(jīng)常有香客問我疾掰,道長,這世上最難降的妖魔是什么徐紧? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任静檬,我火速辦了婚禮,結果婚禮上并级,老公的妹妹穿的比我還像新娘拂檩。我一直安慰自己,他們只是感情好嘲碧,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布稻励。 她就那樣靜靜地躺著,像睡著了一般呀潭。 火紅的嫁衣襯著肌膚如雪钉迷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天钠署,我揣著相機與錄音糠聪,去河邊找鬼。 笑死谐鼎,一個胖子當著我的面吹牛舰蟆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播狸棍,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼身害,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了草戈?” 一聲冷哼從身側(cè)響起塌鸯,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎唐片,沒想到半個月后丙猬,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體涨颜,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年茧球,在試婚紗的時候發(fā)現(xiàn)自己被綠了庭瑰。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡抢埋,死狀恐怖弹灭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情揪垄,我是刑警寧澤穷吮,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站福侈,受9級特大地震影響酒来,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肪凛,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一堰汉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧伟墙,春花似錦翘鸭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拱烁,卻和暖如春生蚁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背戏自。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工邦投, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人擅笔。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓志衣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親猛们。 傳聞我的和親對象是個殘疾皇子念脯,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,527評論 25 707
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結構(3).初始化時...
    歐辰_OSR閱讀 29,321評論 8 265
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn)弯淘,斷路器绿店,智...
    卡卡羅2017閱讀 134,601評論 18 139
  • 活了30多歲我才終于明白 人生不能隨心所欲 有時候需要控制 控制自己的情緒 當你憤怒的時候要克制住憤怒,不要做沖動...
    蒙媽成長記閱讀 552評論 0 0
  • 很久很久以前庐橙,因為和仙女相愛惯吕,被玉帝貶下凡間惕它,看守西湖,他穿著白色的風衣废登,白色戰(zhàn)袍,黑色的長發(fā)順著兩邊的鬢角垂下郁惜,...
    愛生活O閱讀 432評論 1 0