Kotlin_獲取網絡圖片(HttpURLConnection, AsyncTask,協程)

最近學習一下使用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>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章