Android网络编程

Android网络编程

访问网络需要申请权限。


请求网络并显示网页源码

布局文件



    
   

    

MainActivity

package com.example.webdeme;

import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

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

public class MainActivity extends AppCompatActivity {

    private Context mContext;
    private TextView responseText;
    private EditText editText;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = this;    
      
        editText = (EditText) findViewById(R.id.et_url);
        Button sendRequest = (Button) findViewById(R.id.bt_request_source);
        responseText = (TextView) findViewById(R.id.response_text);

        sendRequest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String etUrl = editText.getText().toString().trim();
                sendRequestWithHttpURLConnection(etUrl);
            }
        });

    }

    // 使用HttpURLConnection来发送请求
    private void sendRequestWithHttpURLConnection(final String u) {
        // 请求网络属于耗时操作,为了不影响主线程,开一个子线程来请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                // 整行读取响应流
                BufferedReader reader = null;
                try {
                    // 新建一个URL
                    // 网址来自用户填写,默认访问百度首页 
                    URL url = new URL(u);
                    // 获得连接
                    connection = (HttpURLConnection) url.openConnection();
                    // 设置请求方式
                    connection.setRequestMethod("GET");
                    // 设置连接超时,单位是毫秒
                    connection.setConnectTimeout(8000);
                    // 设置读取超时
                    connection.setReadTimeout(8000);
                    // 获得响应流
                    InputStream in = connection.getInputStream();
                    reader = new BufferedReader(new InputStreamReader(in));
                    String line = null;
                    StringBuilder response = new StringBuilder();
                    while ((line = reader.readLine()) != null) {
                        response.append(line);
                    }
                    // 切换到主线程显示响应内容
                    showResponseText(response.toString());

                } catch (IOException e) {
                    e.printStackTrace();
                    // 关闭资源
                } finally {
                    try {
                        if (reader != null) {
                            reader.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    if (connection != null) {
                        // 关闭连接
                        connection.disconnect();
                    }
                }
            }
        }).start();
    }

    private void showResponseText(final String s) {
        // 只能在主线程才能进行UI操作,这个方法将线程切换到主线程
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                responseText.setText(s);
            }
        });
    }
}

还有一个方法int code = connection.getResponseCode();可以获得状态码。上面的endRequestWithHttpURLConnection()中可以先判断状态码,是200才读取流。状态码不是200请求失败了,也就没必要进行读取流或者其他后续操作了。

Android网络编程_第1张图片
  • Android中耗时的任务,比如请求网络。拷贝大文件,需要在子线程中处理。
  • 但是在子线程中不能进行UI操作。

上面的runOnUiThread()可以很简单地切换到主线程。实际上是对Android中的异步消息处理机制的一个封装。实际上还是原理还是和上面类似。

Handler的使用

用Handler重写上面的功能。

  1. 子线程里面发送Message,sendRequestWithHttpURLConnection()方法里面新增以下代码并注释掉 showResponseText(response.toString());
while ((line = reader.readLine()) != null) {
    response.append(line);
}

Message message = new Message();
// 可以携带对象,这里携带了了浏览器响应内容
message.obj = response;
// 校验字段,因为可以发送多个message,handleMessage要能区分是接收到了哪个message。
message.what = SET_TEXT;
// 发送消息到主线程,handleMessage方法会处理接收到的message。
handler.sendMessage(message);

Log.d("Thread", Thread.currentThread().getName());
  1. 主线程中新建一个成员变量。需要重写handleMessage()方法。这里以匿名类的方式。
  private Handler handler = new Handler() {
        // 接收子线程sendMessage发送过来的message。
        // 由于这个Handler是在主线程创建的。所以这个方法是在主线程执行.
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SET_TEXT:
                    responseText.setText(msg.obj.toString());
                    Log.d("Thread", Thread.currentThread().getName()); //Thread-XXXXX
            }
        }
    };

Message

在线程之间传递信息,msg.obj可以携带任何对象。msg.what携带校验字段,便于handleMessage辨别。

Handler

用于处理上面的Message,可以发送消息sendMessage(),发送的消息传递给到handleMessage()进行处理。

MessageQueue

消息队列,存放所有通过Handler发送的消息。这部分消息一直存在于队列中,等待被处理。每个线程只会有一个MessageQueue对象

Looper

是每个线程中 MessageQueue对象的管家,调用Looper的loop()方法后,进入无限循环。每当发现MessageQueue中有一条消息,就取出来传递给handleMessage(Message msg)方法去处理。每个线程也只有一个Looper对象。

异步消息处理流程

  1. 在主线程创建Handler对象并重写handleMessage()方法。
  2. 在子线程需要进行UI操作时候,就创建一个Message对象,通过上面创建的Handler发送这个消息出去。
  3. 之后这个消息别添加到MessageQueue中。Looper的无限循环会一直尝试从MessageQueue中取出消息。
  4. 取出的消息会传递给handleMessage()方法。

