Android | 网络技术基础梳理——WebView以及HTTP运用、XML以及JSON解析(demo+bug)

本文目录

  • 1.WebView的用法

    1. 使用HTTP协议访问网络
    • 2.1 使用HttpURLConnection
    • 2.2 使用OkHttp
  • 3.解析XML格式数据

    • 3.1 Pull解析方式
    • 3.2 SAX解析方式
  • 4.解析JSON数据

    • 4.1使用JSONObject
    • 4.2 使用GSON
  • 5.网络编程的最佳实践——HttpUtil封装(巧用回调机制、框架)

1.WebView的用法
  • 使用WebView控件,
    借其在自己的应用程序中嵌入一个浏览器
    以轻松展示各种网页

新建一个WebViewTest项目,
修改activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

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

</LinearLayout>

接下来修改MainActivity.java:

public class MainActivity extends AppCompatActivity {

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

        WebView webView = (WebView) findViewById(R.id.web_view);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("http://www.baidu.com");
    }
}
  • 首先是findViewById实例化对象;
  • getSettings()用来设置浏览器属性;
    setJavaScriptEnabled(true)让WebView支持JavaScript脚本;
  • 调用WebView 的setWebViewClient()方法,
    传入一个WebViewClient实例,
    作用是:使当需要从一个网页跳转到另外一个网页时,
    目标网页仍然在当前WebView中显示,而不是打开系统浏览器;
  • loadUrl()传入网址,显示网页内容;

接下来,还需在AndroidManifest.xml中添加访问网络的权限

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.webviewtest">
    
    <uses-permission android:name="android.permission.INTERNET"/>

...

</manifest>

至此,即可运行程序了,其效果如下:

9125154-8087f89e91ad213e.png

当然还要注意一点,如果你的模拟器和SDK是Android 9.0(API级别28),那运行如上代码会出现下面这个问题:

9125154-d2387ace6c44409a.png

原因是从Android 9.0(API级别28)开始,默认情况下禁用明文支持。
因此http的url均无法在webview中加载。

解决方法是在AndroidManifest.xml对应的地方加入一句代码即可:
9125154-4f8ba6215b28ba25.png
android:usesCleartextTraffic="true"

解决之后便可以运行成功了:
9125154-68bf65d3fd74faa6.png

当然,小伙伴们,生活往往没那么简单,
百度搜索引擎框下面有很多吸引我们眼球的文章对吧,

你会发现当你随便点击一篇文章,想要跳转过去的时候,会出现下方这种报错:
9125154-b406a9df5ac2fcd1.png

亦或者:
9125154-fc6ccb8be4ec6f1a.png

莫慌,其实都是一个道理,仔细看一下报错,我们可以发现url的前缀都被替换了:
9125154-0a6903f83f90bca8.png
9125154-2c416cde6a67486f.png
  • 这是因为其自定义了scheme
    类似的还有alipays://weixin://等等。
    webView只能识别http://https://开头的url,因此才会报此错。
    处理方法,对于这种自定义scheme的url 单独处理即可。

修改代码如下:
我们刚刚写了这段代码对吧:
webView.setWebViewClient(new WebViewClient())
现在,
把传入的WebViewClient实例变成一个以WebViewClient父类匿名内部类
并重写shouldOverrideUrlLoading()方法,
在里面对方才报错中的自定义scheme进行单独处理即可:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        final WebView webView = (WebView) findViewById(R.id.web_view);
        ...
        webView.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {

                try{
                    if(url.startsWith("baiduboxapp://") || url.startsWith("baiduboxlite://" )){
                        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                        startActivity(intent);
                        return true;
                    }
                }catch (Exception e){
                    return false;
                }
                webView.loadUrl(url);
                return true;
            }
        });
        webView.loadUrl("http://www.baidu.com");
    }
}

ok,这时候再运行程序,便可以成功打开各种推文了:
9125154-3f498c3c59e6b082.png
9125154-9fdcefa69bc6a70e.png

参考文章



