阅读郭林《第一行代码》的笔记——第10章 看看精彩的世界,使用网络技术

1、WebView的用法

下面是一个很简单的例子,就是显示百度首页。

布局文件:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <WebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

LinearLayout>

启动类:

package com.test.webview;

import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.test.R;

/**
* Created by Administrator on 2016/5/13 0013.
*/
public class WebActivity extends Activity {
    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web);
        webView = (WebView) findViewById(R.id.web_view);
        webView.getSettings().setJavaScriptEnabled(true);//让WebView支持JavaScript脚本
        webView.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);//根据传入的参数再去加载新的网页
                return true;//表示当前WebView可以处理打开新网页的请求,不用借助系统浏览器
            }
        });
        webView.loadUrl("http://www.baidu.com");
    }

}

最后别忘了添加访问网络的权限:


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

2、使用HTTP协议访问网络

对于HTTP协议,你只需要稍微了解一些就足够了,它的工作原理特别的简单,就是客户端向服务器发出一条HTTP请求,服务器收到请求之后会返回一些数据给客户端,然后客户端再对这些数据进行解析和处理就可以了。是不是非常简单?一个浏览器的基本工作原理也就是如此了。在Android上发送HTTP请求的方式一般有两种,HttpURLConnection和HttpClient。
使用HttpURLConnection
下面看一个例子,
布局文件:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/send_request"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send request" />

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/response"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    ScrollView>

LinearLayout>

启动类:

package com.test.http;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.test.R;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

/**
* Created by Administrator on 2016/5/13 0013.
*/
public class HttpURLConnectionActivity extends Activity implements View.OnClickListener {
    private static final int SHOW_RESPONSE = 0;
    private Button btnSend;
    private TextView txtResponse;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_http_urlconnection);

        btnSend = (Button) findViewById(R.id.send_request);
        txtResponse = (TextView) findViewById(R.id.response);
        btnSend.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.send_request) {
            SendRequestWithHttpURLConnection();
        }
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SHOW_RESPONSE:
                    String response = (String) msg.obj;
                    //在这里进行UI操作,将结果显示到界面上
                    txtResponse.setText(response);
                    break;
            }
        }
    };

    private void SendRequestWithHttpURLConnection() {
        //开启线程来发起网络请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection conn = null;
                try {
                    //new一个URL对象,并传入目标的网络地址
                    URL url = new URL("http://www.baidu.com");
                    //调用一下openConnection()方法得到了HttpURLConnection的实例
                    conn = (HttpURLConnection) url.openConnection();
                    //设置HTTP请求所使用的方法,常用的方法主要有两个,GET和POST。GET表示希望从服务器那里获取数据,而POST则表示希望提交数据给服务器。
                    conn.setRequestMethod("GET");
                    //设置连接超时
                    conn.setConnectTimeout(8000);
                    //读取超时的毫秒数
                    conn.setReadTimeout(8000);
                    //调用getInputStream()方法就可以获取到服务器返回的输入流
                    InputStream in = conn.getInputStream();
                    //下面对获取到的输入流进行读取
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    //下面是对输入流进行读取
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = reader.readLine()) != null) {
                        response.append(line);
                    }
                    Message msg = new Message();
                    msg.what = SHOW_RESPONSE;
                    //将服务器返回的结果存放到Message中
                    msg.obj = response.toString();
                    handler.sendMessage(msg);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (conn != null) {
                        //调用disconnect()方法将这个HTTP连接关闭掉
                        conn.disconnect();
                    }
                }

            }
        }).start();
    }

}

声明权限:


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

上面是GET方式(即从服务器那里获取数据),那么如果是想要提交数据给服务器应该怎么办呢?其实也不复杂,只需要将HTTP请求的方法改成POST,并在获取输入流之前把要提交的数据写出即可。注意每条数据都要以键值对的形式存在,数据与数据之间用&符号隔开,比如说我们想要向服务器提交用户名和密码,就可以这样写:

connection.setRequestMethod(" POST");
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.writeBytes("username=admin&password=123456");

使用HttpClient
HttpClient是Apache提供的HTTP网络访问接口,从一开始的时候就被引入到了Android API中。它可以完成和HttpURLConnection几乎一模一样的效果,但两者之间的用法却有较大的差别,下面通过一个例子来说明:

布局文件:
和上个例子一模一样。

启动类:

声明权限:
和上个例子一模一样。

上面是GET方式(即从服务器那里获取数据),那么如果是发起一条POST请求会比GET稍微复杂一点,我们需要创建一个HttpPost对象,并传入目标的网络地址,如下所示:

HttpPost http Post = new Http Post ("http://www.baidu.com");

