Android開發進階之NIO非阻塞包

 

Android開發進階之NIO非阻塞包 這個系列轉載於http://www.android123.com.cn/androidkaifa/695.html,特此說明

 

對於Android的網絡通訊性能的提高,我們可以使用Java上高性能的NIO (New I/O) 技術進行處理,NIO是從JDK 1.4開始引入的,NIO的N我們可以理解爲Noblocking即非阻塞的意思,相對應傳統的I/O,比如Socket的accpet()、read()這些方法而言都是阻塞的。

  NIO主要使用了Channel和Selector來實現,Java的Selector類似Winsock的Select模式,是一種基於事件驅動的,整個處理方法使用了輪訓的狀態機,如果你過去開發過Symbian應用的話這種方式有點像活動對象,好處就是單線程更節省系統開銷,NIO的好處可以很好的處理併發,對於Android網遊開發來說比較關鍵,對於多點Socket連接而言使用NIO可以大大減少線程使用,降低了線程死鎖的概率,畢竟手機遊戲有UI線程,音樂線程,網絡線程,管理的難度可想而知,同時I/O這種低速設備將影響遊戲的體驗。

  NIO作爲一種中高負載的I/O模型,相對於傳統的BIO (Blocking I/O)來說有了很大的提高,處理併發不用太多的線程,省去了創建銷燬的時間,如果線程過多調度是問題,同時很多線程可能處於空閒狀態,大大浪費了CPU時間,同時過多的線程可能是性能大幅下降,一般的解決方案中可能使用線程池來管理調度但這種方法治標不治本。使用NIO可以使併發的效率大大提高。當然NIO和JDK 7中的AIO還存在一些區別,AIO作爲一種更新的當然這是對於Java而言,如果你開發過Winsock服務器,那麼IOCP這樣的I/O完成端口可以解決更高級的負載,當然了今天Android123主要給大家講解下爲什麼使用NIO在Android中有哪些用處。

   NIO我們分爲幾個類型分別描述,作爲Java的特性之一,我們需要了解一些新的概念,比如ByteBuffer類,Channel,SocketChannel,ServerSocketChannel,Selector和SelectionKey。有關具體的使用,Android開發網將在明天詳細講解。網友可以在Android SDK文檔中看下java.nio和java.nio.channels兩個包瞭解。

有關Android NIO我們主要分爲三大類,ByteBufferFileChannelSocketChannel。由於篇幅原因今天Android123只對前兩個做說明。NIO和傳統的I/O比較大的區別在於傳輸方式非阻塞,一種基於事件驅動的模式,將會使方法執行完後立即返回,傳統I/O主要使用了流Stream的方式,而在New I/O中,使用了字節緩存ByteBuffer來承載數據。

   ByteBuffer位於java.nio包中,目前提供了Java基本類型中除Boolean外其他類型的緩衝類型,比如ByteBufferDoubleBufferFloatBufferIntBufferLongBufferShortBuffer  。同時還提供了一種更特殊的映射字節緩衝類型MappedByteBuffer。在傳統IO的輸入輸出流中,InputStream中只提供了字節型或字節數組的訪問對應NIO就是ByteBuffer,但是處理傳統的DataInputStreamint等類型,就是IntBuffer,但是緩衝類型並沒有提供UTF這樣的類型處理,所以我們仍然需要使用ByteBuffer處理字符串,但是NIO提供了一個封裝的類在java.nio.charset包中,通過字符的編碼CharsetEncoder和解碼CharsetDecoder類來處理字符串,同時這些類可以方便轉換編碼比如GBKUTF等等。

  一、ByteBuffer

  1) 實例化

  直接使用ByteBuffer類的靜態方法static ByteBuffer allocate(int capacity)  static ByteBuffer allocateDirect(int capacity)  這兩個方法來分配內存空間,兩種方法的區別主要是後者更適用於繁複分配的字節數組。而 put(ByteBuffer src) 可以從另一個ByteBuffer中構造,也可以通過wrap方法從byte[]中構造,具體參考下面的類型轉化內容。

  2) 類型轉化

   ByteBuffer可以很好的和字節數組byte[]轉換類型,通過執行ByteBuffer類的final byte[]  array() 方法就可以將ByteBuffer轉爲byte[]。從byte[]來構造ByteBuffer可以使用wrap方法,目前Android或者說Java提供了兩種重寫方法,比如爲static ByteBuffer  wrap(byte[] array)   static ByteBuffer  wrap(byte[] array, int start, int len)  ,第二個重載方法中第二個參數爲從array這個字節數組的起初位置,第三個參數爲array這個字節數組的長度。

  3) ByteBuffer中添加元素

  目前ByteBuffer提供了多種put重寫類型來添加,比如put(byte b) putChar(char value) putFloat(float value) 等等,需要注意的是,按照Java的類型長度,一個byte1字節,一個char類型是2字節,一個floatint4字節,一個long則爲8字節,和傳統的C++有些區別。所以內部的相關位置也會發生變化,同時每種方法還提供了定位的方法比如ByteBuffer  put(int index, byte b) 

  4) ByteBuffer中獲取元素

  同上面的添加想法,各種put被換成了get,比如byte  get()  float  getFloat()  ,當然了還提供了一種定位的方式,比如double  getDouble(int index) 

  5) ByteBuffer中字節順序

  對於Java來說默認使用了BIG_ENDIAN方式存儲,和C正好相反的,通過

  final ByteOrder  order() 返回當前的字節順序。

  final ByteBuffer  order(ByteOrder byteOrder)  設置字節順序,ByteOrder類的值有兩個定義,比如LITTLE_ENDIANBIG_ENDIAN,如果使用當前平臺則爲ByteOrder.nativeOrder()Android中則爲 BIG_ENDIAN,當然如果設置爲order(null) 則使用LITTLE_ENDIAN

  二、FileChannel

   NIO中除了Socket外,還提供了File設備的通道類,FileChannel位於java.nio.channels.FileChannel包中,在Android SDK文檔中我們可以方便的找到,對於文件複製我們可以使用ByteBuffer方式作爲緩衝,比如

  String infile = "/sdcard/cwj.dat";
  String outfile = "/sdcard/android123-test.dat";

    FileInputStream fin = new FileInputStream( infile );
    FileOutputStream fout = new FileOutputStream( outfile );

    FileChannel fcin = fin.getChannel();
    FileChannel fcout = fout.getChannel();

    ByteBuffer buffer = ByteBuffer.allocate( 1024 ); //分配1KB作爲緩衝區

    while (true) {
    buffer.clear(); //
每次使用必須置空緩衝區

      int r = fcin.read( buffer );

      if (r==-1) {
        break;
      }

   buffer.flip(); //寫入前使用flip這個方法

      fcout.write( buffer );
    }

   flipclear這兩個方法是java.nio.Buffer包中,ByteBuffer的父類是從Buffer類繼承而來的,這點Android123要提醒大家看Android SDK文檔時注意Inherited Methods,而JDK的文檔就比較直接了,同時複製文件使用FileChanneltransferTo(long position, long count, WritableByteChannel target) 這個方法可以快速的複製文件,無需自己管理ByteBuffer緩衝區。明天Android開發網介紹NIO主要的Socket相關的內容。


