模仿Retrofit封装一个使用更简单的网络请求框架
本文已授權微信公眾號:郭霖??在微信公眾號平臺原創首發。會用Retrofit了?你也能自己動手寫一個!
前言
想封裝一套網絡請求,不想直接上來就用別人寫好的,或者說對項目可以更好的掌控,所以自己模仿著Retrofit來寫一套.
想要有如下實現:
?
定義網絡請求函數(如果不使用key來判斷,甚至不需要定義companion object中的LOGIN),示例:
?
調用網絡請求和接收返回數據,示例:
this回調
或者匿名內部類回調:
準備和前提
需要讀者有如下技能,否則閱讀會比較吃力
?
閱讀完本篇文章可以看到(或學到)的知識點
正式開始(從空項目開始,所以每一步都會提及,使用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對象來實現代理.
原理和字節碼解析:https://mp.weixin.qq.com/s/DMnYWXVx0Gf3Mjs38pfOiA
主要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_8targetCompatibility 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? = nullkotlinFunction?.parameters?.forEachIndexed { index, kParameter ->when (kParameter.name) {null -> {//HttpFunctions對象,我們不需要}"_callback" -> {//回調對象,ps:index-1是因為parameters的第0位置是代理類對象callback = args?.get(index - 1) as? ObserverCallBack}else -> {//其他的就是參數了//進行拼接urlurl.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 lt.dygzs@qq.com** get請求* @param url 請求鏈接* @param isEncryption 是否加密,一般網絡請求都是需要加密的,所以設置了默認參數為true* @param callbackName 回調的參數名*/ @Target(AnnotationTarget.FUNCTION)//表示該注解作用于方法上 @Retention(AnnotationRetention.RUNTIME)//表示該注解保留到運行時 annotation class GET(//在kt中 annotation class 表示注解類,而在java中使用 @interfaceval url: String,val isEncryption: Boolean = true,val callbackName: String = "_callback" )/*** creator: lt 2020/3/26 lt.dygzs@qq.com** 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 {if (method.declaringClass == Any::class.java) {//處理Object類的方法return (if (args == null) method.invoke(this) else method.invoke(this, *args))}//ps:這里為了方便就直接new Thread了,如果是使用的話可以使用線程池或者kt協程,消耗會低很多,一般項目中是不允許直接new Thread的thread {//獲取方法的注解,先獲取get注解,如果為空就獲取post注解; ps:自己用的時候可以先獲取常用的注解,這樣就不用判斷兩次了,比如項目里大部分都是post請求,那就先獲取POSTval annotation =method?.getAnnotation(GET::class.java)?: method?.getAnnotation(POST::class.java)//代碼不要都堆到一塊,而是應該拆成方法或者類,這樣調用的時候只調用一個方法,邏輯會清晰很多; ps:這里其實也可以先判斷常用的,因為kt的when函數的字節碼其實也是if elsewhen (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.callbackNamevar callback: ObserverCallBack? = nullvar 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 {//進行拼接urlif (!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 lt.dygzs@qq.com* 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()//初始化dslif (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)? = nullfun success(listener: (bean: T) -> Unit) {mSuccess = listener}/*** 網絡請求失敗的回調*/var mFailed: ((data: String?) -> Unit)? = nullfun failed(listener: (data: String?) -> Unit) {mFailed = listener}/*** 是否自動彈出和關閉loading*/var isAutoShowLoading = true }改造后的回調
7.擴展
其實還有一個Retrofit很常用的功能我沒有實現出來,那就是方法的返回值,其實我們實現起來也很簡單(當然實現Retrofit那么強很難....)
我們可以使用反射來創建返回值,如下所示
改造HttpFunctionsHandler.invokeval returnType = method?.returnTypeval 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很簡單
如果想直接這么簡單的使用,又不想自己封裝,可以使用我修改Retrofit使其更易于使用的框架,文章地址:https://blog.csdn.net/qq_33505109/article/details/108767068
?
demo鏈接如下:https://github.com/ltttttttttttt/RetrofitDemo
?
總結
以上是生活随笔為你收集整理的模仿Retrofit封装一个使用更简单的网络请求框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构特性解析 (二) ArrayLi
- 下一篇: 查看ndk崩溃