最近学习一下使用Kotlin 从网络获取网络图片,需要学习 HttpURLConnection的使用, 多线程(AsyncTask)的使用等 。
先说总结,获取网络图片有几种方式:
1. 直接创建一个线程获取, 会导致显示的图片错乱。
2. 使用AsyncTask , 确保正常显示图片
3. 使用Kotlin 的协程, 用看似同步的代码写异步的操作。
一、 创建根据URL 获取图片的类
第一种方式为直接创建一个线程获取,但是这种方式是明显不可行的。
// 获取网络图片实现类
class NetworkUtils {
private var picture : Bitmap ?= null
private var context: Context
companion object{
const val TAG = "NetworkUtils"
}
constructor(context: Context) {
this.context = context
}
// 获取网络图片
fun loadPicture(url: URL): Bitmap? {
// 开启一个单独线程进行网络读取
Thread(Runnable {
var bitmap: Bitmap ? = null
try {
// 根据URL 实例, 获取HttpURLConnection 实例
var httpURLConnection: HttpURLConnection = url.openConnection() as HttpURLConnection
// 设置读取 和 连接 time out 时间
httpURLConnection.readTimeout = 2000
httpURLConnection.connectTimeout = 2000
// 获取图片输入流
var inputStream = httpURLConnection.inputStream
// 获取网络响应结果
var responseCode = httpURLConnection.responseCode
// 获取正常
if (responseCode == HttpURLConnection.HTTP_OK) {
// 解析图片
bitmap = BitmapFactory.decodeStream(inputStream)
}
} catch(e: IOException) { // 捕获异常 (例如网络异常)
Log.d(TAG, "loadPicture - error: ${e?.toString()}")
}
this.picture = bitmap
}).start()
// 返回的图片可能为空- 多线程 - 上面的线程还没跑完,已经返回 结果了
return picture
}
}
第二种是使用AsyncTask.
// 三个泛型参数, 第一个为执行,第二个进度,第三个返回
class NetworkUtilsAsyncTask : AsyncTask {
private var resultPicture: Bitmap? = null
private lateinit var context: Context
companion object {
const val TAG = "NetworkUtilsAsyncTask"
}
constructor(context: Context) {
this.context = context
}
override fun doInBackground(vararg params: URL?): Bitmap? {
return loadPicture(params[0])
}
// 获取网络图片
private fun loadPicture(url: URL?): Bitmap? {
// 开启一个单独线程进行网络读取
var bitmapFromNetwork: Bitmap? = null
url?.let {
try {
// 根据URL 实例, 获取HttpURLConnection 实例
var httpURLConnection: HttpURLConnection = url.openConnection() as HttpURLConnection
// 设置读取 和 连接 time out 时间
httpURLConnection.readTimeout = 2000
httpURLConnection.connectTimeout = 2000
// 获取图片输入流
var inputStream = httpURLConnection.inputStream
// 获取网络响应结果
var responseCode = httpURLConnection.responseCode
Log.d(TAG, "loadPicture - responseCode: $responseCode")
// 获取正常
if (responseCode == HttpURLConnection.HTTP_OK) {
// 解析图片
bitmapFromNetwork = BitmapFactory.decodeStream(inputStream)
}
} catch (e: IOException) { // 捕获异常 (例如网络异常)
Log.d(TAG, "loadPicture - error: ${e?.toString()}")
//printErrorMessage(e?.toString())
}
Log.d(TAG, "loadPicture - bitmapFromNetwork: $bitmapFromNetwork")
this.resultPicture = bitmapFromNetwork
// 返回的图片可能为空
}
Log.d(TAG, "loadPicture - resultPicture: $resultPicture")
return resultPicture
}
// 调用UI线程的更新UI操作
override fun onPostExecute(result: Bitmap?) {
super.onPostExecute(result)
Log.d(TAG, "onPostExecute - result: $result")
if (context is MainActivity) {
(context as MainActivity).setResult(result)
}
}
}
使用AsyncTask需要注意几点:
1. 三个泛型参数 AsyncTask
Params: 为你在UI线程启动执行该任务时,需要传递进来的参数
Result: 为你在想在执行任务后,返回什么类型的结果
Progress: 进度条, 一般为Int
2. 每个任务仅能被执行一次,执行多次会报错,记得cancel
AndroidRuntime: Caused by: java.lang.IllegalStateException:
Cannot execute task: the task has already been executed (a task can be executed only once)
3. 任务执行完成后,可以在 onPostExecute 调用UI 逻辑 进行更新UI
第三种是使用Kotlin的协程,其实从网络获取图片的逻辑是一样,区别是怎样调用这个逻辑
class NetworkUtilsCoroutines {
private var resultPicture: Bitmap? = null
private var context: Context
companion object {
const val TAG = "NetworkUtilsCoroutines"
}
constructor(context: Context) {
this.context = context
}
// 获取网络图片
fun loadPicture(url: URL): Bitmap? {
// 开启一个单独线程进行网络读取
var bitmapFromNetwork: Bitmap? = null
try {
// 根据URL 实例, 获取HttpURLConnection 实例
var httpURLConnection: HttpURLConnection = url.openConnection() as HttpURLConnection
// 设置读取 和 连接 time out 时间
httpURLConnection.readTimeout = 2000
httpURLConnection.connectTimeout = 2000
// 获取图片输入流
var inputStream = httpURLConnection.inputStream
// 获取网络响应结果
var responseCode = httpURLConnection.responseCode
// 获取正常
if (responseCode == HttpURLConnection.HTTP_OK) {
// 解析图片
bitmapFromNetwork = BitmapFactory.decodeStream(inputStream)
}
} catch (e: IOException) { // 捕获异常 (例如网络异常)
Log.d(TAG, "loadPicture - error: ${e?.toString()}")
//printErrorMessage(e?.toString())
}
this.resultPicture = bitmapFromNetwork
return resultPicture
}
}
需要注意的是,要在gradle文件配置Kotlin 协程库:
项目根目录的build.gradle文件:
ext.kotlin_coroutines = '1.3.1'
APP目录的build.gradle:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
其中 kotlinx-coroutines-core 为核心库, kotlinx-coroutines-android 为平台库
二、定义网络图片的地址 (单独创建一个常量类,方便管理而已,也可以在用到的地方定义)
object CommonConstants {
const val Address1 =
"http://e.hiphotos.baidu.com/zhidao/pic/item/8cb1cb1349540923f12939199458d109b3de4910.jpg"
const val Address2 =
"http://e.hiphotos.baidu.com/zhidao/pic/item/aec379310a55b31907d3ba3c41a98226cffc1754.jpg"
}
三、定义布局, 获取图片的布局
其中,两个TextView 只是方便调试用的,例如显示当前点击的Button等。
两个Button 分别对应获取两个不同的图片资源。
ImageView 为把从网络获取到的图片显示出来
四、MainActivity 主要调用操作
class MainActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var context: Context
private lateinit var networkUtils: NetworkUtils
private lateinit var networkUtilsAsyncTask: NetworkUtilsAsyncTask
private lateinit var networkUtilsCoroutines: NetworkUtilsCoroutines
companion object {
const val TAG = "MainActivity"
}
//可以变数组, 添加图片URL
private val urlList = mutableListOf(
URL(CommonConstants.Address1), URL(CommonConstants.Address2)
)
//根据Button Id 获取对应的图片URL
private var urlMap = mutableMapOf()
// 根据Button Id 获取对应是第几个Button
private var buttonIndexMap = mutableMapOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
context = this
networkUtils = NetworkUtils(this)
networkUtilsAsyncTask = NetworkUtilsAsyncTask(this)
networkUtilsCoroutines = NetworkUtilsCoroutines(this)
//button1, button2 ...
buttonIndexMap[get_picture_button1.id] = 1
buttonIndexMap[get_picture_button2.id] = 2
urlMap[get_picture_button1.id] = urlList[0]
urlMap[get_picture_button2.id] = urlList[1]
}
override fun onClick(v: View?) {
when (v?.id) {
get_picture_button1.id, get_picture_button2.id -> {
text1.text = "Button : " + buttonIndexMap[v.id] + " is clicked!!!"
//loadPictureDirectly(v.id)
//loadPictureAsyncTask(v.id)
loadPictureCoroutines(v.id)
}
}
}
fun setResult(bitmap: Bitmap?) {
if (bitmap != null) {
Toast.makeText(context, "Load picture success!!!", Toast.LENGTH_SHORT).show()
image_view.setImageBitmap(bitmap)
} else {
Toast.makeText(context, "Can not load picture !!!", Toast.LENGTH_SHORT).show()
}
}
// 1. 使用Thread - 此方法获取的图片存在错误可能,
// 例如第一次点击,获取不到图片; 第二次点击,显示的却是第一次点击的获取的图片?
// --> 多线程问题
private fun loadPictureDirectly(id: Int) {
var bitmap = urlMap[id]?.let { networkUtils.loadPicture(it) }
setResult(bitmap)
}
//2. 使用AsyncTask - 一个AsyncTask 仅能被执行一次
//AndroidRuntime: Caused by: java.lang.IllegalStateException:
// Cannot execute task: the task has already been executed (a task can be executed only once)
private fun loadPictureAsyncTask(id: Int) {
if (networkUtilsAsyncTask != null) {
networkUtilsAsyncTask.cancel(true)
networkUtilsAsyncTask = NetworkUtilsAsyncTask(this)
}
urlMap[id]?.let { networkUtilsAsyncTask.execute(it) }
}
//3. 使用协程 - 看似同步的代码实现异步效果,
private fun loadPictureCoroutines(id: Int) {
// 在主线程开启一个协程
CoroutineScope(Dispatchers.Main).launch {
// 切换到IO 线程 - withContext 能在指定IO 线程执行完成后,切换原来的线程
var bitmap = withContext(Dispatchers.IO) {
text2.text = Thread.currentThread().name.toString()
urlMap[id]?.let { networkUtilsCoroutines.loadPicture(it) }
}
// 切换了UI 线程,更新UI
text2.text = Thread.currentThread().name.toString()
setResult(bitmap)
}
}
/* private suspend fun loadPictureCoroutinesInner(id: Int): Bitmap? {
return withContext(Dispatchers.IO) {
urlMap[id]?.let { networkUtilsCoroutines.loadPicture(it) }
}
}*/
}
其中,两个图片地址定义为:
object CommonConstants {
const val Address1 =
"http://e.hiphotos.baidu.com/zhidao/pic/item/8cb1cb1349540923f12939199458d109b3de4910.jpg"
const val Address2 =
"http://e.hiphotos.baidu.com/zhidao/pic/item/aec379310a55b31907d3ba3c41a98226cffc1754.jpg"
}
五、需要在AndroidManifest.xml中添加网络权限,否则会报错(缺少网络权限)
AndroidRuntime: java.lang.SecurityException: Permission denied (missing INTERNET permission?)
六、使用http 网址,还需要在配置文件中设置 usesCleartextTraffic ,否则会报错
AndroidRuntime: java.io.IOException: Cleartext HTTP traffic to XXX not permitted