第11章 网络
11.1 WebView的用法
- 引入webview
- 逻辑
//设置支持javaScript脚本
webView.settings.setJavaScriptEnabled(true)
//当一个网页跳转另一个网页时,不会打开新的页面,仍然在该页面
webView.webViewClient = WebViewClient()
webView.loadUrl("https://www.baidu.com")
- 权限
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
11.2 使用 HTTP 访问网络
- 客户端向服务端发出HTTP请求
- 服务端收到请求后返回数据给客户端
- 客户端对数据解析处理
Webview 把发送HTTP请求,接收服务器响应,解析返回数据处理好了
- 下面是手动发送HTTP请求
11.2.1 使用 HttpURLConnection
- 具体步骤
//创建URL对象,传入目标网址,然后openConnection
val url = URL("https://www.baidu.com")
val connection = url.openConnection() as HttpURLConnection
//设置HTTP请求所使用的方法,GET或POST
connection.requestMethod = "GET"
//自由定制,如链接超时、读取超时的毫秒数,服务器希望的到的消息头
connection.connectTimeout = 8000
connection.readTimeout = 8000
//服务器返回的输入流,对输入流进行读取
val input = connection.inputStream
//将HTTP链接关掉
connection.disconnect()
- 布局
<?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=".MainActivity">
<!-- 发送HTTP请求-->
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send Request"
android:id="@+id/sendRequestBtn"
></Button>
<!-- 滚动的形式查看屏幕外的内容-->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<!-- 将服务器返回的数据显示-->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/responseText"></TextView>
</ScrollView>
</LinearLayout>
- 逻辑 展示服务器返回的 HTML 代码
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sendRequestBtn.setOnClickListener {
sendRequestWithHttpURLConnection()
}
}
private fun sendRequestWithHttpURLConnection() {
var connection : HttpURLConnection? = null
thread {
try {
var response = StringBuilder()
val url = URL("https://www.baidu.com")
connection = url.openConnection() as HttpURLConnection
connection!!.connectTimeout = 8000
connection!!.readTimeout = 8000
val input = connection!!.inputStream
//下面对获取到的输入流进行读取
val reader = BufferedReader(InputStreamReader(input))
reader.use{
reader.forEachLine {
response.append(it)
}
}
showResponse(response.toString())
} catch (e: Exception) {
e.printStackTrace()
}finally {
connection?.disconnect()
}
}
}
private fun showResponse(response: String) {
runOnUiThread{
responseText.text = response
}
}
}
- 需要发送消息
//在获取输入流前
connection.requestMethod = "POST"
val output = DataOutputStream(connection.outputStream)
//数据以键值对的形式存在,中间用&隔开
output.writeBytes("username=admin&password=123456")
11.2.2 Okhttp
- 引入依赖
https://github.com/square/okhttp
- Get 基本用法
//创建OkhttpClient实例
val client = OkHttpClient()
//创建Request对象,这里在build之前可以连缀很多东西
val request = Request.Builder()
.url("https://www.baidu.com")
.build()
//创建Call对象,调用execute发送请求获取返回数据
val response = client.newCall(request).execute()
//得到返回的具体内容
val responseData = response.body?.string()
- Post 的基本用法
//发送请求,构建Request Body存放待提交的数据
val requestBody = FormBody.Builder()
.add("username","admin")
.add("password","123456")
.build()
val request = Request.Builder()
.url("https://www.baidu.com")
.post(requestBody)
.build()
//之后就和get一样了
- 将上面的发送网络请求的方式换成Okhttp
private fun sendRequestWithOkhttp() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder().url("https://www.baidu.com").build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
showResponse(responseData)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
通过路径找文件夹 cmd+shift+G
11.4 解析JSON格式的数据
11.4.1 使用 JSONObject
private fun parseJSONWithJSONObject(jsonData: String) {
try {
//服务器定义的是一个数组所以用这个接收
val jsonArray = JSONArray(jsonData)
for (i in 0 until jsonArray.length()) {
//数组中取出对象
val jsonObject = jsonArray.getJSONObject(i)
//对象中取出数据
val id = jsonObject.getString("id")
Log.d("MainActivity", "id is $id")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
11.4.2 使用GSON
-
安卓 GsonFormat 插件
-
Gson https://github.com/google/gson
-
基本用法
private fun parseJSONWithGSON(jsonData: String) {
//解析一个对象 {"name":"Tom","age":20}
val gson = Gson()
val person = gson.fromJson(jsonData,Person::class.java)
//解析一个数组 [{"name","Tom","age":20},{"name","Jack","age":25},{"name","Lily","age":22}]
val typeOf = object :TypeToken<List<Person>>(){}.type
val peoples = gson.fromJson<List<Person>>(jsonData,typeOf)
for (a in people) {
Log.d(TAG, "name is ${a.name}")
}
}
class Person(val name:String, val age:Int){
}
11.5 网络请求回调的实现方式
- 发送HTTP请求需要在子线程中,不然会阻塞主线程,并且要用回调的方式处理返回数据,不然接收不到数据
- 发送网络请求工具类
- HttpURLConnection
object HttpUtil {
fun sendHttpRequest(address:String,listener: HttpCallbackListener){
var connection : HttpURLConnection? = null
try {
var response = StringBuilder()
val url = URL(address)
connection = url.openConnection() as HttpURLConnection
connection!!.connectTimeout = 8000
connection!!.readTimeout = 8000
val input = connection!!.inputStream
//下面对获取到的输入流进行读取
val reader = BufferedReader(InputStreamReader(input))
reader.use{
reader.forEachLine {
response.append(it)
}
}
listener.onFinish(response.toString())
} catch (e: Exception) {
e.printStackTrace()
listener.onError(e)
}finally {
connection?.disconnect()
}
}
}
//接口
interface HttpCallbackListener {
fun onFinish(response: String)
fun onError(e:Exception)
}
//调用
HttpUtil.sendHttpRequest(address,object :HttpCallbackListener{
override fun onFinish(response: String) {
TODO("Not yet implemented")
}
override fun onError(e: Exception) {
TODO("Not yet implemented")
}
})
- Okhttp
object HttpUtil {
fun sendOkHttpRequest(address: String, callback: okhttp3.Callback) {
val client = OkHttpClient()
val request = Request.Builder()
.url(address)
.build()
client.newCall(request).enqueue(callback)
}
}
//调用
HttpUtil.sendOkHttpRequest(address,object:Callback{
override fun onFailure(call: Call, e: IOException) {
TODO("Not yet implemented")
}
override fun onResponse(call: Call, response: Response) {
TODO("Not yet implemented")
}
})
11.6 Retrofit
Okhttp侧重底层通信的实现,Retrofit侧重上层接口的封装
项目主页:htt ps://github.com/square/retrofit
11.6.1 基本用法
- 配置好根路径,指定服务器地址时只需要传入相对路径,这样就不用每次传完整的URL地址
- 可以对服务器接口进行归类,将同一类的服务器接口定义到同一个接口文档中
- 不需要网络通信的细节,只需要在接口文件中声明一系列的方法和返回值
- 通过注解的方式指定该方法对应哪个服务器接口,以及需要提供哪些参数
- 调用该方法时Retrofit会自动向服务器接口发起请求,并将响应的数据解析成返回值声明的类型
- 添加依赖:将okhttp,gson,retrofit一起引入
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
- Retrofit会将JSON数据转换成对象,所以新增APP类
class App(val id:String,val name:String,val version:String) {
}
- 根据服务器接口的功能进行归类,创建不同种类的接口文件,并在其中定义具体服务器接口的方法
如果服务器只有一个获取JSON数据的接口,这里只需要定义一个接口文件包含一个方法就行
/**
* 命名:具体的功能种类名开头+Service
*/
interface AppService {
/**
* GET注解,表示调用方法的时候RF发起的是GET请求,请求的地址就是注解中传入的参数,这里是相对路径,根路径在使用的时候指定
* 返回值必须指定成RF内置的Call,并通过泛型来指定服务器响应的数据应该转换成什么对象。
* 由于服务器响应的是一个包含App数据的JSON数组,所以可以指定成List<App>
* p453倒数第二段说还可以自定义返回值类型,和RxJava结合
*/
@GET("get_data.json")
fun getAppData(): Call<List<App>>
}
- 新增一个按钮,用于发送网络请求
- 网络请求逻辑
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getAppDataBtn.setOnClickListener {
val retrofit = Retrofit.Builder()//构建Retrofit对象
.baseUrl("http://10.0.2.2")//指定Rt请求的根路径
.addConverterFactory(GsonConverterFactory.create())//指定解析数据时用的转换库,这两个方法必须调用
.build()
val appService = retrofit.create(AppService::class.java)//创建一个接口的动态代理对象,用它可以随意的调用接口中定义的方法
/**
* Retrofit会自动开启子线程,数据会调到Callback中Rt又会自动切换回主线程
*/
appService.getAppData().enqueue(object : Callback<List<App>> {//enqueue 根据注解中匹配的接口地址去进行网络请求,获得的数据会会调到Callback的实现里
override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
val list = response.body()//得到Rf解析后的对象,也就是List<App>
if (list != null) {
for (app in list) {
Log.d(TAG, "id is ${app.id}")
Log.d(TAG, "name is ${app.name}")
Log.d(TAG, "version is ${app.version}")
}
}
}
override fun onFailure(call: Call<List<App>>, t: Throwable) {
TODO("Not yet implemented")
}
})
}
}
}
- 服务器接口是HTTP,需要进行网络安全配置:安卓9.0开始,HTTP因为有安全隐患不再支持,应用默认使用HTTPS
<!--允许我们以明文的方式在网络上传输数据,HTTP使用的就是明文的方式-->
<network-security-config xmlns:android="http://schemas.android.com/apk/res/android">
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system"/>
</trust-anchors>
</base-config>
</network-security-config>
- 配置文件中进行启用,并且添加权限d
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RetrofitTest"
android:networkSecurityConfig="@xml/network_config">
11.6.2 处理复杂的接口地址类型
- 静态类型的接口 : GET http://example.com/get_data.json
- 例
class Data(val id:String,val content: String)
interface ExampleService{
@GET("get_data.json")
fun getData(): Call<Data>
}
- 动态类型的接口 : GET http://example.com/<page>/get_data.json
interface ExampleService{
@GET("{page}/get_data.json")//使用了{page}占位符
fun getData(@Path("page") page: Int): Call<Data>//添加page参数,使用注解来声明参数,这样当发起网络请求时Rt就会把page参数的值替换到占位符的位置上
}
GET http://example.com/get_data.json?u=<user>&t=<token>
interface ExampleService {
@GET("get_data.json")
fun getData(@Query("u") user: String, @Query("t") token: String): Call<Data>
//添加了两个参数,并用@Query注解对它们进行生命,这样网络请求的时候,这样子
//Rt就会自动按照带参数的 GET 请求的格式将这两个参数构建到请求地址当中
}
-
GET 从服务器获取数据
-
POST用于向服务器提交数据
-
PUT 和 PATCH 用于修改服务器上的数据
-
DELETE 删除服务器上的数据
-
接口:DELETE http://example.com/data/<id>
@DELETE("data/{id}")//使用注解来发出DELETE请求
fun deleteData(@Path("id") id:String): Call<ResponseBody>//使用Path来动态指定id
//返回值为ResponseBody,表示Rf表示能够接受任意类型的响应数据,并且不会对数据进行解析
- 接口:POST http://example.com/data/create
interface ExampleService {
//{"id": 1,"content": "The description for this data."}
@POST("data/create")//利用post请求来提交数据,需要将数据放到HTTP请求的body部分,这里借助@Body注解来完成
fun createData(@Body data:Data): Call<ResponseBody>
/**
* 当Rt发出POST请求时,自动将Data数据解析成JSON格式的文本,并放到HTTP请求的body部分,服务器收到请求后只需要从body中将这部分的数据解析出来即可
* 这种写法也可用于PUT、PATCH、DELETE提交数据
*/
}
- 在HTTP请求的header中指定参数
GET http://example.com/get_data.json
User-Agent: okhttp
Cache-Control: max-age=0
- 静态声明
interface ExampleService {
@Headers("User-Agent: okhttp","Cache-Control: max-age=0")
@GET("get_data.json")
fun getData(): Call<Data>
}
- 动态声明
@GET("get_data.json")//发起网络请求的时候Rt会自动将参数设置到两个参数中,从而完成动态设置
fun getData(@Header("User-Agent") userAgent: String, @Header("Cache-Control") cacheControl: String): Call<Data>
11.6.3 Retrofit 构建器的最佳写法
- 普通的获取接口动态代理对象的方法
val retrofit = Retrofit.Builder()
.baseUrl("http://10.0.2.2")
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(AppService::class.java)
- 构建Rt对象全局通用,知识传入Class类型不同进行优化,将前部分功能封装
//object关键字指定SC为单例类
object ServiceCreator {
//指定根路径
private const val BASE_URL ="http://10.0.2.2/"
//private修饰,外部不可见
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
//外部可见,接收Class类型的参数,当调用这个方法的时候实则就是进行了完整的代理对象的创建过程
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
}
- 对上面的使用
val appService = ServiceCreator.create(AppService::class.java)
//之后就可以随意调用接口中的任意方法了
- 利用第十章的泛型实化来进行进一步的优化
//在上面单例类中加入
/**
* 方法不带参数和inline是实化的两个前提
* 用reified来修饰泛型
* 调用前面定义的带参数create
*/
inline fun<reified T> create(): T = create(T::class.java)
- 加入后的使用
val appService = ServiceCreator.create<AppService>()
11.7 Kotlin课堂:协程:编写高并发程序
- 协程和线程很类似
- 协程理解为轻量级的线程,在变成语言层面实现不同协程之间的切换
- 线程重量级,需要依靠操作系统的调度才能实现线程的切换
- 单线程下模拟多线程编程,代码执行时的挂起和恢复完全由编程语言决定
- 开启10万个线程不可能,10万个协程可能
11.7.1 协程的基本用法
// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1"
// 协程 Android 支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1"
- 添加依赖库 https://developer.android.google.cn/kotlin/coroutines
- 开启协程
fun main() {
//这是一个顶层协程,应用程序结束时一起跟着结束
GlobalScope.launch {//创建一个协程的作用域
print("codes run in coroutine scope")
}
Thread.sleep(1000)//主线程阻塞一秒,否则协程中的代码没来得及运行,就和应用程序一起关闭
}
- 如果代码在1s内没有运行完就会被强制中断
fun main() {
GlobalScope.launch {
print("codes run in coroutine scope")
//非阻塞式挂起函数,只会挂起当前协程,不会挂起其他协程
//只能在协程的作用域和其他挂起函数中调用
delay(1500)//让当前协程延迟指定时间再运行
print("codes run in coroutine scope finished")
}
Thread.sleep(1000)//阻塞当前线程,线程下的所有协程都会被阻塞
}
- 协程中所有代码运行完之后再结束
fun main() {
//创建协程的作用域,可以保证在协程作用域内所有代码和子协程运行完前阻塞当前线程
//测试中用,正式环境中用回影响性能
runBlocking {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
}
- 创建子协程
fun main() {
runBlocking {
//在协程的作用域中才能调用
//在当前协程的作用域下创建子协程
//子协程的特点:当前协程结束了作用域下的子协程会一并结束
//GlobalScope.launch顶层协程和线程比较像,线程没有层级永远是顶层
launch {
println("launch1")
delay(1000)
println("launch1 finish")
}
launch {
println("launch2")
delay(1000)
println("launch2 finish")
}
}
}
- 判断协程的效率:开启10w个协程 VS 开启10w个线程会OOM异常
fun main() {
val start = System.currentTimeMillis()
runBlocking {
repeat(100000){
println(".")
}
}
val end = System.currentTimeMillis()
println(end-start)
}
- 当函数逻辑比较复杂提取到一个单独的函数时没有协程作用域就不能挂起,利用suspend关键字解决
suspend fun printDot(){
println(".")
delay(1000)
}
- 将一个函数声明为挂起函数,而没有协程作用域,不能再创建子协程。解决:coroutineScope
//coroutineScope函数也是一个挂起函数,因此可以再任何其他挂起函数中调用
//继承外部协程作用域,创建一个子作用域
suspend fun printDot() = coroutineScope {
launch {
println(".")
delay(1000)
}
}
- 上面函数还会保证作用域内所有代码和子协程全部执行完前,一直阻塞当前协程
fun main() {
runBlocking {
coroutineScope {
launch {
for(i in 1..10){
println(i)
delay(1000)
}
}
}
println("couroutineScope finished")
}
println("runBlocking finished")
}
作用域构建器
- GlobalScope.launch 、runBlocking 任何地方调用
- coroutineScope 协程作用域和挂起函数中调用
- launch 协程作用域中调用
11.7.2 更多作用域构建器
问题:如果协程中网络请求还没结束,活动被关了,那么协程也要退出,如果是顶层协程,需要自己调方法关闭,维护成本太高。如:
fun main() {
val job = GlobalScope.launch {
//具体逻辑
}
job.cancel()
}
//所以这种在实际项目中不常用
- 比较常用的用法
fun main() {
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
//具体逻辑
}
job.cancel()
//所有调用CoroutineScope的launch的函数都被关联在了job下,只需要调用一次cancel同一作用域内的所有协程全部取消
}
补充:CoroutineScope函数更适用于实际项目中,runBlocking在测试用中比较方便
- async函数:创建协程并获得返回结果。注:在协程作用域中才能调用。如果想获取返回结果调用返回Deferred对象的await方法就行
fun main() {
runBlocking {
val result = async {
5 + 5
}.await()
println(result)
}
}
- 问题
fun main() {
runBlocking {
val start = System.currentTimeMillis()
//如果async代码还没执行完,await方法将当前协程阻塞住,直到获得async函数的执行结果
val result = async {
delay(1000)
5 + 5
}.await()
val result2 = async {
delay(1000)
4+6
}.await()
println("result is ${result + result2}")
val end = System.currentTimeMillis()
println("cost ${end - start}ms.")
}
}
用时2s多
- 解决
fun main() {
//需要的时候再调用await方法获取值
runBlocking {
val start = System.currentTimeMillis()
//如果async代码还没执行完,await方法将当前协程阻塞住,直到获得async函数的执行结果
val deferred1 = async {
delay(1000)
5 + 5
}
val deferred2 = async {
delay(1000)
4+6
}
println("result is ${deferred1.await()+deferred2.await()}")
val end = System.currentTimeMillis()
println("cost ${end - start}ms.")
}
}
//1s多
- withContext函数:是一个挂起函数,可以理解为async的简化版写法
fun main() {
runBlocking {
//这个函数调用后会立即执行代码块中的代码,同时将线程阻塞住,最后一行作为返回值返回
//相当于 val result = async{5 + 5}.await()
//不同在于强制指定了参数
//网络请求在主线程的协程中依然会出错
//通过线程参数给协程制定一个具体的运行线程
val result = withContext(Dispatchers.Default){
5 + 5
}
}
}
- Dispatchers.Default 默认使用低并发的线程策略,当执行的代码属于计算密集型任务时,开启过高的并发反而会影响效率
- Dispatchers.IO 使用高并发的策略
- Dispatchers.Main 不会开启子线程而是在主线程中执行,值只能在安卓项目中使用,存Kotlin程序使用这个类型会出错
补充:协程作用域构造器除了coroutineScope之外,其他都可以指定线程,不过这个withContext是强制要求
11.7.3 协程简化回调的写法
普通写法
HttpUtil.sendHttpRequest(address,object :HttpCallbackListener{
override fun onFinish(response: String) {
TODO("Not yet implemented")
}
override fun onError(e: Exception) {
TODO("Not yet implemented")
}
})
- suspendCoroutine 函数,接收Lambda表达式参数,将当前协程立即挂起,然后在一个普通的线程中执行Lambda表达式中的代码。
suspend fun request(address:String): String{
//suspendCoroutine 必须在协程的作用域或者挂起函数中才能调用
//接收lambda表达式,作用是将当前的协程挂起,然后在普通的线程中执行Lambda表达式中的代码
//它的参数列表传入continuation参数,调用它resume和resumeWithException表达式可以让协程恢复运行
//传入函数中的值会成为sC的返回值
return suspendCoroutine { continuation ->
HttpUtil.sendHttpRequest(address,object:HttpCallbackListener{
override fun onFinish(response: String) {
continuation.resume(response)
}
override fun onError(e: Exception) {
continuation.resumeWithException(e)
}
})
}
}
//它是一个挂起函数,只能在其他挂起函数或者协程作用域中调用
//利用合适的项目架构设计,第十五章
suspend fun getBaiduResponse() {
try {
val response = request("https://www.baidu.com")
//对服务器响应数据进行处理
} catch (e: Exception) {
//对异常情况进行处理
}
}
- 对Retrofit的网络请求进行简化 普通写法:
val appService = ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object : Callback<List<App>>{
override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
TODO("Not yet implemented")
}
override fun onFailure(call: Call<List<App>>, t: Throwable) {
TODO("Not yet implemented")
}
})
- 优化
//如果不处理异常,如果发生异常就会一层层向上抛出,直到被某一层函数处理了为止,所以可以在统一的入口函数进行一次try catch
suspend fun getAppData() {
try {
val appList = ServiceCreator.create<AppService>().getAppData().await()
//对服务器数据进行处理
}catch (e:Exception){
//对异常进行处理
}
}
suspend fun <T> Call<T>.await():T{
return suspendCoroutine { continuation ->
enqueue(object : Callback<T>{
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null) {
continuation.resume(body)
}else{
continuation.resumeWithException(RuntimeException("response body is null"))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(RuntimeException("response body is null"))
}
})
}
}