Android中UI线程与后台线程交互设计的5种方法

今天算是一次学习总结吧。
在android的设计思想中,为了确保用户顺滑的操作体验。
一些耗时的任务不能够在UI线程中运行,
【菜科解读】
我想关于这个话题已经有很多前辈讨论过了。
今天算是一次学习总结吧。
在android的设计思想中,为了确保用户顺滑的操作体验。
一些耗时的任务不能够在UI线程中运行,像访问网络就属于这类任务。
因此我们必须要重新开启一个后台线程运行这些任务。
然而,往往这些任务最终又会直接或者间接的需要访问和控制UI控件。
例如访问网络获取数据,然后需要将这些数据处理显示出来。
就出现了上面所说的情况。
原本这是在正常不过的现象了,但是android规定除了UI线程外,其他线程都不可以对那些UI控件访问和操控。
为了解决这个问题,于是就引出了我们今天的话题。
Android中后台线程如何与UI线程交互。
据我所知android提供了以下几种方法,用于实现后台线程与UI线程的交互。
1、handler
2、Activity.runOnUIThread(Runnable)
3、View.Post(Runnable)
4、View.PostDelayed(Runnabe,long)
5、AsyncTask
方法一:handler
handler是android中专门用来在线程之间传递信息类的工具。
要讲明handler的用法非常简单,但是我在这里会少许深入的讲一下handler的运行机制。
为了能够让handler在线程间传递消息,我们还需要用到几个类。
他们是looper,messageQueue,message。
这里说的looper可不是前段时间的好莱坞大片环形使者,他的主要功能是为特定单一线程运行一个消息环。
一个线程对应一个looper。
同样一个looper对应一个线程。
这就是所谓的特定单一。
一般情况下,在一个线程创建时他本身是不会生产他特定单一的looper的(主线程是个特例)。
因此我们需要手动的把一个looper与线程相关联。
其方法只需在需要关联的looper的线程中调用Looper.prepare。
之后我们再调用Looper.loop启动looper。
说了这么多looper的事情,到底这个looper有什么用哪。
其实之前我们已经说到了,他是为线程运行一个消息环。
具体的说,在我们将特定单一looper与线程关联的时候,looper会同时生产一个messageQueue。
他是一个消息队列,looper会不停的从messageQuee中取出消息,也就是message。
然后线程就会根据message中的内容进行相应的操作。
那么messageQueue中的message是从哪里来的哪?那就要提到handler了。
在我们创建handler的时候,我们需要与特定的looper绑定。
这样通过handler我们就可以把message传递给特定的looper,继而传递给特定的线程。
在这里,looper和handler并非一一对应的。
一个looper可以对应多个handler,而一个handler只能对应一个looper(突然想起了一夫多妻制,呵呵)。
这里补充一下,handler和looper的绑定,是在构建handler的时候实现的,具体查询handler的构造函数。
在我们创建handler并与相应looper绑定之后,我们就可以传递message了。
我们只需要调用handler的sendMessage函数,将message作为参数传递给相应线程。
之后这个message就会被塞进looper的messageQueue。
然后再被looper取出来交给线程处理。
这里要补充说一下message,虽然我们可以自己创建一个新的message,但是更加推荐的是调用handler的obtainMessage方法来获取一个message。
这个方法的作用是从系统的消息池中取出一个message,这样就可以避免message创建和销毁带来的资源浪费了(这也就是算得上重复利用的绿色之举了吧)。
突然发现有一点很重要的地方没有讲到,那就是线程从looper收到message之后他是如何做出响应的嘞。
其实原来线程所需要做出何种响应需要我们在我们自定义的handler类中的handleMessage重构方法中编写。
之后才是之前说的创建handler并绑定looper。
好吧说的可能哟点乱,总结一下利用handler传递信息的方法。
假设A线程要传递信息给B线程,我们需要做的就是
1、在B线程中调用Looper.prepare和Looper.loop。
(主线程不需要)
2、 编写Handler类,重写其中的handleMessage方法。
3、创建Handler类的实例,并绑定looper
4、调用handler的sentMessage方法发送消息。
到这里,我们想handler的运行机制我应该是阐述的差不多了吧,最后再附上一段代码,供大家参考。
1 public class MyHandlerActivity extends Activity { 2 TextView textView; 3 MyHandler myHandler; 5 protected void onCreate(Bundle savedInstanceState) { 6 super.onCreate(savedInstanceState); 7 setContentView(R.layout.handlertest); 9 //实现创建handler并与looper绑定。这里没有涉及looper与 //线程的关联是因为主线程在创建之初就已有looper10 myHandler=MyHandler(MyHandlerActivitythis.getMainLooper());11 textView = (textView) findViewById(R.id.textView);13 MyThread m = new MyThread();14 new Thread(m).start();18 class MyHandler extends Handler {19 public MyHandler() {22 public MyHandler(Looper L) {23 super(L);26 // 必须重写这个方法,用于处理message27 @Override28 public void handleMessage(Message msg) {29 // 这里用于更新UI30 Bundle b = msg.getData();31 String color = b.getString("color");32 MyHandlerActivity.this.textView.setText(color);36 class MyThread implements Runnable {37 public void run() {38 //从消息池中取出一个message39 Message msg = myHandler.obtainMessage();40 //Bundle是message中的数据41 Bundle b = new Bundle();42 b.putString("color", "我的");43 msg.setData(b);44 //传递数据45 myHandler.sendMessage(msg); // 向Handler发送消息,更新UI47 }
方法二:Activity.runOnUIThread(Runnable)
这个方法相当简单,我们要做的只是以下几步
1、编写后台线程,这回你可以直接调用UI控件
2、创建后台线程的实例
3、调用UI线程对应的Activity的runOnUIThread方法,将后台线程实例作为参数传入其中。
注意:无需调用后台线程的start方法
方法三:View.Post(Runnable)
该方法和方法二基本相同,只是在后台线程中能操控的UI控件被限制了,只能是指定的UI控件View。
方法如下
1、编写后台线程,这回你可以直接调用UI控件,但是该UI控件只能是View
2、创建后台线程的实例
3、调用UI控件View的post方法,将后台线程实例作为参数传入其中。
方法四:View.PostDelayed(Runnabe,long)
该方法是方法三的补充,long参数用于制定多少时间后运行后台进程
方法五:AsyncTask
AsyncTask是一个专门用来处理后台进程与UI线程的工具。
通过AsyncTask,我们可以非常方便的进行后台线程和UI线程之间的交流。
那么AsyncTask是如何工作的哪。
AsyncTask拥有3个重要参数
1、Params
2、Progress
3、Result
Params是后台线程所需的参数。
在后台线程进行作业的时候,他需要外界为其提供必要的参数,就好像是一个用于下载图片的后台进程,他需要的参数就是图片的下载地址。
Progress是后台线程处理作业的进度。
依旧上面的例子说,就是下载图片这个任务完成了多少,是20%还是60%。
这个数字是由Progress提供。
Result是后台线程运行的结果,也就是需要提交给UI线程的信息。
按照上面的例子来说,就是下载完成的图片。
AsyncTask还拥有4个重要的回调方法。
1、onPreExecute
2、doInBackground
3、onProgressUpdate
4、onPostExecute
onPreExecute运行在UI线程,主要目的是为后台线程的运行做准备。
当他运行完成后,他会调用doInBackground方法。
doInBackground运行在后台线程,他用来负责运行任务。
他拥有参数Params,并且返回Result。
在后台线程的运行当中,为了能够更新作业完成的进度,需要在doInbackground方法中调用PublishProgress方法。
该方法拥有参数Progress。
通过该方法可以更新Progress的数据。
然后当调用完PublishProgress方法,他会调用onProgressUpdate方法用于更新进度。
onProgressUpdate运行在UI线程,主要目的是用来更新UI线程中显示进度的UI控件。
他拥有Progress参数。
在doInBackground中调用PublishProgress之后,就会自动调onProgressUpdate方法
onPostExecute运行在UI线程,当doInBackground方法运行完后,他会调用onPostExecute方法,并传入Result。
在onPostExecute方法中,就可以将Result更新到UI控件上。
明白了上面的3个参数和4个方法,你要做的就是
1、编写一个继承AsyncTask的类,并声明3个参数的类型,编写4个回调方法的内容。
2、然后在UI线程中创建该类(必须在UI线程中创建)。
3、最后调用AsyncTask的execute方法,传入Parmas参数(同样必须在UI线程中调用)。
这样就大功告成了。
另外值得注意的2点就是,千万不要直接调用那四个回调方法。
还有就是一个AsyncTask实例只能执行一次,否则就出错哦。
以上是AsyncTask的基本用法,更加详细的内容请参考android官方文档。
最后附上一段代码,供大家参考。
1 private class DownloadFilesTask extends AsyncTask 2 //在这里声明了Params、Progress、Result参数的类型 4 //因为这里不需要使用onPreExecute回调方法,所以就没有加入该方法 6 //后台线程的目的是更具URL下载数据 7 protected Long doInBackground(URL... urls) { 8 int count = urls.length;//urls是数组,不止一个下载链接 9 long totalSize = 0;//下载的数据10 for (int i = 0; i Android,中,线程,与,后台,交互,设计,的,5种,方