從0到1模仿Retrofit封裝網絡請求

前言

想封裝一套網絡請求,不想直接上來就用別人寫好的,或者說對項目可以更好的掌控,所以自己模仿着Retrofit來寫一套.

想要有如下實現:

  1. 快捷的網絡請求調用
  2. 聲明式的定義網絡請求函數
  3. 可以很靈活的變更網絡請求的方式(http,https,socket等)
  4. 可以使用自己的線程池或者協程進行線程調度

 

定義網絡請求函數(如果不使用key來判斷,甚至不需要定義companion object中的LOGIN),示例:

 

調用網絡請求和接收返回數據,示例:

this回調

或者匿名內部類回調:

準備和前提

需要讀者有如下技能,否則閱讀會比較喫力

  1. java編程基礎
  2. kotlin編程基礎(java經驗好可能也無所謂) (kotlin下面簡稱kt)
  3. 網絡請求常識

 

閱讀完本篇文章可以看到(或學到)的知識點

  1. 動態代理的使用和工作原理
  2. java和kotlin的部分反射的使用和區別
  3. 聲明和使用運行時註解
  4. dsl的創建,使用和原理
  5. 封裝的思想(我遇到某些代碼時是怎麼想的)

正式開始(從空項目開始,所以每一步都會提及,使用kt寫)

1.測試網絡和url是否通(不然後面沒法驗證到底是哪的問題)

這裏測試的url使用玩安卓的開放api

清單文件加入權限

<uses-permission android:name="android.permission.INTERNET" />

封裝ROOT_URL

object HttpConfig {
    const val ROOT_URL = "https://www.wanandroid.com/"
}

測試如下url是否可用(使用了kt系統庫的擴展函數,和自己定義了一個打印log的函數)

import java.net.URL
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        thread {
            URL(HttpConfig.ROOT_URL + "article/list/0/json?cid=1").readText().print()
        }//這裏,如果沒問題的話就會在logcat中打印出本次網絡請求返回的數據
    }

}

fun Any?.print() = Log.w("lllttt", this.toString())

2.開始仿照Retrofit的聲明式接口,自己定義一個接口

網絡回調的接口

interface ObserverCallBack {
    /**
     * @param data     返回的數據 json
     * @param encoding 網絡請求的狀態(成功,失敗,網絡失敗等)
     * @param method   判斷是哪個接口返回的數據
     */
    fun handleResult(data: String?, encoding: Int, method: Int)
}

聲明的網絡接口

interface HttpFunctions {
    /**
     * 獲取玩安卓的json數據
     * @param cid 這個接口的參數(雖然不知道有什麼用emmm)
     */
    fun getJson(_callback: ObserverCallBack?,
              cid: String
    )
}

顯然上面所聲明的網絡接口是沒法直接調用的,想要調用一個接口的方法,必須有其實現類,而實現該接口對於便捷的網絡封裝是不現實的,而使用動態代理,就可以在運行時動態生成一個實現類,並且還可以使用代碼動態的控制其函數的邏輯

3.使用動態代理獲取獲取運行時的接口實現類,並獲取運行時數據

動態代理平時說的挺玄乎,其實使用和理解起來還是很簡單的

大體原理可以這麼理解:動態的實現一個類,繼承Proxy,並實現所有傳入的接口,然後通過反射創建出來這個類,方法都是默認空實現,並且每次調用方法都會經過InvocationHandler的invoke方法,invoke方法裏有調用方法的Method對象,可以反射Method對象來實現代理.

主要api:

Proxy.newProxyInstance()

該方法一共三個參數,第一個是類加載器,第二個就是被代理的接口class集合,第三個是處理方法的InvocationHandler

我們可以這樣生成動態代理:

interface HttpFunctions {
    companion object {
        /**
         * 動態代理單例對象
         */
        val instance: HttpFunctions = getHttpFunctions()

        //獲取動態代理實例對象
        private fun getHttpFunctions(): HttpFunctions {
            val clazz = HttpFunctions::class.java//拿到我們被代理接口的class對象
            return Proxy.newProxyInstance(//調用動態代理生成的方法來生成動態代理
                    clazz.classLoader,//類加載器對象
                    arrayOf(clazz),//因爲我們的接口不需要繼承別的接口,所以直接傳入接口的class就行
                    HttpFunctionsHandler()//InvocationHandler接口的實現類,用來處理代理對象的方法調用
            ) as HttpFunctions
        }
    }
}

接下來我們實現InvocationHandler接口,可以發現只有一個方法,重寫後打印動態代理對象調用的方法名稱和方法參數(由於使用kt的接口做爲被代理,所以可以返回Unit對象)

/**
 * 動態代理類方法處理對象
 */