有關Android NIO的精髓主要用於高負載的Socket網絡傳輸,相對於傳統I/O模型的Socket傳輸方式的優勢,我們已經在 Android開發進階之NIO非阻塞包(一) 中講到了,這裏不再贅述,一起來看看Android NIO有關Socket操作提供的類吧:

 

  一、ServerSocketChannel 服務器套接字通道在Android SDK中查找package名爲  java.nio.channels.ServerSocketChannel

 

   在Java的NIO中,ServerSocketChannel對應的是傳統IO中的ServerSocket,通過ServerSocketChannel類的socket() 方法可以獲得一個傳統的ServerSocket對象,同時從ServerSocket對象的getChannel() 方法,可以獲得一個ServerSocketChannel()對象,這點說明NIO的ServerSocketChannel和傳統IO的ServerSocket是有關聯的,實例化ServerSocketChannel 只需要直接調用ServerSocketChannel 類的靜態方法open()即可。

 

  二、 SocketChannel 套接字通道 java.nio.channels.SocketChannel   

 

  在Java的New I/O中,處理Socket類對應的東西,我們可以看做是SocketChannel,套接字通道關聯了一個Socket類,這一點使用SocketChannel類的socket() 方法可以返回一個傳統IO的Socket類。SocketChannel()對象在Server中一般通過Socket類的getChannel()方法獲得。

 

 三、SelectionKey 選擇鍵 java.nio.channels.SelectionKey

 

  在NIO中SelectionKey和Selector是最關鍵的地方,SelectionKey類中描述了NIO中比較重要的事件,比如OP_ACCEPT(用於服務器端)、OP_CONNECT(用於客戶端)、OP_READ和OP_WRITE。

 

 四、Selector 選擇器 java.nio.channels.Selector

 

  在NIO中註冊各種事件的方法主要使用Selector來實現的,構造一個Selector對象,使用Selector類的靜態方法open()來實例化。

 

  對於Android平臺上我們實現一個非阻塞的服務器,過程如下:

 

   1. 通過Selector類的open()靜態方法實例化一個Selector對象。

 

   2. 通過ServerSocketChannel類的open()靜態方法實例化一個ServerSocketChannel對象。

 

   3. 顯示的調用ServerSocketChannel對象的configureBlocking(false);方法,設置爲非阻塞模式,Android123提示網友這一步十分重要。

 

   4. 使用ServerSocketChannel對象的socket()方法返回一個ServerSocket對象,使用ServerSocket對象的bind()方法綁定一個IP地址和端口號

 

   5. 調用ServerSocketChannel對象的register方法註冊感興趣的網絡事件,很多開發者可能發現Android SDK文檔中沒有看到register方法,這裏Android開發網給大家一個ServerSocketChannel類的繼承關係  

 

java.lang.Object

   ↳ java.nio.channels.spi.AbstractInterruptibleChannel

     ↳ java.nio.channels.SelectableChannel

       ↳ java.nio.channels.spi.AbstractSelectableChannel

         ↳ java.nio.channels.ServerSocketChannel

 

 

   這裏我們使用的register方法其實來自ServerSocketChannel的父類java.nio.channels.SelectableChannel,該方法原型爲 final SelectionKey  register(Selector selector, int operations)  ,參數爲我們執行第1步時的selector對象,參數二爲需要註冊的事件,作爲服務器,我們當然是接受客戶端發來的請求,所以這裏使用SelectionKey.OP_ACCEPT了。

 

  6. 通過Selector對象的select() 方法判斷是否有我們感興趣的事件發生,這裏就是OP_ACCEPT事件了。我們通過一個死循環獲取Selector對象執行select()方法的值,SDK中的原始描述爲the number of channels that are ready for operation.,就是到底有多少個通道返回。

 

  7. 如果 Selector對象的select()方法返回的結果數大於0,則通過selector對象的selectedKeys()方法獲取一個SelectionKey類型的Set集合,我們使用Java的迭代器Iterator類來遍歷這個Set集合,注意判斷SelectionKey對象,

 

  8. 爲了表示我們處理了SelectionKey對象,需要先移除這個SelectionKey對象從Set集合中。這句很關鍵Android 123提醒網友注意這個地方。

 

  9. 接下來判斷SelectionKey對象的事件,因爲我們註冊的感興趣的是SelectionKey.OP_ACCEPT事件,我們使用SelectionKey對象的isAcceptable()方法判斷,如果是我們創建一個臨時SocketChannel對象類似上面的方法繼續處理,不過這時這個SocketChannel對象主要處理讀寫操作,我們註冊SelectionKey.OP_READ和SelectionKey.OP_WRITE分配ByteBuffer緩衝區,進行網絡數據傳輸。

 

  有關具體的示例和解釋上面的流暢,由於篇幅原因我們明天Android開發網給出源代碼,做詳細的分析。


