1. 程式人生 > >探討android更新UI的幾種方法

探討android更新UI的幾種方法

      作為IT新手,總以為只要有時間,有精力,什麼東西都能做出來。這種念頭我也有過,但很快就熄滅了,因為現實是殘酷的,就算一開始的時間和精力非常充足,也會隨著專案的推進而逐步消磨殆盡。我們會發現,自己越來越消極怠工,只是在無意義的敲程式碼,敲的還是網上抄來的程式碼,如果不行,繼續找。

     這就是專案進度沒有規劃好而導致的。

     最近在做有關藍芽的專案,一開始的進度都安排得很順利,但是因為測試需要兩部手機,而且還要是android手機,暑假已經開始了,同學們都回家了,加上我手機的藍芽壞了,導致我的進度嚴重被打亂!而且更加可怕的是,就算我手機這邊除錯完畢,我最終的目標是實現手機與藍芽模組的通訊,那個測試板至今未送過來,所以,我開始消極怠工了。

     經驗教訓非常簡單:根據整個專案的時間長度規劃好每天的進度,視實際情況的變化而改變規劃,就算真的是無法開展工作,像是現在這樣抽空出來寫寫部落格都要好過無意義的敲程式碼。

     今天講的內容非常簡單,只是講講有關於android介面更新的方面。

1.利用Looper更新UI介面

     如果我們的程式碼需要隨時將處理後的資料交給UI更新,那麼我們想到的方法就是另開一個執行緒更新資料(也必須這麼做,如果我們的資料更新運算量較大,就會阻塞UI執行緒),也就是介面更新和資料更新是在不同執行緒中(android採用的是UI單執行緒模型,所以我們也只能在主執行緒中對UI進行操作),但這會導致另一個問題:如何在兩個執行緒間通訊呢?android提供了Handler機制來保證這種通訊。

     先是一個簡單的例子:

複製程式碼

public class MainActivity extends Activity {
    private Button mButton;
    private TextView mText;
    
    @SuppressLint("HandlerLeak")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        mButton = (Button)this.findViewById(R.id.button);
        mText = (TextView)this.findViewById(R.id.text);
        
        final Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg){
                super.handleMessage(msg);
                if(msg.what == 1){
                    mText.setText("更新後");
                }
            }
        };
        
        mText.setText("更新前");
        final Thread thread = new Thread(new Runnable(){

            @Override
            public void run() {
                 Message message = new Message();
                 message.what = 1;
                 handler.sendMessage(message);
            }
            
        });
        mButton.setOnClickListener(new OnClickListener() {
            
            @Override
            public void onClick(View v) {
                 thread.start();
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

}

複製程式碼

      在Main主執行緒中新開一個執行緒,該執行緒負責資料的更新,然後將更新後的資料放在Message裡面,然後通過Handler傳遞給相應的UI進行更新。

      

   

      使用TextView或者其他元件的時候,如果出現這樣的錯誤:

      android.content.res.Resources$NotFoundException:String resource ID #0x86

      這樣的錯誤誤導性真大!我以為是我的資源ID用錯了,但就是這個ID,一下子就沒法子了,查了很久,結果發現是TextView.setText()要求的是字串,但我傳入了一個int!就這個問題,原本是傳參錯誤,但android竟然沒有報錯,而且這個錯誤提示也太那個了吧!!

      Message的任務很簡單,就是用來傳遞資料更新資訊,但有幾點也是值得注意的:我們可以使用構造方法來建立Message,但出於節省記憶體資源的考量,我們應該使用Message.obtain()從訊息池中獲得空訊息物件,而且如果Message只是攜帶簡單的int資訊,優先使用Message.arg1和Message.arg2來傳遞資訊,這樣比起使用Bundle更省記憶體,而Message.what用於標識資訊的型別。

      我們現在來了解Handler的工作機制。

      Handler的作用就是兩個:在新啟動的執行緒中傳送訊息和在主執行緒中獲取和處理訊息。像是上面例子中的Handler就包含了這兩個方面:我們在新啟動的執行緒thread中呼叫Handler的sendMessage()方法來發送訊息。傳送給誰呢?從程式碼中可以看到,就傳送給主執行緒建立的Handler中的handleMessage()方法處理。這就是回撥的方式:我們只要在建立Handler的時候覆寫handleMessage()方法,然後在新啟動的執行緒傳送訊息時自動呼叫該方法。

      要想真正明白Handler的工作機制,我們就要知道Looper,Message和MessageQueue。

      Looper正如字面上的意思,就是一個"迴圈者",它的主要作用就是使我們的一個普通執行緒變成一個迴圈執行緒。如果我們想要得到一個迴圈執行緒,我們必須要這樣:

複製程式碼

class LooperThread extends Thread{
     public Handler mHandler;
     
     public void run(){
         Looper.prepare();
         mHandler = new Handler(){
              public void handleMessage(Message msg){
                   //process incoming message here
             }
        };
        Looper.loop();
     }
}

複製程式碼

      Looper.prepare()就是用來使當前的執行緒變成一個LooperThread,然後我們在這個執行緒中用Handler來處理訊息佇列中的訊息,接著利用Looper.loop()來遍歷訊息佇列中的所有訊息。

      話是這麼說,但是最後處理的是訊息佇列中的最後一個訊息:

複製程式碼

mHandler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                mTextView.setText(msg.what + "");
            }
        };
        
        mButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                 LooperThread thread = new LooperThread();
                 thread.setHandler(mHandler);
                 thread.start();
            }
        });
    }

    class LooperThread extends Thread {
        Handler handler;
        
        public void setHandler(Handler handler){
            this.handler = handler;
        }

        @Override
        public void run() {
            Looper.prepare();
            for (int i = 0; i < 10; i++) {
                Message message = Message.obtain();
                message.arg1 = i;
                handler.sendMessage(message);
            }
            Looper.loop();
        }
    }