2. 使用HTTP协议访问网络
  • HTTP基于android的工作原理简述
    客户端服务器发出一条HTTP请求,
    服务器收到请求之后会返回一些数据给客户端
    然后客户端再对这些数据进行解析处理就可以。

  • 一个浏览器的基本工作原理也就是如上了。
    上面使用的WebView控件,
    其实也就是app向百度服务器发起一条HTTP请求,
    接着服务器分析出我们想要访问的是百度的首页,
    于是会把该网页的HTML代码进行返回
    然后WebView再调用手机浏览器的内核对返回的HTML代码进行解析
    最终将页面展示出来。

  • 也即WebView封装了发送HTTP请求接受服务响应解析返回数据,以及最终页面的展示这几步工作。


  • 下面暂时摆脱WebView,
    手动发送HTTP请求,直观地理解一下HTTP协议的工作过程。

2.1 使用HttpURLConnection
  • 首先,
    获取HttpURLconnection的实例:

    a. 以参数为目标的网络地址,new 出一个URL对象;
    b. url对象调用openConnection();
     返回的结果转型付给HttpURLConnection对象;
URL url = new URL("http://www.baidu.com");
HttpURLConnection connection = (HttpURLConnection)url.openConnection();


  • 得到HttpURLConnection实例之后,设置HTTP请求所使用的方法;

    常使用的方法主要有两个:GET和POST。
    GET表示希望从服务器获取数据,
    POST希望提交数据给服务器:
connection.setRequestMethod("GET");


  • 接下来进行一些自由的定制,
    如设置连接超时、读取超时的毫秒数、服务器希望得到的一些消息头等。
    这些都是自己根据实际情况进行编写:
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);


  • 之后再调用getInputStream()方法,
    就可以获取到服务器返回的输入流了,
    后续的对输入流进行读取即可:
InputStream in = connection.getInputStream();
  • 最后调用disconnect()关闭HTTP连接:
connection.disconnect();

下面新建一个名为NetworkTest的空活动,调试一下上面的知识点,
修改对应的xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".NetworkTest">

    <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_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </ScrollView>

</LinearLayout>

以上,

  • ScrollView用于滚动查看内容;
  • Button用来发送HTTP请求;
  • TextView用来显示服务器返回的数据;

接着修改NetworkTest.java:

public class NetworkTest extends AppCompatActivity implements View.OnClickListener{

    TextView responseText;

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

        Button sendRequest = (Button) findViewById(R.id.send_request);
        responseText = (TextView) findViewById(R.id.response_text);
        sendRequest.setOnClickListener(this);
    }

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

    private void sendRequestWithHttpURLConnection() {

        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;

                try {
                    URL url = new URL("https://hao.360.cn/");
                    connection = (HttpURLConnection)url.openConnection();

                    connection.setRequestMethod("GET");

                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);

                    InputStream in = connection.getInputStream();

                    //下面对获取的输入流进行读取
                    //InputStreamReader赋值给BufferedReader让其来读
                    reader = new BufferedReader(new InputStreamReader(in));
                    StringBuilder response = new StringBuilder();
                    String line;
                    //一行一行地读取并加进stringbuilder
                    while((line = reader.readLine()) != null){
                        response.append(line);
                    }
                    showResponse(response.toString());
                    Log.d("NetworkTest:  ", response.toString());
                } catch (IOException e) {
                    e.printStackTrace();
                }finally{
                    if(reader !=null){
                        try {
                            reader.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if(connection !=null){
                        connection.disconnect();
                    }
                }
            }
        }).start();

    }

    private void showResponse(final String response) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                responseText.setText(response);
            }
        });
    }
}
  • sendRequestWithHttpURLConnection()中:
    开启一个子线程,
    在子线程里使用HttpURLConnection发出一条HTTP请求,
    请求的目标地址就是百度的首页;
    接着用BufferedReader读取返回的输入流,
    转成string传给showResponse()
  • showResponse()中通过runOnUiThread()将返回的数据显示到界面上;

关于runOnUiThread()方法,
因为Android不允许在子线程中进行UI操作,
我们需要通过这个方法在子线程中将线程切换到主线程,
然后再更新UI元素。

运行效果如下:
9125154-282b39dc260988f0.png