今天我們通過一個實例詳細講解下Android下NIO非阻塞服務器的開發,對於客戶端而言Android123不推薦使用NIO,畢竟NIO相對於傳統IO較爲複雜,最重要的NIO是爲了解決多線程併發問題而解決的技術,可能會因爲管理和複雜性降低最終的結果,畢竟NIO是Java的,相關的類型比較難控制,對於客戶端而言我們可以使用C++、Java、C#甚至Flash Action Script來編寫。

 

    下面我們以一個簡單的Echo Server爲例子來分析

 

 import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.CharBuffer;

import java.nio.channels.SelectionKey;

import java.nio.channels.Selector;

import java.nio.channels.ServerSocketChannel;

import java.nio.channels.SocketChannel;

import java.nio.charset.Charset;

import java.nio.charset.CharsetDecoder;

import java.nio.charset.CharsetEncoder;

import java.util.Iterator;

 

public class Server {

 

 public static void main(String[] args) {

  Selector selector = null;

  ServerSocketChannel ssc = null;

  try {

      selector = Selector.open(); //實例化selector

      ssc = ServerSocketChannel.open(); //實例化ServerSocketChannel 對象

 

      ssc.socket().bind(new InetSocketAddress(1987)); //綁定端口爲1987

 

      ssc.configureBlocking(false); //設置爲非阻塞模式

      ssc.register(selector, SelectionKey.OP_ACCEPT); //註冊關心的事件,對於Server來說主要是accpet了

 

 

   while (true) {

   int n= selector.select(); //獲取感興趣的selector數量

   if(n<1)

          continue; //如果沒有則一直輪訓檢查

    Iterator<SelectionKey> it = selector.selectedKeys().iterator(); //有新的鏈接,我們返回一個SelectionKey集合

    while (it.hasNext()) {

     SelectionKey key = it.next(); //使用迭代器遍歷

     it.remove(); //刪除迭代器

 

     if (key.isAcceptable()) { //如果是我們註冊的OP_ACCEPT事件

      ServerSocketChannel ssc2 = (ServerSocketChannel) key.channel();

      SocketChannel channel = ssc2.accept();

      channel.configureBlocking(false); //同樣是非阻塞

      channel.register(selector, SelectionKey.OP_READ); //本次註冊的是read事件,即receive接受

 

      System.out.println("CWJ Client :" + channel.socket().getInetAddress().getHostName() + ":"  + channel.socket().getPort());

     }

 

    else if (key.isReadable()) { //如果爲讀事件

 

      SocketChannel channel = (SocketChannel) key.channel();

 

      ByteBuffer buffer = ByteBuffer.allocate(1024); //1KB的緩衝區

      channel.read(buffer); //讀取到緩衝區

      buffer.flip(); //準備寫入

      System.out.println("android123 receive info:" + buffer.toString());

 

      channel.write(CharBuffer.wrap("it works".getBytes())); //返回給客戶端

     }

    }

   }

  } catch (IOException e) {

   e.printStackTrace();

  } finally {

   try {

    selector.close();

    server.close();

   } catch (IOException e) {

   }

  }

 }

}

 

 上面是比較簡單的框架,裏面存在很多問題,Android123將在下次詳細闡述下,上面或者說國內有關NIO資料中的通病,如果你看過Mina或GlassFish的源碼,你可能就知道上面的問題大於10種,有關框架的bug佔了大多數,作爲服務器而言很容易CPU超過100%



有關Android NIO的注意點和重點今天Android123着重分析下上次 Android開發進階之NIO非阻塞包() 一文中提到的不足地方改進。由於目前國內很多人資料書籍編寫人員沒有通過NIO實現服務器的經驗,導致了很多例子中存在嚴重的錯誤,由於大多數例子爲Echo這樣的單次交互以及數據量較小所以反映不出問題的所在。

   1. 讀和寫應該分開,NIO使用的是異步的方法但不等於說不會阻塞,在上面的例子中我們可以看到 判斷  key.isReadable() 時,對於這個SelectionKey關聯的SocketChannel儘量不要使用寫入數據量過多時ByteBuffer使用hasRemaining這樣的方法,NIO每次讀寫不一定全部要把數據讀完在一次Selector時。

  2. 對於上面的解決方法我們可以繼續關注感興趣的事件,比如說使用interestOps方法,而很多資料中均使用了錯誤的繼續用Selectorregister方法繼續註冊事件,這樣沒有發生錯誤的原因是每次註冊時會替換上次的這個key註冊的事件,比較僥倖,從效率上講還會判斷這個key上次註冊的是什麼事件,並不是一種正統的方法。

  3. 我們可以繼續判斷寫入事件,比如key.isWritable,在寫入時來處理髮送數據。這樣可以應對很多低速網絡時產生的異常。

  有關的細節還有很多,下一次Android開發網總結出常見的問題,並給大家一個較科學完善的框架,減少不必要的麻煩產生。