複製程式碼

      結果顯示的是9!!難道說MessageQueue是"先進後出"的佇列?

      這只是因為處理得太快,如果我們這樣子:

try{
  Thread.sleep(1000);
  handler.sendMessage(message);
}catch(InterruptedException e){}

       我們就可以看到TextView從0一直數到9。

       由此可知道,sendMessage()方法的實現是回調了handleMessage(),所以說是處理訊息佇列中的所有訊息也是正確的,因為訊息一發送到訊息佇列中就立即被處理。

       Looper執行緒應該怎麼使用,得到一個Looper引用我們能幹嘛?

      讓我們繼續思考這個問題。

      每個執行緒最多隻有一個Looper物件,它的本質是一個ThreadLocal,而ThreadLocal是在JDK1.2中引入的,它為解決多執行緒程式的併發問題提供了一種新思路。

      ThreadLocal並不是一個Thread,它是Thread的區域性變數,正確的命名應該是ThreadLocalVariable才對。如果是經常看android原始碼的同學,有時候也會發現它的一些變數的命名也很隨便。

      ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立的改變自己的副本而不會影響到其他執行緒的副本。這種解決方案就是為每一個執行緒提供獨立的副本,而不是同步該變數。

      但是該變數並不是線上程中宣告的,它是該執行緒使用的變數,因為對於執行緒來說,它所使用的變數就是它的本地變數,所以Local就是取該意。

      學過java的同學都知道,編寫執行緒區域性變數比起同步該變數來說,實在是太笨拙了,所以我們更多使用同步的方式,而且java對該方式也提供了非常便利的支援。

      現在最大的問題就是:ThreadLocal是如何維護該變數的副本呢?

      實現的方式非常簡單:在ThreadLocal中有一個Map,用於儲存每一個執行緒的變數副本,Map中元素的鍵為執行緒物件,而值對應的是該執行緒的變數副本。

      同樣是為了解決多執行緒中相同變數的訪問衝突問題,ThreadLocal和同步機制相比,有什麼優勢呢?

      使用同步機制,我們必須通過物件的鎖機制保證同一時間只有一個執行緒訪問變數。所以,我們必須分析什麼時候對該變數進行讀寫,什麼時候需要鎖定某個物件,又是什麼時候該釋放物件鎖等問題,更糟糕的是,我們根本就無法保證這樣做事萬無一失的。

      ThreadLocal是通過為每一個執行緒提供一個獨立的變數副本,從而隔離了多個執行緒對資料的訪問衝突,所以我們也就沒有必要使用物件鎖這種難用的東西,這種方式更加安全。

      ThreadLocal最大的問題就是它需要為每個執行緒維護一個副本,也就是"以空間換時間"的方式。我們知道,記憶體空間是非常寶貴的資源,這也是我們大部分時候都不會考慮該方式的原因。

     為什麼Looper是一個ThreadLocal呢?Looper本身最大的意義就是它內部有一個訊息佇列,而其他執行緒是可以向該訊息佇列中新增訊息的,所以Looper本身就是一個ThreadLocal,每個執行緒都維護一個副本,新增到訊息佇列中的訊息都會被處理掉。

