当前位置: 移动技术网 > IT编程>移动开发>Android > 学习安卓开发[5] - HTTP、后台任务以及与UI线程的交互

学习安卓开发[5] - HTTP、后台任务以及与UI线程的交互

2019年02月03日  | 移动技术网IT编程  | 我要评论

韩女团aoa劲薄透,安陆市德安中学,官风宝气

在上一篇学习安卓开发[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);
                    }
                }
        );
        ...
    }

如此,后台任务的执行与返回就完成了。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网