响应字节流与显示图片

如果上面例子输入网址是一个图片链接,响应的是字节流,如果还以文本内容显示,肯定是乱码。稍微修改上面的代码,使用ImageView就可以了。

package com.example.webdeme;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;


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

public class MainActivity extends AppCompatActivity {
    public static final int SET_IMAGE = 2;

    private Context mContext;
//    private TextView responseText;
    private EditText editText;
    private ImageView imgView;

    private Handler handler = new Handler() {
        // 接收子线程sendMessage发送过来的message。
        // 由于这个Handler是在主线程创建的。所以这个方法是在主线程执行.
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SET_IMAGE:
                    imgView.setImageBitmap((Bitmap) msg.obj);
                    Log.d("Thread", Thread.currentThread().getName());

            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = this;
        editText = (EditText) findViewById(R.id.et_url);
        imgView = (ImageView) findViewById(R.id.img_view);
        Button sendRequest = (Button) findViewById(R.id.bt_request_source);
//        responseText = (TextView) findViewById(R.id.response_text);

        sendRequest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String etUrl = editText.getText().toString().trim();
                sendRequestWithHttpURLConnection(etUrl);
            }
        });

    }

    // 使用HttpURLConnection来发送请求
    private void sendRequestWithHttpURLConnection(final String u) {
        // 请求网络属于耗时操作,为了不影响主线程,开一个子线程来请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                try {
                    // 新建一个URL
                    URL url = new URL(u);
                    // 获得连接
                    connection = (HttpURLConnection) url.openConnection();
                    // 设置请求方式
                    connection.setRequestMethod("GET");
                    // 设置连接超时,单位是毫秒
                    connection.setConnectTimeout(8000);
                    // 设置读取超时
                    connection.setReadTimeout(8000);
                    // 获得响应流
                    InputStream in = connection.getInputStream();
                    // BitmapFactory:可以将文件,读取流,字节数组转换成一个Bitmap对象。
                    Bitmap bitmap = BitmapFactory.decodeStream(in);
                    // 获得Message,如果之前的Message对象存在,则复用。否则new一个新的message
                    Message message = Message.obtain();
                    // 可以携带对象,这里携带了了浏览器响应内容
                    message.obj = bitmap;
                    // 校验字段,因为可以发送多个message,handleMessage要能区分是接收到了哪个message。
                    message.what = SET_IMAGE;
                    // 发送消息到主线程,handleMessage方法会处理接收到的message。
                    handler.sendMessage(message);

                } catch (IOException e) {
                    e.printStackTrace();
                    // 关闭连接
                } finally {
                    if (connection != null) {
                        // 关闭连接
                        connection.disconnect();
                    }
                }
            }
        }).start();
    }
}

布局文件里仅仅是把TextView换成ImageView就行。

Android网络编程_第2张图片

子线程一定不能更新UI吗?

不是。

package com.example.threadtest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
      
        final TextView textView = (TextView) findViewById(R.id.text_view);

        new Thread(new Runnable() {
            @Override
            public void run() {
                textView.setText("aa");
                Log.d("Thread", Thread.currentThread().getName());
            }
        }).start();  
    }
}

上面的例子的确是在子线程里面,且更新了UI。因为Android的审计机制:activity完全显示的时候审计机制才会去检测子线程有没有更新Ui。上面代码只要稍微改一下,让子线程sleep一段时间,足够让activiti完全显示。此时代码就报错了。

new Thread(new Runnable() {
    @Override
     public void run() {
         try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
             e.printStackTrace();
         }
         textView.setText("aa");
         Log.d("Thread", Thread.currentThread().getName());
     }
}).start();

其他情况

  • SurfaceView :多媒体视频播放 ,可以在子线程中更新UI;
  • Progress(进度)相关的控件:也是可以在子线程中更新Ui。

runOnUiThread()

无论当前线程是否是主线程,都将在主线程执行

runOnUiThread(new Runnable() {
    @Override
    public void run() {
    // 可以在此进行UI操作,这里当前线程是main
  }
});

看下上面方法的源码

public final void runOnUiThread(Runnable action) {
     // 如果当前线程不是在主线程,则将这个Runnable用Handler的post方法发出去
    if (Thread.currentThread() != mUiThread) { // private Thread mUiThread;
        mHandler.post(action); // final Handler mHandler = new Handler();
    // 如果在主线程,立即执行run里面的代码。注意不是start()。因为run只是执行一个方法,并没有开启线程(就是在主线程里无需开线程);而start()方法才是真正的开启线程
    } else {
        action.run();
    }
}

// 查看post方法,实际上就是发送了Message到MessageQueue,只是这个方法发送的是带有延迟的

