Android中更新UI的方法


1、利用Looper更新UI界面(使用Handler消息传递机制)

    如果我们的代码需要随时将处理后的数据交给UI更新,那么我们想到的方法就是另开一个线程更新数据(也必须这么做,如果我们的数据更新运算量较大,就会阻塞UI线程),也就是界面更新和数据更新是在不同线程中(android采用的是UI单线程模型,所以我们也只能在主线程中对UI进行操作),但这会导致另一个问题:如何在两个线程间通信呢?android提供了Handler机制来保证这种通信。


    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正如字面上的意思,就是一个"循环者",它的主要作用就是使我们的一个普通线程变成一个循环线程。 Looper.prepare()就是用来使当前的线程变成一个LooperThread,然后我们在这个线程中用Handler来处理消息队列中的消息,接着利用Looper.loop()来遍历消息队列中的所有消息。由此可知道,sendMessage()方法的实现是回调了handleMessage(),所以说是处理消息队列中的所有消息也是正确的,因为消息一发送到消息队列中就立即被处理。每个线程最多只有一个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,每个线程都维护一个副本,添加到消息队列中的消息都会被处理掉。


     上面这段代码是新建两个线程,每个线程都维护一个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的更新,它本身的真正意义就是实现线程间的通信:


      上面是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的更新,还可以有另一种使用方式:


       使用Handler的post()方法就显得UI的更新处理非常简单:在一个Runnable对象中更新UI,然后在另一个线程中通过Handler的post()执行该更新动作。值得注意的是,我们就算不用新开一个新线程照样可以更新UI,因为UI的更新线程就是Handler的创建线程---主线程。


      表面上Handler似乎可以发送两种消息:Runnable对象和Message对象,实际上Runnable对象会被封装成Message对象。


public class SplashActivity extends Activity { public static final int SHOW_UPDATE_DIALOG = 1; public static final int ERROR = 2; private TextView tv_splash_version; private Handler handler = new Handler(){ //利用消息处理机制更新系统的ui界面 public void handleMessage(android.os.Message msg) { switch (msg.what) { case SHOW_UPDATE_DIALOG: String description = (String) msg.obj; showUpdateDialog(description); break; case ERROR: Toast.makeText(SplashActivity.this, "错误-"+msg.obj, Toast.LENGTH_LONG).show(); loadMianUI(); break; } }; }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); new Thread(){ public void run() { SystemClock.sleep(1500); loadMianUI(); }; }.start(); } }

  1. /**

* 获取服务器端的版本号,开始子线程访问网络 */ private class getServerVersion implements Runnable{ @Override public void run() { String path = getResources().getString(R.string.url); if(TextUtils.isEmpty(path)){ Toast.makeText(SplashActivity.this, "版本号路径不能为空", Toast.LENGTH_LONG).show(); return; } long startTime = System.currentTimeMillis(); Message msg = Message.obtain(); try { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(2000); int code = conn.getResponseCode(); if(code == 200){ InputStream is = conn.getInputStream(); String result = StreamTools.readStream(is); JSONObject json = new JSONObject(result); String serverVersion = json.getString("version"); String description = json.getString("description"); downloadPath = json.getString("downloadpath"); Log.i(tag , "服务器端的版本为:"+serverVersion); String locationVersion = PackageInfoUtils.getPackageVersion(SplashActivity.this); Log.i(tag , "本地的版本为:"+locationVersion); if(serverVersion.equals(locationVersion)){ Log.i(tag, "版本号一致,不需要进行升级"); loadMianUI(); }else{ Log.i(tag, "版本号不一致,提示用户进行升级"); msg.what = SHOW_UPDATE_DIALOG; msg.obj = description; } }else{ msg.what = ERROR; msg.obj = "code:410"; } } catch (MalformedURLException e) { msg.what = ERROR; msg.obj = "code:404"; e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); msg.what = ERROR; msg.obj = "code:408"; } catch (JSONException e) { e.printStackTrace(); msg.what = ERROR; msg.obj = "code:409"; }finally{ long endTime = System.currentTimeMillis(); long dTime = endTime - startTime; if(dTime > 2000){ }else{ SystemClock.sleep(2000-dTime); } handler.sendMessage(msg); } } }




2、AsyncTask利用线程任务异步更新UI界面


