网络技术

网络技术

WebView

用于在程序里简单展示网页。

注意,使用网络功能需要访问权限的。

使用HTTP 访问网络

对于HTTP,需要在改AndroidManifest.xml中配置,允许进行http的明文数据传输。

简单使用

主要使用HttpURLConnection实现。

注意,常用的Http请求有两种,GET和POST。GET表示获取数据,POST表示提交数据,每条数据使用键值对形式存在,中间使用&隔开。

OkHttp

这是第三方开源的项目,使用如下:

  1. 添加依赖;
  2. 创建一个OkHttpClient实例;
  3. 创建Request对象,并在.build()方法之前可以添加很多前缀来丰富这个对象;
  4. 调用newCall()方法来创建Call对象,并调用execute()来执行。

解析XML数据

网络返回数据主要是xml格式或者json格式的。

解析xml格式的数据主要方式为Pull和SAX两种方式。

Pull解析

  1. 创建XmlPullParserFactory实例,并借助这个实例得到XmlPullParser对象;
  2. 调用XmlPullParser的setInput()方法将服务器返回的XML数据设置进去;
  3. 通过getEventType()可以得到当前的解析事件,然后在一个while循环中不断地进行解析,如果当前的解析事件不等于XmlPullParser.END_DOCUMENT,说明解析工作还没完成,调用next()方法后可以获取下一个解析事件。

SAX解析

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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() {
    }
    }
  2. 创建一个SAXParserFactory的对象;

  3. 然后再获取XMLReader对象;

  4. 接着将我们编写的ContentHandler的实例设置到XMLReader中,最后调用parse()方法开始执行解析。

解析JSON数据

JSON的好处在于体积小,能装载的信息量更大。

JSON的解析方式很多种,推荐使用官方的JSONObject和谷歌开源GSON。

JSONObject解析

基本就是键值对的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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")
val name = jsonObject.getString("name")
val version = jsonObject.getString("version")
Log.d("MainActivity", "id is $id")
Log.d("MainActivity", "name is $name")
Log.d("MainActivity", "version is $version")
}
} catch (e: Exception) {
e.printStackTrace()
}
}

GSON解析

它的强大之处就在于可以将一段JSON格式的字符串自动映射成一个对象

使用GSON需要添加依赖库。

1
2
3
4
5
6
7
8
9
10
class Person(val name:String, val age:Int)

// 数据:{"name":"Tom","age":20}
val gson = Gson()
val person = gson.fromJson(jsonData, Person::class.java)

// 如果需要解析的是一段JSON数组,需要借助TypeToken
// [{"name":"Tom","age":20}, {"name":"Jack","age":25}, {"name":"Lily","age":22}]
val typeOf = object : TypeToken<List<Person>>() {}.type
val people = gson.fromJson<List<Person>>(jsonData, typeOf)

网络回调

实现网络回调的方式可以手动实现,具体逻辑为:

  1. 创建一个监听器类,创建完成时的处理方法和错误时的处理方法;
  2. 开启线程发起网络请求,收到网络回复之后调用监听器的完成方法,遇到错误调用监听器的错误方法;

如果使用OkHttp库,可以使用sendOkHttpRequest()方法中的okhttp3.Callback参数,直接编写回调类传入即可。

Retrofit——最好用

这个和OkHttp库师出同门,是在OkHttp和GSON基础上的进一步封装,面向开发者更加友好,更加体现面向对象的编程思维。

而Retrofit的用法就是基于以下几点来设计的:

  1. 首先我们可以配置好一个根路径,然后在指定服务器接口地址时只需要使用相对路径即可;
  2. 另外,Retrofit允许我们对服务器接口进行归类,将功能同属一类的服务器接口定义到同一个接口文件当中,从而让代码结构变得更加合理。
  3. 最后,我们也完全不用关心网络通信的细节,只需要在接口文件中声明一系列方法和返回值,然后通过注解的方式指定该方法对应哪个服务器接口,以及需要提供哪些参数。当我们在程序中调用该方法时,Retrofit会自动向对应的服务器接口发起请求,并将响应的数据解析成返回值声明的类型。

具体用法不赘述,可以查看书籍,很好懂的。


Kotlin课堂

主要和协程相关。

它其实和线程是有点类似的,可以简单地将它理解成一种轻量级的线程。线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换。协程仅在编程语言的层面实现不同协程之间的切换,从而大大提升了并发编程的运行效率。

协程不吃资源,线程很吃操作系统资源的。

基本用法

协程没有定义在Kotlin的官方API里,需要添加依赖才能够使用。

GlobalScope.launch

开启协程,GlobalScope.launch函数可以创建一个协程的作用域,每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟着一起结束。

