8月9日 第 10 章 看看精彩的世界,使用网络技术

1.WebView的用法

新建一个 WebViewTest项目,然后修改activity_main.xml中的代码

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

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

我们在布局文件中使用到了一个新的控件:WebView。这个控件就是用来显示网页 的,这里的写法很简单,给它设置了一个id,并让它充满整个屏幕。

然后修改MainActivity中的代码

package com.example.webviewtest;

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

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

import java.util.WeakHashMap;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        WebView webView = findViewById(R.id.webView);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("https://www.baidu.com");
    }
}

首先获得了webview示例调用了setJavaScriptEnabled()方法,让 WebView支持JavaScript脚本。

调用了WebView的setWebViewClient()方法,并传入 了一个WebViewClient的实例。需要从一个网页跳转到另一个网页时, 我们的目标网页仍然在当前WebView中显示,而不是打开系统浏览器。

调用WebView的loadUrl()方法,将网址传入,即可展示相应网页的内容

由于本程序使用到了网络功能,访问网络需要声明权限

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

2.使用HTTP访问网络

HTTP,它的工作原理特别简单,就是客户端向服务器发出一条HTTP请求,服务器收到请求之后会返回一些数据给客户端,然后客户端再对这些数据进行解析和处理就可以了。

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

接下来通过手动发送HTTP请求的方式来更加深入地理解这个过程。

1.使用HttpURLConnection

新建一个 NetworkTest项目,首先修改activity_main.xml中的代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/sendRequestBtn"
        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/responseText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </ScrollView>

</LinearLayout>

这里我们使用了一个新的控件:ScrollView。

由于手机屏幕的空间一般比较小,有些时候过多的内容一屏是显示不下的,借助ScrollView控件,我们就可以以滚动的形式查看屏幕外的内容。

布局中还放置了一个Button和一个TextView,Button 用于发送HTTP请求,TextView用于将服务器返回的数据显示出来。

接着修改MainActivity中的代码

package com.example.networktest;

import android.os.Bundle;
import android.os.HandlerThread;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

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


public class MainActivity extends AppCompatActivity {

    TextView responseText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        responseText = (TextView) findViewById(R.id.responseText);
        Button button = findViewById(R.id.sendRequestBtn);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                sendRequestWithHttpURLConnection();
            }
        });
    }

    private void sendRequestWithHttpURLConnection() {
        // 开启线程来发起网络请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("https://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream in = connection.getInputStream();
                    // 下面对获取到的输入流进行读取
                    reader = new BufferedReader(new InputStreamReader(in));
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = reader.readLine()) != null) {
                        response.append(line);
                    }
                    showResponse(response.toString());
                } catch (Exception 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() {
                // 在这里进行 UI 操作,将结果显示到界面上
                responseText.setText(response);
            }
        });
    }



}

我们在 Send Request 按钮的点击事件里调用了 sendRequestWithHttpURLConnection()方法,

在这个方法中开启了一个子线程,在子线程里使用 HttpURLConnection 发出一条 HTTP 请求,请求的目标地址是百度首页。利用 BufferedReader 对服务器返回的流进行读取,并将结果传到 showResponse()方法中。

showResponse() 方法里则是调用了一个 runOnUiThread()方法,在这个方法的匿名类参数中进行操作,将返回的数据显示到界面上。

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

声明一下网络权限

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

运行,程序崩溃了,并且在logcat中出现了如下的报错信息

Installing profile for com.example.networktest
queryResolvNetidForUid nid:0, uid:10254, command.cmdId:15
[Posix_connect Debug]Process com.example.networktest :443 
netdClientConnect netId:0, uid:10254, nid:0, addr:240e:ff:e020:966:0:ff:b042:f296, lan:0, localhost:0, sa_family:10, sockfd:78, thread:Thread-3, threadsocketid:0
Davey! duration=12471ms; Flags=0, IntendedVsync=15854330214904, Vsync=15854330214904, OldestInputEvent=9223372036854775807, NewestInputEvent=0, HandleInputStart=15854330963377, AnimationStart=15854330969784, PerformTraversalsStart=15854331368065, DrawStart=15866799654362, SyncQueued=15866800221081, SyncStart=15866800704570, IssueDrawCommandsStart=15866800747487, SwapBuffers=15866801434675, FrameCompleted=15866802046029, DequeueBufferDuration=133073, QueueBufferDuration=207135, GpuCompleted=35192962023424, 
Skipped 747 frames!  The application may be doing too much work on its main thread.

