16 HTTP与后台任务

创建PhotoGallery应用

  1. 新建继承自AppCompatActivity类的SingleFragmentActivity抽象类,使PhotoGalleryActivity类继承它;
  2. 新建布局文件fragment_photo_gallery.xml,布置RecyclerView视图;
  3. 新建PhotoGalleryFragment类,使其继承Fragment,初始化RecyclerView视图,并设置此fragment为保留fragment。

PhotoGalleryFragment.java

private RecyclerView mPhotoRecyclerView;

public static PhotoGalleryFragment newInstance() {
    return new PhotoGalleryFragment();
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    setRetainInstance(true);

    new FetchItemTask().execute();
}

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_photo_gallery, container, false);

    mPhotoRecyclerView = view.findViewById(R.id.fragment_photo_gallery_recycler_view);
    mPhotoRecyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 1));

    setAdapter();

    return view;
}

网络连接基本

PhotoGallery应用中,需要一个网络连接专用类。

FlickrFetcher.java

public byte[] getUrlBytes(String urlSpec) throws IOException {
    URL url = new URL(urlSpec);
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();

    try {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        //真正连接到指定的URL地址
        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));
}

在 getUrlBytes(String) 方法中,首先根据传入的字符串参数,如https://www.bignerdranch.com,创建一个 URL 对象。然后调用 openConnection() 方法创建一个指向要访问URL的连接对象。URL.openConnection() 方法默认返回的是 URLConnection 对象,但要连接的是 httpURL,因此需将其强制类型转换为 HttpURLConnection 对象 。 这让我们得以调用它的 getInputStream() 、getResponseCode() 等方法。

虽然 HttpURLConnection 对象提供了一个连接,但只有在调用 getInputStream() 方法时
(如果是POST请求,则调用 getOutputStream() 方法),它才会真正连接到指定的URL地址。
创建了URL并打开网络连接之后,我们便可循环调用 read() 方法读取网络数据,直到取完为止。只要还有数据,InputStream 类就会不断地输出字节流数据。数据全部取回后,关闭网络连接,并将读取的数据写入 ByteArrayOutputStream 字节数组中。

获取网络使用权限

AndroidManifest.xml

...
	<uses-permission android:name="android.permission.INTERNET" />

    <application
        ...
    </application>
...

使用AsyncTask在后台线程上运行代码

不要直接在 PhotoGalleryFragment 类中调用 FlickrFetchr.getURLString(String) 方法。正确的做法是,创建一个后台线程,然后在该线程中运行代码。

使用后台线程最简便的方式是使用 AsyncTask 工具类。 AsyncTask 创建后台线程后,我们便可在该线程上调用 doInBackground(…) 方法运行代码。

在PhotoGalleryFragment.java中,添加一个名为 FetchItemsTask 的内部类。覆盖 AsyncTask.doInBackground(…) 方法,从目标网站获取数据并记录日志:

PhotoGalleryFragment.java

private class FetchItemTask extends AsyncTask<Void, Void, List<Image>> {
    @Override
    protected List<Image> doInBackground(Void... voids) {
        Log.d(TAG, "FetchItemTask -> doInBackground...");
        List<Image> imgUrlList = new ArrayList<>();
        try {
            Image image = new Image();
            new BaiduFetcher().getJumpUrlImage("https://api.blogbig.cn/random/api.php", image);
            imgUrlList.add(image);
            Log.i(TAG, "获取到URL的内容:" + image.getImgUrl());
        }
        catch (IOException ioe) {
            Log.e(TAG, "获取URL内容失败!", ioe);
        }

        return imgUrlList;
    }

    @Override
    protected void onPostExecute(List<Image> imgUrlList) {
        Log.d(TAG, "FetchItemTask -> onPostExecute...");
        mImgUrlList = imgUrlList;
        setAdapter();
    }
}

BaiduFetcher.java

