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

Android程序員面試會遇到的算法系列:java

Android程序員面試會遇到的算法(part 1 關於二叉樹的那點事) 附Offer狀況node

Android程序員面試會遇到的算法(part 2 廣度優先搜索)程序員

Android程序員面試會遇到的算法(part 3 深度優先搜索-回溯backtracking)面試

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

Android程序員會遇到的算法(part 5 字典樹)數組

Android程序員會遇到的算法(part 6 優先級隊列PriorityQueue)緩存

Android程序員會遇到的算法(part 7 拓撲排序)網絡

很久沒有更新了,前段時間由於簽證的問題一直很鬧心因此沒有寫東西。數據結構

今天雖然依然沒有好消息,並且按照往年的數據,如今還抽不中H1b的估計都沒戲了,也可能個人硅谷夢就會就此破滅。。。併發

可是想了想,生活還得繼續,學習不能停下。我仍是要按照正常的節奏來。

這一期就主要給你們介紹在安卓應用或者輪子中最多見的一個設計,就是消息隊列

message-queue-small.png

我此次會以一個簡單的例子來一步步的展現消息隊列這種設計的應用,最後會借鑑Java和安卓源碼中對消息隊列實現的實例來作一個簡化版的代碼,但願你們在看完這篇文章以後在本身從此的app開發,或者輪子開發中能利用消息隊列設計來優化代碼結構,讓代碼更加可讀。

1.網絡請求(Volley)

相信大部分安卓開發者都有用過這個叫Volley的網絡請求庫,底層的網絡請求其實是用HttpUrlConnection類或者HttpClient這個庫作的。Volley在這些基礎庫上作了封裝,例如線程的控制,緩存和回調。這裏咱們詳細說說大部分網絡請求隊列的處理。

一個最基本最簡單的設計是,使用一個線程(非主線程),不停的從一個隊列中獲取請求,處理完畢以後從隊列拋出而且發射回調,回調確保在主線程運行。

實現起來很是簡單,這裏借鑑Volley源碼的設計,簡化一下:

/** 簡化版本的請求類,包含請求的Url和一個Runnable 回調 **/
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循環不停的從隊列裏面獲取第一個request而且處理
		while(true){
    		if( !requestQueue.isEmpty() ){
        		Request request = requestQueue.poll();
        		String response = // 處理request 的 url,這一步將是耗時的操做,省略細節
            	new Handler( Looper.getMainLooper() ).post( request.callback )
       		 }
    	}
    }
}).start();


複製代碼

上面這一系列代碼就把咱們的準備工做作好了。那麼往這個傻瓜版輪子裏面添加一個請求就很是簡單了。