有關Android NIO的相關內容,本次Android123整理並歸類如下,爲了讓大家感覺NIOAndroid平臺聯繫的緊密,這裏我們結合ADT插件的重要開發工具DDMS中的源碼進行分析。在android git中的sdk.git文件中,可以找到ddmlib這個文件夾。有關PC和手機的互通內核在這裏使用了Java來完全實現。這裏Android開發網一起幫助大家瞭解下PC同步軟件的開發原理同時學習下Java中的New I/O技術。

   比較重要的代碼段我們貼出,逐一分析,其他的網友可以直接預讀源碼:

   AdbHelper.java文件中

   public static SocketChannel open(InetSocketAddress adbSockAddr,
            Device device, int devicePort) //
這是一個重載版本,主要是關聯Device實例。
            throws IOException, TimeoutException, AdbCommandRejectedException {

        SocketChannel adbChan = SocketChannel.open(adbSockAddr); //構造SocketChannel對象,使用常規的open方法創建
        try {
            adbChan.socket().setTcpNoDelay(true); //
設置TCP非延遲
            adbChan.configureBlocking(false); //
非阻塞

            setDevice(adbChan, device); //本句和NIO沒有多大關係,這句是指定具體的設備,比如模擬器,或Android手機的廠家代號,比如宏達電的以HTXXXXX這樣的方式

            byte[] req = createAdbForwardRequest(null, devicePort); //設置端口轉發,這句很關鍵,否則PC和手機通過USB是無法互通的。
            write(adbChan, req); //
發送數據

            AdbResponse resp = readAdbResponse(adbChan, false); //讀取收到的內容
            if (resp.okay == false) {
                throw new AdbCommandRejectedException(resp.message);
            }

            adbChan.configureBlocking(true);
        } catch (TimeoutException e) { //
一般要處理超時異常
            adbChan.close(); //
釋放channel句柄
            throw e;
        } catch (IOException e) { //
處理常規的IO異常
            adbChan.close();
            throw e;
        }

        return adbChan;
    }

   有關讀取ADB返回的報文方法

  static AdbResponse readAdbResponse(SocketChannel chan, boolean readDiagString)
            throws TimeoutException, IOException {

        AdbResponse resp = new AdbResponse();

        byte[] reply = new byte[4]; //創建4字節數組,主要檢測成功與否,adb的協議是成功返回 okay,失敗fail,等等。
        read(chan, reply); //
讀取具體的返回

        if (isOkay(reply)) { //判斷是否成功
            resp.okay = true;
        } else {
            readDiagString = true; // look for a reason after the FAIL
            resp.okay = false;
        }

        // not a loop -- use "while" so we can use "break"
        try {
            while (readDiagString) {
                // length string is in next 4 bytes
                byte[] lenBuf = new byte[4];
                read(chan, lenBuf); //
讀取一個字節數組,最終爲了轉爲一個整形

                String lenStr = replyToString(lenBuf); //字節數組轉爲String

                int len;
                try {
                    len = Integer.parseInt(lenStr, 16); //String
轉爲整形,這裏Android123提示,這種寫法可能比較愚蠢,但是下面爲Log輸出提供了一點點的便利。
                } catch (NumberFormatException nfe) {
                    Log.w("ddms", "Expected digits, got '" + lenStr + "': "
                            + lenBuf[0] + " " + lenBuf[1] + " " + lenBuf[2] + " "
                            + lenBuf[3]);
                    Log.w("ddms", "reply was " + replyToString(reply));
                    break;
                }

                byte[] msg = new byte[len];
                read(chan, msg);

                resp.message = replyToString(msg);
                Log.v("ddms", "Got reply '" + replyToString(reply) + "', diag='"
                        + resp.message + "'");

                break;
            }
        } catch (Exception e) {
            // ignore those, since it's just reading the diagnose string, the response will
            // contain okay==false anyway.
        }

        return resp;
    }

   有關PC上對Android手機屏幕截圖的方法之一:

   static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device)
            throws TimeoutException, AdbCommandRejectedException, IOException {

        RawImage imageParams = new RawImage();
        byte[] request = formAdbRequest("framebuffer:"); // 
讀取手機端adbd服務器的framebuffer調用返回的數組

        byte[] nudge = {
            0
        };
        byte[] reply;

        SocketChannel adbChan = null;
        try {
            adbChan = SocketChannel.open(adbSockAddr);
            adbChan.configureBlocking(false); //
非阻塞

            setDevice(adbChan, device); //設置我們關係的設備

            write(adbChan, request); //發送framebuffer這個請求了

            AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
            if (resp.okay == false) {   //
判斷返回是否ok
                throw new AdbCommandRejectedException(resp.message);
            }

            reply = new byte[4];
            read(adbChan, reply); //
首先返回的是一個協議,目前分爲兩個版本,主要是兼容模式和標準的模式,兼容模式比較少見,在2.0以後幾乎看不到了。部分早期的1.6或更老的T-Mobile G1會使用兼容模式,模式不同,輸出的截圖中的顏色編碼方式略有不同。

            ByteBuffer buf = ByteBuffer.wrap(reply);
            buf.order(ByteOrder.LITTLE_ENDIAN); //
小頭字節順序

            int version = buf.getInt(); //ByteBuffer直接轉int的方法,比較方便不用自己從字節數組中構造,按位計算

            int headerSize = RawImage.getHeaderSize(version); //根據返回的adb截圖協議版本判斷將收到的字節大小

            reply = new byte[headerSize * 4]; //分配空間,具體大小需要看協議版本
            read(adbChan, reply);

            buf = ByteBuffer.wrap(reply); //reply數組實例化ByteBuffer
            buf.order(ByteOrder.LITTLE_ENDIAN); //
注意字節序列,畢竟遠端的adbd是工作在linux系統的手機上。

            if (imageParams.readHeader(version, buf) == false) { //判斷是否有效,兼容這種截圖協議。
                Log.e("Screenshot", "Unsupported protocol: " + version);
                return null;
            }

            Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size="
                    + imageParams.size + ", width=" + imageParams.width
                    + ", height=" + imageParams.height); //
打印下截圖的基本信息,比如bpp代表色深,size是需要分配dib圖像的字節數組。比較原始,

            write(adbChan, nudge); //發送一個字節,代表準備接收字節數組了

            reply = new byte[imageParams.size]; //分配和圖像大小一樣的字節數組
            read(adbChan, reply); //
接收圖像字節數組,這裏Android開發網提示大家對於Android 1.x可能爲RGB565,分配大小爲 wxhx2xsize ,而2.x以後基本上爲32位的RGB8888,分配大小爲wxhx4xsize

            imageParams.data = reply;
        } finally {
            if (adbChan != null) {
                adbChan.close();
            }
        }

        return imageParams;
    }

  有關Android平臺PC通過USBADB方式和手機同步原理和NIO相關技術,Android123明天繼續講解。