注:
如网址不正确,
会出现
NetworkSecurityConfig: No Network Security Config specified, using platform default的提示。
另外,用手机热点也是如此!!!
只有稳定PC端接线网络或其热点才可顺利使用!!

  • 服务器返回的就是这些HTML代码,
    只是通常浏览器都会将这些代码解析成漂亮的网页再展示出来;

  • 如果想提交数据给服务器,
    只需将HTTP请求方法改成POST
    并在获取输入流之前要提交的数据写出即可。
    每条数据都要以键值对的形式存在,
    数据与数据之间用“&”符号隔开,如提交用户名和密码

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



2. 使用OkHttp
  • OkHttp由Square公司开发,其不仅在接口封装上面做的简单易用,
    就连在底层实现上也是自成一派,
    比起原生的HttpURLConnection,可以说是有过之而无不及,
    现在已经成了广大Android开发者的首选网络通信库。
  • OkHttp项目主页地址:https://github.com/square/okhttp

  • 使用之前,需添加OkHttp库依赖,
    打开app/buid.gradle,在dependencies闭包中添加如下内容:

    implementation("com.squareup.okhttp3:okhttp:3.14.0")

添加此依赖,会自动下载两个库:OkHttp库、Okio库(是前者的通信基础)。

  • 注意,添加前最好是访问一下OkHttp项目主页查看当前最新的版本是多少,再在gradle处添加依赖;
下面是OkHttp具体用法
  • 首先,需要创建OkHttpClient实例,如下:
OkHttpClient client = new OkHttpClient();
  • 接下来,如想发起一条HTTP请求,需创建Request对象
Request request = new Request.Builder().build();
  • 当然上述代码只是创建一个空的Request对象,
    需要在build()方法之前可连缀很多其他方法丰富此Request对象。
Request request = new Request.Builder()
.url("https://www.baidu.com")
.build();
  • 之后调用OkHttpCilent的newCall()方法创建一个Call对象
    并调用它的execute()方法发送请求
    获取服务器返回的数据:
Response response = client.newCall(request).execute();
  • request存请求;
  • newCall接收request
  • execute执行request
  • Response对象接收服务器返回的数据;
  • 下面得到返回的具体内容
String responseData = response.body().string();



如果发起一条POST请求,会比GET复杂些;

  • 需先构建RequestBody对象存放待提交的参数
RequestBody requestBody = new FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build();
  • 然后在Request.Builder中以RequestBody对象为传入的参数调用post()方法,:
Request request = new Request.Builder()
.url("http://www.baidu.com")
.post(requestBody)
.build();
  • 接下来的操作就和GET请求一样了,
    调用execute()方法发送请求并获取服务器返回的数据即可。

现在改用OkHttp的方式把刚刚NewworkTest的内容再实现一遍:

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

private void sendRequestWithOkHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();

                    Request request = new Request.Builder()
                            .url("https://hao.360.cn/")
                            .build();

                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    showResponse(responseData);
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }

    private void showResponse(final String response) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                responseText.setText(response);
            }
        });
    }

运行时有可能出现下面的问题,解决方法是把gradle中minsdk改成26以上即可:
9125154-dd959e923d9a6748.png

运行效果图:
9125154-18e8b323641e641d.png



3.解析XML格式数据
  • 通常,每个需要访问网络的应用程序都会有一个自己的服务器,
    我们可以向服务器提交数据或者从服务器上获取数据;
  • 为了双方能够快速知道文本的用途,一般在网络传输的数据都是格式化后的;
    这种数据会有一定的结构规格和语义;
    当另一方收到数据消息之后就可以按照相同的结构规格进行解析,从而去除他想要的那部分内容;



搭建一个本地服务器
  • 在网络上传输数据最常用的格式有两种:XMLJSON

在开始学习这两种数据格式之前,
我们还需要搭建一个本地服务器

  • 进度大概进行到
    可以在本地服务器文件夹下放置文件,
    然后在本地浏览器可以访问即可;

这里提供两种方法:

  1. 可以使用单模块原生的本地服务器Apache,
    具体的操作我之前已经写过一篇详细的博文:
9125154-4d3019bacf3d1251.png
博文剪影1
9125154-d06316cb4a8a8af2.png
博文剪影2
  1. 或者学过PHP的朋友也可以使用PhpStudy集成环境(中的Apache模块)来做服务器,具体的相关我也写过相关的博文哈:

PHPStudy的话,在如下文件途径放下文件即可:
9125154-26f194bcdac7cbf5.png