然后通过一个NameValuePair集合来存放待提交的参数,并将这个参数集合传入到一个 UrlEncodedFormEntity中,然后调用HttpPost的setEntity()方法将构建好的 UrlEncodedFormEntity传入,如下所示:

List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("username", "admin"));
params.add(new BasicNameValuePair("password", "123456"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "utf-8");
httpPost.setEntity(entity);

接下来的操作就和HttpGet一样了。

3、解析XML格式数据

通常情况下,每个需要访问网络的应用程序都会有一个自己的服务器,我们可以向服务器提交数据,也可以从服务器上获取数据。不过这个时候就出现了一个问题,这些数据到底要以什么样的格式在网络上传输呢?随便传递一段文本肯定是不行的,因为另一方根本就不会知道这段文本的用途是什么。因此,一般我们都会在网络上传输一些格式化后的数据,这种数据会有一定的结构规格和语义,当另一方收到数据消息之后就可以按照相同的结构规格进行解析,从而取出他想要的那部分内容。
在网络上传输数据时最常用的格式有两种,XML和JSON。

(1)Pull解析方式

在上面一节的例子上加代码,详见代码注释:

public class MainActivity extends Activity implements OnClickListener {
    ……
    private void sendRequestWithHttpClient() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    HttpClient httpClient = new DefaultHttpClient();
                         // 指定访问的服务器地址是电脑本机
                     HttpGet httpGet = new HttpGet("http://10.0.2.2/get_data.xml");
                    HttpResponse httpResponse = httpClient.execute(httpGet);
                    if (httpResponse.getStatusLine().getStatusCode() == 200) {
                            // 请求和响应都成功了
                        HttpEntity entity = httpResponse.getEntity();
                        String response = EntityUtils.toString(entity, "utf-8");
                         parseXMLWithPull(response);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //解析服务器返回的数据
    private void parseXMLWithPull(String xmlData) {
        try {
//获取到一个XmlPullParserFactory的实例
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
//借助这个实例得到XmlPullParser对象
            XmlPullParser xmlPullParser = factory.newPullParser();
//调用XmlPullParser的setInput()方法将服务器返回的XML数据设置进去就可以开始解析了
            xmlPullParser.setInput(new StringReader(xmlData));
//通过getEventType()可以得到当前的解析事件
            int eventType = xmlPullParser.getEventType();
            String id = "";
            String name = "";
            String version = "";
//在一个while循环中不断地进行解析,如果当前的解析事件不等于XmlPullParser.END_DOCUMENT,说明解析工作还没完成
            while (eventType != XmlPullParser.END_DOCUMENT) {
//通过getName()方法得到当前结点的名字
                String nodeName = xmlPullParser.getName();
                switch (eventType) {
                // 开始解析某个结点
//如果发现结点名等于id、name或version,就调用nextText()方法来获取结点内具体的内容
                case XmlPullParser.START_TAG: {
                    if ("id".equals(nodeName)) {
//调用next()方法后可以获取下一个解析事件
                        id = xmlPullParser.nextText();
                    } else if ("name".equals(nodeName)) {
                        name = xmlPullParser.nextText();
                    } else if ("version".equals(nodeName)) {
                        version = xmlPullParser.nextText();
                    }
                    break;
                }
                // 完成解析某个结点,每当解析完一个app结点后就将获取到的内容打印出来
                case XmlPullParser.END_TAG: {
                    if ("app".equals(nodeName)) {
                        Log.d("MainActivity", "id is " + id);
                        Log.d("MainActivity", "name is " + name);
                        Log.d("MainActivity", "version is " + version);
                    }
                    break;
                }
                default:
                    break;
                }
                eventType = xmlPullParser.next();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

(2)SAX解析方式

SAX解析也是一种特别常用的XML解析方式,虽然它的用法比Pull解析要复杂一些,但在语义方面会更加的清楚。
通常情况下我们都会新建一个类继承自DefaultHandler,并重写父类的五个方法,如下所示:

public class ContentHandler extends DefaultHandler {

    private String nodeName;

//我们首先给id、name和version结点分别定义了一个StringBuilder对象
    private StringBuilder id;
    private StringBuilder name;
    private StringBuilder version;

//startDocument()方法会在开始XML解析的时候调用
    @Override
    public void startDocument() throws SAXException {
//并在startDocument()方法里对它们进行了初始化
        id = new StringBuilder();
        name = new StringBuilder();
        version = new StringBuilder();
    }

//startElement()方法会在开始解析某个结点的时候调用,其中localName参数记录着当前结点的名字,这里我们把它记录下来。
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
          // 记录当前结点名
        nodeName = localName;
    }

//characters()方法会在获取结点中内容的时候调用,根据当前的结点名进行判断,将解析出的内容添加到哪一个StringBuilder对象中。
//需要注意的是,在获取结点中的内容时,characters()方法可能会被调用多次,一些换行符也被当作内容解析出来,我们需要针对这种情况在代码中做好控制。
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
          // 根据当前的结点名判断将内容添加到哪一个StringBuilder对象中
        if ("id".equals(nodeName)) {
            id.append(ch, start, length);
        } else if ("name".equals(nodeName)) {
            name.append(ch, start, length);
        } else if ("version".equals(nodeName)) {
            version.append(ch, start, length);
        }
    }

//endElement()方法会在完成解析某个结点的时候调用
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
//如果app结点已经解析完成,就打印出id、name和version的内容
//需要注意的是,目前id、name和version中都可能是包括回车或换行符的,因此在打印之前我们还需要调用一下trim()方法,
//并且打印完成后还要将StringBuilder的内容清空掉,不然的话会影响下一次内容的读取。
        if ("app".equals(localName)) {
            Log.d("ContentHandler", "id is " + id.toString().trim());
            Log.d("ContentHandler", "name is " + name.toString().trim());
            Log.d("ContentHandler", "version is " + version.toString().trim());
              // 最后要将StringBuilder清空掉
            id.setLength(0);
            name.setLength(0);
            version.setLength(0);
        }
    }

//endDocument()方法会在完成整个XML解析的时候调用
    @Override
    public void endDocument() throws SAXException {
    }

}

接下来的工作就非常简单了,修改MainActivity中的代码,如下所示:

public class MainActivity extends Activity implements OnClickListener {
    ……
    private void sendRequestWithHttpClient() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    HttpClient httpClient = new DefaultHttpClient();
                        // 指定访问的服务器地址是电脑本机
                    HttpGet httpGet = new HttpGet("http://10.0.2.2:8080/ get_data.xml");
                    HttpResponse httpResponse = httpClient.execute(httpGet);
                    if (httpResponse.getStatusLine().getStatusCode() == 200) {
                            // 请求和响应都成功了
                        HttpEntity entity = httpResponse.getEntity();
                        String response = EntityUtils.toString(entity, "utf-8");
                         parseXMLWithSAX(response);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    ……
//调用parseXMLWithSAX()方法来解析XML数据
    private void parseXMLWithSAX(String xmlData) {
        try {
//创建了一个SAXParserFactory的对象
            SAXParserFactory factory = SAXParserFactory.newInstance();
//再获取到XMLReader对象
            XMLReader xmlReader = factory.newSAXParser().getXMLReader();
            ContentHandler handler = new ContentHandler();
// 将ContentHandler的实例设置到XMLReader中
            xmlReader.setContentHandler(handler);
// 开始执行解析
            xmlReader.parse(new InputSource(new StringReader(xmlData)));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4、解析JSON格式数据

比起XML,JSON的主要优势在于它的体积更小,在网络上传输的时候可以更省流量。但缺点在于,它的语义性较差,看起来不如XML直观。

(1)使用JSONObject(谷歌官方提供)

解析方法:

private void parseJSONWithJSONObject(String jsonData) {
        try {
//将服务器返回的数据传入到了一个JSONArray对象中
            JSONArray jsonArray = new JSONArray(jsonData);
//然后循环遍历这个JSONArray,从中取出的每一个元素都是一个JSONObject对象,
//每个JSONObject对象中又会包含id、name和version这些数据。
//接下来只需要调用getString()方法将这些数据取出,并打印出来即可。
            for (int i = 0; i < jsonArray.length(); i++) {
                JSONObject jsonObject = jsonArray.getJSONObject(i);
                String id = jsonObject.getString("id");
                String name = jsonObject.getString("name");
                String version = jsonObject.getString("version");
                Log.d("MainActivity", "id is " + id);
                Log.d("MainActivity", "name is " + name);
                Log.d("MainActivity", "version is " + version);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

(2)使用GSON(谷歌的开源库)

GSON并没有被添加到Android官方的API中,因此如果想要使用这个功能的话,则必须要在项目中添加一个GSON的Jar包。首先我们需要将GSON的资源压缩包下载下来,下载地址是:http://code.google.com/p/google-gson/downloads/list。

GSON库究竟是神奇主要就是可以将一段JSON格式的字符串自动映射成一个对象,从而不需要我们再手动去编写代码进行解析了。

比如说一段JSON格式的数据如下所示:

{"name":"Tom","age":20}

那我们就可以定义一个Person类,并加入name和age这两个字段,然后只需简单地调用如下代码就可以将JSON数据自动解析成一个Person对象了:

Gson gson = new Gson();
Person person = gson.fromJson(jsonData, Person.class);

如果需要解析的是一段JSON数组会稍微麻烦一点,我们需要借助TypeToken将期望解析成的数据类型传入到fromJson()方法中,如下所示:

List people = gson.fromJson(jsonData, new TypeToken<List>() {}.getType());

5、网络编程的最佳实践(利用Java的回调机制来将服务器响应的数据进行返回)

通常情况下我们都应该将这些通用的网络操作提取到一个公共的类里,并提供一个静态方法,当想要发起网络请求的时候只需简单地调用一下这个方法即可。
比如使用如下的写法:

public class HttpUtil {

    public static String sendHttpRequest(String address) {
        HttpURLConnection connection = null;
        try {
            URL url = new URL(address);
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(8000);
            connection.setReadTimeout(8000);
            connection.setDoInput(true);
            connection.setDoOutput(true);
            InputStream in = connection.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            return response.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return e.getMessage();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }

}

以后每当需要发起一条HTTP请求的时候就可以这样写:

String address = "http://www.baidu.com";
String response = HttpUtil.sendHttpRequest(address);

在获取到服务器响应的数据后我们就可以对它进行解析和处理了。但是需要注意,网络请求通常都是属于耗时操作,而sendHttpRequest()方法的内部并没有开启线程,这样就有可能导致在调用sendHttpRequest()方法的时候使得主线程被阻塞住。
你可能会说,很简单嘛,在sendHttpRequest()方法内部开启一个线程不就解决这个问题了吗?其实不是像你想象中的那么容易,因为如果我们在sendHttpRequest()方法中开启了一个线程来发起HTTP请求,那么服务器响应的数据是无法进行返回的,所有的耗时逻辑都是在子线程里进行的,sendHttpRequest()方法会在服务器还来得及响应的时候就执行结束了,当然也就无法返回响应的数据了。
那么遇到这种情况应该怎么办呢?其实解决方法并不难,只需要使用Java的回调机制就可以了,下面就让我们来学习一下回调机制到底是如何使用的。
首先需要定义一个接口,比如将它命名成HttpCallbackListener,代码如下所示:

public interface HttpCallbackListener {

    void onFinish(String response);

    void onError(Exception e);

}

可以看到,我们在接口中定义了两个方法,onFinish()方法表示当服务器成功响应我们请求的时候调用,onError()表示当进行网络操作出现错误的时候调用。这两个方法都带有参数,onFinish()方法中的参数代表着服务器返回的数据,而onError()方法中的参数记录着错误的详细信息。
接着修改HttpUtil中的代码,如下所示:

public class HttpUtil {

    public static void sendHttpRequest(final String address, final HttpCallbackListener listener) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                try {
                    URL url = new URL(address);
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    connection.setDoInput(true);
                    connection.setDoOutput(true);
                    InputStream in = connection.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = reader.readLine()) != null) {
                        response.append(line);
                    }
                     if (listener != null) {
                        // 回调onFinish()方法
                        listener.onFinish(response.toString());
                    }
                } catch (Exception e) {
                     if (listener != null) {
                        // 回调onError()方法
                        listener.onError(e);
                    }
                } finally {
                    if (connection != null) {
                        connection.disconnect();
                    }
                }
            }
        }).start();
    }

我们首先给sendHttpRequest()方法添加了一个HttpCallbackListener参数,并在方法的内部开启了一个子线程,然后在子线程里去执行具体的网络操作。注意子线程中是无法通过return语句来返回数据的,因此这里我们将服务器响应的数据传入了HttpCallbackListener的onFinish()方法中,如果出现了异常就将异常原因传入到onError()方法中。
现在sendHttpRequest()方法接收两个参数了,因此我们在调用它的时候还需要将HttpCallbackListener的实例传入,如下所示:

HttpUtil.sendHttpRequest(address, new HttpCallbackListener() {
    @Override
    public void onFinish(String response) {
          // 在这里根据返回内容执行具体的逻辑
    }

    @Override
    public void onError(Exception e) {
          // 在这里对异常情况进行处理
    }
});

这样的话,当服务器成功响应的时候我们就可以在onFinish()方法里对响应数据进行处理了,类似地,如果出现了异常,就可以在onError()方法里对异常情况进行处理。如此一来,我们就巧妙地利用回调机制将响应数据成功返回给调用方了。
另外需要注意的是,onFinish()方法和onError()方法最终还是在子线程中运行的,因此我们不可以在这里执行任何的UI操作,如果需要根据返回的结果来更新UI,则仍然要使用上一章中我们学习的异步消息处理机制。

你可能感兴趣的:(阅读郭林《第一行代码》的笔记)