今天我們繼續就Android DDMS源碼一起分析NIO非阻塞通訊方式,Android123也會給大家分享下手機和PC互通中的一些技術。在NIO中有關SocketChannelByteBuffer的使用細節,可以在今天文章中

   static void read(SocketChannel chan, byte[] data, int length, int timeout)
            throws TimeoutException, IOException {
        ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length); //
從字節數組中實例化ByteBuffer
        int numWaits = 0;

        while (buf.position() != buf.limit()) {  //循環接收數據
            int count;

            count = chan.read(buf);
            if (count < 0) {
                    throw new IOException("EOF"); //
讀到末尾
            } else if (count == 0) {
                if (timeout != 0 && numWaits * WAIT_TIME > timeout) {
                    throw new TimeoutException();
                }
                 try {
                    Thread.sleep(WAIT_TIME);
                } catch (InterruptedException ie) {
                }
                numWaits++;
            } else {
                numWaits = 0;
            }
        }
    }

    有關SocketChannel的寫操作,就是發送數據代碼如下:

  static void write(SocketChannel chan, byte[] data, int length, int timeout)
            throws TimeoutException, IOException {
        ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length);
        int numWaits = 0;

        while (buf.position() != buf.limit()) {
            int count;

            count = chan.write(buf); //發送數據從ByteBuffer
            if (count < 0) {
                       throw new IOException("channel EOF");
            } else if (count == 0) {
                             if (timeout != 0 && numWaits * WAIT_TIME > timeout) {
                            throw new TimeoutException();
                }
                 try {
                    Thread.sleep(WAIT_TIME);
                } catch (InterruptedException ie) { 
                }
                numWaits++;
            } else {
                numWaits = 0;
            }
        }
    }

   有關ADB如何選擇一個具體的設備,可以使用 setDevice 方法,這樣當電腦中有模擬器或鏈接了多個手機,可以通過設備序列號,選擇需要通訊的設備。

  static void setDevice(SocketChannel adbChan, IDevice device)
            throws TimeoutException, AdbCommandRejectedException, IOException {
        // if the device is not -1, then we first tell adb we're looking to talk
        // to a specific device
        if (device != null) {
            String msg = "host:transport:" + device.getSerialNumber(); // 
最後的獲取序列號,android123提示大家在adb命令中是adb get-serialno

            byte[] device_query = formAdbRequest(msg);

            write(adbChan, device_query);

            AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
            if (resp.okay == false) {
                throw new AdbCommandRejectedException(resp.message,
                        true/*errorDuringDeviceSelection*/);
            }
        }
    }

    通過PC控制手機重啓的代碼,當然這裏需要Root權限才能執行

   public static void reboot(String into, InetSocketAddress adbSockAddr,
            Device device) throws TimeoutException, AdbCommandRejectedException, IOException {
        byte[] request;
        if (into == null) {
            request = formAdbRequest("reboot:"); //$NON-NLS-1$
        } else {
            request = formAdbRequest("reboot:" + into); //$NON-NLS-1$
        }

        SocketChannel adbChan = null;
        try {
            adbChan = SocketChannel.open(adbSockAddr);
            adbChan.configureBlocking(false);

            // if the device is not -1, then we first tell adb we're looking to talk
            // to a specific device
            setDevice(adbChan, device);

            write(adbChan, request);
        } finally {
            if (adbChan != null) {
                adbChan.close();
            }
        }
    }

  我們可以看到基本上,每個命令的執行,都是用了單獨SocketChannel通過非阻塞方式執行,這樣大大加強了併發,所以DDMS可以一邊處理Logcat打印,顯示堆信息,處理文件管理等等,有關NIO服務器的內容,Android開發網將着重分析MonitorThread.java這個文件,一起說下NIO的框架。