複製程式碼

mHandler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                if(msg.what == 1){
                mTextView.setText(msg.what + "");
                }else{
                    Toast.makeText(MainActivity.this, msg.what + "", Toast.LENGTH_LONG).show();
                }
            }
        };
        
        mButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                 Thread1 thread1 = new Thread1();
                 thread1.setHandler(mHandler);
                 thread1.start();
                 Thread2 thread2 = new Thread2();
                 thread2.setHandler(mHandler);
                 thread2.start();
            }
        });
    }
    
    class  Thread2 extends Thread {
        Handler handler;
        
        public void setHandler(Handler handler){
            this.handler = handler;
        }

        @Override
        public void run() {
                Message message = Message.obtain();
                message.what = 2;
                handler.sendMessage(message);
                   
        }
    }

    class  Thread1 extends Thread {
        Handler handler;
        
        public void setHandler(Handler handler){
            this.handler = handler;
        }

        @Override
        public void run() {
                Message message = Message.obtain(); 
                message.what = 1;
                handler.sendMessage(message);
                   
        }
    }

複製程式碼

      上面這段程式碼是新建兩個執行緒,每個執行緒都維護一個Handler,然後都向這個Handler傳送訊息,結果就是這兩個訊息同時被處理。
      Hanlder本身就持有一個MessageQueue和Looper的引用,預設情況下是建立該Handler的執行緒的Looper和該Looper的MessageQueue。

      Hanler只能處理由自己發出的訊息,它會通知MessageQueue,表明它要執行一個任務,然後在輪到自己的時候執行該任務,這個過程是非同步的,因為它不是採用同步Looper的方式而是採用維護副本的方式解決多執行緒共享的問題。

      一個執行緒可以有多個Handler,但是隻能有一個Looper,理由同上:維護同一個Looper的副本。

      到了這裡,我們可以發現:新開一個執行緒用於處理資料的更新,在主執行緒中更新UI,這種方式是非常自然的,而且這也是所謂的觀察者模式的使用(使用回撥的方式來更新UI,幾乎可以認為是使用了觀察者模式)。

      我們繼續就著Looper探討下去。

      因為Handler需要當前執行緒的MessageQueue,所以我們必須通過Looper.prepare()來為Handler啟動MessageQueue,而主執行緒預設是有MessageQueue,所以我們不需要在主執行緒中呼叫prepare()方法。在Looper.loop()後面的程式碼是不會被執行的,除非我們顯式的呼叫Handler.getLooper().quit()方法來離開MessageQueue。

      到了這裡,我們之前的問題:LooperThread應該如何使用?已經有了很好的答案了: LooperThread用於UI的更新,而其他執行緒向其Handler傳送訊息以更新資料。因為主執行緒原本就是一個LooperThread,所以我們平時的習慣都是在主執行緒裡建立Handler,然後再在其他執行緒裡更新資料,這種做法也是非常保險的,因為UI元件只能在主執行緒裡面更新。

      當然,Handler並不僅僅是用於處理UI的更新,它本身的真正意義就是實現執行緒間的通訊:

複製程式碼

