- 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,多线程下载文件,并将每个线程的下载进度显示在界面上