public void getJumpUrlImage(String urlSpec, Image image) throws IOException {
    URL url = new URL(urlSpec);
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();

    switch (connection.getResponseCode()) {
        case HttpURLConnection.HTTP_MOVED_TEMP:
            String imgUrl = connection.getHeaderField("location");
            image.setImgUrl(imgUrl);
            getJumpUrlImage(imgUrl, image);
            break;

        case HttpURLConnection.HTTP_OK:
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            //真正连接到指定的URL地址
            InputStream in = connection.getInputStream();
            int bytesRead = 0;
            byte[] buffer = new byte[1024];
            while ((bytesRead = in.read(buffer)) > 0) {
                out.write(buffer, 0, bytesRead);
            }
            out.close();

            byte[] imgBytes = out.toByteArray();
            Bitmap bmpImg = BitmapFactory.decodeByteArray(imgBytes, 0, imgBytes.length);
            image.setImg(bmpImg);
            break;

        default:
            throw new IOException(connection.getResponseMessage() + ": with " + urlSpec);
    }
}

然后,在 PhotoGalleryFragment.onCreate(…) 方法中,调用 FetchItemsTask 新实例的execute() 方法。(见创建PhotoGallery应用 => onCreate方法)


线程与主线程

网络连接需要时间。Web服务器可能需要1~2秒的时间来响应访问请求,文件下载则耗时更久。考虑到这个因素,Android禁止任何主线程网络连接行为。即使强行为之,Android也会抛出NetworkOnMainThreadException 异常。

线程是个单一执行序列。所有Android应用的运行都是从主线程开始的。然而,主线程不是线程那样的预定执行序列。相反,它处于一个无限循环的运行状态,等待着用户或系统触发事件的发生。事件触发后,主线程便负责执行代码,以响应这些事件。

在这里插入图片描述


从Flickr获取JSON数据

添加fetchItems()方法

登录Flickr网站获取API key,并添加fetchItems()方法:

FlickrFetcher.java

private static final String TAG = "FlickrFetcher";
private static final String API_KEY = "6feaae933e5e5fea2b3250027be7b5bd";
//获取并解析JSON数据
public List<GalleryItem> fetchItems() {
    List<GalleryItem> itemList = new ArrayList<>();

    try {
        String url = Uri.parse("https://api.flickr.com/services/rest/")
                .buildUpon()
                .appendQueryParameter("method", "flickr.photos.getRecent")
                .appendQueryParameter("api_key", API_KEY)
                .appendQueryParameter("format", "json")
                .appendQueryParameter("nojsoncallback", "1")
                .appendQueryParameter("extras", "url_s")
                .build().toString();
        String jsonString = getUrlString(url);
        Log.i(TAG, "获取到的JSON数据:" + jsonString);

        JSONObject jsonBody = new JSONObject(jsonString);
        parseItems(itemList, jsonBody);
    }
    catch (JSONException je) {
        Log.e(TAG, "解析JSON数据失败!");
    }
    catch (IOException ioe) {
        Log.e(TAG, "获取网站数据失败!");
    }

    return itemList;
}

在后台线程调用fetchItems()方法

PhotoGalleryFragment.java

private class FetchItemsTask extends AsyncTask<Void, Void, List<GalleryItem>> {
    @Override
    protected List<GalleryItem> doInBackground(Void... voids) {
        return new FlickrFetcher().fetchItems();
    }
    
    //...
}

创建模型类GalleryItem

GalleryItem.java

public class GalleryItem {
    private String mCaption;
    private String mId;
    private String mUrl;
    //...
    @NonNull
    @Override
    public String toString() {
        return mCaption;
    }
}

解析JSON数据

JSON对象是一系列包含在 { } 中的名值对。JSON数组是包含在 [ ] 中用逗号隔开的JSON对象列表。对象彼此嵌套形成层级关系。

在这里插入图片描述

json.org API提供有对应 JSON 数据的Java对象,如 JSONObject 和 JSONArray 。使用JSONObject(String) 构造函数,可以很方便地把 JSON 数据解析进相应的 Java对象 。

解析Flickr图片

FlickrFetcher.java