我反复检查了代码最后选择直接复制第二版的代码粘贴覆盖我的代码,上文中的代码是复制后的代码,依然报错

经过搜索,貌似是应用在主线程上执行了大量工作,导致了帧跳过(Skipped 747 frames)

好吧,经过数小时的搜索查询以及尝试,问题出在布局上,我不知道为什么不行,但是我将布局修改为相对布局就成功了。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/sendRequestBtn"
        tools:ignore="MissingConstraints">


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

    <Button
        android:id="@+id/sendRequestBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Send Request"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>

服务器返回给我们的是这种 HTML 代码

通常情况下浏览器 会将这些代码解析成漂亮的网页后再展示出来

如果想要提交数据给服务器,只需要将 HTTP 请求的方法改成 POST,并在获取输入流之前把要提交的数据写出即可。

每条数据都要以键值对的形式存在,数据与数据之间用“&”符号隔开,比如说我们想要向服务器提交用户名和密码

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

2.使用 OkHttp

OkHttp的项目主页地址是:https://github.com/square/okhttp。

我们需要先在项目中添加OkHttp库的依赖

添加方式主页有写,可以前往github主页寻找最新版依赖添加代码

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

添加上述依赖会自动下载两个库:一个是OkHttp库,一个是Okio库,后者是前者的通信基础

看一下 OkHttp 的具体用法

首先需要创建一个 OkHttpClient 的实例

OkHttpClient client = new OkHttpClient();

如果想要发起一条 HTTP 请求,就需要创建一个 Request 对象

Request request = new Request.Builder().build();

上述代码只是创建了一个空的 Request 对象

我们可以在最终的 build()方法之前连缀很多其他方法来丰富这个 Request 对象。

Request request = new Request.Builder().url("http://www.baidu.com").build();

之后调用 OkHttpClient 的 newCall()方法来创建一个 Call 对象,并调用它的 execute()方 法来发送请求并获取服务器返回的数据

Response response = client.newCall(request).execute();

Response对象就是服务器返回的数据了,我们可以使用如下写法来得到返回的具体内容

String responseData = response.body().string();

如果是发起一条 POST 请求会比 GET 请求稍微复杂一点,我们需要先构建出一个 Request Body 对象来存放待提交的参数

RequestBody requestBody = new FormBody.Builder().add("username","admin").add("password", "123456").build();

然后在 Request.Builder 中调用一下 post()方法,并将 RequestBody 对象传入

Request request = new Request.Builder().url("http://www.baidu.com").post(requestBody).build();

接下来调用execute()方法来发送请求并获取服务器返回的数据即可。

现在我们先把NetworkTest这个项目改用OkHttp的方式再实现一遍

    responseText = findViewById(R.id.responseText);
    Button button = findViewById(R.id.sendRequestBtn);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            //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://www.baidu.com")
                        .build();
                Response response1 = client.newCall(request).execute();
                String responseData = response1.body().string();
                showResponse(responseData);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

添加了一个 sendRequestWithOkHttp()方法,并在 Send Request 按钮的点击事件里去调用这个方法。

在这个方法开启了一个子线程, 在子线程里使用 OkHttp 发出一条 HTTP 请求,请求百度的首页,OkHttp 的用法和前面一样。最后调用了 showResponse()方法来将服务器返回的数据显示到界面上

3.解析XML格式数据

每个需要访问网络的应用程序都会有一个自己的服务器,我们可以向服务器提交数据,也可以从服务器上获取数据。

这些数据以文本的形式传递肯定是不行的,因为另一方根本就不知道这段文本的用途是什么。因此,一般会在网络上传输一些格式化后的数据,这种数据会有一定的结构规则和语义,当另一方收到数据消息之后,就可以按照相同的结构规则进行解析,从而取出想要的那部分内容。

在网络上传输数据时最常用的格式有两种:XML和JSON。下面学习XML

我们还需要先解决从哪儿获取一段XML格式的数据,这里准备搭建一个最简单的Web服务器,在这个服务器上提供一段XML文本,在程序里去访问这个服务器,再对得到的XML文本进行解析。

搭建Web服务器的过程非常简单,也有很多种服务器类型可供选择,我们使用Apache 服务器。

官方下载 地址是:http://httpd.apache.org