new LooperThread().start();
  mButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                final int MESSAGE_HELLO = 0;
                String message = "hello";
                mHandler.obtainMessage(MESSAGE_HELLO, message).sendToTarget();
            }
        });
    }

    class LooperThread extends Thread {

        @Override
        public void run() {
            Looper.prepare();
            mHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                    case MESSAGE_HELLO:
                        Toast.makeText(MainActivity.this, (String) msg.obj,
                                Toast.LENGTH_SHORT).show();
                        break;
                    default:
                        break;
                    }

                }
            };
            Looper.loop();
        }
    }

複製程式碼

      上面是Handler非常經典的用法:我們通過Handler的obtainMessage()方法來建立一個新的Message(int what, Object obj),然後通過sendToTarget()傳送到建立該Handler的執行緒中。如果大家做過類似藍芽程式設計這樣需要通過socket通訊的專案,就會清楚的知道,判斷socket的狀態是多麼重要,而Message的what就是用來儲存這些狀態值(通常這些狀態值是final int),值得注意的是,obj是Object,所以我們需要強制轉型。但這樣的編碼會讓我們的程式碼擁有一大堆常量值,而且switch的使用是不可避免的,如果狀態值很多,那這個switch就真的是太臃腫了,就連android的藍芽官方例項也無法避免這點。

      總結一下:Android使用訊息機制實現執行緒間的通訊,執行緒通過Looper建立自己的訊息迴圈,MessageQueue是FIFO的訊息佇列,Looper負責從MessageQueue中取出訊息,並且分發到引用該Looper的Handler物件,該Handler物件持有執行緒的區域性變數Looper,並且封裝了傳送訊息和處理訊息的介面。

      如果Handler僅僅是用來處理UI的更新,還可以有另一種使用方式:

複製程式碼

mHandler = new Handler();
        mRunnable = new Runnable() {

            @Override
            public void run() {
                mTextView.setText("haha");
            }
        };
        mButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                new Thread() {
                    public void run() {
                        mHandler.post(mRunnable);
                    }
                }.start();
            }
        });
    }

複製程式碼

      使用Handler的post()方法就顯得UI的更新處理非常簡單:在一個Runnable物件中更新UI,然後在另一個執行緒中通過Handler的post()執行該更新動作。值得注意的是,我們就算不用新開一個新執行緒照樣可以更新UI,因為UI的更新執行緒就是Handler的建立執行緒---主執行緒。

      表面上Handler似乎可以傳送兩種訊息:Runnable物件和Message物件,實際上Runnable物件會被封裝成Message物件。

2.AsyncTask利用執行緒任務非同步更新UI介面

      AsyncTask的原理和Handler很接近,都是通過往主執行緒傳送訊息來更新主執行緒的UI,這種方式是非同步的,所以就叫AsyncTask。使用AsyncTask的場合像是下載檔案這種會嚴重阻塞主執行緒的任務就必須放在非同步執行緒裡面:

複製程式碼

public class MainActivity extends Activity {
    private Button mButton;
    private ImageView mImageView;
    private ProgressBar mProgressBar;