// **** mHandler.post(action); ****
public final boolean post(Runnable r)
{
   return  sendMessageDelayed(getPostMessage(r), 0);
}

Thread.run()和Thread.start()的区别

  • start()方法是真正开启了一个线程,且会调用run()方法
  • run()只是一个普通的方法而已,调用它和调用一般方法没区别,即是顺序执行而非并发执行。并不会开启新线程。
new Thread(new Runnable() {
    @Override
    public void run() {
    Log.d("Thread", Thread.currentThread().getName());
    // 这里用了start方法,会打印 Thread-xxxxx,表示确实是开启了子线程,在子线程里运行
    }
}).start();

// **** 

new Thread(new Runnable() {
    @Override
    public void run() {
    Log.d("Thread", Thread.currentThread().getName());
    // 这里用了run方法,会打印 main,表示run里面执行的代码实际上还是在主线程
    }
}).run();

同样的,下面调用的run(),即使activity完全显示,android的审计机制开始检测是否在子线程里更新UI了。但是因为调用run方法并没有开启线程。所以不会报错。

new Thread(new Runnable() {
    @Override
    public void run() {
        try {
        Thread.sleep(500);
        } catch (InterruptedException e) {
        e.printStackTrace();
        }
        textView.setText("aa");
        Log.d("Thread", Thread.currentThread().getName());
    }
}).run();

再来一个例子,用的是start()。下面的代码是开启了两个子线程,并发、交替执行。

 public static void main(String[] args) {  
        Thread t1 = new Thread(new T1());  // T1实现了Runnable
        Thread t2 = new Thread(new T2());  // T2实现了Runnable
        t1.start();  
        t2.start();
        
}  

而下面,调用的是run方法。是顺序执行,等t1全部执行完毕,才轮到t2执行。和普通方法没区别。

public static void main(String[] args) {  
    Thread t1 = new Thread(new T1());  // T1实现了Runnable
    Thread t2 = new Thread(new T2());  // T2实现了Runnable
    t1.run();  
    t2.run();       
}  

Handler的post方法

了解了runOnUiThread()的原理之后,我们可以用post实现UI更新。

package com.example.test;

import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private Handler handler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final TextView textView = (TextView) findViewById(R.id.text_view);
        new Thread(new Runnable() {
            @Override
            public void run() {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        textView.setText("Hello");
                        Log.d("Thread", Thread.currentThread().getName()); // main
                    }
                });
            }
        }).start();
    }
}

新闻客户端--网络版

改进之前写的不像新闻客户端的新闻客户端。之前是直接在代码里给条目的title、des、url、imgurl赋值。现在从服务器(Tomcat)获得新闻数据,这里就简化成获得一个Json数据。然后在客户端去请求这个文件,获得响应后。解析json,存入bean,再存入数据库中。每次打开应用都先从数据库获得数据显示,再尝试去网络请求最新的新闻资源(当然这个例子中是固定不变的数据)。

新闻资源部署到服务器

news.json部署到Tomacat的webapps下某个工程下,这里是hello。json数据如下

[
    {
    "imgUrl":"https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/bd_logo1_31bdc765.png",
    "title":"百度搜索",
    "des":"全球最大的中文搜索引擎、致力于让网民更便捷地获取信息,找到所求。百度超过千亿的中文网页数据库,可以瞬间找到相关的搜索结果。",
    "url":"https://www.baidu.com/"
    },

    {
    "imgUrl":"https://ss1.baidu.com/70cFfyinKgQFm2e88IuM_a/forum/pic/item/3bf33a87e950352ad1552f1b5a43fbf2b2118be8.jpg",
    "title":"高德地图",
    "des":"高德地图官方网站,提供全国地图浏览,地点搜索,公交驾车查询服务。可同时查看商家团购、优惠信息。高德地图,您的出行、生活好帮手。",
    "url":"http://ditu.amap.com/"
    },

    {
    "imgUrl":"http://is3.mzstatic.com/image/thumb/Purple122/v4/24/31/a6/2431a645-7c63-b87e-cafa-64b7742770b5/source/512x512bb.jpg",
    "title":"豆瓣电影",
    "des":"豆瓣电影提供最新的电影介绍及评论包括上映影片的影讯查询及购票服务。你可以记录想看、在看和看过的电影电视剧,顺便打分、写影评。",
    "url":"https://movie.douban.com/"
    },

    {
    "imgUrl":"https://ss1.baidu.com/70cFfyinKgQFm2e88IuM_a/forum/pic/item/6709c93d70cf3bc71923c096d200baa1cd112aac.jpg",
    "title":"知乎",
    "des":"一个真实的网络问答社区,帮助你寻找答案,分享知识",
    "url":"https://www.zhihu.com/"
    }
]

