第九章 网络编程

  • Http协议简介
  • 使用Handler进行线程间通信
  • AsyncTask
  • 使用HttpURLConnection、HttpClient访问网络提交数据
  • AsyncHttpClient、SmartImageView开源项目的使用
  • 多线程下载文件

Android系统提供了以下几种方式实现网络通信:Socket通信、HTTP通信、URL通信和WebView,其中最常用的是HTTP通信。

一、网络编程入门

1. HTTP协议简介

HTTP:超文本传输协议,它规定了浏览器和万维网服务器之间互相通信的规则。

当客户端在与服务器端建立连接后,向服务器端发送的请求,被称作HTTP请求。服务器端接收到请求后会做出响应,称为HTTP响应。

2. Handler消息机制原理

在使用Android手机下载软件时,通常都能在界面上看到一个下载的进度条,这个进度条用来表示当前软件下载的进度。但是Android4.0以后不能在UI线程中访问网络,子线程也不能更新UI界面。为了根据下载进度实时更新UI界面,就需要用到Handler消息机制来实现线程间的通信。

Handler机制主要包括4个关键对象,分别是Message、Handler、MessageQueue和Looper。

(1) Message

Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同的线程之间交换数据。

Message的what字段可以用来携带一些整型数据,obj字段可以用来携带一个Object对象。

(2) Handler

Handler主要用于发送消息和处理消息。

一般使用Handler对象的sendMessage()方法发送消息,发出的消息经过一系列的辗转处理后,最终会传递到Handler对象的handleMessage()方法中。

(3) MessageQueue

MessageQueue主要用来存放通过Handler发送的消息。通过Handler发送的消息会存在Message中等待处理。

每个线程中只会有一个MessageQueue对象。

(4) Looper

Looper是每个线程中的MessageQueue的管家。调用Looper的loop()方法后,就会进入一个无限循环中。然后,每当发现MessageQueue中存在一条消息,就会将它取出,并传递到Handler的handleMessage()方法中。

此外,每个线程也只有一个Looper对象。在主线程中创建Handler对象时,系统已经创建了Looper对象,所以不用手动创建Looper对象,而在子线程中的Handler对象,需要调用Looper.loop()方法开启消息循环。


图 9-1

Handler消息机制处理流程如图9-1所示:

  • 在主线程(UI线程)创建一个Handler对象

  • 在子线程中调用Handler的sendMessage()方法发送消息

  • 这个消息会存放在主线程的MessageQueue中,通过Looper对象取出MessageQueue中的消息,最后分发回Handler的handleMessage()方法中进行处理,更新UI。

3. AsyncTask

为了方便在子线程中对UI进行操作,Android提供了一些好用的工具类,AsyncTask就是其中之一。利用AsyncTask,可以十分简单地从子线程切换到主线程,它的原理是基于异步消息处理机制实现的。

AsyncTask是一个抽象类,使用它必须创建一个类继承它。在继承AsyncTask时,可以为其执行三个泛型参数,这三个参数的用途如下:

  • Params:在执行AsyncTask时需要传入的参数,用于后台任务中使用。
  • Progress:后台任务执行时,如果需要在界面上显示当前的进度,则使用该参数作为进度单位。
  • Result:当任务执行完毕后,如果需要对结果进行返回,则使用该参数作为返回值类型。

下面定义了一个AsyncTask类,它的三个泛型参数分别被指定为Void, Integer, Boolean。第一个参数表示在执行AsyncTask时,不需要传递参数给后台任务;第个参数表示使用整数来作为进度显示单位;第三个参数表示使用布尔值来返回执行结果。

通常在使用AsyncTask时,需要重写它的4个方法:

public class MyAsyncTask extends AsyncTask<Void, Integer, Boolean> {
    // 这个方法在后台任务执行之前调用,一般用于界面上的初始化操作,例如,显示一个进度条对话框
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    }

    //这个方法在子线程中运行,用于处理耗时操作,操作一旦完成即可以通过return语句将任务的执行结果返回。
    //如果AsyncTask的第三个泛型参数指定的是void,则可以不返回执行结果。
    //注意:这个方法不能进行更新UI操作,如果要在该方法中更新UI,可以调用publishProgress()方法来完成。
    @Override
    protected Boolean doInBackground(Void... params) {
        publishProgress(50);
        return null;
    }

    //如果在doInBackground()方法中调用了publishProgress()方法,这个方法就会很快被调用,方法中携带的参数就是后台任务传递过来的。
    //在这个方法中可以对UI进行操作,利用参数values就可以对UI进行相应的更新。
    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
    }

    //当doInBackground()方法执行完毕并且通过return语句返回时,这个方法会很快被调用。在doInBackground()中返回的数据会作为参数传递到该方法中。
    //此时可以利用返回的参数进行UI操作,例如,提醒某个任务完成了。
    @Override
    protected void onPostExecute(Boolean aBoolean) {
        super.onPostExecute(aBoolean);
    }
}

