学习安卓开发[5] - HTTP、后台任务以及与UI线程的交互
在上一篇学习安卓开发[4] - 使用隐式intent启动短信、联系人、相机应用中了解了在调用其它应用的功能时隐式intent的使用,本次基于一个图片浏览app的开发,记录使用asynctask在后台执行http任务以获取图片url,然后使用handlerthread动态下载和显示图片
- http
- 请求数据
- 解析json数据
- asynctask
- 主线程与后台线程
- 后台线程的启动与结果返回
- handlerthread
- asynctask不适用于批量下载图片
- threadhandler的启动和注销
- 创建并发送消息
- 处理消息并返回结果
http
请求数据
这里使用java.net.httpurlconnection来执行http请求,get请求的基本用法如下,默认执行的就是get,所以可以省略connection.setrequestmethod("get"),connection.getinputstream()取得inputstream后,再循环执行read()方法将数据从流中取出、写入bytearrayoutputstream中,然后通过bytearrayoutputstream.tobytearray返回为byte数组格式,最后转换为string。网上还有一种方法是使用bufferedreader.readline()来逐行读取输入缓冲区的数据并写入stringbuilder。对于post方法,可以使用getoutputstream()来写入参数。
public byte[] geturlbytes(string urlspec) throws ioexception { url url = new url(urlspec); httpurlconnection connection = (httpurlconnection) url.openconnection(); try { bytearrayoutputstream out = new bytearrayoutputstream(); inputstream in = connection.getinputstream(); if (connection.getresponsecode() != httpurlconnection.http_ok) { throw new ioexception(connection.getresponsemessage() + "with" + urlspec); } int bytesread = 0; byte[] buffer = new byte[1024]; while ((bytesread = in.read(buffer)) > 0) { out.write(buffer, 0, bytesread); } out.close(); return out.tobytearray(); } finally { connection.disconnect(); } } public string geturlstring(string urlspec) throws ioexception { return new string(geturlbytes(urlspec)); }
解析json数据
url为百度的图片接口,返回json格式数据,所以将api返回的json字符串转换为jsonobject,然后遍历json数组,将其转换为指定的对象。
... string url = "http://image.baidu.com/channel/listjson?pn=0&rn=25&tag1=明星&ie=utf8"; string jsonstring = geturlstring(url); jsonobject jsonbody = new jsonobject(jsonstring); parseitems(items, jsonbody); ... private void parseitems(list<galleryitem> items, jsonobject jsonobject) throws ioexception, jsonexception { jsonarray photojsonarray = jsonobject.getjsonarray("data"); for (int i = 0; i < photojsonarray.length() - 1; i++) { jsonobject photojsonobject = photojsonarray.getjsonobject(i); if (!photojsonobject.has("id")) { continue; } galleryitem item = new galleryitem(); item.setid(photojsonobject.getstring("id")); item.setcaption(photojsonobject.getstring("desc")); item.seturl(photojsonobject.getstring("image_url")); items.add(item); } }
asynctask
主线程与后台线程
http相关的代码准备好了,但无法在fragment类中被直接调用。因为网络操作通常比较耗时,如果在主线程(ui线程)中直接操作,会导致界面无响应的现象发生。所以android系统禁止任何主线程的网络连接行为,否则会报newworkonmainthreadexception。
主线程不同于普通的线程,后者在完成预定的任务后便会终止,但主线程则处于无限循环的状态,以等待用户或系统的触发事件。
后台线程的启动与结果返回
至于网络操作,正确的做法是创建一个后台线程,在这个线程中进行。asynctask提供了使用后台线程的简便方法。代码如下:
private class fetchitemstask extends asynctask<void, void, list<galleryitem>> { @override protected list<galleryitem> doinbackground(void... voids) { list<galleryitem> items = new flickrfetchr().fetchitems(); return items; } @override protected void onpostexecute(list<galleryitem> galleryitems) { mitems = galleryitems; setupadapter(); } }
重写了asynctask的doinbackground方法和onpostexecute方法,另外还有两个方法可重写,它们的作用分别是:
- onpreexecute(), 在后台操作开始前被ui线程调用。可以在该方法中做一些准备工作,如在界面上显示一个进度条,或者一些控件的实例化,这个方法可以不用实现。
- doinbackground(params...), 将在onpreexecute 方法执行后马上执行,该方法运行在后台线程中。这里将主要负责执行那些很耗时的后台处理工作。可以调用 publishprogress方法来更新实时的任务进度。该方法是抽象方法,子类必须实现。
- onprogressupdate(progress...),在publishprogress方法被调用后,ui 线程将调用这个方法从而在界面上展示任务的进展情况,例如通过一个进度条进行展示。
- onpostexecute(result), 在doinbackground 执行完成后,onpostexecute 方法将被ui 线程调用,后台的计算结果将通过该方法传递到ui 线程,并且在界面上展示给用户
- oncancelled(),在用户取消线程操作的时候调用。在主线程中调用oncancelled()的时候调用
asynctask的三个泛型参数就是对应doinbackground(params...)、onprogressupdate(progress...)、onpostexecute(result)的,这里设置为
asynctask<void, void, list<galleryitem>>
所以线程完成后返回的结果类型为list
@override public void oncreate(@nullable bundle savedinstancestate) { ... new fetchitemstask().execute(); }
handlerthread
asynctask不适用于批量下载图片
前面通过asynctask创建的后台线程获取到了所有图片的url信息,接下来需要下载这些图片并显示到recyclerview。但如果要在doinbackground中直接下载这些图片则是不合理的,这是因为:
- 图片下载比较耗时,如果要下载的图片较多,需要等这些图片都下载成功后才去更新ui,体验很差。
- 下载的图片还涉及到保存的问题,数量较大的图片不宜直接存放在内存,而且如果要实现无限滚动来显示图片,内存很快就会耗尽
所以对于类似这种重复且数量较大、耗时较长的任务来说,asyncview便不再适合了。
换一种实现方式,既然用recyclerview显示图片,在加载每个holder时,单独下载对应的图片,这样便不会存在前面的问题了,于是该是handlerthread登场的时候了,handlerthread使用消息队列工作,这种使用消息队列的线程也叫消息循环,消息队列由线程和looper组成,looper对象管理着线程的消息队列,会循环检查队列上是否有新消息。
创建继承了handlerthread的thumbnaildownloader:
public class thumbnaildownloader<t> extends handlerthread
这里t设置为之后thumbnaildownloader的使用者,即photoholder。
threadhandler的启动和注销
在fragment创建时启动线程:
@override public void oncreate(@nullable bundle savedinstancestate) { ... mthumbnaildownloader.start(); mthumbnaildownloader.getlooper(); ... }
在fragment销毁时终止线程:
@override public void ondestroy() { super.ondestroy(); mthumbnaildownloader.quit(); }
这一步是必要的,否则即使fragment已被销毁,线程也会一直运行下去。
创建并发送消息
先了解一下message和handler
message
给消息队列发送的就是message类的实例,message类用户需要定义这几个变量:
- what, 用户自定义的int型消息标识代码
- obj,随消息发送的对象
-
target, 处理消息的handler
target是一个handler类实例,创建的message会自动与一个handler关联,message待处理时,handler对象负责触发消息事件handler
handler是处理message的target,也是创建和发布message的接口。而looper拥有message对象的收件箱,所以handler总是引用着looper,在looper上发布或处理消息。handler与looper为多对一关系;looper拥有整个message队列,为一对多关系;多个message可引用同一个handler,为多对一关系。
使用handler
调用handler.obtainmessage方法创建消息,而不是手动创建,obtainmessage会从公共回收池中获取消息,这样做可以避免反复创建新的message对象,更加高效。获取到message,随后调用sendtotarget()将其发送给它的handler,handler会将这个message放置在looper消息队列的尾部。这些操作在queuethumbnail中完成:
public void queuethumbnail(t target, string url) { log.i(tag, "got a url: " + url); if (url == null) { mrequestmap.remove(target); } else { mrequestmap.put(target, url); mrequesthandler.obtainmessage(message_download, target) .sendtotarget(); } }
然后在recyclerview的adapter绑定holder的时候,调用queuethumbnail,将图片url发送给后台线程。
public class photoadapter extends recyclerview.adapter<photoholder> { ... @override public void onbindviewholder(photoholder holder, int position) { ... mthumbnaildownloader.queuethumbnail(holder, galleryitem.geturl()); }
但后台线程的消息队列存放的不是url,而是对应的holder,url存放在concurrentmap型的mrequestmap中,concurrentmap是一种线程安全的map结构。存放了holder对对应url的map关系,这样在消息队列中处理某个holder时,可以从mrequestmap拿到它的url。
private concurrentmap<t, string> mrequestmap
处理消息并返回结果
消息的处理
具体处理消息的动作通过重写handler.handlemessage方法实现。onlooperprepared在looper首次检查消息队列之前调用,所以在此可以实例化handler并重写handlemessage。下载图片的实现在handlerequest方法中,将请求api拿到的byte[]数据转换成bitmap。
public class thumbnaildownloader<t> extends handlerthread { ... @override protected void onlooperprepared() { mrequesthandler = new handler() { @override public void handlemessage(message msg) { if (msg.what == message_download) { t target = (t) msg.obj; log.i(tag, "get a request for url: " + mrequestmap.get(target)); handlerequest(target); } } }; } private void handlerequest(final t target) { try { final string url = mrequestmap.get(target); if (url == null) { return; } byte[] bitmapbytes = new flickrfetchr().geturlbytes(url); final bitmap bitmap = bitmapfactory.decodebytearray(bitmapbytes, 0, bitmapbytes.length); log.i(tag, "bitmap created"); mresponsehandler.post(new runnable() { @override public void run() { if(mrequestmap.get(target)!=url||mhasquit){ return; } mrequestmap.remove(target); mthumbnaildownloadlistener.onthumbnaildownload(target,bitmap); } }); } catch (ioexception ioe) { log.e(tag, "error downloading image", ioe); } }
结果的返回
下载得到的bitmap需要返回给ui线程的holder以显示到屏幕。如何做呢?ui线程也是一个拥有handler和looper的消息循环。所以要返回结果给ui线程,就可以反过来,从后台线程使用主线程的handler。
那么,后台线程首先需要持有ui线程的handler:
public class photogalleryfragment extends fragment { @override public void oncreate(@nullable bundle savedinstancestate) { ... handler responsehandler = new handler(); mthumbnaildownloader = new thumbnaildownloader<>(responsehandler); ... }
thumbnaildownloader的构造函数中接收ui线程的handler。图片下载完成后就要向ui线程发布message了,可以通过handler.post(runnable)进行,重写runable.run()方法,不让halder处理消息,而是在这里触发thumbnaildownloadlistener。
public class thumbnaildownloader<t> extends handlerthread { ... public interface thumbnaildownloadlistener<t>{ void onthumbnaildownload(t target, bitmap thumbnail); } public void setthumbnaildownloadlistener(thumbnaildownloadlistener<t> listener){ mthumbnaildownloadlistener=listener; } public thumbnaildownloader(handler responsehandler) { super(tag); mresponsehandler=responsehandler; } private void handlerequest(final t target) { ... mresponsehandler.post(new runnable() { @override public void run() { if(mrequestmap.get(target)!=url||mhasquit){ return; } mrequestmap.remove(target); mthumbnaildownloadlistener.onthumbnaildownload(target,bitmap); } }); ... } }
mthumbnaildownloadlistener被触发后,ui线程的注册方法就会将后台返回的图片绑定到其holder。
public class photogalleryfragment extends fragment { @override public void oncreate(@nullable bundle savedinstancestate) { ... mthumbnaildownloader.setthumbnaildownloadlistener( new thumbnaildownloader.thumbnaildownloadlistener<photoholder>() { @override public void onthumbnaildownload(photoholder target, bitmap thumbnail) { drawable drawable = new bitmapdrawable(getresources(), thumbnail); target.binddrawable(drawable); } } ); ... }
如此,后台任务的执行与返回就完成了。
上一篇: 鱼腥草乙肝患者能吃吗,鱼腥草怎么吃
下一篇: 葡萄的种类,你知道葡萄有多少种吗?