在android中,網(wǎng)絡(luò)模塊是一個不可或缺的模塊括改,相信很多公司都會有自建的網(wǎng)絡(luò)庫腻豌。目前市面上主流的網(wǎng)絡(luò)請求框架都是基于okHttp做的延伸和擴展,并且android底層的網(wǎng)絡(luò)庫實現(xiàn)也使用OkHttp了,可見okHttp應(yīng)用的廣泛性吝梅。
Retrofit本身就是對于OkHttp庫的封裝虱疏,它的優(yōu)點很很多,比如注解來實現(xiàn)的苏携,配置簡單做瞪,使用方便等。那為什么我們要做二次封裝呢右冻?最根本的原因還是我們現(xiàn)有的業(yè)務(wù)過于復(fù)雜装蓬,我們期望有更多的自定義的能力,有更好用的使用方式等纱扭。就好比下面這些自定義的能力
- 屏蔽底層的網(wǎng)絡(luò)庫實現(xiàn)
- 網(wǎng)絡(luò)層統(tǒng)一處理code碼和線程回調(diào)問題
- 網(wǎng)絡(luò)請求綁定生命周期
- 網(wǎng)絡(luò)層的全局監(jiān)控
- 網(wǎng)絡(luò)的調(diào)試能力
- 網(wǎng)絡(luò)層對于組件化的通用能力支持
這些目前能力目前如果直接使用Retrofit牍帚,基本都是滿足不了的。 本文是基于Retrofit + OkHttp提供的基礎(chǔ)能力上乳蛾,做的網(wǎng)絡(luò)庫的二次封裝履羞。主要介紹下如何在retrofit和Okhhtp的基礎(chǔ)上,提供上述幾個通用的能力屡久。 本文需要有部分okHttp和retrofit源碼的了解忆首。 有興趣的可以先查看官方文檔,傳送門:
屏蔽底層的網(wǎng)絡(luò)庫實現(xiàn)
雖然Retrofit是一個非常強大的封裝框架被环,但是它并沒有完全把網(wǎng)路庫底層的實現(xiàn)的屏蔽掉糙及。 默認的內(nèi)部網(wǎng)絡(luò)請求使用的okHttp,在我們創(chuàng)建Retrofit實例的時候筛欢,如果需要配置攔截器浸锨,就會直接依賴到底層的OkHttp,導(dǎo)致上層業(yè)務(wù)直接訪問到了網(wǎng)絡(luò)庫的底層實現(xiàn)版姑。這個對于后續(xù)的網(wǎng)絡(luò)庫底層的替換會是一個不小的成本柱搜。 因此,我們希望能夠封裝一層網(wǎng)絡(luò)層剥险,讓業(yè)務(wù)的使用僅僅依賴到網(wǎng)絡(luò)庫的封裝層聪蘸,而不會使用到網(wǎng)絡(luò)庫的底層實現(xiàn)。 首先表制,我們需要先知道業(yè)務(wù)層當(dāng)前使用到了哪些網(wǎng)絡(luò)庫底層的API健爬, 其實最主要的還是攔截器這一層的封裝。 攔截器這一層么介,主要涉及到幾個類:
- Request
- Response
- Chain和Intercept
我們可以針對這幾個類進行封裝娜遵,定義對象接口,IRequest壤短、IResponse设拟、IChain和INetIntercept慨仿,這套接口不帶任何具體實現(xiàn)。 然后在真正需要訪問到具體的實例的時候纳胧,轉(zhuǎn)化成具體的Request和Response等镰吆。我們可以看看在自己定義了一套攔截器之后,如何添加到之前OkHttp的流程中躲雅。 先看看IChain和INetIntercept的定義。
interface IChain {
fun getRequestInfo(): IRequest
@Throws(IOException::class)
fun proceed(request: IRequest): IResponse?
}
interface INetInterceptor {
@Throws(IOException::class)
fun intercept(chain: IChain): IResponse?
}
在構(gòu)造Retrofit的實例時骡和,內(nèi)部會嘗試創(chuàng)建OkHttpClient相赁,在此時把外部傳入的INetInterceptor合并組裝成一個OkHttp的攔截器,添加到OkHttpClient中。
fun swicherToIntercept(list: MutableList<INetInterceptor>): Interceptor {
return object: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response? {
val netRequest = IRequest(chain.request())
val realChain = IRealChain(0, netRequest, list as MutableList<IInterceptor>, chain, this)
val response: Response?
return (realChain.proceed(netRequest) as? IResponse)?.response
}
}
}
整體修改后的攔截器的調(diào)用鏈如下所示:
上面舉的只是在構(gòu)建攔截器中的隔離慰于,如果你們項目還有訪問到其他內(nèi)部的OkHttp的能力钮科,也可以參照上面的封裝流程,定義接口婆赠,在需要使用的地方轉(zhuǎn)換為具體實現(xiàn)绵脯。
Retrofit的Call自定義
對于Retrofit,我們在接口中定義的方法就是每一個請求的配置休里,每一個請求都會被包裝成Call蛆挫。我們想要的請求做一些通用的邏輯處理和自定義,就比如在請求前做一些邏輯處理妙黍,請求后做一些邏輯處理悴侵,最后才返回給上層,就需要hook這個請求流程拭嫁,可以做Retrofit的二次動態(tài)代理可免。 如果希望做一些更精細化的處理,hook能力就滿足不了了做粤。這種時候浇借,可以選擇使用自定義Call對象。如果整個Call對象都是我們提供的怕品,我們當(dāng)然可以在里面實現(xiàn)任何我們期望的邏輯妇垢。接下來簡單介紹下如何自定義Retrofit的Call對象。
定義Call類型
class TestCall<T>(internal var call: Call<T>) {}
自定義CallAdapter
自定義CallAdapter時肉康,需要使用我們前面自定義的返回值類型修己,并將call對象轉(zhuǎn)化為我們我們自定義的返回值類型。
class NetCallAdapter<R>(repsoneType: Type): CallAdapter<R, TestCall<R>> {
override fun adapt(call: Call<R>): TestCall<R> {
return TestCall(call)
}
override fun responseType(): Type {
return responseType
}
}
- 首先需要在class的繼承關(guān)系上迎罗,顯式的標明CallAdapter的第二個泛型參數(shù)是我們自定義的Call類型睬愤。
- 在adapt適配方法中,通過原始的call纹安,轉(zhuǎn)化為我們期望的TestCall尤辱。
自定義Factory
class NetCallAdapterFactory: CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
val rawType = getRawType(returnType)
if (rawType == TestCall::class.java && returnType is ParameterizedType) {
val callReturnType = getParameterUpperBound(0, returnType)
return NetCallAdapter<ParameterizedType>(callReturnType)
}
return null
}
}
在自定義的Factory中砂豌,根據(jù)從接口定義中獲取到的網(wǎng)絡(luò)返回值,匹配TestCall類型光督,如果匹配上阳距,就返回我們定義的CallAdapter。
注冊Factory
val builder = Retrofit.Builder()
.baseUrl(retrofitBuilder.baseUrl!!)
.client(client)
.addCallAdapterFactory(NetCallAdapterFactory())
網(wǎng)絡(luò)層統(tǒng)一處理code碼和線程回調(diào)問題
code碼統(tǒng)一處理
相信每一個產(chǎn)品都會定義業(yè)務(wù)錯誤碼结借,每一個業(yè)務(wù)都可能有自己的一套錯誤碼筐摘,有一些錯誤碼可能是全局的,比如說登錄過期船老、被封禁等咖熟,這種錯誤碼可能跟特定的接口無關(guān),而是一個全局的業(yè)務(wù)錯誤碼柳畔,在收到這些錯誤碼時馍管,會有統(tǒng)一的邏輯處理。 我們可以先定義code碼解析的接口
interface ICodehandler {
fun handle(context: Context?, code: Int, message: String?, isBackGround: Boolean): Boolean
}
code碼處理器的注冊薪韩。
code碼處理器的注冊方式有兩種确沸,一種是全局的code碼處理器。 在創(chuàng)建Retrofit實例的傳入俘陷。
NetWorkClientBuilder()
.addNetCodeHandler(SocialCodeHandler())
.build()
另一種是在具體的網(wǎng)絡(luò)請求時罗捎,傳入錯誤碼處理器,
TestInterface.inst.testCall().backGround(true)
.withInterceptor(new CodeRespHandler() {
@Override
public boolean handle(int code, @Nullable String message) {
....
}
})
.enqueue(null)
code碼處理的調(diào)用
因為Call是我們自定義的拉盾,我們可以在網(wǎng)絡(luò)成功的返回時宛逗,優(yōu)先執(zhí)行錯誤碼處理器,如果命中業(yè)務(wù)錯誤碼盾剩,那么對外返回失敗雷激。否則正常返回成功。
線程回調(diào)
OkHttp的callback線程回調(diào)默認是在子線程告私,retrofit的回調(diào)線程取決于創(chuàng)建實例時的配置屎暇,可以配置callbackExecutor,這個是對整個實例生效的驻粟,在這個實例內(nèi)根悼,所有的網(wǎng)絡(luò)返回都會通過callbackExecutor。我們希望能夠針對每一個接口單獨配置回調(diào)的線程蜀撑,所以同樣基于自定義call的前提下挤巡,我們自定義Callback和UiCallback。
- Callback: 表示當(dāng)前回調(diào)線程無需主線程
- UICallback: 表示當(dāng)前回調(diào)線程需要在主線程
通用業(yè)務(wù)傳入的接口類型就標識了當(dāng)前回調(diào)的線程.
網(wǎng)絡(luò)請求綁定生命周期
大部分網(wǎng)絡(luò)請求都是異步發(fā)起的酷麦。所以可能會導(dǎo)致下面兩個問題:
- 內(nèi)存泄漏問題
- 空指針問題
先看一個比較常見的內(nèi)存泄漏的場景
class XXXFragment {
var unBinder: Unbinder? = null
@BindView(R.id.xxxx)
val view: AView;
@Override
public void onDestroyView() {
unBinder?.unbind();
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view= super.onCreateView(inflater, container, savedInstanceState)
unBinder = ButterKnife.bind(this, view)
loadDataOfPay(1, 20)
return view
}
private void testFun() {
TestInterface.getInst().getTestFun()
.enqueue(new UICallback<TestResponse>() {
@Override
public void onSuccessful(TestResponse test) {
view.xxxx = test.xxx
}
@Override
public void onFailure(@NotNull NetException e) {
....
}
});
}
}
在上面的例子中矿卑,在testFun方法中編譯后會創(chuàng)建匿名內(nèi)部類,并且顯式的調(diào)用了外部Fragment的View沃饶,一旦這個網(wǎng)絡(luò)請求阻塞了母廷,或者晚于這個Fragment的銷毀時機回調(diào)轻黑,就會導(dǎo)致這個Fragment出現(xiàn)內(nèi)存泄漏,直至這個請求正常結(jié)束返回琴昆。
更嚴重的是氓鄙,這個被操作的view是通過ButterKnife綁定的,在Fragment走到onDestory之后就進行了解綁业舍,會將這個View的值設(shè)置為null抖拦,導(dǎo)致在這個callback的回調(diào)時候,可能出現(xiàn)view為null的情況舷暮,導(dǎo)致空指針态罪。 對于空指針的問題,我們可以看到有很多網(wǎng)絡(luò)請求的回調(diào)都可能會出現(xiàn)類似下面的代碼段脚牍。
TestInterface.getInst().getTestFun()
.enqueue(new UICallback<TestResponse>() {
@Override
public void onSuccessful(TestResponse test) {
if(!isFinishing() && view != null) {
view.xxxx = test.xxx
}
}});
在匿名內(nèi)部類回調(diào)時向臀,通過判斷頁面是否已經(jīng)銷毀巢墅,以及view是否為空诸狭,再進行對應(yīng)的UI操作。 我們通過動態(tài)代理來解決了這個空指針和內(nèi)存泄漏的問題君纫。 詳細的方案可以閱讀下這個文章匿名內(nèi)部類導(dǎo)致內(nèi)存泄漏的解決方案 因為我們把Activity驯遇、Fragment抽象為UIContext。在網(wǎng)絡(luò)接口調(diào)用時蓄髓,傳入對應(yīng)的UIContext叉庐,會將網(wǎng)絡(luò)請求的Callabck通過動態(tài)代理,將Callback和UIContext進行關(guān)聯(lián)会喝,在頁面銷毀時陡叠,不進行回調(diào)。
自動Cancel無用請求
很多的業(yè)務(wù)場景中肢执,在頁面一進去就會觸發(fā)很多網(wǎng)絡(luò)請求枉阵,這個請求可能有一部分處于網(wǎng)絡(luò)庫的請求等待隊列中,一部分處于進行中预茄。當(dāng)我們退出了這個頁面之后兴溜,這些網(wǎng)絡(luò)請求其實都已經(jīng)沒有了存在的意義。 所以我們可以在頁面銷毀時耻陕,取消還未發(fā)起和進行中的網(wǎng)絡(luò)請求拙徽。 我們可以通過上面提過的UIContext,將網(wǎng)絡(luò)請求跟頁面進行關(guān)聯(lián)诗宣。監(jiān)聽頁面的生命周期膘怕,在頁面關(guān)閉時,cancel掉對應(yīng)的網(wǎng)絡(luò)請求召庞。
頁面關(guān)聯(lián)
在網(wǎng)絡(luò)請求發(fā)起前淳蔼,把當(dāng)前的網(wǎng)絡(luò)請求關(guān)聯(lián)上對應(yīng)的頁面侧蘸。
class TestCall {
fun enqueue(uiCallBack: Callback, uiContext: UIContext?) {
LifeCycleRequestManager.registerCall(this, uiContext)
....
}
}
internal object LifeCycleRequestManager {
init {
registerApplicationLifecycle()
}
private val registerCallMap = ConcurrentHashMap<Int, MutableList<BaseNetCall>>()
}
ConcurrentHashMap的key為頁面的HashCode,value的請求list鹉梨。每一個頁面都會關(guān)聯(lián)一個請求List讳癌。
cancel請求
通過Application監(jiān)聽Activity、Fragment的生命周期存皂。在頁面銷毀時晌坤,調(diào)用cancel取消對應(yīng)的網(wǎng)絡(luò)請求。
private fun registerActivityLifecycle(app: Application) {
app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityDestroyed(activity: Activity?) {
registerCallMap.remove(activity.hashCode())
}})
}
這個是針對Activity的生命周期的監(jiān)聽旦袋。對于Fragment的生命周期的監(jiān)聽其實和Activity類似骤菠。
private fun registerActivityLifecycle(app: Application) {
app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {
(activity as? FragmentActivity)?.supportFragmentManager
?.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
}})
}
網(wǎng)絡(luò)監(jiān)聽
網(wǎng)絡(luò)模使用的場景非常多,當(dāng)前出現(xiàn)問題的概率也更好疤孕,網(wǎng)絡(luò)相關(guān)的問題非常多商乎,比如網(wǎng)絡(luò)異常、DNS解析失敗祭阀、連接超時等鹉戚。所以一套完善網(wǎng)絡(luò)流程監(jiān)控是非常有必要的,可以幫助我們在很多問題中快速分析出問題专控,提高我們的排查問題的效率
網(wǎng)絡(luò)流程監(jiān)控
根據(jù)OkHttp官方的EventListener提供的回調(diào):OkHttpEvent事件抹凳,我們定義了以下幾個一個網(wǎng)絡(luò)請求中可能 觸發(fā)的Action事件。
enum class NetEventType {
EN_QUEUE, //入隊
NET_START, //網(wǎng)絡(luò)請求真正開始執(zhí)行
DNS_START, //開始DNS解析
DNS_END, //DNS解析結(jié)束
CONNECT_START, //開始建立連接
TLS_START, // TLS握手開始
TLS_END, //TLS握手結(jié)束
CONNECT_END, //建立連接結(jié)束
RETRY, //嘗試重新連接
REUSE, //連接重用伦腐,從連接池中獲取到連接
CONNECTION_ACQUIRE, //獲取到鏈接(可能不走連接建立赢底,直接從連接池中獲取)
CONNECT_FAILED, // 連接失敗
REQUEST_HEADER_START, // request寫Header開始
REQUEST_HEADER_END, // request寫Header結(jié)束
REQUEST_BODY_START, // request寫B(tài)ody開始
REQUEST_BODY_END, // request寫B(tài)ody結(jié)束
RESPONSE_HEADER_START, // response寫Header開始
RESPONSE_HEADER_END, // response寫Header結(jié)束
RESPONSE_BODY_START, // response寫B(tài)ody開始
RESPONSE_BODY_END, // response寫B(tài)ody結(jié)束
FOLLOW_UP, // 是否發(fā)生重定向
CALL_END, //請求正常結(jié)束
CONNECTION_RELEASE, // 連接釋放
CALL_FAILED, // 請求失敗
NET_END, // 網(wǎng)絡(luò)請求結(jié)束(包括正常結(jié)束和失敯啬ⅰ)
}
可以看到幸冻,除了okHttp原有的幾個Event,還額外多了一個ENQUEUE事件咳焚。這個時機最主要的作用是計算出請求從調(diào)用到真正發(fā)起接口請求的等待時間洽损。 當(dāng)我們調(diào)用了RealCall.enqueue方法時,實際上這個接口請求并不是都會立即執(zhí)行黔攒,OkHttp對于同一個時刻的請求數(shù)有限制趁啸。
- 同一個Dispatcher,同一時刻并發(fā)數(shù)不能超過64
- 同一個Host督惰,同一時刻并發(fā)數(shù)不能超過5
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
所以一旦超過對應(yīng)的閾值不傅,當(dāng)次請求就會被添加到readyAsyncCalls中,等待被執(zhí)行赏胚。
根據(jù)這幾個Action访娶,我們可以將統(tǒng)計的時間分為下面幾個階段
enum class NetRecordItemType {
WAIT, // 等待時間,入隊到真正開始執(zhí)行耗時
DNS, // DNS耗時
TLS, // TLS耗時
RequestHeader, // request寫入Header耗時
RequestBody, // request寫入Body耗時
Request, // request寫入header和body總耗時
NetworkLatency, // 網(wǎng)絡(luò)請求延時
ResponseHeader, // response寫入Header耗時
ResponseBody, // response寫入Body耗時
Response, // response寫入header和body總耗時
Connect, // 連接建立總耗時
RequestAndResponse, // 數(shù)據(jù)傳輸耗時
CallTime, // 單次網(wǎng)絡(luò)請求總耗時(包含排隊時間)
UNKNOWN
}
唯一ID
我們不僅僅想對整個網(wǎng)絡(luò)的大盤進行監(jiān)控觉阅,我們還希望能夠精細化到每一個獨立的網(wǎng)絡(luò)請求進行監(jiān)控崖疤。針對單個網(wǎng)絡(luò)請求進行的監(jiān)控的難點是我們?nèi)绾稳酥境鰜砻恳粋€網(wǎng)絡(luò)請求,因為EventListener回調(diào)只會返回對應(yīng)的call秘车。
public abstract class EventListener {
public void callStart(Call call) {}
public void callEnd(Call call) {}
}
而這個Call沒有辦法與單個監(jiān)控的請求進行關(guān)聯(lián)州既。 并且在網(wǎng)絡(luò)請求發(fā)起的階段就需要標識出來烦磁,所以需要在Request創(chuàng)建的最前頭就生成這個唯一ID锭亏。通過閱讀源碼贷揽,我們發(fā)現(xiàn)可以生成唯一id最早時機是在OkHttp的RealCall創(chuàng)建的最前頭。
RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
final EventListener.Factory eventListenerFactory = client.eventListenerFactory();
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
this.eventListener = eventListenerFactory.create(this);
}
其中赖钞,eventListenerFactory是由外部傳遞到Okhttp中的肢扯。
public Builder eventListenerFactory(EventListener.Factory eventListenerFactory) {
if (eventListenerFactory == null) {
throw new NullPointerException("eventListenerFactory == null");
}
this.eventListenerFactory = eventListenerFactory;
return this;
}
因此辫秧,我們可以在EventListener.Factory中生成標記request的唯一Id般码。
internal class CallEventFactory(var configuration: CallEventConfiguration?) : EventListener.Factory {
companion object {
private val nextCallId = AtomicLong(1L)
}
override fun create(call: Call): EventListener {
val callId = nextCallId.getAndIncrement()
}
}
那生成的callId如何與request進行關(guān)聯(lián)呢妻率?最直接的是給Request添加一個Header的key。Request本身沒有提供Api去修改Header板祝。 所以這個時候就需要通過反射來設(shè)置, 先獲取當(dāng)前的Header宫静,然后給header新增這個CallId,最后通過反射設(shè)置到request的header字段上券时。
fun appendToHeader(request: Request?, key: String?, value: String?) {
key ?: return
request ?: return
value ?: return
val headerBuilder = request.headers().newBuilder().add(key, value)
ReflectUtils.setFieldValue(Request::class.java, request, NetCallAdapter.HEADER_NAME, headerBuilder.build())
}
需要注意的是孤里,因為使用了反射,所以需要在proguard文件中keep住request革为。 當(dāng)然這個key最好能夠不帶到服務(wù)端扭粱,所以需要新增一個攔截器舵鳞,添加到所有攔截器最后震檩,這個這個唯一id的key就不會被添加到真正的請求上了。
class NetLastInterceptor: Interceptor {
companion object {
const val TAG = "NetLastInterceptor"
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val requestBuilder = request
.newBuilder()
.removeHeader(NetConstants.CALL_ID)
return chain.proceed(requestBuilder.build())
}
}
監(jiān)控
在生成完唯一Id之后蜓堕,我們再來看看如何外部是如何添加期望的網(wǎng)絡(luò)監(jiān)控的抛虏。
基于Client的監(jiān)控
networkClient = NetWorkClientBuilder()
.addLifecycleListener("*", object : INetLifecycleListener {
override fun onLifecycle(info: INetLifecycleInfo) { }})
.registerEventListener("xxxUrl", NetEventType.CALL_END, object : INetEventListener {
override fun onEvent(event: NetEventType, request: NetRequest) { }})
.build()
基于單個請求的監(jiān)控
TestInterface.inst.testFun()
.addLifeCycleListener(object : INetLifecycleListener {
override fun onLifecycle(info: INetLifecycleInfo) {} })
.registerEventListener(mutableListOf(NetEventType.CALL_END, NetEventType.NET_START), object : INetEventListener {
override fun onEvent(event: NetEventType, request: NetRequest) {} })
.enqueue(null)
在創(chuàng)建EventListener時,按照下面的規(guī)則添加套才。
- 添加網(wǎng)絡(luò)庫系統(tǒng)的內(nèi)部監(jiān)聽
- 添加OkHttpClient初始化配置的監(jiān)聽
- 添加單個請求配置的監(jiān)聽
基于單個請求的網(wǎng)絡(luò)監(jiān)控迂猴,需要提前把這個Request和網(wǎng)絡(luò)監(jiān)聽的listener的關(guān)聯(lián)關(guān)系存起來,因為EventListener的設(shè)置是針對整個OkHttpClient的生效的背伴,所以需要在EventListener處理的過程中沸毁,獲取當(dāng)前的Request設(shè)置進去的listener。
網(wǎng)速檢測
如果可以獲取到當(dāng)前手機的網(wǎng)速傻寂,就可以做很多額外的操作息尺。 比如在圖片場景中,可以基于當(dāng)前的實時網(wǎng)速進行圖片的質(zhì)量的變換疾掰,在網(wǎng)速快的場景下搂誉,加載高質(zhì)量的圖片,在網(wǎng)速慢的場景下静檬,加載低質(zhì)量的圖片炭懊。 我們?nèi)绾稳ビ嬎阋粋€比較準確的網(wǎng)速呢并级,比如下面列舉的幾個場景
- 當(dāng)前app沒有發(fā)起網(wǎng)絡(luò)請求,但是存在其他進程在使用網(wǎng)絡(luò)侮腹,占用網(wǎng)速
- 當(dāng)前app發(fā)起了一個網(wǎng)絡(luò)請求嘲碧,計算當(dāng)前網(wǎng)絡(luò)請求的速度
- 當(dāng)前app并發(fā)多個網(wǎng)絡(luò)請求,導(dǎo)致每個網(wǎng)絡(luò)請求的速度都比較慢
可能還會存在一些其他的場景父阻,那么在這么復(fù)雜的場景呀潭,我們通過兩種不同的計算方式進行合并計算
- 基于當(dāng)前網(wǎng)絡(luò)接口的response讀取的速度,進行網(wǎng)速的動態(tài)計算
- 基于流量和時間計算出網(wǎng)速
通過計算出來的兩者至非,取最大值的網(wǎng)速作為當(dāng)前的網(wǎng)速值钠署。
基于當(dāng)前接口動態(tài)計算
基于前面網(wǎng)絡(luò)請求的全流程監(jiān)控,我們可以在全局添加所有網(wǎng)絡(luò)接口的監(jiān)聽荒椭,在ResponseBody這個周期內(nèi)谐鼎,基于response的byte數(shù)和時間,可以計算每一個網(wǎng)絡(luò)body讀取速度趣惠。之所以要選取body讀取的時間來計算網(wǎng)速狸棍,主要是為了防止把網(wǎng)絡(luò)建連的耗時影響了最終的網(wǎng)速計算。 不過接口網(wǎng)速的動態(tài)計算需要針對不同場景去做不同的計算味悄。
- 當(dāng)前只有一個網(wǎng)絡(luò)請求
在當(dāng)前只有一個網(wǎng)絡(luò)請求的場景下草戈, 當(dāng)前body計算出來請求速度就是當(dāng)前的網(wǎng)速。
- 當(dāng)前同時存在多個網(wǎng)絡(luò)請求發(fā)起時
每一個請求都會瓜分網(wǎng)速侍瑟,所以在這個場景下唐片,每個網(wǎng)絡(luò)請求的網(wǎng)速都有其對應(yīng)的網(wǎng)速占比。比如當(dāng)前有6個網(wǎng)絡(luò)請求涨颜,每個網(wǎng)絡(luò)請求的網(wǎng)速近似為1/6费韭。
當(dāng)然,為了防止網(wǎng)速的短時間的波動庭瑰,每個網(wǎng)絡(luò)請求對于當(dāng)前的網(wǎng)速的影響是有固定的占比的, 比如我們可以設(shè)置的占比為5%星持。
currentSpeed = (requestSpeed * concurrentRequestCount - preSpeed) * ratePercent + preSpeed
其中
- requestSpeed:表示為當(dāng)前網(wǎng)絡(luò)請求計算出來的網(wǎng)速。
- concurrentRequestCount:表示當(dāng)前網(wǎng)絡(luò)請求的總數(shù)
- preSpeed:表示先前計算出來的網(wǎng)速
- ratePercent:表示當(dāng)前計算出來網(wǎng)速對于真正的網(wǎng)速影響占比
為了防止body過小導(dǎo)致的計算出來網(wǎng)速不對的場景弹灭,我們選取當(dāng)前body大小超過20K的請求參與進行計算督暂。
基于流量動態(tài)計算
基于流量的計算,可以參照TrafficState進行計算穷吮÷呶蹋可以參照facebook的network-connection-class 【评矗可以通過每秒獲取系統(tǒng)當(dāng)前進程的流量變化卢未,網(wǎng)速 = 流量總量 / 計算時間。 它內(nèi)部也有一個計算公式:
public void addMeasurement(double measurement) {
double keepConstant = 1 - mDecayConstant;
if (mCount > mCutover) {
mValue = Math.exp(keepConstant * Math.log(mValue) + mDecayConstant * Math.log(measurement));
} else if (mCount > 0) {
double retained = keepConstant * mCount / (mCount + 1.0);
double newcomer = 1.0 - retained;
mValue = Math.exp(retained * Math.log(mValue) + newcomer * Math.log(measurement));
} else {
mValue = measurement;
}
mCount++;
}
自定義注解處理
假如我們現(xiàn)在有一個需求,在我們的網(wǎng)絡(luò)庫中有一套內(nèi)置的接口加密算法辽社,現(xiàn)在我們期望針對某幾個網(wǎng)絡(luò)請求做單獨的配置伟墙,我們有什么樣的解決方案呢? 比較容易能夠想到的方案是添加給網(wǎng)絡(luò)添加一個全局攔截器滴铅,在攔截器中進行接口加密戳葵,然后在攔截器中對符合要求的請求URL的進行加密。但是這個攔截器可能是一個網(wǎng)絡(luò)庫內(nèi)部的攔截器汉匙,在這里面去過濾不同的url可能不太合適拱烁。 那么,有什么方式可以讓這個配置通用并且簡潔呢噩翠? 其中一種方式是通過接口配置的地方戏自,添加一個Header,然后在網(wǎng)絡(luò)庫內(nèi)部攔截器中獲取Header中有沒有這個key伤锚,但是這個這個使用起來并且沒有那么方便擅笔。首先業(yè)務(wù)方并不知道header里面key的值是什么,其次在添加到header之后屯援,內(nèi)部還需要在攔截器中把這個header的key給移除掉猛们。
最后我們決定對于網(wǎng)絡(luò)庫給單接口提供的能力都通過注解來提供。 就拿接口加密為例子狞洋,我們期望加密的配置方式如下所示
@Encryption
@POST("xxUrl)
fun testRequest(@Field("xxUrl") nickname: String)
這個注解是如何能夠透傳到網(wǎng)絡(luò)庫中的內(nèi)部攔截器呢弯淘。首先需要把在Interface中配置的注解獲取出來。CallAdapter.Factory可以拿到網(wǎng)絡(luò)請求中配置的注解吉懊。
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>庐橙? {}
我們可以在這里講注解和同一個url的request的關(guān)聯(lián)起來。 然后在攔截器中獲取是否有對應(yīng)的注解惕它。
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (!NetAnnotationUtil.isAnntationExsit(request, Encryption::class)) {
return chain.proceed(request)
}
//do encrypt we want
...
}
調(diào)試工具
對于網(wǎng)絡(luò)能力怕午,我們經(jīng)常會去針對網(wǎng)絡(luò)接口進行調(diào)試废登。最簡單的方式是通過charles抓包淹魄。 通過charles抓包我們可以做到哪些調(diào)試呢?
- 查看請求參數(shù)堡距、查看網(wǎng)絡(luò)返回值
- mock網(wǎng)絡(luò)數(shù)據(jù)
看著上面2個能力似乎已經(jīng)滿足我們了日常調(diào)試了甲锡,但是它還是有一些缺陷的:
- 必須要借助PC
- 在App關(guān)閉了可抓包能力之后羽戒,就不能再抓包了
- 無法針對于post請求參數(shù)區(qū)分
所以,我們需要有一個強大的網(wǎng)絡(luò)調(diào)試能力易稠,既滿足了charles的能力缸废, 也可以不借助PC,并且不論apk是否開啟了抓包能力也能夠允許抓包。 可以添加一個專門Debug網(wǎng)絡(luò)攔截器企量,在攔截器中實現(xiàn)這個能力测萎。
- 把網(wǎng)絡(luò)的debug文件配置在本地Sdcard下(也可以配置在遠端統(tǒng)一的地址中)
- 通過攔截器,進行url届巩、參數(shù)匹配硅瞧,如果命中,將本地json返回恕汇。否則腕唧,正常走網(wǎng)絡(luò)請求。
data class GlobalDebugConfig(
@SeerializedName("printToConsole") var printData: Boolean = false,
@SeerializedName("printToPage") var printData: Boolean = false
)
data class NetDebugInfo(
@SerializedName("filter") var debugFilterInfo: NetDebugFilterInfo?,
@SerializedName("response") var responseString: Any?,
@SerializedName("code") var httpCode: Int,
@SerializedName("message") var httpMessage: String? = null,
@SeerializedName("printToConsole") var printData: Boolean = true,
@SeerializedName("printToPage") var printData: Boolean = true)
data class NetDebugFilterInfo(
@SerializedName("host") var host: String? = null,
@SerializedName("path") var path: String? = null,
@SerializedName("parameter") var paramMap: Map<String, String>? = null)
首先日志輸出有個全局配置和單個接口的配置瘾英,單接口配置優(yōu)于全局配置枣接。
- printToConsole表示輸出到控制臺
- printToPage表示將接口記錄到本地中,可以在本地頁面查看請求數(shù)據(jù)
其次filterInfo就是我們針對接口請求的匹配規(guī)則缺谴。
- host表示域名
- path表示接口請求地址
- parameter表示請求參數(shù)的值月腋,如果是post請求,會自動匹配post請求的body參數(shù)瓣赂。如果是get請求榆骚,會自動匹配get請求的query參數(shù)。
val host = netDebugInfo.debugFilterInfo?.host
if (!TextUtils.isEmpty(host) && UriUtils.getHost(request.url().toString()) != host) {
return chain.proceed(request)
}
val filterPath = netDebugInfo.debugFilterInfo?.path
if (!TextUtils.isEmpty(filterPath) && path != filterPath) {
return chain.proceed(request)
}
val filterRequestFilterInfo = netDebugInfo.debugFilterInfo?.paramMap
if (!filterRequestFilterInfo.isNullOrEmpty() && !checkParam(filterRequestFilterInfo, request)) {
return chain.proceed(request)
}
val resultResponseJsonObj = netDebugInfo.responseString
if (resultResponseJsonObj == null) {
return chain.proceed(request)
}
return Response.Builder()
.code(200)
.message("ok")
.protocol(Protocol.HTTP_2)
.request(request)
.body(NetResponseHelper.createResponseBody(GsonUtil.toJson(resultResponseJsonObj)))
.build()
對于配置文件煌集,最好能夠共同維護mock數(shù)據(jù)妓肢。 本地可以提供mock數(shù)據(jù)展示列表。
組件化上網(wǎng)絡(luò)庫的能力支持
在組件化中苫纤,各個組件都需要使用網(wǎng)絡(luò)請求碉钠。 但是在一個App內(nèi),都會有一套統(tǒng)一的網(wǎng)絡(luò)請求的Header卷拘,例如AppInfo喊废,UA,Cookie等參數(shù)栗弟。。在組件化中瓣蛀,針對這幾個參數(shù)的配置有下面幾個比較容易想到的解決方案:
- 在各個組件單獨配置這幾個Header
- 每個組件都需要但單獨配置Header雷厂,會存在很多重復(fù)代碼
- 通用信息很大概率在各個組件中獲取不到
- 由主工程實現(xiàn)代理發(fā)起網(wǎng)絡(luò)請求
這種實現(xiàn)方式也有下面幾個缺陷
- 主工程需要關(guān)注所有組件改鲫,隨著集成的組件越來越多,主工程需要初始化網(wǎng)絡(luò)代理接口會越來越多
- 由于主工程并不知道組件什么時候會啟動诊县,只能App啟動就初始化網(wǎng)絡(luò)代理依痊,導(dǎo)致組件初始化提前
- 所有直接和間接依賴的模塊都需要由主工程來實現(xiàn)代理胸嘁,很容易遺漏
通用信息攔截器自動注入
正因為上面兩個實現(xiàn)方式或多或少都有問題性宏,所以需要從網(wǎng)絡(luò)庫這一層來解決這個問題毫胜。 我們可以在網(wǎng)絡(luò)層通過服務(wù)發(fā)現(xiàn)的能力酵使,給外部提供一個通用網(wǎng)絡(luò)信息攔截器注解, 一般由主工程實現(xiàn), 完成默認信息的Header修改口渔。創(chuàng)建網(wǎng)絡(luò)Client實例時,自動查找app中被通用網(wǎng)絡(luò)信息攔截器注解標注的攔截器悦穿。
線程池礁扮、連接池復(fù)用
各個組件都會有自己的網(wǎng)絡(luò)Client實例深员,導(dǎo)致在同一個進程中,創(chuàng)建出來網(wǎng)絡(luò)Client實例過多绣的,同時線程池芭概、連接池并沒有復(fù)用惩嘉。所以在網(wǎng)絡(luò)庫中惹苗,各個組件創(chuàng)建的網(wǎng)絡(luò)Client默認會共享網(wǎng)絡(luò)連接池和線程池桩蓉,有特殊需要的模塊院究,可以強制使用獨立線程池和連接池。