class HttpFunctionsHandler : InvocationHandler {
    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any {
        method?.name.print()//打印方法名
        args?.forEach { it.print() }//打印參數值
        return Unit
    }
}

接下來我們調用動態代理,測試一下

HttpFunctions.instance.getJson(null, "1")
打印如下:
W/lllttt: getJson
W/lllttt: null
W/lllttt: 1

可以看到我們確實拿到了方法名稱和參數的值

4.動態代理結合反射實現網絡請求

現在我們修改HttpFunctionsHandler的代碼來通過反射拿到參數名

    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any {
        method?.name.print()//打印方法名
        args?.forEach { it.print() }//打印參數值
        method?.parameters?.forEach { it.name.print() }//打印參數名
        return Unit
    }
但是發現打印如下:
W/lllttt: getJson
W/lllttt: null
W/lllttt: 1
W/lllttt: arg0
W/lllttt: arg1

參數名變成了argx(而且在安卓項目上需要api26以上才能使用),這是爲什麼呢?

原來java8之前的版本因爲某些原因沒有支持保留方法參數名的功能,直到java8才支持,且需要手動設置編譯參數,所以此種方案無法實現

ps:安卓項目使用如下方式只能開啓java8的部分能力(如lambda和stream),不能開啓全部

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

那Retrofit是怎麼繞開這個限制的呢?使用參數註解,如下:

    //發表評論
    @FormUrlEncoded
    @POST("v1/comment/create")
    Observable<NetBean<Boolean>> commentCreate(
            @Field("scene") String scene,
            @Field("scene_id") Long scene_id,
            @Field("reply_id") Long reply_id,
            @Field("content") String content
    );

這也太麻煩了,每一個參數都得對應一個註解,而方法上還需要加兩個註解

所以我們使用一種更便捷的方式:kt反射

首先引入kt的反射庫(大小几百k)

implementation 'org.jetbrains.kotlin:kotlin-reflect:1.3.71'

然後改造HttpFunctionsHandler

    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any {
//        method?.name.print()//打印方法名
//        args?.forEach { it.print() }//打印參數值
        method?.kotlinFunction?.parameters?.forEach {
            "${it.type} - ${it.name}".print()//打印參數類型和參數名
        }
        return Unit
    }
打印結果如下:
W/lllttt: com.lt.retrofitdemo.http.HttpFunctions - null
W/lllttt: com.lt.retrofitdemo.http.ObserverCallBack? - _callback
W/lllttt: kotlin.String - cid

我們成功的獲取到了參數名,現在可以再次改造HttpFunctionsHandler,使調用HttpFunctions的方法就相當於調用網絡請求

改造HttpFunctionsHandler (爲了方便演示,只適配get請求,且網絡請求方式比較簡單)

/**
 * 動態代理類方法處理對象
 */
class HttpFunctionsHandler : InvocationHandler {
    val handler = Handler(Looper.getMainLooper())

    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any {
        thread {
            val kotlinFunction = method?.kotlinFunction//獲取到KFunction對象
            val url = StringBuilder(HttpConfig.ROOT_URL)
                .append("article/list/0/json?")
            var callback: ObserverCallBack? = null
            kotlinFunction?.parameters?.forEachIndexed { index, kParameter ->
                when (kParameter.name) {
                    null -> {//HttpFunctions對象,我們不需要
                    }
                    "_callback" -> {//回調對象,ps:index-1是因爲parameters的第0位置是代理類對象
                        callback = args?.get(index - 1) as? ObserverCallBack
                    }
                    else -> {//其他的就是參數了
                        //進行拼接url
                        url.append(kParameter.name)
                            .append('=')
                            .append(args?.get(index - 1))
                            .append('&')
                    }
                }
            }
            if (url.endsWith('&'))
                url.deleteCharAt(url.length - 1)//清除最後一個&
            url.print()
            val data = URL(url.toString()).readText()//請求網絡
            handler.post {
                callback?.handleResult(data, 0, 0)//在主線程回調
            }
        }
        return Unit
    }
}

然後調用封裝後的方法:

        HttpFunctions.instance.getJson(object : ObserverCallBack {
            override fun handleResult(data: String?, encoding: Int, method: Int) {
                data.print()
            }
        }, "1")
打印如下:
W/lllttt: https://www.wanandroid.com/article/list/0/json?cid=1
W/lllttt: {"data":{"curPage":1,"datas":[],"offset":0,"over":true,"pageCount":0,"size":20,"total":0},"errorCode":0,"errorMsg":""}

可以看到網絡請求調用很方便,不用使用參數註解就可以,那kt反射是怎麼實現的呢?我們來看一下kt文件反編譯後的字節碼

