1. 程式人生 > >Android 非同步更新UI的幾種方式

Android 非同步更新UI的幾種方式

一、為什麼不能在主執行緒更新UI

  • ViewRootImpl通過 checkThread() 方法檢查更新UI操作是否是在主執行緒當中

    • 原因:Android的UI是執行緒不安全的,存在併發訪問的問題。加鎖也不合適:
      • 加鎖會讓UI訪問的邏輯變得複雜
      • 加鎖會降低UI訪問的效率,因為鎖會阻塞某些執行緒的執行
  • 直接在子執行緒修改UI

     @Override
     protected void onResume() {
         super.onResume();
    
         btn.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
    
                 Log.d(TAG, "onClick: " + Thread.currentThread().getName());
    
                 new Thread(new Runnable() {
                     @Override
                     public void run() {
                         
                         Log.d(TAG, "run: 前" + Thread.currentThread().getName());
                         tv.setText("更新後的UI");
                         Log.d(TAG, "run: 後" + Thread.currentThread().getName());
                     }
                 }).start();
    
             }
         });
     }
    
    • API 27可成功修改

      • 原因未知
    • API 23報錯

      • android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    • 原因分析:

      • ViewRootImpl類中的checkThread()方法需要匹配Native方法中的Thread
      void checkThread() {
          if (mThread != Thread.currentThread()) {
              throw new CalledFromWrongThreadException(
                      "Only the original thread that created a view hierarchy can touch its views.");
          }
      }
      
      • ViewRootImpl在WindowManager類中的addView()方法中例項化,在Activity的onResume中呼叫,所以繫結的是主執行緒

二、非同步更新UI的五種常用方式

  • eg:在請求網路圖片並更新UI(會引發非UI執行緒更新UI操作的報錯)
public void onClick(View v) { 
    new Thread(new Runnable() { 
        public void run() { 
           Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png"); 
           mImageView.setImageBitmap(bitmap);     
        } 
    }).start(); 
}
1、Activity.runOnUiThread(Runnable)
  • 只有在Activity中才可使用此方法
public void onClick(View v) { 
    new Thread(new Runnable() { 
        public void run() { 
           Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
           runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mImageView.setImageBitmap(bitmap);  
                }
           });              
        } 
    }).start(); 
}
2、View.post(Runnable)
  • View類及其子類提供了一個post(Runable)方法,允許重寫其中的run()更新UI
public void onClick(View v) { 
    new Thread(new Runnable() { 
        public void run() { 
           Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
           imageView.post(new Runnable() {
              @Override
              public void run() {
                  mImageView.setImageBitmap(bitmap);  
              }
           });             
        } 
    }).start(); 
}
3、View.postDelayed(Runnable, long)
  • 與第二種相同,只是多了一個延遲更新的時間(ms為單位)
public void onClick(View v) { 
    new Thread(new Runnable() { 
        public void run() { 
           Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");  
           imageView.postDelayed(new Runnable() {
              @Override
              public void run() {
                  mImageView.setImageBitmap(bitmap);  
              }
           },2000);          
        } 
    }).start(); 
}
4、使用handler(執行緒間通訊)(推薦)
  • 當在同一個執行緒中更新多個UI時使用
  • 一定要在主執行緒中定義接收
    • 每個Hanlder都關聯了一個執行緒,每個執行緒內部都維護了一個訊息佇列MessageQueue,這樣Handler實際上也就關聯了一個訊息佇列
    • 在執行new Handler()的時候,預設情況下Handler會綁定當前程式碼執行的執行緒程
//傳送訊息

new Thread(new Runnable() { 
    public void run() { 
        Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png"); 
        
        //網路請求任務結束後執行下面的程式碼傳送Message
        Message message = mHandler.obtainMessage();
        message.what = 1;
        //傳遞物件
        message.obj = bitmap;
        mHandler.sendMessage(message);        
    } 
}).start();
//主執行緒中定義接收

Handler mHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what){
            case 1:
                Bitmap bitmap = (Bitmap) msg.obj;
                imageView.setImageBitmap(bitmap);
                break;
            case 2:
                // ...
                break;
            default:
                break;
        }
    }
};
5、AsyncTask(推薦)
  • doInBackground
    • 引數型別是AsyncTask.execute方法的引數
    • 後臺(子執行緒)執行耗時操作,返回值將作為onPostExecute方法的引數
  • onPostExecute
    • UI執行緒執行
    • 引數型別是doInBackground方法的返回值型別,即後臺處理耗時操作返回的結果
//定義AsyncTask

AsyncTask<String,Void,Bitmap> asyncTask = new AsyncTask<String, Void, Bitmap>() {

    /**
     * 即將要執行耗時任務時回撥,這裡可以做一些初始化操作
     */
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    }

    /**
     * 在後臺執行耗時操作,其返回值將作為onPostExecute方法的引數
     * @param params
     * @return
     */
    @Override
    protected Bitmap doInBackground(String... params) {
        Bitmap bitmap = loadImageFromNetwork(params[0]);
        return bitmap;
    }

    /**
     * 當這個非同步任務執行完成後,也就是doInBackground()方法完成後,
     * 其方法的返回結果就是這裡的引數
     * @param bitmap
     */
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        imageView.setImageBitmap(bitmap);
    }
};
//呼叫

asyncTask.execute("http://example.com/image.png");