由于只是简单演示,这里只有4条数据。

访问http://127.0.0.1:8080/hello/news.json,就能获取到上面的json内容。注意:127.0.0.1换成localhost也是可以的,8080端口号是因为使用的Tomcat。

数据库的创建和操作

新建一个bean,字段和数据库的字段尽量相同。

package com.example.newsdemo.bean;


public class NewsBean {

    public int _id;
    public String imgUrl;
    public String title;
    public String des;
    public String url;
}

数据库帮助类,只做了建表的工作。

package com.example.newsdemo.dao;


import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class MyDatabaseHelper extends SQLiteOpenHelper {

    private Context mContext;
    public static final String CREATE_NEWS = "create table news ("
            + "_id integer primary key autoincrement,"
            + "imgUrl varchar(255),"
            +"title varchar(50),"
            +"des varchar(255),"
            +"url varchar(255));";

    public MyDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_NEWS);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // TODO: 2017/4/20
    }
}

实现对数据库的增删查操作

package com.example.newsdemo.dao;


import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.example.newsdemo.bean.NewsBean;

import java.util.ArrayList;
import java.util.List;

public class NewsDao {
    private SQLiteDatabase db;

    public NewsDao(Context context, String dbName, int version) {
        MyDatabaseHelper databaseHelper = new MyDatabaseHelper(context, dbName, null, version);
        db = databaseHelper.getReadableDatabase();
    }

    // 保存从服务器获取的新闻到数据库
    public void saveNews(List beanList) {
        ContentValues values = new ContentValues();
        for (NewsBean bean : beanList) {
            values.put("imgUrl", bean.imgUrl);
            values.put("title", bean.title);
            values.put("des", bean.des);
            values.put("url", bean.url);
            db.insert("news", null, values);
            // 添加后将values清空,以便放入下一个bean
            values.clear();
        }

    }

    // 为了防止添加重复数据,每次请求网络获取新的数据时,先把原来数据库里的内容删除。再保存最新的到数据库
    public void delete() {
        db.delete("news", null, null);
    }

    // 从数据库获取所有新闻
    public List getAllNews() {
        List list = new ArrayList<>();
        Cursor cursor = db.query("news", null, null, null, null, null, null);
        // 条件,游标能否定位到下一行
        while (cursor.moveToNext()) {
            NewsBean bean = new NewsBean();
            bean._id = cursor.getInt(cursor.getColumnIndex("_id"));
            bean.imgUrl = cursor.getString(cursor.getColumnIndex("imgUrl"));
            bean.title = cursor.getString(cursor.getColumnIndex("title"));
            bean.des = cursor.getString(cursor.getColumnIndex("des"));
            bean.url = cursor.getString(cursor.getColumnIndex("url"));
            list.add(bean);
        }
        // 关闭结果集
        cursor.close();
        return list;
    }

}

请求服务器,返回数据

接下来比较关键的,请求的服务器的时候当然不能填localhost或者127.0.0.1了,这样写是永远是访问模拟器的本身。对于模拟器可以填写10.0.2.2,对于真机,访问电脑的ip地址就可以了,cmd里输入ipconfig即可查看。为了统一,还是都用电脑IP比较好。我做测试的时候,用真机不能连上PC的服务器,还没解决,异常原因好像是和路由有关。故这个测试是模拟器上运行的。

package com.example.newsdemo.utils;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import com.example.newsdemo.bean.NewsBean;
import com.example.newsdemo.dao.NewsDao;

import org.json.JSONArray;
import org.json.JSONObject;

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

public class NewsUtils {

    // 从数据库获得
    public static List getAllNewsFromDatabase(Context context) {
        return new NewsDao(context, "news.db", 1).getAllNews();
    }

