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()方法,
    完成任务!





参考自《第一行代码》

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

凌川江雪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值