使用kt後會出現上面這個選項,使用該選項可以看到kt文件生成的字節碼,然後點擊Decompile按鈕可以生成反編譯後的java文件,這樣就能看到我們HttpFunctions.kt類到底有什麼

@Metadata(
   mv = {1, 1, 16},
   bv = {1, 0, 3},
   k = 1,
   d1 = {"\u0000\u001e\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\bf\u0018\u0000 \b2\u00020\u0001:\u0001\bJ\u001a\u0010\u0002\u001a\u00020\u00032\b\u0010\u0004\u001a\u0004\u0018\u00010\u00052\u0006\u0010\u0006\u001a\u00020\u0007H&¨\u0006\t"},
   d2 = {"Lcom/lt/retrofitdemo/http/HttpFunctions;", "", "getJson", "", "_callback", "Lcom/lt/retrofitdemo/http/ObserverCallBack;", "cid", "", "Companion", "app_debug"}
)
public interface HttpFunctions {
   HttpFunctions.Companion Companion = HttpFunctions.Companion.$$INSTANCE;

   void getJson(@Nullable ObserverCallBack var1, @NotNull String var2);
//只展示我們需要的

可以看到,kt自動爲我們的.kt類生成了@Metadata註解(元數據註解),其中d2的元數據中把我們的類簽名,方法名和參數名等都列了出來,所以kt反射取到的參數名就是從這裏面取出來的

5.使用註解來增強功能

現在我們的HttpFunctions只支持get請求,url也沒地方設置,並且自定義化還沒法做,所以我們使用註解,並搭配運行時反射來增強功能

創建GET和POST兩個註解

/**
 * creator: lt  2020/3/26  [email protected]
 *
 * get請求
 * @param url               請求鏈接
 * @param isEncryption      是否加密,一般網絡請求都是需要加密的,所以設置了默認參數爲true
 * @param callbackName      回調的參數名
 */
@Target(AnnotationTarget.FUNCTION)//表示該註解作用於方法上
@Retention(AnnotationRetention.RUNTIME)//表示該註解保留到運行時
annotation class GET(//在kt中 annotation class 表示註解類,而在java中使用 @interface
    val url: String,
    val isEncryption: Boolean = true,
    val callbackName: String = "_callback"
)

/**
 * creator: lt  2020/3/26  [email protected]
 *
 * post請求
 * @param url               請求鏈接
 * @param isEncryption      是否加密
 * @param callbackName      回調的參數名
 */
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class POST(
    val url: String,
    val isEncryption: Boolean = true,
    val callbackName: String = "_callback"
)

接下來我們改造HttpFunctionsHandler的invoke方法,加入註解的判斷

/**
 * 動態代理類方法處理對象
 */
class HttpFunctionsHandler : InvocationHandler {
    val handler = Handler(Looper.getMainLooper())

    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any {
        //ps:這裏爲了方便就直接new Thread了,如果是使用的話可以使用線程池或者kt協程,消耗會低很多,一般項目中是不允許直接new Thread的
        thread {
            //獲取方法的註解,先獲取get註解,如果爲空就獲取post註解; ps:自己用的時候可以先獲取常用的註解,這樣就不用判斷兩次了,比如項目裏大部分都是post請求,那就先獲取POST
            val annotation =
                method?.getAnnotation(GET::class.java)
                    ?: method?.getAnnotation(POST::class.java)
            //代碼不要都堆到一塊,而是應該拆成方法或者類,這樣調用的時候只調用一個方法,邏輯會清晰很多; ps:這裏其實也可以先判斷常用的,因爲kt的when函數的字節碼其實也是if else
            when (annotation) {
                is GET -> startGet(proxy, method, args, annotation)
                is POST -> startPost(proxy, method, args, annotation)
                else -> throw RuntimeException("親,${method?.name}方法是不是忘加註解了?")//如果出現異常情況,最好不要藏着,及時告訴開發人員,不然出了問題也不知道是怎麼回事,得找好長時間
            }
        }
        return Unit
    }

    //post請求
    private fun startPost(proxy: Any?, method: Method?, args: Array<out Any>?, post: POST) {
        //post就不寫了,大家可以在這裏二次封裝網絡請求,比如使用okhttp,或者使用Socket,甚至可以用別人二次或者三次封裝好的網絡請求
    }

