前言
想封裝一套網絡請求,不想直接上來就用別人寫好的,或者說對項目可以更好的掌控,所以自己模仿着Retrofit來寫一套.
想要有如下實現:
- 快捷的網絡請求調用
- 聲明式的定義網絡請求函數
- 可以很靈活的變更網絡請求的方式(http,https,socket等)
- 可以使用自己的線程池或者協程進行線程調度
定義網絡請求函數(如果不使用key來判斷,甚至不需要定義companion object中的LOGIN),示例:
調用網絡請求和接收返回數據,示例:
this回調
或者匿名內部類回調:
準備和前提
需要讀者有如下技能,否則閱讀會比較喫力
- java編程基礎
- kotlin編程基礎(java經驗好可能也無所謂) (kotlin下面簡稱kt)
- 網絡請求常識
閱讀完本篇文章可以看到(或學到)的知識點
- 動態代理的使用和工作原理
- java和kotlin的部分反射的使用和區別
- 聲明和使用運行時註解
- dsl的創建,使用和原理
- 封裝的思想(我遇到某些代碼時是怎麼想的)
正式開始(從空項目開始,所以每一步都會提及,使用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