最后,我们可以在UI线程中创建AsyncTask实例,并调用它的execute()方法。

new MyAsyncTask().execute();

以上便士AsyncTask的基本用法,使用AsyncTask可以不使用Handler发送和接收消息,只需要在doInBackground()方法中调用publicProgress()方法,即可实现从子线程切换到UI线程。

二、HttpURLConnection和HttpClient

在实际开发中,绝大多数App都需要和服务器进行数据交互,也就是访问网络。

Android客户端访问网络发送HTTP请求的方式一般有两种:HttpURLConnection和HttpClient。

HttpURLConnection是一个标准的Java类,HttpClient是一个开源项目。

1. 使用HttpURLConnection访问网络

使用HttpURLConnection,客户端与服务器建立连接并获取服务器返回的数据的过程:

try{
    //在URL的构造方法中传入要访问资源的路径
    URL url=new URL("https://www.baidu.com/");
    HttpURLConnection conn=(HttpURLConnection)url.openConnection();
    conn.setRequestMethod("GET");         //设置请求方式
    conn.setConnectionTimeout(5000);      //设置超时时间
    int code=conn.getResponseCode();
    if(code==200){  //请求网络成功后返回码是200
        InputStream is=conn.getInputStream(); //获取服务器返回的输入流
        //读取流信息,获取服务器返回的数据

        conn.disconnect(); //关闭http连接
    }else{ //返回码不是200,请求服务器失败
    }
}catch(Exception e){
}

需要注意的是,在使用HttpURLConnection对象访问网络时,需要设置超时时间。如果没有设置超时时间,在网络异常的情况下,程序会因取不到数据而一直等待,导致程序僵死不往下执行。

案例:Demo0901,网络图片浏览器,演示手机端使用HttpURLConnection和客户端进行通信的过程

2. 使用HttpClient访问网络

HttpClient是Apache的一个开源项目,从一开始就被引入到Android的API中。HttpClient可以完成和HttpURLConnection一样的效果,但使用起来更简单。简单来说,HttpClient是HttpURLConnection的增强版。

图9-2是使用HttpClient访问网络时所需要用到的几个常用类的介绍。


图 9-2

使用HttpClient访问网络与HttpURLConnection的过程大致相同,具体步骤如下:

  • 创建HttpClient对象
  • 指定访问网络的方式,创建一个HttpPost对象或者HttpGet对象
  • 如果需要发送请求参数,可调用HttpGet、HttpPost的setParams方法,对于HttpPost对象而言,也可以调用setEntity()方法来设置请求参数
  • 调用HttpClient对象的execute()方法访问网络,并获取HttpResponse对象
  • 调用HttpResponse.getEntity()方法获取HttpEntity对象,该对象包装了服务器的响应内容,即所请求的数据。

使用HttpClient访问服务器并获取返回数据的示例代码

HttpClient client=new DefaultHttpClient();
HttpPost httpPost=new HttpPost("https://www.baidu.com/");

List<NameValuePair> params=new ArrayList<NameValuePair>(); //创建一个NameValuePair集合,用于添加参数
params.add(new BasicNameValuePair("username", "admin"));
UrlEncodedFormEntity entity=new UrlEncodedFormEntity(params, "utf-8"); //给参数设置编码
httpPost.setEntity(entity);//设置参数

HttpResponse httpResponse=client.execute(httpPost);
int statusCode=httpResponse.getStatusLine().getStatusCode();
if(statusCode==200){
    HttpEntity httpEntity=httpResponse.getEntity();//获取HttpEntity实例
    String response=EntityUtils.toString(httpEntity, "utf-8");//设置编码格式
    InputStream content = httpEntity.getContent();  //拿到输入流
}

需要注意的是,使用Post方式设置参数时,需要创建一个NameValuePair的集合来添加参数。在给参数设置编码时,需要与服务器的解码格式保持一致,否则会出现中文乱码的情况。

案例:Demo0902,网络图片浏览器,,演示手机端使用HttpClient和客户端进行通信的过程

3. 数据提交方式

HTTP/1.1协议中共定义了8种方法来表明Request-URI指定的资源的不同的操作方式,其中最常用的两种请求方式是GET和POST。

(1) GET方式