    // 请求服务器获得数据
    public static List getAllNewsFromNetwork(Context context) {
        List list = new ArrayList<>();
        NewsDao newsDao = new NewsDao(context, "news.db", 1);
        // 电脑本机映射到模拟器的地址是10.0.2.2 
        String u = "http://10.175.42.160:8080/hello/news.json"; // 10.175.42.160是电脑的IP地址
        HttpURLConnection connection = null;
        // 整行读取响应流
        BufferedReader reader = null;
        try {
            // 新建一个URL
            URL url = new URL(u);
            // 获得连接
            connection = (HttpURLConnection) url.openConnection();
            // 设置请求方式
            connection.setRequestMethod("GET");
            // 设置连接超时,单位是毫秒
            connection.setConnectTimeout(8000);
            // 设置读取超时
            connection.setReadTimeout(8000);
            // 获得响应流

            InputStream in = connection.getInputStream();
            reader = new BufferedReader(new InputStreamReader(in));
            String line = null;
            StringBuilder response = new StringBuilder();

            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            // ** 至此获得了json的字符串表示, 接下来就是解析 **
            JSONArray jsonArray = new JSONArray(response.toString());
            for (int i = 0; i < jsonArray.length(); i++) {
                NewsBean bean = new NewsBean();
                JSONObject jsonObject = jsonArray.getJSONObject(i);
                bean.imgUrl = jsonObject.getString("imgUrl");
                bean.title = jsonObject.getString("title");
                bean.des = jsonObject.getString("des");
                bean.url = jsonObject.getString("url");
                list.add(bean);
            }
            // 每次从服务器请求,也保存到数据库。此时先删除数据库的旧的数据。始终存入刚刚请求到的最新数据
            newsDao.delete();
            newsDao.saveNews(list);
        } catch (Exception e) {
            e.printStackTrace();
            // 关闭资源
        } finally {
            try {
                if (reader != null) {   
                    reader.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (connection != null) {
                connection.disconnect();
            }
        }
        return list;
    }
    // 根据图片地址获得bitmap
    public static Bitmap getWebImageAsBitmap(final String imgUrl) {
        HttpURLConnection connection = null;
        Bitmap bitmap = null;
        try {
            // 新建一个URL
            URL url = new URL(imgUrl);
            // 获得连接

            connection = (HttpURLConnection) url.openConnection();
            // 设置请求方式
            connection.setRequestMethod("GET");
            // 设置连接超时,单位是毫秒
            connection.setConnectTimeout(8000);
            // 设置读取超时
            connection.setReadTimeout(8000);
            // 获得响应流
            InputStream in = connection.getInputStream();
            bitmap = BitmapFactory.decodeStream(in);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                // 关闭连接
                connection.disconnect();
            }
        }
        return bitmap;
    }
}

注意:以下返回的list和bitmap指定为局部变量,因为每一次请求将所有数据返回即可,而不是设置一个全局的成员变量,每请求一次,都会在上次的基础上累积。最终返回的数据时有重复的。这不是我们的初衷

还有,这里面确实在请求网络,先不用在这里面开子线程。待到MainActivity里面再开也不迟。因为需要使用到Handler,而handler得在主线程里面new出来,才能实现将请求网络得到的数据更新到UI。如果在这里开了子线程,必然需要handle将Message传出去,但是这个类不在MainActivity中。我们也不希望在MainActivity中设置一个public static Handler。(有种更好的做法可以给getAllNewsFromNetwork(Context context)增加一个Hanlder参数,在MainActivity中调用这个方法时,将MainActivity创建的Handler作为参数传递过去,即getAllNewsFromNetwork(Context context, Hnadler handler),这样子线程可以开在这个方法体内,并发送Message。可以使得MainActivity的代码不那么臃肿。

况且,子线程里是无法通过return语句返回数据的,因为耗时逻辑都在子线程里面执行,可能还没等到服务器响应的数据,就已经执行结束了,所以无法返回数据。

所以这里的做法是将请求网络将返回的数据赋给变量都是在同一个(子)线程里进行。而不是子线程里返回数据,主线程里去接收。看下MainActivity里面的一段代码,确实是这样。这样就保证了肯定能返回数据。

// 请求网络是耗时任务,需在子线程中执行
new Thread(new Runnable() {
    @Override
    public void run() {
    List list = NewsUtils.getAllNewsFromNetwork(mContext);
    // 将接收到的数据用Message发送出去
    }
}).start();

子项布局以及Adapter

显示图片,标题和描述




    

    

        

        

    

Adapter

package com.example.newsdemo.adapter;


import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.newsdemo.R;
import com.example.newsdemo.bean.NewsBean;

import java.util.List;

public class NewsAdapter extends ArrayAdapter {
    private int resourceId;
    // 接收从MainActivity传来的图片集合
    private List bitemapList;

    public NewsAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List objects, List bitmaps) {
        super(context, resource, objects);
        resourceId = resource;
        bitemapList = bitmaps;
    }

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        // 根据所在位置获得新闻实例
        NewsBean newsBean = getItem(position);
        View view;
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
        } else {
            view = convertView;
        }

        ImageView newsImg = (ImageView) view.findViewById(R.id.iv_news);
        TextView newsTitle = (TextView) view.findViewById(R.id.tv_news_title);
        TextView newsDes = (TextView) view.findViewById(R.id.tv_news_des);
        // 网络图片转成Bitmap传入
        // 没网络时从数据库获得,是以字符串url存的,没有bitmap。故要判断
        if (bitemapList != null) {
            Bitmap bitmap = bitemapList.get(position);
            newsImg.setImageBitmap(bitmap);
        }

        newsTitle.setText(newsBean.title);
        newsDes.setText(newsBean.des);

        return view;

    }
}

主布局和MainActivity

只是简单放置了一个listview




   
   


