更新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。
     本來只是因爲藍牙項目而開始這篇博客,但沒想到在寫的過程發現越來越多的東西,於是也一起寫上來了,寫得不好是一定的,因爲是大三菜鳥,正在拼命增強自己薄弱的編程基礎中,如果錯誤的地方,還希望能夠指點迷津。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章