GET方式是以实体的方式得到由请求URL所指向的资源信息,它向服务器提交的参数跟在请求URL后面。

使用GET方式访问网络URL的长度是有限制的,HTTP协议规范规定GET方式请求URL的长度不超过4K。但是IE浏览器GET方式请求URL的长度不能超过1K。

示例代码:使用HttpURLConnection GET方式提交数据到服务器:

//将用户名和密码拼装在指定资源路径后面,并给用户名和密码进行编码
String path="http://192.168.1.100:8080/web/LoginServlet?username="
        +URLEncoder.encode("zhangsan")+"&password="+URLEncoder.encode("123");
URL url=new URL(path);
HttpURLConnection conn=url.openConnection();
conn.setRequestMethod("GET");             //设置请求方式
conn.setConnectTimeout(5000);             //设置超时时间
int responseCode=conn.getResponseCode();  //获取状态码
if(responseCode==200){//访问成功
    InputStream is=conn.getInputStream(); //获取服务器返回的输入流
    try{
        //读取流里面的信息
    }catch(Exception e){
    }
}

(2) POST方式

POST方式在向服务器发出请求时,它向服务器提交的参数跟在请求实体中。

它提交的参数是浏览器通过流的方式直接写给服务器的,用户不能在浏览器中看到向服务器提交的请求参数,因此POST方式比GET方式相对安全。

此外,POST方式对URL的长度没有限制。

示例代码:使用HttpURLConnection POST方式提交数据到服务器:

//使用HttpURLConnection
String path="http://192.168.1.100:8080/web/LoginServlet";
//准备数据并给参数进行编码
String data="username="+URLEncoder.encode("zhangsan")+"&password="+URLEncoder.encode("123");

URL url=new URL(path);
HttpURLConnection conn=(HttpURLConnection)url.openConnection();
conn.setRequestMethod("POST");            //设置请求方式
conn.setConnectTimeout(5000);             //设置超时时间
//设置请求头-数据提交方式,这里是以form表单方式提交
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
//设置请求头-设置提交数据的长度
conn.setRequestProperty("Content-Length", data.length()+"");

//post方式,实际上是浏览器把数据写给了服务器
conn.setDoOutput(true);                  //设置允许向外写数据
OutputStream os=conn.getOutputStream();  
os.write(data.getBytes());               //利用输出流往服务器写数据

int code=conn.getResponseCode();          //获取状态码
if(responseCode==200){                    //访问成功
    InputStream is=conn.getInputStream(); //获取服务器返回的输入流
    try{
        //读取流里面的信息
    }catch(Exception e){
    }
}

注意:使用HttpURLConnection POST方式提交数据时,是以流的方式直接将参数写到服务器上的,需要设置数据的提交方式和数据的长度。

在实际开发中,手机端和服务器进行交互的过程中,避免不了提交中文到服务器,这时就会出现中文乱码的情况。

无论是GET方式还是POST方式,提交参数时都要给参数进行编码。需要注意的是,编码方式必须与服务器解码方式统一。同样在获取服务器返回的中文数据时,也需要用指定格式进行解码。

案例:Demo0903和Demo0903Server:提交数据到服务器,演示HttpURLConnection/HttpClient分别使用Get方式和Post方式提交数据到服务器

三、开源项目

在实际开发中,使用Android自带的API与服务器通信比较麻烦。一些热心的开发者为了节约开发成本、节约开发时间,开发出了一些开源的项目方便大家使用。因此网上出现了各种各样的开源项目。

1. AsyncHttpClient

由于访问网络是一个耗时的操作,放在主线程里面会影响用户体验,因此Google规定Android 4.0以后访问网络的操作都必须放在子线程中。

但是,在Android中,发送、处理HTTP请求十分常见,如果每次和服务器进行交互都需要开启一个子线程,这样是非常麻烦的。为了解决这个问题,一些开发者开发出了一个开源项目:AsyncHttpClient,它是HttpClient的再次包装。

AsyncHttpClient的特点有:发送异步HTTP请求,HTTP请求发生在UI线程之外,内部采用了线程池来处理并发请求。

AsyncHttpClient的常用类:

  • AsyncHttpClient:用来访问网络的类
  • RequestParams:用来添加参数的类
  • AsyncHttpResponseHandler:访问网络后的回调接口

使用AsyncHttpClient:创建AsyncHttpClient的实例;设置参数;通过AsyncHttpClient的实例对象访问网络。如果访问成功则会回调AsyncHttpResponseHandler接口中的OnSucess()方法;失败则会回调OnFailure()方法。