下载后安装请参考:https://blog.csdn.net/weixin_63357306/article/details/126454639

安装好后在地址栏输入127.0.0.1

接下来进入到 你的安装目录\Apache24\htdocs 目录下,在这里新建一个名为 get_data.xml 的文件,然后编辑这个文件,并加入如下 XML 格式的内容。

<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> 

这时在浏览器中访问 http://127.0.0.1/get_data.xml 这个网址

接下来在 Android 程序里去获取并解析这段 XML 数据

1.Pull 解析方式

解析XML格式的数据其实也有挺多种方式的,我们学习比较常用的两种:Pull解析和SAX 解析。这里仍然是在NetworkTest项目的基础上继续开发,这样我们就可以重用之前网络通信部分的代码,从而把工作的重心放在XML数据解析上。

修改MainActivity中的代码

    responseText = findViewById(R.id.responseText);
    Button button = findViewById(R.id.sendRequestBtn);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            //sendRequestWithHttpURLConnection();
            sendRequestWithOkHttp();
        }
    });


private void sendRequestWithOkHttp(){
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                        // 指定访问的服务器地址是电脑本机
                        .url("http://10.0.2.2/get_data.xml")
                        .build();
                Response response1 = client.newCall(request).execute();
                String responseData = response1.body().string();
                parseXMLWithPull(responseData);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

private void parseXMLWithPull(String xmlData) {
    try {
        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
        XmlPullParser xmlPullParser = factory.newPullParser();
        xmlPullParser.setInput(new StringReader(xmlData));
        int eventType = xmlPullParser.getEventType();
        String id = "";
        String name = "";
        String version = "";
        while (eventType != 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)) {
                        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();
    }
}

这里首先是将 HTTP 请求的地址改成了 http://10.0.2.2/get_data.xml,10.0.2.2 对于 模拟器来说就是电脑本机的 IP 地址。在得到了服务器返回的数据后,我们调用了 parseXMLWithPull()方法来解析服务器返回的数据。

首先获取到一个 XmlPullParserFactory 的实例,并借助这个实例得到 XmlPullParser 对象,然后调用 XmlPullParser 的 setInput()方法将服务器返回的 XML 数据设置进去开始解析。

解析通过 getEventType()可以得到当前的解析事件,然后在一个 while 循环中不断地进行解析,如果当前的解析事件不等于 XmlPullParser.END_DOCUMENT,说明解析工作还没完成,调用 next()方法后可以获取下一个解析事件。

在 while 循环中,我们通过 getName()方法得到当前节点的名字,如果发现节点名等于 id、 name 或 version,就调用 nextText()方法来获取节点内具体的内容,每当解析完一个 app 节点后 就将获取到的内容打印出来。

从Android 9.0系统开始,应用程序默认只允许使用HTTPS类型的网络请求,HTTP类型的网络请求因为有安全隐患默认不再被支持,而我们搭建的Apache服务器现在使用的就是HTTP。 那么为了能让程序使用HTTP,我们还要进行如下配置才可以。

右击res目录 →New→Directory,创建一个xml目录,接着右击xml目录→New→File,创建一个 network_config.xml文件。然后修改network_config.xml文件中的内容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true">
        <trust-anchors>
            <certificates src="system" />
        </trust-anchors>
    </base-config>
</network-security-config>

接下来修改AndroidManifest.xml中的代码来启用我们刚才创建的配置文件

<application android:networkSecurityConfig="@xml/network_config" ... </application>

然后报错啦:

java.net.SocketTimeoutException: failed to connect to /10.0.2.2 (port 80) from /192.168.1.11 (port 48146) after 10000ms

这个异常是由于在尝试建立到远程服务器的连接时超出了指定的时间限制。这个异常通常意味着套接字连接正常建立,但是在等待对方发送数据或确认时,没有在预期的时间内完成交互。

这似乎是实机调试才会遇到的情况,首先查询自己的电脑ip,win+R输入cmd,打开cmd输入ipconfig

WLAN下的IPV4后面的就是电脑的ip地址

使用手机浏览器输入http://(电脑的ip地址)//get_data.xml。如果能打开那么将上文中的代码中的10.0.2.2改为自己电脑的ip地址即可

否则参考

https://blog.csdn.net/qianfeifeio/article/details/113632503

完成防火墙设置后,更改代码即可

2.SAX解析方式

Pull解析方式虽然非常好用,但它并不是我们唯一的选择。SAX解析也是一种特别常用的XML解 析方式,虽然它的用法比Pull解析要复杂一些,但在语义方面会更加清楚。

要使用SAX解析,通常情况下我们会新建一个类继承自DefaultHandler,并重写父类的5个方法

class MyHandler : DefaultHandler() {
    override fun startDocument() {
    }
    override fun startElement(uri: String, localName: String, qName: String, attributes:
    Attributes) {
    }
    override fun characters(ch: CharArray, start: Int, length: Int) {
    }
    override fun endElement(uri: String, localName: String, qName: String) {
    }
    override fun endDocument() {
    }
}

这5个方法一看就很清楚。

startDocument()方法会在开始XML解析的时候调用,

startElement()方法会在开始解析某个节点的时候调用,

characters()方法会在获取节点 中内容的时候调用,

endElement()方法会在完成解析某个节点的时候调用,

endDocument()方法会在完成整个XML解析的时候调用。

其中,startElement()、 characters()和endElement()这3个方法是有参数的,从XML中解析出的数据就会以参数的形式传入这些方法中。

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

新建一个 ContentHandler类继承自DefaultHandler,并重写父类的5个方法

package com.example.networktest;

import android.util.Log;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class ContentHandler extends DefaultHandler {

    private String nodeName;
    private StringBuilder id;
    private StringBuilder name;
    private StringBuilder version;

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

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

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        super.characters(ch, start, length);
        // 根据当前的节点名判断将内容添加到哪一个 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 {
        super.endElement(uri, localName, qName);
        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);
        }
    }

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