requestQueue.add( new Request("http.....", new Runnable(  -> //do something )) );

複製代碼

就這樣,一個簡化版的網絡請求的輪子就完成了,是否是很簡單,雖然咱們沒有考慮同步,緩存等問題,但其實看過Volley源碼的朋友也應該清楚,Volley的核心就是這樣的隊列,只不過不是一個隊列,而是兩種隊列(一個隊列真正的進行網絡請求,一個是嘗試從緩存中找對應request的返回內容)

代碼的核心也就是用while循環不停的彈出請求,再處理而已。

2.發送延遲消息

消息隊列的還有一種玩法就是發送延遲消息,好比說我想控制當前發送的消息在三秒以後處理,那這樣應該怎麼寫咱們的代碼呢,畢竟在網絡請求的例子裏面,咱們徹底不在意消息的執行順序,把請求丟進隊列以後就就開始等待回調了。

這個時候咱們能夠採用鏈表這個數據結構來取代隊列(固然Java裏面鏈表能夠做爲隊列的實例),按照每一個請求或者消息的執行時間進行排序。

廢話很少說,先上簡版代碼。

//一個消息的類結構,除了runnable,還有一個該Message須要被執行的時間execTime,兩個引用,指向該Message在鏈表中的前任節點和後繼節點。
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的狀況,這樣代碼寫起來更加簡潔,也是一個小技巧。
    //頭的執行時間設置爲-1,尾是Long的最大值,這樣能夠保證其餘正常的Message確定會落在這兩個點之間。
	private Message head = new Message(null,-1);
    private Message tail = new Message(null,Long.MAX_VALUE);
    
    public void run(){
    
    	new Thread( new Runnable(){
        	public void run(){
            //用死循環來不停處理消息
            while(true){
            		//這裏是關鍵,當頭不是dummy頭,而且當前時間是大於或者等於頭節點的執行時間的時候,咱們能夠執行頭節點的任務task。
            			if( head.next != tail && System.currentTimeMillis()>= head.next.execTime ){
                    	//執行的過程須要把頭結點拿出來而且從鏈表結構中刪除
             			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的執行時間是當前時間+延遲的秒數。
        Message message = new Message( task,  System.currentMilliSec()+milliSec);

		//這裏使用一個while循環去找第一個執行時間在新建立的Message以前的Message,新建立的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),你們能夠想一想若是換成數組會是什麼複雜度。
  2. 代碼中能夠用兩個Dummy node做爲頭和尾,這樣咱們每次插入新消息的時候不須要檢查空指針, 若是頭爲空,咱們插入Message還須要作 if(head == null){ head = message } else if( tail == null ){head.next = message; tail = message} 這樣的檢查。 3.每次發送延遲消息的時候,遍歷循環找到第一個時間比當前要插入的消息的時間小。如下面這個圖爲例子。

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

當前插入Message時間爲3的時候,它須要插入在1和5中間,那麼1節點就是咱們上面代碼循環中的最後的Target了。

這樣,咱們就完成了一個延遲消息的輪子了!哈哈,調用代碼很是簡單。

MessageQueue queue = new MessageQueue();
//開啓queue的while循環
queue.run();

queue.post( new Runnable(....) )

//三秒以後執行
queue.postDelay( new Runnable(...) , 3*1000 )

複製代碼

你們可能以爲post,和postDelay看起來很是眼熟,沒錯,這個就是安卓裏面Handler的經典方法

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

在安卓系統中的源代碼裏面,postDelay就是運用上述的原理,只不過安卓系統對回收Message還有額外的處理。可是對於延遲消息的發送,安卓的Handler就是對其對應的Looper裏面的消息鏈表進行處理,比較執行時間從而實現延遲消息發送的。

最後你們再思考一下,像上述代碼的例子裏面,延遲三秒,是否是精確的作到了在當前時間的三秒後運行

答案固然是NO!

在這個設計下,咱們只能保證:

假如消息A延遲的秒數爲X,當前時間爲Y,系統能保證A不會在X+Y以前執行。 這樣其實很好理解,由於若是使用隊列來執行代碼的話,你永遠不知道你前面那個Message的執行時間是多少,假如前面的Message執行時間異常的長。。。。那麼輪到當前Message執行的時候,確定會比它本身的execTime偏後。可是這是可接受的。

若是咱們須要嚴格讓每一個Message按照設計的時間執行,那就須要Alarm,相似鬧鐘的設計了。你們有興趣能夠想一想看怎麼用最基本的數據結構實現。

3.線程池的實現

說到線程池,我一直有不少疑惑,網上不少文章都會以線程池最全解析,或者史上最詳細Java線程池原理諸如此類的Title爲標題,但卻主要以怎麼操做Java線程池的API爲內容。

在我看來這類文章都是耍流氓,對於一個合格的Java開發來講,若是連API都不會查,那乾脆別幹了,還須要你專門寫一篇文章來介紹API怎麼用嘛。。。。。我也一直在問我本身,爲啥你們都對源代碼沒有興趣。。。。

download.jpeg

這個章節我就會用簡單版本的代碼把線程池的實現給展現一下。

其實線程池的實現很簡單,就是使用一個隊列若干Thread就好了。

public class ThreadPool{

	//用一個Set或者其餘數據結構把建立的線程保存起來,爲的是方便之後獲取線程的handle,作其餘操做。
	Set<WorkerThread> set = null;
    private Queue<Runnable> queue;
    //初始化線程池,建立內部類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用一個死循環不停的去向Runnable隊列拿Runnable執行。
    public class WorkerThread extends Thread{
        @Override
        public void run() {
            super.run();
            while(true){
            	synchronized (queue){
                	Runnable current = queue.poll();
                    current.run();
                }
            }
        }
    }
    

}


複製代碼

這樣,一個簡單版本的線程池就完成了。。。。使用一組Thread,不停的向Runnable隊列去拿Runnable執行就行了。。。看起來徹底沒有技術含量。可是這倒是Java的線程池的基本原理。你們抽空能夠去看看源碼。還有不少細節我都沒有寫出來,好比說怎麼shutdown線程池,或者線程池內部的WorkerThread怎麼處理異常。怎麼設置最大線程數量等等。

注意點很少,就是要使用synchronized對併發部分的代碼作好同步就能夠了。

調用代碼簡單

ThreadPool pool = new ThreadPool(5);

pool.submit(new Runnable(...))

複製代碼

華麗麗的分割線


後記

這一期的分享結束啦,其實上面三個例子都是大部分安卓開發者會接觸到的,若是稍微有點興趣和耐心就能夠明白其原理,都是用最簡單的數據結構加最「幼稚」的設計完成的。

最後我還想說,但願每一個安卓開發者都能有一顆疑問的心, 好比線程池,基於Java的Thread這個類,怎麼去完成一個線程池的實現,若是每次在使用這些API以後都能問問本身,爲何,保持一顆願意提問的心,這些都能學會。願你們都能有且保持這種熱忱。

我也須要時刻提醒本身,不管能不能去硅谷都好,都要一直有這種熱情,一刻也不能懈怠。若是個人熱情由於不能去硅谷而破滅,那個人堅持也太脆弱了。

相關文章
相關標籤/搜索