文件内容:
9125154-7fb9e8ffc4322e03.png
<apps>
    <app>
        <id>1</id>
        <name>Google Maps</name>
        <version>1.0</version>
    </app>
    <app>
        <id>2</id>
        <name>Chrome</name>
        <version>2.1</version>
    </app>
    <app>
        <id>3</id>
        <name>Google Play</name>
        <version>2.3</version>
    </app>
</apps>

接着浏览器键入localhost/get_data.xml即可访问:

9125154-13cab4b3993e690e.png

  • 当然,键入127.0.0.1/get_data.xml也是可以的:
    9125154-68c2465b01b69c23.png



  • 解析XML格式数据有很多方式,PullSAX解析是常用的两种。
3.1 Pull解析方式
  • 这里我们依旧在NetworkTest 这个活动上面做开发,重用方才网络通信的代码,把重心放在XML数据解析上;

  • 以上,我们已经准备好XML格式的数据,
    现在编写代码从中解析出我们想要得到的那部分内容;

修改NetworkTest.java:

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

    private void sendRequestWithOkHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();

                    //http://10.0.2.2/对于模拟器来说是电脑本机IP地址**
                    Request request = new Request.Builder()
                            .url("http://47.100.78.251/testPackage/get_data.xml")
                            .build();

                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();

                    parseXMLWithPull(responseData);//解析服务器返回的数据**

//                    showResponse(responseData);
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }

    private void parseXMLWithPull(String xmlData) {
        try{
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();//获取一个XmlPullParserFactory 实例
            XmlPullParser xmlPullParser = factory.newPullParser();//借助XmlPullParserFactory 实例得到 XmlPullParser对象
            xmlPullParser.setInput(new StringReader(xmlData));//XmlPullParser对象 setInput 把服务器返回的数据传入即可开始解析

            int eventType = xmlPullParser.getEventType();//通过getEventType()得到当前的解析事件
            String id = "";
            String name = "";
            String version = "";

            //如果不到文末
            //解析事件不为XmlPullParser.END_DOCUMENT,说明解析工作没完成
            responseText.setText("");

            while(eventType != XmlPullParser.END_DOCUMENT){
                //解析事件不为XmlPullParser.END_DOCUMENT,说明解析工作没完成

                String nodeName = xmlPullParser.getName();//获取当前结点名字

                switch (eventType) {
                    //开始解析某个节点
                    case XmlPullParser.START_TAG:
                        if ("id".equals(nodeName)) {
                            id = xmlPullParser.nextText();//获取节点具体内容
                        } else if ("name".equals(nodeName)) {
                            name = xmlPullParser.nextText();
                        } else if ("version".equals(nodeName)) {
                            version = xmlPullParser.nextText();
                        }
                        break;
                    //完成解析某个节点
                    case XmlPullParser.END_TAG:
                        if ("app".equals(nodeName)) {

                            //解析完一个app节点后打印获取到的内容
                            String finalId = id;
                            String finalName = name;
                            String finalVersion = version;

                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    responseText.append("MainActivity: id is " + finalId + "\n");
                                    responseText.append("MainActivity: name is " + finalName + "\n");
                                    responseText.append("MainActivity: version is " + finalVersion + "\n");
                                }
                            });

                        }
                        break;
                    default:
                        break;
                }
                eventType = xmlPullParser.next();//获取下一个解析事件
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
9125154-fdc50350190593fd.png

Pull解析的使用思路是:

  • 通过XmlPullParserFactory等一系列API,
    得到一个XmlPullParser实例,
    再把数据
    传给XmlPullParser实例xmlPullParser.setInput(new StringReader(xmlData));
    接着就可以开始解析了;

  • 把XML的文末标志、起始标签以及结束标签,
    约定为一个事件int eventType = xmlPullParser.getEventType();

    • 文末标志事件用于判断文件是否解析完,
    • 起始标签事件用于 判断 以及 获取标签节点中的内容,
    • 结束标签事件则用于 判断 以及
      去实现一个解析阶段结束后的业务逻辑;


3.2 SAX解析方式
  • 除了Pull解析,SAX解析也是一种常用的解析方式,
    其用法比Pull解析复杂一些,
    但语义上会更清楚;

用法:

  • 新建一个类继承自DefaultHandler,并重写父类5个方法。