我们首先给 id、name 和 version 节点分别定义了一个 StringBuilder 对象, 并在 startDocument()方法里对它们进行了初始化。

每当开始解析某个节点的时候,startElement()方法就会得到调用,其中 localName 参数记录着当前节点的名字,这里我们把它记录下来。

接着在解析节点中具体内容的时候就会调用 characters()方法,我们会根据当前的节点名进行判断,将解析出的内容添加到哪一个 StringBuilder 对象中。

最后在 endElement() 方法中进行判断,如果 app 节点已经解析完成,就打印出 id、name 和 version 的内容。

目前 id、name 和 version 中都可能是包括回车或换行符的,因此在打印之前我们还需要调用一下 trim()方法,并且打印完成后还要将 StringBuilder 的内容清空掉,不然的话会影响下一次内容的读取

修改 MainActivity 中的代码

private void sendRequestWithOkHttp(){
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                        // 指定访问的服务器地址是电脑本机
                        .url("http://192.168.1.8/get_data.xml")
                        .build();
                Response response1 = client.newCall(request).execute();
                String responseData = response1.body().string();
                parseXMLWithSAX(responseData);
            } catch (Exception 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();
    }
}

在得到了服务器返回的数据后,我们这次通过调用parseXMLWithSAX()方法来解析XML数 据。parseXMLWithSAX()方法中先是创建了一个SAXParserFactory的对象,然后再获取 XMLReader对象,接着将我们编写的ContentHandler的实例设置到XMLReader中,最后调用parse()方法开始执行解析。

4.解析JSON格式数据

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

我们还需要在刚刚建xml的目录中新建一个get_data.json的文件,然后编 辑这个文件,并加入如下JSON格式的内容

[{"id":"5","version":"5.5","name":"Clash of Clans"},  
{"id":"6","version":"7.0","name":"Boom Beach"},  
{"id":"7","version":"3.5","name":"Clash Royale"}] 

这时在浏览器中访问http://127.0.0.1/get_data.json

1.使用JSONObject

解析JSON数据也有很多种方法,可以使用官方提供的JSONObject,也可以使用 Google的开源库GSON。另外,一些第三方的开源库如Jackson、FastJSON等也非常不错。我们学习前两种解析方式的用法

修改MainActivity中的代码