    @SuppressLint("HandlerLeak")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button) this.findViewById(R.id.button);
        mImageView = (ImageView) this.findViewById(R.id.image);
        mProgressBar = (ProgressBar) this.findViewById(R.id.progressBar);
        mButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                AsyncTaskThread thread = new AsyncTaskThread();
                thread.execute("http://g.search2.alicdn.com/img/bao/uploaded/i4/"
                        + "i4/12701024275153897/T1dahpFapbXXXXXXXX_!!0-item_pic.jpg_210x210.jpg");
            }
        });
    }

    class AsyncTaskThread extends AsyncTask<String, Integer, Bitmap> {

        @Override
        protected Bitmap doInBackground(String... params) {
            publishProgress(0);
            HttpClient client = new DefaultHttpClient();
            publishProgress(30);
            HttpGet get = new HttpGet(params[0]);
            final Bitmap bitmap;
            try {
                HttpResponse response = client.execute(get);
                bitmap = BitmapFactory.decodeStream(response.getEntity()
                        .getContent());
            } catch (Exception e) {
                return null;
            }
            publishProgress(100);
            return bitmap;
        }

        protected void onProgressUpdate(Integer... progress) {
            mProgressBar.setProgress(progress[0]);
        }

        protected void onPostExecute(Bitmap result) {
            if (result != null) {
                Toast.makeText(MainActivity.this, "成功獲取圖片", Toast.LENGTH_LONG)
                        .show();
                mImageView.setImageBitmap(result);
            } else {
                Toast.makeText(MainActivity.this, "獲取圖片失敗", Toast.LENGTH_LONG)
                        .show();
            }
        }

        protected void onPreExecute() {
            mImageView.setImageBitmap(null);
            mProgressBar.setProgress(0);
        }

        protected void onCancelled() {
            mProgressBar.setProgress(0);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
}

複製程式碼

     實際的效果如圖:

    

      當我們點選下載按鈕的時候,就會啟動下載圖片的執行緒,主執行緒這裡顯示下載進度條,然後在下載成功的時候就會顯示圖片,這時我們再點選按鈕的時候就會清空圖片,進度條也重新清零。

      仔細看上面的程式碼,我們會發現很多有趣的東西。

      AsyncTask是為了方便編寫後臺執行緒與UI執行緒互動的輔助類,它的內部實現是一個執行緒池,每個後臺任務會提交到執行緒池中的執行緒執行,然後通過向UI執行緒的Handler傳遞訊息的方式呼叫相應的回撥方法實現UI介面的更新。

     AsyncTask的構造方法有三個模板引數:Params(傳遞給後臺任務的引數型別),Progress(後臺計算執行過程中,進度單位(progress units)的型別,也就是後臺程式已經執行了百分之幾)和Result(後臺執行返回的結果的型別)。

複製程式碼

protected Bitmap doInBackground(String... params) {
            publishProgress(0);
            HttpClient client = new DefaultHttpClient();
            publishProgress(30);
            HttpGet get = new HttpGet(params[0]);
            final Bitmap bitmap;
            try {
                HttpResponse response = client.execute(get);
                bitmap = BitmapFactory.decodeStream(response.getEntity()
                        .getContent());
            } catch (Exception e) {
                return null;
            }
            publishProgress(100);
            return bitmap;
        }

複製程式碼

       params是一個可變引數列表,publishProgress()中的引數就是Progress,同樣是一個可變引數列表,它用於向UI執行緒提交後臺的進度,這裡我們一開始設定為0,然後在30%的時候開始獲取圖片,一旦獲取成功,就設定為100%。中間的程式碼用於下載和獲取網上的圖片資源。

protected void onProgressUpdate(Integer... progress) {
    mProgressBar.setProgress(progress[0]);
}

      onProgressUpdate()方法用於更新進度條的進度。

複製程式碼

protected void onPostExecute(Bitmap result) {
   if (result != null) {
       Toast.makeText(MainActivity.this, "成功獲取圖片", Toast.LENGTH_LONG).show();
       mImageView.setImageBitmap(result);
   } else {
       Toast.makeText(MainActivity.this, "獲取圖片失敗", Toast.LENGTH_LONG).show();
   }
}

複製程式碼

     onPostExecute()方法用於處理Result的顯示,也就是UI的更新。

複製程式碼

protected void onPreExecute() {
    mImageView.setImageBitmap(null);
    mProgressBar.setProgress(0);
}

protected void onCancelled() {
    mProgressBar.setProgress(0);
}

複製程式碼

      這兩個方法主要用於在執行前和執行後清空圖片和進度。
      最後我們只需要呼叫AsyncTask的execute()方法並將Params引數傳遞進來進行。完整的流程是這樣的:

      UI執行緒執行onPreExecute()方法把ImageView的圖片和ProgressBar的進度清空,然後後臺執行緒執行doInBackground()方法,千萬不要在這個方法裡面更新UI,因為此時是在另一條執行緒上,在使用publishProgress()方法的時候會呼叫onProgressUpdate()方法更新進度條,最後返回result---Bitmap,當後臺任務執行完成後,會呼叫onPostExecute()方法來更新ImageView。

      AsyncTask本質上是一個靜態的執行緒池,由它派生出來的子類可以實現不同的非同步任務,但這些任務都是提交到該靜態執行緒池中執行,執行的時候通過呼叫doInBackground()方法執行非同步任務,期間會通過Handler將相關的資訊傳送到UI執行緒中,但神奇的是,並不是呼叫UI執行緒中的回撥方法,而是AsyncTask本身就有一個Handler的子類InternalHandler會響應這些訊息並呼叫AsyncTask中相應的回撥方法。從上面的程式碼中我們也可以看到,UI的ProgressBar的更新是在AsyncTask的onProgressUpdate(),而ImageView是在onPostExecute()方法裡。這是因為InternalHandler其實是在UI執行緒裡面建立的,所以它能夠呼叫相應的回撥方法來更新UI。

      AsyncTask就是專門用來處理後臺任務的,而且它針對後臺任務的五種狀態提供了五個相應的回撥介面,使得我們處理後臺任務變得非常方便。

      如果只是普通的UI更新操作,像是不斷更新TextView這種動態的操作,可以使用Handler,但如果是涉及到後臺操作,像是下載任務,然後根據後臺任務的進展來更新UI,就得使用AsyncTask,但如果前者我們就使用AsyncTask,那真的是太大材小用了!!

      要想真正理解好AsyncTask,首先就要理解很多併發知識,像是靜態執行緒池這些難以理解的概念是必不可少的,作為新手,其實沒有必要在實現細節上過分追究,否則很容易陷入細節的泥潭中,我們先要明白它是怎麼用的,等用得多了,就會開始思考為什麼它能這麼用,接著就是怎麼才能用得更好,這都是一個自然的學習過程,誰也無法越過,什麼階段就做什麼事。因此,關於AsyncTask的討論我就先放到一邊,接下來的東西我也根本理解不了,又怎能講好呢?

3.利用Runnable更新UI介面

      剩下的方法都是圍繞著Runnable物件來更新UI。

      一些元件本身就有提供方法來更新自己,像是ProgressBar本身就有一個post()方法,只要我們傳進一個Runnable物件,就能更新它的進度。只要是繼承自View的元件,都可以利用post()方法,而且我們還可以使用postDelay()方法來延遲執行該Runnable物件。android的這種做法就真的是讓人稱道了,至少我不用為了一個ProgressBar的進度更新就寫出一大堆難懂的程式碼出來。

      還有另一種利用Runnable的方式:Activity.runOnUiThread()方法。這名字實在是太直白了!!使用該方法需要新啟一個執行緒:

複製程式碼

class ProgressThread extends Thread {
        @Override
        public void run() {
            super.run();
            while (mProgress <= 100) {
                runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        mProgressBar.setProgress(mProgress);
                        mProgress++;
                    }
                });
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {


                }
            }
        }
    }

複製程式碼

4.總結

     上面提供了三種思路來解決UI更新的問題,有些地方的討論已經嚴重脫離標題,那也是沒有辦法,因為要說明一些概念,就必須涉及到併發的其他相關知識。方法很多,但它們都有自己適合的場合:

1.如果只是單純的想要更新UI而不涉及到多執行緒的話,使用View.post()就可以了;

2.需要另開執行緒處理資料以免阻塞UI執行緒,像是IO操作或者是迴圈,可以使用Activity.runOnUiThread();

3.如果需要傳遞狀態值等資訊,像是藍芽程式設計中的socket連線,就需要利用狀態值來提示連線狀態以及做相應的處理,就需要使用Handler + Thread的方式;

4.如果是後臺任務,像是下載任務等,就需要使用AsyncTask。
     本來只是因為藍芽專案而開始這篇部落格,但沒想到在寫的過程發現越來越多的東西,於是也一起寫上來了,寫得不好是一定的,因為是大三菜鳥,正在拼命增強自己薄弱的程式設計基礎中,如果錯誤的地方,還希望