最近學習一下使用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<URL, Int, Bitmap> {
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, Progress, Result>
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"
}
三、定義佈局, 獲取圖片的佈局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<TextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/text_size_20"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/text_size_20"
app:layout_constraintStart_toStartOf="@id/text1"
app:layout_constraintTop_toBottomOf="@id/text1"/>
<Button
android:id="@+id/get_picture_button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Get picture1"
android:onClick="onClick"
app:layout_constraintStart_toStartOf="@id/text2"
app:layout_constraintTop_toBottomOf="@id/text2"/>
<Button
android:id="@+id/get_picture_button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Get picture2"
android:onClick="onClick"
app:layout_constraintStart_toEndOf="@id/get_picture_button1"
app:layout_constraintTop_toBottomOf="@id/text2"/>
<ImageView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/get_picture_button1"
app:layout_constraintTop_toBottomOf="@id/get_picture_button1" />
</androidx.constraintlayout.widget.ConstraintLayout>
其中,兩個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<Int, URL>()
// 根據Button Id 獲取對應是第幾個Button
private var buttonIndexMap = mutableMapOf<Int, Int>()
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?)
<uses-permission android:name="android.permission.INTERNET" />
六、使用http 網址,還需要在配置文件中設置 usesCleartextTraffic ,否則會報錯
AndroidRuntime: java.io.IOException: Cleartext HTTP traffic to XXX not permitted
<application
...
android:usesCleartextTraffic="true"
...
</application>