//解析JSON数据
private void parseItems(List<GalleryItem> itemList, JSONObject jsonBody) throws JSONException {
    JSONObject photosJsonObject = jsonBody.getJSONObject("photos");
    JSONArray photoJsonArray = photosJsonObject.getJSONArray("photo");

    for (int i = 0; i < photoJsonArray.length(); i++) {
        JSONObject photoJsonObject = photoJsonArray.getJSONObject(i);

        GalleryItem item = new GalleryItem();
        item.setId(photoJsonObject.getString("id"));
        item.setCaption(photoJsonObject.getString("title"));

        //并不是每张图片都有对应的url_s链接,需要每一个进行检查
        //url_s这个参数告诉Flickr,如果有小尺寸图片,也一并返回其URL
        if (!photoJsonObject.has("url_s")) continue;

        item.setUrl(photoJsonObject.getString("url_s"));
        itemList.add(item);
    }
}

从AsyncTask回到主线程

添加ViewHolder实现

PhotoGalleryFragment.java

private class PhotoHolder extends RecyclerView.ViewHolder {
    private TextView mTitleTextView;

    public PhotoHolder(@NonNull View itemView) {
        super(itemView);

        mTitleTextView = (TextView) itemView;
    }

    public void bindGalleryItem(GalleryItem item) {
        mTitleTextView.setText(item.toString());
    }
}

添加Adapter实现

PhotoGalleryFragment.java

private class PhotoAdapter extends RecyclerView.Adapter<PhotoHolder> {
    private List<GalleryItem> mGalleryItemList;

    public PhotoAdapter(List<GalleryItem> galleryItemList) {
        mGalleryItemList = galleryItemList;
    }

    @NonNull
    @Override
    public PhotoHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
        View view = layoutInflater.inflate(android.R.layout.simple_list_item_1, parent, false);

        return new PhotoHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull PhotoHolder holder, int position) {
        GalleryItem galleryItem = mGalleryItemList.get(position);
        holder.bindGalleryItem(galleryItem);
    }

    @Override
    public int getItemCount() {
        return mGalleryItemList.size();
    }
}

添加setupAdapter()方法

PhotoGalleryFragment.java

private List<GalleryItem> mGalleryItemList = new ArrayList<>();
private void setupAdapter() {
    //检查fragment是否与activity相关联
    if (isAdded()) {
        mPhotoRecyclerView.setAdapter(new PhotoAdapter(mGalleryItemList));
    }
}

应在 onCreateView(…) 方法中调用该方法,这样每次因设备旋转重新生成 RecyclerView 时,可重新为其配置对应的adapter。另外,每次模型层对象发生改变时,也应及时调用该方法。

注意,配置adapter前,应检查 isAdded() 的返回值是否为 true 。该检查确认fragment已与目标activity相关联,进而保证 getActivity() 方法返回结果不为空。fragment可脱离任何activity而独立存在。在这之前,所有的方法调用都是由系统框架的回调方法驱动的,所以不会出现这种情况。如果fragment在接收回调指令,则它必然关联着某个activity;如它单独存在,就会收不到回调指令。

既然在用 AsyncTask ,我们就正在从后台进程触发回调指令。因而不能确定fragment是否关联着activity。必须检查确认fragment是否仍与activity关联。如果没有关联,依赖于activity的操作(如创建PhotoAdapter,进而还会使用托管activity作为context来创建 TextView )就会失败。

添加adapter更新代码

AsyncTask 提供有另一个可覆盖的 onPostExecute(…) 方法。 onPostExecute(…) 方法在 doInBackground(…) 方法执行完毕后才会运行。更为重要的是,它是在主线程而非后台线程上运行的。因此,在该方法中更新UI比较安全。

PhotoGalleryFragment.java

private class FetchItemsTask extends AsyncTask<Void, Void, List<GalleryItem>> {
    @Override
    protected List<GalleryItem> doInBackground(Void... voids) {
        return new FlickrFetcher().fetchItems();
    }

    @Override
    protected void onPostExecute(List<GalleryItem> galleryItemList) 	{
        mGalleryItemList = galleryItemList;
        setupAdapter();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值