public class MyHandler extends DefaultHandler {

    @Override
    public void startDocument() throws SAXException {
        super.startDocument();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        super.startElement(uri, localName, qName, attributes);
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        super.characters(ch, start, length);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        super.endElement(uri, localName, qName);
    }

    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
    }
}

  • startDocument()在开始XML解析时候调用;
  • startElement()在开始解析某个节点时调用;
  • characters()在获取节点中的内容时候调用;
  • endElement()在完成解析某个节点的时候调用;
  • endDocument()在完成整个XML解析时调用;
  • startElement()、characters()、endElement()三个方法是有参数的,
    从XML中解析的数据会以参数的形式传入到这些方法中;

  • 在获取节点中的内容时,
    characters()方法可能会被调用多次,
    一些换行符也被当做内容解析出来,
    我们需要针对这种情况在代码中做好控制;

实践

  • 新建一个类继承自DefaultHandler,并重写父类5个方法:
    (注意代码中的注释)
public class ContentHandler extends DefaultHandler {

    public String nodeName;

    //面向XML文件配置全局属性
    public StringBuilder id;

    public StringBuilder name;

    public StringBuilder version;

    @Override
    public void startDocument() throws SAXException {
        id = new StringBuilder();
        name = new StringBuilder();
        version = new StringBuilder();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        //记录当前节点
        nodeName = localName;
    }

    @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);
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if ("app".equals(localName)){

            // !!!!!!!!!!!
            //id\name\version中可能包含回车或换行符,需调用trim()方法除去
            // !!!!!!!!!!!
            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);
        }
    }

    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
    }
}

修改MainActivity:

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

    private void sendRequestWithOkHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();

                    //http://10.0.2.2/对于模拟器来说是电脑本机IP地址**
                    Request request = new Request.Builder()
                            .url("http://47.100.78.251/testPackage/get_data.xml")
                            .build();

                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();

//                    parseXMLWithPull(responseData);//解析服务器返回的数据**

                    parseXMLWithSAX(responseData);//解析服务器返回的数据**

//                    showResponse(responseData);
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }

    private void parseXMLWithSAX(String xmlData) {
        try {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            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();
        }
    }

运行程序,点击按钮,查看日志:
9125154-5f1c15240edd9db6.png

除了Pull和SAX,还有DOM解析方式可用;




4.解析JSON数据

  • JSON的体积比XML更小,网络传输更省流量,
    但语义性差,不如XML直观。

  • 在对应的服务器目录下,新建一个get_data.json文件,内容如下:

[{"id":"5","name":"Clash of Clans","version":"5.5"},
{"id":"6","name":"Boom Beach","version":"7.0"},
{"id":"7","name":"Clash Royale","version":"3.5"}]
4.1使用JSONObject
  • 解析JSON数据也有很多方法,可使用官方的JSONObject,
    谷歌的开源库GSON,
    或第三方的开源库如Jackson、FastJSON等.

  • 我们在服务器中定义的json文件get_data.json的内容是一个JSON数组,
    因此这里获取到服务器的数据之后,
    直接将数据传入到一个JSONArray对象中;

  • 然后循环遍历这个JSONArray,
    从中取出的每一个元素都是一个JSONObject对象;

  • 这个JSONObject对象又会包含id、name和version这些数据,
    即我们定义的json文件中的键值;

  • 接着只要调用getString()将这些数据取出来即可;

使用JSONObject,修改MainActivity:

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

    //子线程中进行!!!!!!!!!
    private void sendRequestWithOkHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();

                    //http://10.0.2.2/对于模拟器来说是电脑本机IP地址**
                    Request request = new Request.Builder()
                            .url("http://47.100.78.251/testPackage/get_data.json")
                            .build();

                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();

                    parseJSONWithJSONObject(responseData);

//                    parseXMLWithPull(responseData);//解析服务器返回的数据**

//                    parseXMLWithSAX(responseData);//解析服务器返回的数据**

//                    showResponse(responseData);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (JSONException e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }

    private void parseJSONWithJSONObject(String responseData) throws JSONException {
        //我们在服务器中定义的json文件get_data.json的内容是一个JSON数组
        JSONArray jsonArray = new JSONArray(responseData);


        responseText.setText("");
        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");

            runOnUiThread(new Runnable() {
                @Override
                public void run() {

                    responseText.append("MainActivity: id is " + id + "\n");
                    responseText.append("MainActivity: name is " + name + "\n");
                    responseText.append("MainActivity: version is " + version + "\n");
                }
            });

        }
    }