MainActivity

package com.example.newsdemo;

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;

import com.example.newsdemo.adapter.NewsAdapter;
import com.example.newsdemo.bean.NewsBean;
import com.example.newsdemo.utils.NewsUtils;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    public static final int TEXT = 1;
    public static final int IMAGE = 2;
    private List bitmapList;

    private ListView listView;
    private Context mContext;
    private List latestNews;
    // 切换到主线程更新UI
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case TEXT:
                    latestNews = (List) msg.obj;
                    break;
                case IMAGE:
                    bitmapList = (List) msg.obj;
                    break;
                default:
            }
            // 等确实接收到全部信息, 对象不为空且有内容时候才覆盖从数据库获取的显示数据。
            if (latestNews != null && bitmapList != null && latestNews.size() > 0 && bitmapList.size() > 0) {
                NewsAdapter adapter = new NewsAdapter(mContext, R.layout.list_item, latestNews, bitmapList);
                listView.setAdapter(adapter);
            }
        }
    };

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

        listView = (ListView) findViewById(R.id.lv_news);
//         先从数据库获取所有新闻
        List localNews = NewsUtils.getAllNewsFromDatabase(mContext);
//         数据库有内容时候显示, 避免无网络时候显示空白.但是这里的bitmapList是空,因为此时还没从网络获取到bitmap
        if (localNews != null && localNews.size() > 0) {
            NewsAdapter adapter = new NewsAdapter(mContext, R.layout.list_item, localNews, bitmapList);
            listView.setAdapter(adapter);
        }

        // 请求网络是耗时任务,需在子线程中执行
        new Thread(new Runnable() {
            @Override
            public void run() {
                List list = NewsUtils.getAllNewsFromNetwork(mContext);

                // 发送newsbean
                Message msg = Message.obtain();
                msg.what = TEXT;
                msg.obj = list;
                handler.sendMessage(msg);
                // 发送bitmap
                List bitmaps = new ArrayList<>();

                for (NewsBean newsBean : list) {
                    Bitmap bitmap = NewsUtils.getWebImageAsBitmap(newsBean.imgUrl);
                    bitmaps.add(bitmap);
                }
                Message msgImg = Message.obtain();
                msgImg.what = IMAGE;
                msgImg.obj = bitmaps;
                handler.sendMessage(msgImg);
            }
        }).start();


        // 设置子项点击事件监听,点击跳转到浏览器
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            /**
             * listview的条目点击时会调用该方法
             * @param parent 代表listviw
             * @param view 击的条目上的那个view对象
             * @param position 条目的位置
             * @param id 条目的id
             */
            @Override
            public void onItemClick(AdapterView parent, View view, int position, long id) {
                NewsBean bean = (NewsBean) parent.getItemAtPosition(position);
//                NewsBean bean = news.get(position); // 和上面功能一样
                String url = bean.url;
                // 这个构造器接收action和uri,相当于setAction和setData两个方法
                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                startActivity(intent);
            }
        });
    }
}

由于解析到的图片地址imgUrl是网络链接形式。newsImg.setImageURI(Uri.parse(imgUrl));并不能解析成功Web图片链接。

所以需要先将从网络获得的流转换成Bitmap,使用newsImg.setImageBitmap(bitmap);才可以。而且,数据库保存的依然是字符串形式网络链接的,而非bitmap,所以当没有网络的时候,从数据库获得数据时候,bitmapList是空集合,导致图片显示一片空白。这样粗糙的写法用户体验很糟糕。

Android网络编程_第3张图片

上图是联网情况下,能显示图片。注意看右上角的4G

Android网络编程_第4张图片

上面是断网时候,不能显示图片。

优化-- 使用Okhttp3、GSON、Glide

还是上面的例子,使用Okhttp3库替代HttpURLConnection。GSON替代JSONObject,使用Glide直接加载网络图片,这样即使断网时也能显示图片了

添加依赖包

先在app下的build.gradle下添加

dependencies {
    compile 'com.google.code.gson:gson:2.8.0'
    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'com.squareup.okhttp3:okhttp:3.7.0'
}

使用Gson

可以直接使用现成的NewsBean,已经与json的字段一一对应了。

其实也不一定要一一对应,只要添加了注解@SerializedName("true_name")就可以了。举个例子

// 服务器发送过来的json数据有一个是{"text":"Google"}, 但不是很直观。可以用下面的方式,既能直观表示其意思,又不影响字段的对应。
@SerializedName("text") // 在解析json数据的时候,使用的是text而不是company,但是之后的代码里面可以直接用company来操作这个字段,只是在解析时才当作"text"
private String company;

由于NewsBean还对应了数据库的字段,所以这里就不改成员变量名,也不加@SerializedName()注解了。

对于JSONObject对象

