Android内部分享[3]——网络请求和图片加载

概述

前两次分享已经对 Android 内部的组件和存储有了认识,接下来研究一下 Android 内从服务器如何请求数据,如何加载网络图片,对图片如何裁切和缓冲,以及近几年来比较流行的图片加载框架。

这次分享我们通过一个简单的案例贯穿始终来探究实现过程比较偏向实践,所以下面代码比较多。

接口文档:

请求方式:POST 请求地址:https://api.apiopen.top/getImages

Body参数名类型必需描述
pagestring页码(传0或者不传会随机推荐)
countstring返回总数

返回示例:

{
    "code": 200,
    "message": "成功!",
    "result": [
        {
            "id": 666,
            "time": "2018-12-14 04:00:01",
            "img": "https://ws1.sinaimg.cn/large/0065oQSqgy1fy58bi1wlgj30sg10hguu.jpg"
        },
        {
            "id": 665,
            "time": "2018-11-29 04:00:00",
            "img": "https://ws1.sinaimg.cn/large/0065oQSqgy1fxno2dvxusj30sf10nqcm.jpg"
        },
        {
            "id": 664,
            "time": "2018-11-20 04:00:01",
            "img": "https://ws1.sinaimg.cn/large/0065oQSqgy1fxd7vcz86nj30qo0ybqc1.jpg"
        },
        {
            "id": 663,
            "time": "2018-11-07 04:00:01",
            "img": "https://ws1.sinaimg.cn/large/0065oQSqgy1fwyf0wr8hhj30ie0nhq6p.jpg"
        },
        {
            "id": 662,
            "time": "2018-10-23 04:00:00",
            "img": "https://ws1.sinaimg.cn/large/0065oQSqgy1fwgzx8n1syj30sg15h7ew.jpg"
        }
    ]
}

请求接口

在 Android 2.2 版本之前 sdk 为我们提供了 HttpClient, 在 2.3 版本开始引入了 HttpURLConnection 来实现网络请求。HttpURLConnection 相比于 HttpClient ,其 API 简单,体积小,而且其压缩和缓存机制可以有效的减少网络访问的流量,在提升速度和省电方面都很有优势,但是在 2.3 之前存在很多bug。

HttpsURLConnectionHttpURLConnection 差别不大,通过 HttpsURLConnection 类的 setSSLSocketFactory() 方法获取 SSLSocketFactory实例后就可当做 HttpURLConnection 类来进行使用。而 SSLSocketFactory 类用于操作 SSL 套接字,需要通过 SSLContext 类的 getSockeFactory 方法获取,SSLContext 在初始化的时候需要配置对应的密匙库与信任库。

Android 中如果涉及到网络相关,需要申请以下权限:

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

Android 平台包含 HttpsURLConnection 客户端,该客户端支持传输层安全协议 (TLS)、流式上传与下载、可配置超时、IPv6,以及连接池。

/**
* 网络请求(POST方式)
* @param url
* @return
* @throws IOException
*/
private String getRemoteDataByURL(URL url) throws IOException {
    HttpsURLConnection connection = null;
    InputStream stream = null;
    String result = null;
    try {
        connection = (HttpsURLConnection) url.openConnection();
        connection.setReadTimeout(3000); //设置读取超时时间
        connection.setConnectTimeout(3000); //设置连接超时时间
        connection.setRequestMethod("POST");
        connection.setDoInput(true); //使用 URL 连接进行输入, 默认true
        connection.setDoOutput(true);  //使用 URL 连接进行输出,默认false
        connection.connect();

        int responseCode = connection.getResponseCode();
        if(responseCode != HttpsURLConnection.HTTP_OK){
            throw new IOException("HTTP error code: " + responseCode);
        }
        stream = connection.getInputStream();
        if(stream != null){
            result = readStream(stream, 1024 * 1024);
        }
    }finally {
        if(stream != null){
            stream.close();
        }
        if(connection != null){
            connection.disconnect();
        }
    }
    return result;
}


/**
* 从流中读取数据
* @param stream
* @param maxReadSize
* @return
* @throws IOException
* @throws UnsupportedEncodingException
*/
private String readStream(InputStream stream, int maxReadSize)
        throws IOException, UnsupportedEncodingException {
    Reader reader = null;
    reader = new InputStreamReader(stream, "UTF-8");
    char[] rawBuffer = new char[maxReadSize];
    int readSize;
    StringBuffer buffer = new StringBuffer();
    while (((readSize = reader.read(rawBuffer)) != -1) && maxReadSize > 0) {
        if (readSize > maxReadSize) {
            readSize = maxReadSize;
        }
        buffer.append(rawBuffer, 0, readSize);
        maxReadSize -= readSize;
    }
    return buffer.toString();
}