    //get請求
    private fun startGet(proxy: Any?, method: Method?, args: Array<out Any>?, get: GET) {
        //獲取url並拼接
        val url = StringBuilder(HttpConfig.ROOT_URL).append(get.url)
        val callbackName = get.callbackName
        var callback: ObserverCallBack? = null
        var isAddQuestionMark = false//是否追加了'?'
        method?.kotlinFunction?.parameters?.forEachIndexed { index, kParameter ->
            when (kParameter.name) {
                null -> {//HttpFunctions對象,我們不需要
                }
                callbackName -> {//回調對象,ps:index-1是因爲parameters的第0位置是代理類對象
                    callback = args?.get(index - 1) as? ObserverCallBack
                }
                else -> {//其他的就是參數了
                    if (get.isEncryption) {
                        //加密操作
                    } else {
                        //進行拼接url
                        if (!isAddQuestionMark) {
                            url.append('?')
                            isAddQuestionMark = true
                        }
                        url.append(kParameter.name)
                            .append('=')
                            .append(args?.get(index - 1))
                            .append('&')
                    }
                }
            }
        }
        if (url.endsWith('&'))
            url.deleteCharAt(url.length - 1)//清除最後一個&
        url.print()
        val data = URL(url.toString()).readText()//請求網絡
        handler.post {
            callback?.handleResult(data, 0, 0)//在主線程回調
        }
    }
}

然後改變網絡請求方法,再調用測試成功

//修改網絡請求
@GET("article/list/0/json", isEncryption = false)
fun getJson(
    _callback: ObserverCallBack?,
    cid: String
)

6.使用dsl封裝回調,使其更方便的處理

寫一個簡單的dsl,裏面參數比較少,可以根據業務需求自行添加參數

import com.alibaba.fastjson.JSONObject
import com.lt.retrofitdemo.print

/**
 * creator: lt  2020/3/26  [email protected]
 * effect : 網絡請求回調的sdl封裝
 * warning:
 */
/**
 * 使用dsl的callback
 * ps: CallBackDsl.()這種語法相當於CallBackDsl的一個擴展函數,把CallBackDsl當做這個函數的this,所以該函數中可以不用this.就可以調用CallBackDsl的參數和方法
 */
inline fun <reified T> callbackOf(initDsl: CallBackDsl<T>.() -> Unit): ObserverCallBack {
    val dsl = CallBackDsl<T>()
    dsl.initDsl()//初始化dsl
    if (dsl.isAutoShowLoading)
        "Show loading dialog".print()
    return object : ObserverCallBack {
        override fun handleResult(data: String?, encoding: Int, method: Int) {
            if (dsl.isAutoShowLoading)
                "Dismiss loading dialog".print()
            //可以在這裏根據業務判斷是否請求成功
            //引入fastjson來解析json    implementation 'com.alibaba:fastjson:1.2.67'
            val bean = JSONObject.parseObject(data, T::class.java)
            if (bean != null) {
                dsl.mSuccess?.invoke(bean)
            } else {
                dsl.mFailed?.invoke(data)
            }
        }
    }
}

class CallBackDsl<T> {
    /**
     * 網絡請求成功的回調
     */
    var mSuccess: ((bean: T) -> Unit)? = null

    fun success(listener: (bean: T) -> Unit) {
        mSuccess = listener
    }

    /**
     * 網絡請求失敗的回調
     */
    var mFailed: ((data: String?) -> Unit)? = null

    fun failed(listener: (data: String?) -> Unit) {
        mFailed = listener
    }

    /**
     * 是否自動彈出和關閉loading
     */
    var isAutoShowLoading = true
}

改造後的回調

7.擴展

其實還有一個Retrofit很常用的功能我沒有實現出來,那就是方法的返回值,其實我們實現起來也很簡單(當然實現Retrofit那麼強很難....)

我們可以使用反射來創建返回值,如下所示

改造HttpFunctionsHandler.invoke

        val returnType = method?.returnType
        val newInstance = returnType?.newInstance()
        returnType?.fields?.forEach {
            it.set(newInstance, "根據業務邏輯來判斷設置什麼內容")
        }
        return newInstance!!

8.混淆

如果打開了混淆的話,不配置以下內容會導致運行時報錯;如果不開啓混淆則可以忽略

-keepclassmembers public interface com.lt.retrofitdemo.http.HttpFunctions {*;}#防止自定的接口方法名被混淆
-keepclasseswithmembernames public interface com.lt.retrofitdemo.http.ObserverCallBack {*;}#因爲使用到了反射,所以回調的類名稱也不能被混淆
-keep class kotlin.reflect.jvm.internal.impl.load.java.**{*; }#防止kt反射被混淆
-keep class kotlin.Metadata{*; }#防止kt元註解被混淆

結語

這樣一個網絡請求的封裝基本就搞定了,聲明和調用都很方便

中間由於演示,有很多功能都沒有實現或者實現的不完全,大家可以在實現自己的框架的時候可以自行完善,並且可以添加更多的功能

而且這樣封裝比較靈活,因爲具體的邏輯都在HttpFunctionsHandler的startGet和startPost中,所以要更改網絡請求的框架或者切換http和Socket很簡單

 

demo鏈接如下:https://github.com/ltttttttttttt/RetrofitDemo

 

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