探讨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。
本来只是因为蓝牙项目而开始这篇博客,但没想到在写的过程发现越来越多的东西,于是也一起写上来了,写得不好是一定的,因为是大三菜鸟,正在拼命增强自己薄弱的编程基础中,如果错误的地方,还希望能够指点迷津。