封装一个 RequestParam 类来设置请求参数:

public class RequestParam {

    private String url;
    private Map<String, String> parms = new HashMap<>();

    public RequestParam(String url) {
        this.url = url;
    }

    public RequestParam setParam(String key, String value){
        parms.put(key, value);
        return this;
    }

    public RequestParam setParam(String key, int value){
        parms.put(key, String.valueOf(value));
        return this;
    }

    public URL getURL(){
        URL uRL = null;
        if(url == null || url.length() == 0) return uRL;
        StringBuilder sbUrl = new StringBuilder(url);
        try {
            boolean isfist = true;
            for(Map.Entry<String, String> entry : parms.entrySet()){
                if(isfist){
                    isfist = false;
                    sbUrl.append("?");
                }else {
                    sbUrl.append("&");
                }
                sbUrl.append(entry.getKey()).append("=");
                String value = entry.getValue();
                if(!TextUtils.isEmpty(value)){
                    sbUrl.append(URLEncoder.encode(value, "utf-8"));
                }
            }
            uRL = new URL(sbUrl.toString());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return uRL;
    }
}

需要注意的是我们的网络请求是一个耗时操作,需要在非 UI 线程中操作,接下来使用一个 AsyncTask 异步任务类来实现网络请求和 UI 刷新:

private class RequestPostTask extends AsyncTask<RequestParam, Integer, List<AdviceImageBean>>{

    public static final int CODE_RESULT_OK = 200;
    private int currentPage;
    public RequestPostTask() {
        currentPage = mCurrentPage;
    }

    @Override
    protected void onPreExecute() {
        //TODO 检查网络连接状态
    }