运行程序:
9125154-66eeba09cae4a990.png


4.2 使用GSON
  • 添加依赖:
    implementation 'com.google.code.gson:gson:2.8.5'
  • 它主要可以将一段JSON格式的字符串自动映射成一个对象(定义一个类对应),
    不需手动编写代码解析。

  • 比如说一段json格式的数据如下所示:
    {"name":"Tom","age":20}
    则定义一个Person类,
    加入name和age这两个字段;

面向需要解析的json数据定义JavaBean类
如果一个json数据有十几几十个键值对
而我们的业务只需要取其中的5个键值
那这个JavaBean类,就定义需要的5个字段即可,
Gson会将json数据字符串
根据我们定义JavaBean类
提取出相应的数据并映射对应的List
json字符串中有多少套JavaBean类字段对应的键值
映射得到的Listsize就有多少;

  • 接着简单调用如下代码即可将JSON数据
    自动解析成一个Person对象了:
Gson gson = new Gson();
Person person = gson.fromJson(jsonData, Person.class);
  • 如果需要解析的是一段JSON数组会稍微麻烦一点,
    需要借助TypeToken将期望解析成的数据类型传入到fromJson()方法中,如:
    List<Person> people = gson.fromJson(jsonData, new TypeToken<List<Person>>(){}.getType());

基本用法就如上所述了,
接下来用GSON解析一下一开始的数据;

  • 首先新建一个App类:
public class App {
    private String id;
    private String name;
    private String version;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }
}

修改MainActivity:


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

    //!!!!!!!!
    //注意这里在子线程中进行!!
    // UI更新需要切到主线程
    // !!!!!!!
    private void sendRequestWithOkHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();

                    //http://10.0.2.2/对于模拟器来说是电脑本机IP地址**
                    Request request = new Request.Builder()
                            .url("http://47.100.78.251/testPackage/get_data.json")
                            .build();

                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();

//                    parseJSONWithJSONObject(responseData);

                    parsJSONWithGSON(responseData);

//                    parseXMLWithPull(responseData);//解析服务器返回的数据**

//                    parseXMLWithSAX(responseData);//解析服务器返回的数据**

//                    showResponse(responseData);
                } catch (IOException e) {
                    e.printStackTrace();
                }
//                catch (JSONException e) {
//                    e.printStackTrace();
//                }

            }
        }).start();
    }

    private void parsJSONWithGSON(String jsonData) {
        Gson gson = new Gson();
        List<App> appList = gson.fromJson(jsonData, new TypeToken<List<App>>(){}.getType());

        for (App app : appList) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {

                    responseText.append("MainActivity: id is " + app.getId() + "\n");
                    responseText.append("MainActivity: name is " + app.getName() + "\n");
                    responseText.append("MainActivity: version is " + app.getVersion() + "\n");
                }
            });
        }
    }

运行程序,效果同上。





