基于OkHttp+Retrofit的網(wǎng)絡(luò)庫二次封裝

在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ù)雜装蓬,我們期望有更多的自定義的能力,有更好用的使用方式等纱扭。就好比下面這些自定義的能力

  1. 屏蔽底層的網(wǎng)絡(luò)庫實現(xiàn)
  2. 網(wǎng)絡(luò)層統(tǒng)一處理code碼和線程回調(diào)問題
  3. 網(wǎng)絡(luò)請求綁定生命周期
  4. 網(wǎng)絡(luò)層的全局監(jiān)控
  5. 網(wǎng)絡(luò)的調(diào)試能力
  6. 網(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健爬, 其實最主要的還是攔截器這一層的封裝。 攔截器這一層么介,主要涉及到幾個類:

  1. Request
  2. Response
  3. 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
    }
 }

  1. 首先需要在class的繼承關(guān)系上迎罗,顯式的標明CallAdapter的第二個泛型參數(shù)是我們自定義的Call類型睬愤。
  2. 在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ī)則添加套才。

  1. 添加網(wǎng)絡(luò)庫系統(tǒng)的內(nèi)部監(jiān)聽
  2. 添加OkHttpClient初始化配置的監(jiān)聽
  3. 添加單個請求配置的監(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ù)雜的場景呀潭,我們通過兩種不同的計算方式進行合并計算

  1. 基于當(dāng)前網(wǎng)絡(luò)接口的response讀取的速度,進行網(wǎng)速的動態(tài)計算
  2. 基于流量和時間計算出網(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)試呢?

  1. 查看請求參數(shù)堡距、查看網(wǎng)絡(luò)返回值
  2. mock網(wǎng)絡(luò)數(shù)據(jù)

看著上面2個能力似乎已經(jīng)滿足我們了日常調(diào)試了甲锡,但是它還是有一些缺陷的:

  1. 必須要借助PC
  2. 在App關(guān)閉了可抓包能力之后羽戒,就不能再抓包了
  3. 無法針對于post請求參數(shù)區(qū)分

所以,我們需要有一個強大的網(wǎng)絡(luò)調(diào)試能力易稠,既滿足了charles的能力缸废, 也可以不借助PC,并且不論apk是否開啟了抓包能力也能夠允許抓包。 可以添加一個專門Debug網(wǎng)絡(luò)攔截器企量,在攔截器中實現(xiàn)這個能力测萎。

  1. 把網(wǎng)絡(luò)的debug文件配置在本地Sdcard下(也可以配置在遠端統(tǒng)一的地址中)
  2. 通過攔截器,進行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ù)的配置有下面幾個比較容易想到的解決方案:

  1. 在各個組件單獨配置這幾個Header
  • 每個組件都需要但單獨配置Header雷厂,會存在很多重復(fù)代碼
  • 通用信息很大概率在各個組件中獲取不到
  1. 由主工程實現(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ò)連接池和線程池桩蓉,有特殊需要的模塊院究,可以強制使用獨立線程池和連接池。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蔬胯,一起剝皮案震驚了整個濱河市氛濒,隨后出現(xiàn)的幾起案子舞竿,更是在濱河造成了極大的恐慌骗奖,老刑警劉巖执桌,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異膘壶,居然都是意外死亡颓芭,警方通過查閱死者的電腦和手機官紫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門束世,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人薪丁,你說我怎么就攤上這事严嗜。” “怎么了睦优?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長询一。 經(jīng)常有香客問我菱阵,道長缩功,這世上最難降的妖魔是什么晴及? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮掂之,結(jié)果婚禮上抗俄,老公的妹妹穿的比我還像新娘脆丁。我一直安慰自己世舰,他們只是感情好动雹,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著跟压,像睡著了一般茸塞。 火紅的嫁衣襯著肌膚如雪笋庄。 梳的紋絲不亂的頭發(fā)上静暂,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死适掰,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播斗躏,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼窿给!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起谒所,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤著觉,失蹤者是張志新(化名)和其女友劉穎镇辉,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡芋绸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年摔敛,在試婚紗的時候發(fā)現(xiàn)自己被綠了马昙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖琢歇,靈堂內(nèi)的尸體忽然破棺而出味榛,到底是詐尸還是另有隱情,我是刑警寧澤考阱,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布翠忠,位于F島的核電站,受9級特大地震影響乞榨,放射性物質(zhì)發(fā)生泄漏秽之。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一吃既、第九天 我趴在偏房一處隱蔽的房頂上張望考榨。 院中可真熱鬧,春花似錦鹦倚、人聲如沸河质。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掀鹅。三九已至散休,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乐尊,已是汗流浹背戚丸。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扔嵌,地道東北人限府。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像痢缎,于是被迫代替她去往敵國和親胁勺。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容