    @Override
    protected List<AdviceImageBean> doInBackground(RequestParam... strings) {
        try {
            String result = getRemoteDataByURL(strings[0].getURL());
            Log.d("TEST", "result = " + result);
            RequestResultBean resultObj = new Gson().fromJson(result, RequestResultBean.class);
            if(resultObj.code == CODE_RESULT_OK){
                return resultObj.result;
            }else{
                mAdapter.loadMoreFail();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onPostExecute(List<AdviceImageBean> adviceImageBeans) {
        if(currentPage == 1) {
            mAdapter.setNewData(adviceImageBeans);
        }else{
            mAdapter.addData(adviceImageBeans);
        }
        mAdapter.loadMoreComplete();
    }
}

上面的 doInBackground() 是非 UI 线程, 而 onPreExecute()onPostExecute() 是在 UI 线程中。另外这里面使用了 Gson 来从 json 字符串中解析对象, 接下来启动 AsyncTask 来调用接口:

private void requestNewData(){
    String requestUrl = "https://api.apiopen.top/getImages";
    RequestParam requestParam = new RequestParam(requestUrl)
            .setParam("page", mCurrentPage)
            .setParam("count", PAGE_SIZE);
    new RequestPostTask().execute(requestParam);
}

事实上我们实际项目中很少这样去请求接口,而是使用一些比较方便的网络请求框架,目前比较流行的网络框架有 Volley, OkHttp, Retrofit, android-async-http等。这里暂不讨论这些框架,毕竟这些东西是根据文档和示例去具体使用的,没必要每个都了解的很透彻,原理性的和共性的东西这里已有体现。

OkHttp + Retrofit

2013年 Google IO 大会后, Google 官方团队推出的 volley 网络框架,Android6.0 以后 Google 官方 Api 移除 HttpClient(继续使用HttpClient及基于其封装的网络库会出异常),而现在比较流行的是 OkHttp + Retrofit 实现网络请求。

Retrofit是一个RESTful的可用于Android和Java的HTTP网络请求框架的封装,使用它可以简化我们的网络操作,提高效率和正确率。它将请求过程和底层代码封装起来只暴露我们业务中的请求和返回数据模型。值得注意的是,Retrofit并不是完成了网络请求,而是对网络请求框架Okhttp的封装,在Retrofit 2.0开始内置了Okhttp。下面简单演示一下使用过程:

第一步:引入依赖

compile 'com.squareup.retrofit2:retrofit:2.0.2'

第二步:定义 Service 接口:

public interface GitHubService {
	@GET("users/{user}/repos")
	Call<List<Repo>> listRepos(@Path("user") String user);
}

第三步:初始化 Retrofit:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();
GitHubService service = retrofit.create(GitHubService.class);

第四步:调用接口

Call<List<Repo>> repos = service.listRepos("octocat");

AsyncTask ,Volley和Retrofit的对比

One DiscussionDashboard( 7 request )25 Discussions
AsyncTask941ms4539ms13957ms
Volley560ms2202ms4275ms
Retrofit312ms889ms1059ms

图片加载

Android 中从网络请求图片有很多常用框架,目前来说比较流行的有 ImageLoad, Picasso, Glide 和 Fresco, 我使用比较多的是后两者,下面来看看 Glide 的使用:

dependencies {
    //Glide图片加载框架
    implementation 'com.github.bumptech.glide:glide:4.5.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.5.0'
}

异步加载图片:

@Override
protected void convert(BaseViewHolder helper, AdviceImageBean item) {
    Glide.with(mContext).load(item.img).into((ImageView) helper.getView(R.id.item_imageview));
    helper.setText(R.id.item_time, item.time);
}

瀑布流分页加载图片

源码获取地址:https://github.com/lxqxsyu/InnerShareCode2

网络管理

上面的应用我们可以增加一个对当前网络情况的判断:

@Override
protected void onPreExecute() {
    ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
    if(networkInfo == null || !networkInfo.isConnected() ||
            (networkInfo.getType() != ConnectivityManager.TYPE_WIFI
                    && networkInfo.getType() != ConnectivityManager.TYPE_MOBILE)){
        mCancel = true;
    }
}

如果当前网络不是 WIFI 或者 移动蜂窝网络,或者当前网络不可用则停止请求数据接口。

很多时候我们需要监听网络的变化,这个时候就需要监听网络变化的广播 (BroadcastReceiver):

全局注册广播:

<receiver android:name=".reciver.NetworkReceiver">
    <intent-filter>
        <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</receiver>

处理收到的广播:

public class NetworkReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d("TEST", "网络状态发生变化");
        //检测API是不是小于23,因为到了API23之后getNetworkInfo(int networkType)方法被弃用
        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
            //获得ConnectivityManager对象
            ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
            //获取ConnectivityManager对象对应的NetworkInfo对象
            //获取WIFI连接的信息
            NetworkInfo wifiNetworkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
            //获取移动数据连接的信息
            NetworkInfo dataNetworkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
            if (wifiNetworkInfo.isConnected() && dataNetworkInfo.isConnected()) {
                Toast.makeText(context, "WIFI已连接,移动数据已连接", Toast.LENGTH_SHORT).show();
            } else if (wifiNetworkInfo.isConnected() && !dataNetworkInfo.isConnected()) {
                Toast.makeText(context, "WIFI已连接,移动数据已断开", Toast.LENGTH_SHORT).show();
            } else if (!wifiNetworkInfo.isConnected() && dataNetworkInfo.isConnected()) {
                Toast.makeText(context, "WIFI已断开,移动数据已连接", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(context, "WIFI已断开,移动数据已断开", Toast.LENGTH_SHORT).show();
            }
        //API大于23时使用下面的方式进行网络监听
        }else {
            Log.d("TEST", "API level 大于23");
            //获得ConnectivityManager对象
            ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

            //获取所有网络连接的信息
            Network[] networks = connMgr.getAllNetworks();
            //用于存放网络连接信息
            StringBuilder sb = new StringBuilder();
            //通过循环将网络信息逐个取出来
            for (int i=0; i < networks.length; i++){
                //获取ConnectivityManager对象对应的NetworkInfo对象
                NetworkInfo networkInfo = connMgr.getNetworkInfo(networks[i]);
                sb.append(networkInfo.getTypeName() + " connect is " + networkInfo.isConnected());
            }
            ToastUtil.showToast(context, sb.toString());
        }
    }
}

动态注册广播:

//注册网络状态监听广播
private void registNetworkChange(){
    if(mNetworkReceiver == null) {
        mNetworkReceiver = new NetworkReceiver();
    }
    IntentFilter filter = new IntentFilter();
    filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
    filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
    filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
    registerReceiver(mNetworkReceiver, filter);
}

//取消注册网络状态广播
private void unRegistNetworkChange(){
    if(mNetworkReceiver == null) return;
    unregisterReceiver(mNetworkReceiver);
}

另外需要注意的是,Android 7.0 及更高版本的应用必须使用 registerReceiver(BroadcastReceiver,IntentFilter)注册 CONNECTIVITY_ACTION 广播。 在清单中声明接收器不起作用。