使用AsyncHttpClient的GET方式访问网络并提交数据实例代码:

//拼接URL,注意将参数编码
String path="http://192.168.1.100:8080/web/LoginServlet?username="+URLEncoder.encode("zhang")+"&password="+URLEncoder.encode("123");

//创建AsyncHttpClient实例
AsyncHttpClient asyncHttpClient = new AsyncHttpClient();
//使用GET方式请求
asyncHttpClient.get(path, new AsyncHttpResponseHandler() {
    //请求成功
    public void onSuccess(String content) {
        super.onSuccess(content);
        Toast.makeText(MainActivity.this, "请求成功"+content, Toast.LENGTH_SHORT).show();
    }
    //请求失败
    public void onFailure(Throwable error, String content) {
        super.onFailure(error, content);
        Toast.makeText(MainActivity.this, "请求失败"+content, Toast.LENGTH_SHORT).show();
    }
});

使用AsyncHttpClient的POST方式访问网络并提交数据实例代码:

String path="http://192.168.1.100:8080/web/LoginServlet";
//添加参数
RequestParams params=new RequestParams();
params.put("username", "zhang");
params.put("password", "123");

//创建AsyncHttpClient实例
AsyncHttpClient asyncHttpClient = new AsyncHttpClient();
//使用GET方式请求
asyncHttpClient.post("", new RequestParams(), new AsyncHttpResponseHandler(){
    //请求成功
    @Override
    public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
        super.onSuccess(statusCode, headers, responseBody);
        Toast.makeText(MainActivity.this, "请求成功"+new String(responseBody), Toast.LENGTH_SHORT).show();
    }
    //请求失败
    @Override
    public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
        super.onFailure(statusCode, headers, responseBody, error);
        Toast.makeText(MainActivity.this, "请求失败"+new String(responseBody), Toast.LENGTH_SHORT).show();
    }
});

从上面的实例代码可以看出,使用AsyncHttpClient访问网络并不需要创建子线程,而且不需要切换线程更新UI,使用起来更加方便。

2. SmartImageView

应用市场上的常见软件都加载了大量的网络上的图片。如果使用Android自带的API实现这一功能,首先需要请求网络,然后获取服务器返回的图片信息,转换成输入流,使用BitmapFactory生成Bitmap对象,最后再设置到指定的控件中,这种操作步骤十分麻烦而且耗时。为了,开发者们开发了一个开源项目:SmartImageView。它继承自ImageView,支持根据URL地址加载图片、支持异步加载图片、支持图片缓存等。

使用SmartImageView加载网络图片的实例代码:

(1) 在布局文件中添加SmartImageView控件

<com.loopj.android.image.SmartImageView
    android:id="@+id/siv_icon"
    android:layout_width="80dip"
    android:layout_height="60dip"
    android:layout_alignParentLeft="true"
    android:layout_marginBottom="5dip"
    android:layout_marginLeft="5dip"
    android:layout_marginTop="5dip"
    android:scaleType="centerCrop"
    android:src="@drawable/ic_launcher" />

(2) 在Activity中使用SmartImageView控件

SmartImageView siv = (SmartImageView) view.findViewById(R.id.siv_icon);
//SmartImageView加载指定路径图片
siv.setImageUrl(newsInfo.getIconPath(), R.drawable.ic_launcher, R.drawable.ic_launcher);

在加载指定图片时,setImageUrl()方法指定了图片的路径,加载中显示的图片以及加载失败时显示的图片。

案例:Demo0904,新闻客户端,演示AsyncHttpClient和SmartImageView的综合使用,该案例使用AsyncHttpClient实现获取服务器的XML文件并将其解析出来捆绑显示到ListView上,然后使用SmartImageView获取网络图片

四、多线程下载

下载一个文件时,使用多线程下载速度通常比使用单线程下载速度快。

使用多线程下载资源时,先要获取到服务器资源文件的大小,然后在本地创建一个与服务器资源一样大的文件,接着在客户端开启若干个线程去下载服务器资源。需要注意的是,每个线程必须要下载对应的模块,然后将每个线程下载的模块按顺序组装成资源文件。


图 9-3

图9-3展示了多线程下载的原理:从图中可以看出,每个线程下载的区域就是总大小/线程个数,但不能保证每个文件都可以完全平均分配资源,因此最后一个线程需要下载到文件的末尾。

需要注意的是,使用多线程下载时,需要在请求头中设置Range字段获取到指定位置的数据,例如Range:byte=100-200

案例:Demo0905,多线程下载文件,并将每个线程的下载进度显示在界面上

分享到 评论