// 如果是解析json对象就好办。
NewsBean bean = gson.fromJson(response.toString(), NewsBean.class);

对于JSONArray

在NewsUtils的getAllNewsFromNetwork里面把原来的JSONObject解析代码删除,只此一行就可以将JSON数组映射到List

list = gson.fromJson(response.toString(), new TypeToken>() {}.getType());

使用Glide加载图片

由于可以正确解析图片网址,直接加载网络图片。所以之前写的NewsUtils.getWebImageAsBitmap方法直接删除,handler也不用发送接收到的bitmap了。直接在adapter里面一行就可以(又是一行!)

Glide.with(getContext()).load(newsBean.imgUrl).into(newsImg);

这里图片网址是http/https协议,直接传入字符串即可,放心直接填没关系。下面的写法也可以。

Glide.with(getContext()).load(Uri.parse(newsBean.imgUrl).into(newsImg);

使用OkHttp

简单用法

OkHttpClient okHttpClient = new OkHttpClient();
    // 一个请求,可以链式调用
Request request = new        Request.Builder().url("https://www.baidu.com").build();
try {
 // 访问并返回响应
    Response response = okHttpClient.newCall(request).execute();
} catch (IOException e) {
    e.printStackTrace();
}

如果是POST提交需要先构建RequestBody来存放提交的参数,然后post出去

OkHttpClient okHttpClient = new OkHttpClient();
RequestBody requestBody = new FormBody.Builder().add("username", "admin").add("password", "1234").build();
Request request = new Request.Builder().url("https://www.baidu.com").post(requestBody).build();
try {
    Response response = okHttpClient.newCall(request).execute();
} catch (IOException e) {
    e.printStackTrace();
}

注意上面的代码使用execute()并没有开启子线程,需要自行new Thread()。而且在子线程中也不能return数据。OkHttp为我们提供了便捷的方式。使用okhttp3.Callback的回调函数,重写它的onResponse和onFailure方法。最后请求的时候使用enqueue()而不是execute()。

public static void sendOkHttpRequest(String address, Callback callback) {
    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder().url(address).build();
    // 传入回调函数
    client.newCall(request).enqueue(callback);
}

Response response = okHttpClient.newCall(request).enqueue(callback);enqueue方法里面需要传一个参数,这个参数就是okhttp3.Callback。可以用匿名类的方式传入并重写方法。enqueue(callback)方法已经在内部为我们开启好了子线程

在调用的时候,实例化这个Callback类并重写方法就可以了。

// 需要注意的是,这两个回调方法依然还在子线程中
sendOkHttpRequest(url, new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
      // 发生异常来这里处理
      e.printStackTrace();
    }

    // 响应成功来这里处理
    @Override
    public void onResponse(Call call, Response response) throws IOException {

    }
});

HttpURLConnection自定义监听器和回调函数

运用回调机制将响应数据返回,解决了子线程中不能返回数据的困扰。其原理和Okhttp类似。成功响应就回调listener.onResponse();响应失败就回调listener.onFailure()

  1. 先自定义回调函数接口
public interface CCallbackListener {
  void onResponse();
  void onFailure();
}
  1. 封装发送请求的方法
private static void sendRequest(final String u, final CallbackListener listener) {
    new Thread(new Runnable() {
        @Override
        public void run() {

        HttpURLConnection connection = null;
        // 整行读取响应流
        BufferedReader reader = null;
        try {
            // 新建一个URL
            URL url = new URL(u);
            // 获得连接
          connection = (HttpURLConnection) url.openConnection();
            // 设置请求方式
            connection.setRequestMethod("GET");
            // 设置连接超时,单位是毫秒
          connection.setConnectTimeout(8000);
          // 设置读取超时
          connection.setReadTimeout(8000);
          // 获得响应流

          InputStream in = connection.getInputStream();
          reader = new BufferedReader(new InputStreamReader(in));
          String line = null;
          StringBuilder response = new StringBuilder();

          while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            // 能执行到这说明响应成功,调用回调函数
            if (listener != null) {
                listener.onResponse();
            }
          // 发生异常,调用回调函数处理
         } catch (IOException e) {
             if (listener != null) {
                listener.onFailure();
                }
            }
        }
    }).start();
}
  1. 调用上面的方法并重写CallbackListener未实现的两个方法
sendRequest(url, new CallbackListener() {
    @Override
     public void onResponse() {
    // TODO: 响应成功会调用这个方法,返回数据
     }

    @Override
     public void onFailure() {
    // TODO: 响应失败梳理异常
     }
});

了解上面的原理后,回到正题。我们可以不用NewsUtils来返回数据到MainActivity了,由于代码减少了很多,直接写在MainActivity中,还能在响应成功后切换到主线程更新UI.

上述功能的实现