private void sendRequestWithOkHttp(){
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                        // 指定访问的服务器地址是电脑本机
                        .url("http://192.168.1.8/get_data.json")
                        .build();
                Response response1 = client.newCall(request).execute();
                String responseData = response1.body().string();
                parseJSONWithJSONObject(responseData);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

private void parseJSONWithJSONObject(String jsonData) {
    try {
        JSONArray jsonArray = new JSONArray(jsonData);
        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();
    }
}

首先将 HTTP 请求的地址改成 http://10.0.2.2/get_data.json,得到了服务器返回的数据后调用 parseJSONWithJSONObject()方法来解析数据。

解析 JSON 的代码非常简单,我们在服务器中定义的是一个 JSON 数组,因此这里首先是将服务器返回的数据传入到了一个 JSONArray 对象中。然后循环遍历这个 JSONArray,从中取出的每一个元素都是一个 JSONObject 对象,每个 JSONObject 对象中又会包含 id、name 和 version 这些数据。 接下来只需要调用 getString()方法将这些数据取出,并打印出来即可。

2.使用GSON

如果你认为使用JSONObject来解析JSON数据已经非常简单了,那你就太容易满足了。Google 提供的GSON开源库可以让解析JSON数据的工作简单到让你不敢想象的地步。

GSON并没有被添加到Android官方的API中,因此如果想要使用这个功能的话,就必须在项目中添加GSON库的依赖。编辑app/build.gradle文件,在dependencies闭包中添加

implementation("com.google.code.gson:gson:2.11.0")

它的强大之处就在于可以将一段JSON格式的字符串自动映 射成一个对象,从而不需要我们再手动编写代码进行解析了

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

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

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

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

如果需要解析的是一段JSON数组,会稍微麻烦一点,比如如下格式的数据

[{"name":"Tom","age":20}, {"name":"Jack","age":25}, {"name":"Lily","age":22}]

这个时候,我们需要借助TypeToken将期望解析成的数据类型传入fromJson()方法中,如下所示:

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

基本的用法就是这样,下面就让我们来真正地尝试一下吧。首先新增一个App类,并加入 id、name和version这3个字段

package com.example.networktest;

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 中的代码

private void sendRequestWithOkHttp(){
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                        // 指定访问的服务器地址是电脑本机
                        .url("http://192.168.1.8/get_data.json")
                        .build();
                Response response1 = client.newCall(request).execute();
                String responseData = response1.body().string();
                parseJSONWithGSON(responseData);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

private void parseJSONWithGSON(String jsonData) {
    Gson gson = new Gson();
    List<App> appList = gson.fromJson(jsonData, new TypeToken<List<App>>()
    {}.getType());
    for (App app : appList) {
        Log.d("MainActivity", "id is " + app.getId());
        Log.d("MainActivity", "name is " + app.getName());
        Log.d("MainActivity", "version is " + app.getVersion());
    }
}

5.网络请求回调的实现方式

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

package com.example.networktest;

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()方法中开启了一个线程来发起 HTTP 请求,那么服务器响应的数据是无法进行返回的,所有的耗时逻辑都是在子线程里进行的,sendHttpRequest()方法会在服务器还没来得及响应的时候就执行结束了,也就无法返回响应的数据了。

使用 Java 的回调机制就可以了,下面就让我们来学习一下回调机制到底是如何使用的

首先需要定义一个接口,比如将它命名成HttpCallbackListener

package com.example.networktest;

public interface HttpCallbackListener {
    void onFinish(String response);
    void onError(Exception e);
}

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

接着修改 HttpUtil 中的代码

package com.example.networktest;

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

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()方法里对异常情况进行处理。

这样我们就利用回调机制将响应数据成功返回给调用方了

上述使用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);
}

sendOkHttpRequest()方法中有一个 okhttp3.Callback 参数,这个是 OkHttp 库中自带的一个回调接口,类似于我们刚才自己编写的 HttpCallbackListener。

然后在 client.newCall()之后调用了一个 enqueue()方法,并把 okhttp3.Callback 参数传入。

OkHttp 在 enqueue()方法的内部已经帮我们开好子线程了,然后会在子线程中去执行 HTTP 请求,并将最终的请求结果回调到 okhttp3.Callback 当中。

我们在调用sendOkHttpRequest()方法的时候就可以这样写

HttpUtil.sendOkHttpRequest("http://www.baidu.com", new okhttp3.Callback() {
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        // 得到服务器返回的具体内容
        String responseData = response.body().string();
    }
    @Override
    public void onFailure(Call call, IOException e) {
        // 在这里对异常情况进行处理
    }
}); 

不管是使用HttpURLConnection还是OkHttp,最终的回调接口都还是在子线程中运行的

我们不可以在这里执行任何的UI操作,除非借助runOnUiThread() 来进行线程转换

  • 14
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值