欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2022-05-15 21:16:31
在上一篇 学习安卓开发[4] 使用隐式Intent启动短信、联系人、相机应用 中了解了在调用其它应用的功能时隐式Intent的使用,本次基于一个图片浏览APP的开发,记录使用AsyncTask在后台执行HTTP任务以获取图片URL,然后使用HandlerThread动态下载和显示图片 HTTP 请求 ......

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

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