需要改动的就三个文件,其他不用动。

  1. 首先删除NewsUtils类,方法挪到MainActivity。
  2. MainActivity
package com.example.newsdemo;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;

import com.example.newsdemo.adapter.NewsAdapter;
import com.example.newsdemo.bean.NewsBean;
import com.example.newsdemo.dao.NewsDao;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.io.IOException;
import java.util.List;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity {

    private ListView listView;
    private Context mContext;

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

        listView = (ListView) findViewById(R.id.lv_news);

//         先从数据库获取所有新闻
        List localNews = getAllNewsFromDatabase(mContext);
//         数据库有内容时候显示, 避免无网络时候显示空白.但是这里的bitmapList是空,因为此时还没从网络获取到bitmap
        if (localNews.size() > 0) {
            NewsAdapter adapter = new NewsAdapter(mContext, R.layout.list_item, localNews);
            listView.setAdapter(adapter);
        }

        RequestAllNewsFromNetwork(mContext);
        // 设置子项点击事件监听,点击跳转到浏览器
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            /**
             * listview的条目点击时会调用该方法
             * @param parent 代表listviw
             * @param view 击的条目上的那个view对象
             * @param position 条目的位置
             * @param id 条目的id
             */
            @Override
            public void onItemClick(AdapterView parent, View view, int position, long id) {
                NewsBean bean = (NewsBean) parent.getItemAtPosition(position);
//                NewsBean bean = news.get(position); // 和上面功能一样
                String url = bean.url;
                // 这个构造器接收action和uri,相当于setAction和setData两个方法
                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                startActivity(intent);
            }
        });
    }

    // 从数据库获得
    public List getAllNewsFromDatabase(Context context) {
        return new NewsDao(context, "news.db", 1).getAllNews();
    }

    // 请求服务器获得数据
    public void RequestAllNewsFromNetwork(final Context context) {

        // localhost映射到手机的地址是10.0.2.2 http://localhost:8080/hello/news.json
        String url = "http://10.175.42.160:8080/hello/news.json"; // 10.175.42.160

        sendOkHttpRequest(url, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                // 发生异常来这里处理
                e.printStackTrace();
            }

            // 响应成功来这里处理
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                // 返回json,以字符串形式
                String jsonData = response.body().toString();

                Gson gson = new Gson();
                // 解析json数据需要用到TypeToken,传入类型,解析好的数据传入到这个类型中。这里是List.
                // 注意TypeToken>(){}.getType()中大括号是必须的
                final List news = gson.fromJson(jsonData, new TypeToken>() {
                }.getType());
                // 每次从服务器请求,也保存到数据库。此时先删除数据库的旧的数据。始终存入刚刚请求到的最新数据
                // 响应成功了才删除和保存
                NewsDao newsDao = new NewsDao(context, "news.db", 1);
                newsDao.delete();
                newsDao.saveNews(news);
                // 响应成功就显示请求到的最新数据
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        // news需判断不为空,否则不能调用size函数,最好判断size必须大于零才显示,因为size为0的话显示空白也没意义
                        if (news != null && news.size() > 0) {
                            NewsAdapter adapter = new NewsAdapter(mContext, R.layout.list_item, news);
                            listView.setAdapter(adapter);
                        }
                    }
                });
            }
        });
    }
    // 封装了发送请求的方法
    private void sendOkHttpRequest(String address, Callback callback) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(address).build();
        // 传入回调函数
        client.newCall(request).enqueue(callback);
    }
}

  1. Adapter
package com.example.newsdemo.adapter;


import android.content.Context;
import android.net.Uri;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.example.newsdemo.R;
import com.example.newsdemo.bean.NewsBean;

import java.net.URI;
import java.util.List;

public class NewsAdapter extends ArrayAdapter {
    private int resourceId;

    public NewsAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List objects) {
        super(context, resource, objects);
        resourceId = resource;
    }

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        // 根据所在位置获得新闻实例
        NewsBean newsBean = getItem(position);
        View view;
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
        } else {
            view = convertView;
        }

        ImageView newsImg = (ImageView) view.findViewById(R.id.iv_news);
        TextView newsTitle = (TextView) view.findViewById(R.id.tv_news_title);
        TextView newsDes = (TextView) view.findViewById(R.id.tv_news_des);

        // load里面的参数可以是URL地址,本地路径,资源id等,into里面的参数一般就是ImageView控件
        Glide.with(getContext()).load(newsBean.imgUrl).into(newsImg);
        newsTitle.setText(newsBean.title);
        newsDes.setText(newsBean.des);

        return view;

    }
}

OK!使用了上面三个库大大减少代码量,开源的好处不言自明。截图就不上了,和上面的一样的。而且连不上服务器时候也能显示图片。


by @sunhaiyu

2017.4.23

你可能感兴趣的:(Android网络编程)