在整個DDMS中體現Android NIO主要框架的要數MonitorThread.java這個文件了,有關PCAndroid手機同步以及NIO非阻塞編程的精髓可以在下面的文件中充分體現出來。

  final class MonitorThread extends Thread {

    private static final int CLIENT_READY = 2;

    private static final int CLIENT_DISCONNECTED = 3;

    private volatile boolean mQuit = false;

    private ArrayList<Client> mClientList; //用一個數組保存客戶端信息

    private Selector mSelector;

    private HashMap<Integer, ChunkHandler> mHandlerMap; //這裏Android123提示大家,由於在多線程中concurrentHashMap效率比HashMap更安全高效,推薦使用併發庫的這個替代版本。

    private ServerSocketChannel mDebugSelectedChan; //一個用於調試的服務器通道

    private int mNewDebugSelectedPort;

    private int mDebugSelectedPort = -1;

    private Client mSelectedClient = null;

    private static MonitorThread mInstance;

    private MonitorThread() {
        super("Monitor");
        mClientList = new ArrayList<Client>();
        mHandlerMap = new HashMap<Integer, ChunkHandler>();

        mNewDebugSelectedPort = DdmPreferences.getSelectedDebugPort();
    }

    static MonitorThread createInstance() {  //創建實例
        return mInstance = new MonitorThread();
    }

    static MonitorThread getInstance() { //獲取實例
        return mInstance;
    }

    synchronized void setDebugSelectedPort(int port) throws IllegalStateException { //設置調試端口號
        if (mInstance == null) {
            return;
        }

        if (AndroidDebugBridge.getClientSupport() == false) {
            return;
        }

        if (mDebugSelectedChan != null) {
            Log.d("ddms", "Changing debug-selected port to " + port);
            mNewDebugSelectedPort = port;
            wakeup(); //
這裏用來喚醒所有的Selector
        } else {
            // we set mNewDebugSelectedPort instead of mDebugSelectedPort so that it's automatically
            mNewDebugSelectedPort = port;
        }
    }

    synchronized void setSelectedClient(Client selectedClient) {
        if (mInstance == null) {
            return;
        }

        if (mSelectedClient != selectedClient) {
            Client oldClient = mSelectedClient;
            mSelectedClient = selectedClient;

            if (oldClient != null) {
                oldClient.update(Client.CHANGE_PORT);
            }

            if (mSelectedClient != null) {
                mSelectedClient.update(Client.CHANGE_PORT);
            }
        }
    }

    Client getSelectedClient() {
        return mSelectedClient;
    }

    boolean getRetryOnBadHandshake() {
        return true; // TODO? make configurable
    }

    Client[] getClients() {
        synchronized (mClientList) {
            return mClientList.toArray(new Client[0]);
        }
    }

    synchronized void registerChunkHandler(int type, ChunkHandler handler) {
        if (mInstance == null) {
            return;
        }

        synchronized (mHandlerMap) {
            if (mHandlerMap.get(type) == null) {
                mHandlerMap.put(type, handler);
            }
        }
    }

    @Override
    public void run() { //
本類的主要線程
        Log.d("ddms", "Monitor is up");

        try {
            mSelector = Selector.open();
        } catch (IOException ioe) {
            Log.logAndDisplay(LogLevel.ERROR, "ddms",
                    "Failed to initialize Monitor Thread: " + ioe.getMessage());
            return;
        }

        while (!mQuit) {

            try {
                synchronized (mClientList) {
                }

                try {
                    if (AndroidDebugBridge.getClientSupport()) {
                        if ((mDebugSelectedChan == null ||
                                mNewDebugSelectedPort != mDebugSelectedPort) &&
                                mNewDebugSelectedPort != -1) {
                            if (reopenDebugSelectedPort()) {
                                mDebugSelectedPort = mNewDebugSelectedPort;
                            }
                        }
                    }
                } catch (IOException ioe) {
                    Log.e("ddms",
                            "Failed to reopen debug port for Selected Client to: " + mNewDebugSelectedPort);
                    Log.e("ddms", ioe);
                    mNewDebugSelectedPort = mDebugSelectedPort; // no retry
                }

                int count;
                try {
                    count = mSelector.select();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                    continue;
                } catch (CancelledKeyException cke) {
                    continue;
                }

                if (count == 0) {
                    continue;
                } //
這裏代碼寫的不是很好,Android開發網提示大家因爲這個NIODDMS工作在PC端的還不明顯,這樣輪訓的在一個while中,效率不是很高,CPU很容易佔用率很高。

                Set<SelectionKey> keys = mSelector.selectedKeys();
                Iterator<SelectionKey> iter = keys.iterator(); //
使用迭代器獲取這個選擇鍵

                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                    iter.remove();

                    try {
                        if (key.attachment() instanceof Client) { //
判斷收到的key的附件是否是Client的實例
                            processClientActivity(key);
                        }
                        else if (key.attachment() instanceof Debugger) { //
如果是Debug實例
                            processDebuggerActivity(key);
                        }
                        else if (key.attachment() instanceof MonitorThread) {
                            processDebugSelectedActivity(key);
                        }
                        else {
                            Log.e("ddms", "unknown activity key");
                        }
                    } catch (Exception e) {
                        Log.e("ddms", "Exception during activity from Selector.");
                        Log.e("ddms", e);
                    }
                }
            } catch (Exception e) {
                Log.e("ddms", "Exception MonitorThread.run()");
                Log.e("ddms", e);
            }
        }
    }

    int getDebugSelectedPort() {
        return mDebugSelectedPort;
    }

    private void processClientActivity(SelectionKey key) {
        Client client = (Client)key.attachment();

        try {
            if (key.isReadable() == false || key.isValid() == false) {
                Log.d("ddms", "Invalid key from " + client + ". Dropping client.");
                dropClient(client, true /* notify */);
                return;
            }

            client.read();

            JdwpPacket packet = client.getJdwpPacket();
            while (packet != null) {
                if (packet.isDdmPacket()) {
                    // unsolicited DDM request - hand it off
                    assert !packet.isReply();
                    callHandler(client, packet, null);
                    packet.consume();
                } else if (packet.isReply()
                        && client.isResponseToUs(packet.getId()) != null) {
                    // reply to earlier DDM request
                    ChunkHandler handler = client
                            .isResponseToUs(packet.getId());
                    if (packet.isError())
                        client.packetFailed(packet);
                    else if (packet.isEmpty())
                        Log.d("ddms", "Got empty reply for 0x"
                                + Integer.toHexString(packet.getId())
                                + " from " + client);
                    else
                        callHandler(client, packet, handler);
                    packet.consume();
                    client.removeRequestId(packet.getId());
                } else {
                    Log.v("ddms", "Forwarding client "
                            + (packet.isReply() ? "reply" : "event") + " 0x"
                            + Integer.toHexString(packet.getId()) + " to "
                            + client.getDebugger());
                    client.forwardPacketToDebugger(packet);
                }

                packet = client.getJdwpPacket();
            }
        } catch (CancelledKeyException e) { //
注意正確處理這個異常
            dropClient(client, true /* notify */);
        } catch (IOException ex) {
            dropClient(client, true /* notify */);
        } catch (Exception ex) {
            Log.e("ddms", ex);

            dropClient(client, true /* notify */);

            if (ex instanceof BufferOverflowException) { //可能存在緩衝區異常
                Log.w("ddms",
                        "Client data packet exceeded maximum buffer size "
                                + client);
            } else {
                // don't know what this is, display it
                Log.e("ddms", ex);
            }
        }
    }

    private void callHandler(Client client, JdwpPacket packet,
            ChunkHandler handler) {

        // on first DDM packet received, broadcast a "ready" message
        if (!client.ddmSeen())
            broadcast(CLIENT_READY, client);

        ByteBuffer buf = packet.getPayload();
        int type, length;
        boolean reply = true;

        type = buf.getInt();
        length = buf.getInt();

        if (handler == null) {
            // not a reply, figure out who wants it
            synchronized (mHandlerMap) {
                handler = mHandlerMap.get(type);
                reply = false;
            }
        }

        if (handler == null) {
            Log.w("ddms", "Received unsupported chunk type "
                    + ChunkHandler.name(type) + " (len=" + length + ")");
        } else {
            Log.d("ddms", "Calling handler for " + ChunkHandler.name(type)
                    + " [" + handler + "] (len=" + length + ")");
            ByteBuffer ibuf = buf.slice();
            ByteBuffer roBuf = ibuf.asReadOnlyBuffer(); // enforce R/O
            roBuf.order(ChunkHandler.CHUNK_ORDER);
            synchronized (mClientList) {
                handler.handleChunk(client, type, roBuf, reply, packet.getId());
            }
        }
    }

    synchronized void dropClient(Client client, boolean notify) {
        if (mInstance == null) {
            return;
        }

        synchronized (mClientList) {
            if (mClientList.remove(client) == false) {
                return;
            }
        }
        client.close(notify);
        broadcast(CLIENT_DISCONNECTED, client);

        /*
         * http://forum.java.sun.com/thread.jspa?threadID=726715&start=0
         * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5073504
         */
        wakeup();
    }

    /*
     * Process activity from one of the debugger sockets. This could be a new
     * connection or a data packet.
     */
    private void processDebuggerActivity(SelectionKey key) {
        Debugger dbg = (Debugger)key.attachment();

        try {
            if (key.isAcceptable()) { //
處理Server響應這個事件
                try {
                    acceptNewDebugger(dbg, null); 
                } catch (IOException ioe) {
                    Log.w("ddms", "debugger accept() failed");
                    ioe.printStackTrace();
                }
            } else if (key.isReadable()) { //
如果是收到的數據,則可讀取
                processDebuggerData(key);
            } else {
                Log.d("ddm-debugger", "key in unknown state");
            }
        } catch (CancelledKeyException cke) { //
記住,NIO處理這個異常,很多入門的開發者很容易忘記
            // key has been cancelled we can ignore that.
        }
    }

     private void acceptNewDebugger(Debugger dbg, ServerSocketChannel acceptChan) //這裏用到了阻塞方式
            throws IOException {

        synchronized (mClientList) {
            SocketChannel chan;

            if (acceptChan == null)
                chan = dbg.accept(); 
            else
                chan = dbg.accept(acceptChan);

            if (chan != null) {
                chan.socket().setTcpNoDelay(true);

                wakeup();

                try {
                    chan.register(mSelector, SelectionKey.OP_READ, dbg);
                } catch (IOException ioe) {
                    // failed, drop the connection
                    dbg.closeData();
                    throw ioe;
                } catch (RuntimeException re) {
                    // failed, drop the connection
                    dbg.closeData();
                    throw re;
                }
            } else {
                Log.w("ddms", "ignoring duplicate debugger");
            }
        }
    }

    private void processDebuggerData(SelectionKey key) {
        Debugger dbg = (Debugger)key.attachment();

        try {
            dbg.read();

            JdwpPacket packet = dbg.getJdwpPacket();
            while (packet != null) {
                Log.v("ddms", "Forwarding dbg req 0x"
                        + Integer.toHexString(packet.getId()) + " to "
                        + dbg.getClient());

                dbg.forwardPacketToClient(packet);

                packet = dbg.getJdwpPacket();
            }
        } catch (IOException ioe) {
            Log.d("ddms", "Closing connection to debugger " + dbg);
            dbg.closeData();
            Client client = dbg.getClient();
            if (client.isDdmAware()) {
                   Log.d("ddms", " (recycling client connection as well)");

                    client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client,
                        IDebugPortProvider.NO_STATIC_PORT);
            } else {
                Log.d("ddms", " (recycling client connection as well)");
                // we should drop the client, but also attempt to reopen it.
                // This is done by the DeviceMonitor.
                client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client,
                        IDebugPortProvider.NO_STATIC_PORT);
            }
        }
    }

    private void wakeup() {
        mSelector.wakeup();
    }

    synchronized void quit() {
        mQuit = true;
        wakeup();
        Log.d("ddms", "Waiting for Monitor thread");
        try {
            this.join();
            // since we're quitting, lets drop all the client and disconnect
            // the DebugSelectedPort
            synchronized (mClientList) {
                for (Client c : mClientList) {
                    c.close(false /* notify */);
                    broadcast(CLIENT_DISCONNECTED, c);
                }
                mClientList.clear();
            }

            if (mDebugSelectedChan != null) {
                mDebugSelectedChan.close();
                mDebugSelectedChan.socket().close();
                mDebugSelectedChan = null;
            }
            mSelector.close();
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        mInstance = null;
    }

    synchronized void addClient(Client client) {
        if (mInstance == null) {
            return;
        }

        Log.d("ddms", "Adding new client " + client);

        synchronized (mClientList) {
            mClientList.add(client);

            try {
                wakeup();

                client.register(mSelector);

                Debugger dbg = client.getDebugger();
                if (dbg != null) {
                    dbg.registerListener(mSelector);
                }
            } catch (IOException ioe) {
                // not really expecting this to happen
                ioe.printStackTrace();
            }
        }
    }

    /*
     * Broadcast an event to all message handlers.
     */
    private void broadcast(int event, Client client) {
        Log.d("ddms", "broadcast " + event + ": " + client);

        /*
         * The handler objects appear once in mHandlerMap for each message they
         * handle. We want to notify them once each, so we convert the HashMap
         * to a HashSet before we iterate.
         */
        HashSet<ChunkHandler> set;
        synchronized (mHandlerMap) {
            Collection<ChunkHandler> values = mHandlerMap.values();
            set = new HashSet<ChunkHandler>(values);
        }

        Iterator<ChunkHandler> iter = set.iterator();
        while (iter.hasNext()) {
            ChunkHandler handler = iter.next();
            switch (event) {
                case CLIENT_READY:
                    try {
                        handler.clientReady(client);
                    } catch (IOException ioe) {
                        // Something failed with the client. It should
                        // fall out of the list the next time we try to
                        // do something with it, so we discard the
                        // exception here and assume cleanup will happen
                        // later. May need to propagate farther. The
                        // trouble is that not all values for "event" may
                        // actually throw an exception.
                        Log.w("ddms",
                                "Got exception while broadcasting 'ready'");
                        return;
                    }
                    break;
                case CLIENT_DISCONNECTED:
                    handler.clientDisconnected(client);
                    break;
                default:
                    throw new UnsupportedOperationException();
            }
        }

    }

    /**
     * Opens (or reopens) the "debug selected" port and listen for connections.
     * @return true if the port was opened successfully.
     * @throws IOException
     */
    private boolean reopenDebugSelectedPort() throws IOException {

        Log.d("ddms", "reopen debug-selected port: " + mNewDebugSelectedPort);
        if (mDebugSelectedChan != null) {
            mDebugSelectedChan.close();
        }

        mDebugSelectedChan = ServerSocketChannel.open();
        mDebugSelectedChan.configureBlocking(false); // required for Selector

        InetSocketAddress addr = new InetSocketAddress(
                InetAddress.getByName("localhost"), //$NON-NLS-1$
                mNewDebugSelectedPort);
        mDebugSelectedChan.socket().setReuseAddress(true); // enable SO_REUSEADDR

        try {
            mDebugSelectedChan.socket().bind(addr);
            if (mSelectedClient != null) {
                mSelectedClient.update(Client.CHANGE_PORT);
            }

            mDebugSelectedChan.register(mSelector, SelectionKey.OP_ACCEPT, this);

            return true;
        } catch (java.net.BindException e) {
            displayDebugSelectedBindError(mNewDebugSelectedPort);

            // do not attempt to reopen it.
            mDebugSelectedChan = null;
            mNewDebugSelectedPort = -1;

            return false;
        }
    }

    /*
     * We have some activity on the "debug selected" port. Handle it.
     */
    private void processDebugSelectedActivity(SelectionKey key) {
        assert key.isAcceptable();

        ServerSocketChannel acceptChan = (ServerSocketChannel)key.channel();

        /*
         * Find the debugger associated with the currently-selected client.
         */
        if (mSelectedClient != null) {
            Debugger dbg = mSelectedClient.getDebugger();

            if (dbg != null) {
                Log.d("ddms", "Accepting connection on 'debug selected' port");
                try {
                    acceptNewDebugger(dbg, acceptChan);
                } catch (IOException ioe) {
                    // client should be gone, keep going
                }

                return;
            }
        }

        Log.w("ddms",
                "Connection on 'debug selected' port, but none selected");
        try {
            SocketChannel chan = acceptChan.accept();
            chan.close();
        } catch (IOException ioe) {
            // not expected; client should be gone, keep going
        } catch (NotYetBoundException e) {
            displayDebugSelectedBindError(mDebugSelectedPort);
        }
    }

    private void displayDebugSelectedBindError(int port) {
        String message = String.format(
                "Could not open Selected VM debug port (%1$d). Make sure you do not have another instance of DDMS or of the eclipse plugin running. If it's being used by something else, choose a new port number in the preferences.",
                port);

        Log.logAndDisplay(LogLevel.ERROR, "ddms", message);
    }
}

  從上面來看Android的開源代碼有關PC上的寫的不是很好,很多實現的地方都是用了嚴重的縫縫補補方式解決,有些習慣不是很到位,有關本NIO例子由於涉及的項目對象多,理解需要網友深入分析DDMS源碼中的每個對象。細節寫的不是很理想,Android123推薦大家,畫出UML後再分析更清晰。



發佈了37 篇原創文章 · 獲贊 2 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章