    AsyncTask的原理和Handler很接近,都是通过往主线程发送消息来更新主线程的UI,这种方式是异步的,所以就叫AsyncTask。使用AsyncTask的场合像是下载文件这种会严重阻塞主线程的任务就必须放在异步线程里面:


注:更新UI的操作只能在onPostExecute(String result)方法中。


public class SplashActivity extends Activity {
	public static final int SHOW_UPDATE_DIALOG = 1;
	public static final int ERROR = 2;
	private TextView tv_splash_version;
	private Handler handler = new Handler(){							//利用消息处理机制更新系统的ui界面
		public void handleMessage(android.os.Message msg) {
			switch (msg.what) {
			case SHOW_UPDATE_DIALOG:
				String description = (String) msg.obj;
				showUpdateDialog(description);
				break;
			case ERROR:
				Toast.makeText(SplashActivity.this, "错误-"+msg.obj, Toast.LENGTH_LONG).show();
				loadMianUI();
				break;
			}
		};
	};
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        	new Thread(){
        		public void run() {
        			SystemClock.sleep(1500);
        			loadMianUI();
        		};
        	}.start();
        }
    }
    
  
   /**
     * 获取服务器端的版本号,开始子线程访问网络
     */
    private class getServerVersion implements Runnable{
		@Override
		public void run() {
			String path = getResources().getString(R.string.url);
			if(TextUtils.isEmpty(path)){
				Toast.makeText(SplashActivity.this, "版本号路径不能为空", Toast.LENGTH_LONG).show();
				return;
			}
			long startTime = System.currentTimeMillis();
			Message msg = Message.obtain();
			try {
				URL url = new URL(path);
				HttpURLConnection conn = (HttpURLConnection) url.openConnection();
				conn.setRequestMethod("GET");
				conn.setConnectTimeout(2000);
				int code = conn.getResponseCode();
				if(code == 200){
					InputStream is = conn.getInputStream();
					String result = StreamTools.readStream(is);
					JSONObject json = new JSONObject(result);
					String serverVersion = json.getString("version");
					String description = json.getString("description");
					downloadPath = json.getString("downloadpath");
					Log.i(tag , "服务器端的版本为:"+serverVersion);
					String locationVersion = PackageInfoUtils.getPackageVersion(SplashActivity.this);
					Log.i(tag , "本地的版本为:"+locationVersion);
					if(serverVersion.equals(locationVersion)){
						Log.i(tag, "版本号一致,不需要进行升级");
						loadMianUI();
					}else{
						Log.i(tag, "版本号不一致,提示用户进行升级");
						msg.what = SHOW_UPDATE_DIALOG;
						msg.obj = description;
					}
				}else{
					msg.what = ERROR;
					msg.obj = "code:410";
				}
			} catch (MalformedURLException e) {
				msg.what = ERROR;
				msg.obj = "code:404";
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
				msg.what = ERROR;
				msg.obj = "code:408";
			} catch (JSONException e) {
				e.printStackTrace();
				msg.what = ERROR;
				msg.obj = "code:409";
			}finally{
				long endTime = System.currentTimeMillis();
				long dTime = endTime - startTime;
				if(dTime > 2000){
					
				}else{
					SystemClock.sleep(2000-dTime);
				}
				handler.sendMessage(msg);
			}
		}	
    }
  1. @Override
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);
        }
    }
}

     AsyncTask是为了方便编写后台线程与UI线程交互的辅助类,它的内部实现是一个线程池,每个后台任务会提交到线程池中的线程执行,然后通过向UI线程的Handler传递消息的方式调用相应的回调方法实现UI界面的更新。



     AsyncTask的构造方法有三个模板参数:Params(传递给后台任务的参数类型),Progress(后台计算执行过程中,进度单位(progress units)的类型,也就是后台程序已经执行了百分之几)和Result(后台执行返回的结果的类型)。


     params是一个可变参数列表,publishProgress()中的参数就是Progress,同样是一个可变参数列表,它用于向UI线程提交后台的进度,这里我们一开始设置为0,然后在30%的时候开始获取图片,一旦获取成功,就设置为100%。中间的代码用于下载和获取网上的图片资源。


  onProgressUpdate()方法用于更新进度条的进度。


  onPostExecute()方法用于处理Result的显示,也就是UI的更新。


    AsyncTask本质上是一个静态的线程池,由它派生出来的子类可以实现不同的异步任务,但这些任务都是提交到该静态线程池中执行,执行的时候通过调用doInBackground()方法执行异步任务,期间会通过Handler将相关的信息发送到UI线程中,但神奇的是,并不是调用UI线程中的回调方法,而是AsyncTask本身就有一个Handler的子类InternalHandler会响应这些消息并调用AsyncTask中相应的回调方法。从上面的代码中我们也可以看到,UI的ProgressBar的更新是在AsyncTask的onProgressUpdate(),而ImageView是在onPostExecute()方法里。这是因为InternalHandler其实是在UI线程里面创建的,所以它能够调用相应的回调方法来更新UI。


      AsyncTask就是专门用来处理后台任务的,而且它针对后台任务的五种状态提供了五个相应的回调接口,使得我们处理后台任务变得非常方便。




3、使用runOnUiThread(action)方法


    剩下的方法都是围绕着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、使用Handler的post(Runnable)方法


package com.example.runonuithreadtest; 
import android.app.Activity; 
import android.os.Bundle; 
import android.os.Handler; 
import android.widget.TextView; 
public class MainActivity extends Activity { 
private TextView tv; 
@Override 
protected void onCreate(Bundle savedInstanceState) { 
  super.onCreate(savedInstanceState); 
  setContentView(R.layout.activity_main); 
  tv = (TextView) findViewById(R.id.tv); 
  Handler handler = new Handler(); 
  handler.post(new Runnable(){ 
   @Override 
   public void run() { 
    try { 
     //延迟两秒更新 
     Thread.sleep(2000); 
    } catch (InterruptedException e) { 
     e.printStackTrace(); 
    } 
    tv.setText("更新后的TextView"); 
   } 
  }); 
} 
}



总结:


方法很多,但它们都有自己适合的场合:



1.如果只是单纯的想要更新UI而不涉及到多线程的话,使用View.post()就可以了;



2.需要另开线程处理数据以免阻塞UI线程,像是IO操作或者是循环,可以使用Activity.runOnUiThread();



3.如果需要传递状态值等信息,像是蓝牙编程中的socket连接,就需要利用状态值来提示连接状态以及做相应的处理,就需要使用Handler + Thread的方式;



4.如果是后台任务,像是下载任务等,就需要使用AsyncTask。