1
2
3
4
5
6
7
8
fun main() { 
GlobalScope.launch {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
Thread.sleep(1000)
}

delay()函数是一个非阻塞式的挂起函数,它只会挂起当前协程,并不会影响其他协程的运行。而Thread.sleep()方法会阻塞当前的线程,这样运行在该线程下的所有协程都会被阻塞。注意,delay()函数只能在协程的作用域或其他挂起函数中调用。

runBlocking

runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题

1
2
3
4
5
6
7
fun main() { 
runBlocking {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
}

创建多条协程

使用launch函数。注意这里的launch函数和我们刚才所使用的GlobalScope.launch函数不同。首先它必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。GlobalScope.launch函数创建的永远是顶层协程,这一点和线程比较像,因为线程也没有层级这一说,永远都是顶层的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() { 
runBlocking {
launch {
println("launch1")
delay(1000)
println("launch1 finished")
}
launch {
println("launch2")
delay(1000)
println("launch2 finished")
}
}
}

挂起函数

Kotlin提供了一个suspend关键字,使用它可以将任意函数声明成挂起函数,而挂起函数之间都是可以互相调用的。

1
2
3
4
suspend fun printDot() { 
println(".")
delay(1000)
}

suspend关键字只能将一个函数声明成挂起函数,无法提供协程作用域,因此无法在内部调用launch函数。

coroutineScope

coroutineScope函数也是一个挂起函数,因此可以在任何其他挂起函数中调用。它的特点是会继承外部的协程的作用域并创建一个子协程,借助这个特性,我们就可以给任意挂起函数提供协程作用域了。另外,coroutineScope函数和runBlocking函数还有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起。

但是coroutineScope函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此是不会造成任何性能上的问题的。

更多作用域

常用写法

不太建议使用顶层协程,管理成本很高:

1
2
3
4
val job = GlobalScope.launch { 
// 处理具体的逻辑
}
job.cancel()

使用job.cancel() 可以取消协程,主要是每一个顶层协程都需要一一取消。

实际项目中常用写法:

1
2
3
4
5
6
val job = Job() 
val scope = CoroutineScope(job)
scope.launch {
// 处理具体的逻辑
}
job.cancel()

注意这里的CoroutineScope()是个函数,虽然它的命名更像是一个类。CoroutineScope()函数会返回一个CoroutineScope对象,这种语法结构的设计更像是我们创建了一个CoroutineScope的实例,可能也是Kotlin有意为之的。

现在所有调用CoroutineScope的launch函数所创建的协程,都会被关联在Job对象的作用域下面。这样只需要调用一次cancel()方法,就可以将同一作用域内的所有协程全部取消,从而大大降低了协程管理的成本。

获取执行结果

获取协程的执行结果,使用async函数。async函数必须在协程作用域当中才能调用,它会创建一个新的子协程并返回一个Deferred对象,如果我们想要获取async函数代码块的执行结果,只需要调用Deferred对象的await()方法即可。

1
2
3
4
5
6
7
8
fun main() { 
runBlocking {
val result = async {
5 + 5
}.await()
println(result)
}
}

async函数是一个串行的关系。

withContext

withContext()函数是一个挂起函数,大体可以将它理解成async函数的一种简化版写法。

1
2
3
4
5
6
7
8
fun main() { 
runBlocking {
val result = withContext(Dispatchers.Default) {
5 + 5
}
println(result)
}
}

调用withContext()函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为withContext()函数的返回值返回。

withContext()函数强制要求我们指定一个线程参数,线程参数主要有以下3种值可选:Dispatchers.Default、Dispatchers.IO和Dispatchers.Main。

  • Dispatchers.Default表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。
  • Dispatchers.IO表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用Dispatchers.IO。
  • Dispatchers.Main则表示不会开启子线程,而是在Android主线程中执行代码,但是这个值只能在Android项目中使用,纯Kotlin程序使用这种类型的线程参数会出现错误。

简化回调

只需要借助suspendCoroutine函数就能将传统回调机制的写法大幅简化。

suspendCoroutine函数必须在协程作用域或挂起函数中才能调用,它接收一个Lambda表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行Lambda表达式中的代码。Lambda表达式的参数列表上会传入一个Continuation参数,调用它的resume()方法或resumeWithException()可以让协程恢复执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener { 
override fun onFinish(response: String) {
// 得到服务器返回的具体内容
}
override fun onError(e: Exception) {
// 在这里对异常情况进行处理
}
})

// 等价于以下写法
suspend fun request(address: String): String {
return suspendCoroutine { continuation ->
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
continuation.resume(response)
}
override fun onError(e: Exception) {
continuation.resumeWithException(e)
}
})
}
}

事实上,suspendCoroutine函数几乎可以用于简化任何回调的写法。