5.网络编程的最佳实践

  • 方法提取
    应用程序很可能会在许多地方都使用网络功能,
    而发送HTTP请求的代码基本相同,
    所以我们不能每次都去编写一遍发送HTTP请求的代码,
    通常应该把通用的网络操作提取到一个公共类里,
    并提供一个静态方法,
    当想要发起网络请求的时候,
    只需要简单地调用一下这个方法即可。

    耗时操作
    另外,
    网络请求通常都是属于耗时操作
    我们提取的发送HTTP请求的方法内部
    如果没有开启子线程,
    则有可能导致在调用的时候使得主线程阻塞
    这里则需开启子线程来发起HTTP请求,

    数据返回
    另外还要考虑到,
    如果我们在一个请求方法内部的
    开启了一个子线程来发送HTTP请求,
    服务器响应的数据是无法进行返回的,
    所有的耗时逻辑都是在子线程里进行的,
    这个请求方法会在服务器还没来得及响应的时候就执行结束了,
    当然也就无法返回响应的数据了;
  • 遇到这种既需要子线程来处理耗时操作
    又要求能实时接收到服务器响应到的数据的情况,
    可以考虑使用Java的回调机制来实现:
  • 实现一个接口就是写一个插座,
    把封装的东西写进实现接口的类中,
    把这个(匿名内部)类赋给回调方法(如setOnClickListener())
  • 内部抽象调用,外部具体实现(的方法);
    内部只管调用
    外部只管实现(具体的处理方法、逻辑)
  • 连接处:
    外部实现的方法,
    封装在一个匿名内部类接口实现类实例中,
    实例传给抽象调用的工具类设置方法或者构造方法
    实现内外连接

    这样内部调用的接口方法
    就是源自外部传进来
    接口实例
    (在外部具体实现好的)接口方法(逻辑)

    数据在内部抽象调用时传递给内部 未具体实现的 接口方法
    这里抽象调用未具体实现的 接口方法实际上在程序运行使用时,
    就是外部具体实现的接口及其方法(处理逻辑)
    (来自set传进来的 外部实现好诸接口方法接口实例
    如此一来数据便通过回调机制
    由内而外间接传递给了外部(给外部处理)

    小结:
    内部形式抽象调用
    外部具体逻辑实现
    外部实现封装在接口类实例中传进来,
    使得内部形式调用能够调用到了外部具体逻辑实现
  • 首先需要定义一个接口,这里取名HttpCallbackListener
    onFinish(String response)
    当服务器成功响应请求时调用,参数为服务器返回的数据;
    onError(Exception e)
    当进行网络操作出现错误时调用,参数记录错误的详细信息;
public interface HttpCallbackListener {
    void onFinish(String response);
    void onError(Exception e);
}
  • 接着新建一个刚刚说的放着提取了通用网络操作公共类
    listener.onFinish(response.toString());
    回调外部传进来写好
    匿名内部接口类具体实现好的方法,
    这里公共类抽象调用
    调用公共类方法的地方具体实现接口类(常用匿名内部类方式实现),
    实现好了赋到这里来
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语句来返回数据的,
      因此这里将服务器响应的数据
      传入了HttpCallbackListeneronFinish()方法中,
      在调用者(调用公共类方法者)处的接口(匿名)实现类中处理,
      调用刚刚说的在外部(调用者处)
      实现好接口(匿名)实现类实例中的具体的onFinish()方法
      异常原因
      传入了HttpCallbackListeneronError()方法中,
      在调用者(调用公共类方法者)处的接口(匿名)实现类中处理,
      调用刚刚说的在外部(调用者处)
      实现好接口(匿名)实现类实例中的具体的onError()方法

  • 公共类调用案例:(如上所述)利用回调机制将响应数据成功返回给调用方:

String address = "http://www/baidu.com";
HttpUtil.sendHttpRequest(address, new HttpCallbackListener(){
            @Override
            public void onFinish(String response){
                //在这里根据返回内容执行具体的逻辑
            }
            @Override
            public void onError(Exception e){
                //在这里对异常情况进行处理
            }
        });
  • 使用HttpURLConnection的写法总体比较复杂;
    使用OkHttp会简单一些;

  • 在HttpUtil中加入一个sendOkHttpRequest()方法:

public static void sendOkHttpRequest(String address, okhttp3.Callback callback) {
        OkHttpClient client = new OkHttpClient();
        
        Request request = new Request.Builder()
                .url(address)
                .build();

        client.newCall(request).enqueue(callback);
    }
  • 注意:
    方法中有一个okhttp3.Callback参数,
    这个是OkHttp库中自带的一个回调接口,
    类似于我们刚刚自己编写的HttpCallbackListener

    然后在client.newCall()之后,
    没有像之前那样直接调用execute()
    而是调用了enqueue()
    并把okhttp3.Callback参数传入,

    OkHttpenqueue()中已经帮我们开好了子线程
    在子线程中去执行HTTP请求
    并将最后的请求结果回调到okhttp3.Callback
    (也就是说,
    我们刚刚在sendHttpRequest()做的事情,
    子线程、请求、数据返回
    OkHttp都帮我们做好了)

    最后,
    我们在外部实例化一个接口对象并具体实现方法,
    再把接口实例传进来sendOkHttpRequest()
    赋值给对应的enqueue()方法,
    完成任务!





参考自《第一行代码》

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凌川江雪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值