【Android】做一个具有高可用性的网络库

在android中,网络模块是一个不可或缺的模块,相信很多公司都会有自建的网络库。目前市面上主流的网络请求框架都是基于okHttp做的延伸和扩展,并且android底层的网络库实现也使用OkHttp了,可见okHttp应用的广泛性。

Retrofit本身就是对于OkHttp库的封装,它的优点很很多,比如注解来实现的,配置简单,使用方便等。那为什么我们要做二次封装呢?最根本的原因还是我们现有的业务过于复杂,我们期望有更多的自定义的能力,有更好用的使用方式等。就好比下面这些自定义的能力

  1. 屏蔽底层的网络库实现
  2. 网络层统一处理code码和线程回调问题
  3. 网络请求绑定生命周期
  4. 网络层的全局监控
  5. 网络的调试能力
  6. 网络层对于组件化的通用能力支持

这些目前能力目前如果直接使用Retrofit,基本都是满足不了的。 本文是基于Retrofit + OkHttp提供的基础能力上,做的网络库的二次封装。主要介绍下如何在retrofit和Okhhtp的基础上,提供上述几个通用的能力。 本文需要有部分okHttp和retrofit源码的了解。 有兴趣的可以先查看官方文档,传送门:

屏蔽底层的网络库实现

虽然Retrofit是一个非常强大的封装框架,但是它并没有完全把网路库底层的实现的屏蔽掉。 默认的内部网络请求使用的okHttp,在我们创建Retrofit实例的时候,如果需要配置拦截器,就会直接依赖到底层的OkHttp,导致上层业务直接访问到了网络库的底层实现。这个对于后续的网络库底层的替换会是一个不小的成本。 因此,我们希望能够封装一层网络层,让业务的使用仅仅依赖到网络库的封装层,而不会使用到网络库的底层实现。 首先,我们需要先知道业务层当前使用到了哪些网络库底层的API, 其实最主要的还是拦截器这一层的封装。 拦截器这一层,主要涉及到几个类:

  1. Request
  2. Response
  3. Chain和Intercept

我们可以针对这几个类进行封装,定义对象接口,IRequest、IResponse、IChain和INetIntercept,这套接口不带任何具体实现。 然后在真正需要访问到具体的实例的时候,转化成具体的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?
} 

在构造Retrofit的实例时,内部会尝试创建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
                }
            }
        } 

整体修改后的拦截器的调用链如下所示:
image

上面举的只是在构建拦截器中的隔离,如果你们项目还有访问到其他内部的OkHttp的能力,也可以参照上面的封装流程,定义接口,在需要使用的地方转换为具体实现。

Retrofit的Call自定义

对于Retrofit,我们在接口中定义的方法就是每一个请求的配置,每一个请求都会被包装成Call。我们想要的请求做一些通用的逻辑处理和自定义,就比如在请求前做一些逻辑处理,请求后做一些逻辑处理,最后才返回给上层,就需要hook这个请求流程,可以做Retrofit的二次动态代理。 如果希望做一些更精细化的处理,hook能力就满足不了了。这种时候,可以选择使用自定义Call对象。如果整个Call对象都是我们提供的,我们当然可以在里面实现任何我们期望的逻辑。接下来简单介绍下如何自定义Retrofit的Call对象。

定义Call类型

class TestCall<T>(internal var call: Call<T>) {} 

自定义CallAdapter

自定义CallAdapter时,需要使用我们前面自定义的返回值类型,并将call对象转化为我们我们自定义的返回值类型。

 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的继承关系上,显式的标明CallAdapter的第二个泛型参数是我们自定义的Call类型。
  2. 在adapt适配方法中,通过原始的call,转化为我们期望的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中,根据从接口定义中获取到的网络返回值,匹配TestCall类型,如果匹配上,就返回我们定义的CallAdapter。

注册Factory

val builder = Retrofit.Builder()
    .baseUrl(retrofitBuilder.baseUrl!!)
    .client(client)
    .addCallAdapterFactory(NetCallAdapterFactory()) 

网络层统一处理code码和线程回调问题

code码统一处理

相信每一个产品都会定义业务错误码,每一个业务都可能有自己的一套错误码,有一些错误码可能是全局的,比如说登录过期、被封禁等,这种错误码可能跟特定的接口无关,而是一个全局的业务错误码,在收到这些错误码时,会有统一的逻辑处理。 我们可以先定义code码解析的接口

interface ICodehandler {
    fun handle(context: Context?, code: Int, message: String?, isBackGround: Boolean): Boolean
} 

code码处理器的注册。

code码处理器的注册方式有两种,一种是全局的code码处理器。 在创建Retrofit实例的传入。

NetWorkClientBuilder()
        .addNetCodeHandler(SocialCodeHandler())
        .build() 

另一种是在具体的网络请求时,传入错误码处理器,

TestInterface.inst.testCall().backGround(true)
        .withInterceptor(new CodeRespHandler() {
            @Override
            public boolean handle(int code, @Nullable String message) {
                  ....
            }
        })
        .enqueue(null) 

code码处理的调用

因为Call是我们自定义的,我们可以在网络成功的返回时,优先执行错误码处理器,如果命中业务错误码,那么对外返回失败。否则正常返回成功。

线程回调

OkHttp的callback线程回调默认是在子线程,retrofit的回调线程取决于创建实例时的配置,可以配置callbackExecutor,这个是对整个实例生效的,在这个实例内,所有的网络返回都会通过callbackExecutor。我们希望能够针对每一个接口单独配置回调的线程,所以同样基于自定义call的前提下,我们自定义Callback和UiCallback。

  • Callback: 表示当前回调线程无需主线程
  • UICallback: 表示当前回调线程需要在主线程

通用业务传入的接口类型就标识了当前回调的线程.

网络请求绑定生命周期

大部分网络请求都是异步发起的。所以可能会导致下面两个问题:

  • 内存泄漏问题
  • 空指针问题

先看一个比较常见的内存泄漏的场景

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方法中编译后会创建匿名内部类,并且显式的调用了外部Fragment的View,一旦这个网络请求阻塞了,或者晚于这个Fragment的销毁时机回调,就会导致这个Fragment出现内存泄漏,直至这个请求正常结束返回。

更严重的是,这个被操作的view是通过ButterKnife绑定的,在Fragment走到onDestory之后就进行了解绑,会将这个View的值设置为null,导致在这个callback的回调时候,可能出现view为null的情况,导致空指针。 对于空指针的问题,我们可以看到有很多网络请求的回调都可能会出现类似下面的代码段。

 TestInterface.getInst().getTestFun()
                .enqueue(new UICallback<TestResponse>() {
                    @Override
                    public void onSuccessful(TestResponse test) {
                      if(!isFinishing() && view != null) {
                          view.xxxx = test.xxx
                      }  
                    }}); 

在匿名内部类回调时,通过判断页面是否已经销毁,以及view是否为空,再进行对应的UI操作。 我们通过动态代理来解决了这个空指针和内存泄漏的问题。 详细的方案可以阅读下这个文章匿名内部类导致内存泄漏的解决方案 因为我们把Activity、Fragment抽象为UIContext。在网络接口调用时,传入对应的UIContext,会将网络请求的Callabck通过动态代理,将Callback和UIContext进行关联,在页面销毁时,不进行回调。

自动Cancel无用请求

很多的业务场景中,在页面一进去就会触发很多网络请求,这个请求可能有一部分处于网络库的请求等待队列中,一部分处于进行中。当我们退出了这个页面之后,这些网络请求其实都已经没有了存在的意义。 所以我们可以在页面销毁时,取消还未发起和进行中的网络请求。 我们可以通过上面提过的UIContext,将网络请求跟页面进行关联。监听页面的生命周期,在页面关闭时,cancel掉对应的网络请求。

页面关联

在网络请求发起前,把当前的网络请求关联上对应的页面。

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。每一个页面都会关联一个请求List。

cancel请求

通过Application监听Activity、Fragment的生命周期。在页面销毁时,调用cancel取消对应的网络请求。

 private fun registerActivityLifecycle(app: Application) {
        app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
            override fun onActivityDestroyed(activity: Activity?) {
                registerCallMap.remove(activity.hashCode())
            }})
    } 

这个是针对Activity的生命周期的监听。对于Fragment的生命周期的监听其实和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)
            }})
    } 

网络监听

网络模使用的场景非常多,当前出现问题的概率也更好,网络相关的问题非常多,比如网络异常、DNS解析失败、连接超时等。所以一套完善网络流程监控是非常有必要的,可以帮助我们在很多问题中快速分析出问题,提高我们的排查问题的效率

网络流程监控

根据OkHttp官方的EventListener提供的回调:OkHttpEvent事件,我们定义了以下几个一个网络请求中可能 触发的Action事件。

enum class NetEventType {
    EN_QUEUE, //入队
    NET_START, //网络请求真正开始执行
    DNS_START, //开始DNS解析
    DNS_END, //DNS解析结束
    CONNECT_START, //开始建立连接
    TLS_START, // TLS握手开始
    TLS_END, //TLS握手结束
    CONNECT_END, //建立连接结束
    RETRY, //尝试重新连接
    REUSE, //连接重用,从连接池中获取到连接
    CONNECTION_ACQUIRE, //获取到链接(可能不走连接建立,直接从连接池中获取)
    CONNECT_FAILED, // 连接失败
    REQUEST_HEADER_START, // request写Header开始
    REQUEST_HEADER_END, // request写Header结束
    REQUEST_BODY_START, // request写Body开始
    REQUEST_BODY_END, // request写Body结束
    RESPONSE_HEADER_START, // response写Header开始
    RESPONSE_HEADER_END, // response写Header结束
    RESPONSE_BODY_START, // response写Body开始
    RESPONSE_BODY_END, // response写Body结束
    FOLLOW_UP, // 是否发生重定向
    CALL_END, //请求正常结束
    CONNECTION_RELEASE, // 连接释放
    CALL_FAILED, // 请求失败
    NET_END, // 网络请求结束(包括正常结束和失败)

} 

可以看到,除了okHttp原有的几个Event,还额外多了一个ENQUEUE事件。这个时机最主要的作用是计算出请求从调用到真正发起接口请求的等待时间。 当我们调用了RealCall.enqueue方法时,实际上这个接口请求并不是都会立即执行,OkHttp对于同一个时刻的请求数有限制。

  • 同一个Dispatcher,同一时刻并发数不能超过64
  • 同一个Host,同一时刻并发数不能超过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);
    }
  } 

所以一旦超过对应的阈值,当次请求就会被添加到readyAsyncCalls中,等待被执行。

根据这几个Action,我们可以将统计的时间分为下面几个阶段

enum class NetRecordItemType {
    WAIT, // 等待时间,入队到真正开始执行耗时
    DNS, // DNS耗时
    TLS, // TLS耗时
    RequestHeader, // request写入Header耗时
    RequestBody, // request写入Body耗时
    Request, // request写入header和body总耗时
    NetworkLatency, // 网络请求延时
    ResponseHeader, // response写入Header耗时
    ResponseBody, // response写入Body耗时
    Response, // response写入header和body总耗时
    Connect, // 连接建立总耗时
    RequestAndResponse, // 数据传输耗时
    CallTime, // 单次网络请求总耗时(包含排队时间)
    UNKNOWN
} 

唯一ID

我们不仅仅想对整个网络的大盘进行监控,我们还希望能够精细化到每一个独立的网络请求进行监控。针对单个网络请求进行的监控的难点是我们如何去标志出来每一个网络请求,因为EventListener回调只会返回对应的call。

public abstract class EventListener {
    public void callStart(Call call) {}
    
    public void callEnd(Call call) {}
} 

而这个Call没有办法与单个监控的请求进行关联。 并且在网络请求发起的阶段就需要标识出来,所以需要在Request创建的最前头就生成这个唯一ID。通过阅读源码,我们发现可以生成唯一id最早时机是在OkHttp的RealCall创建的最前头。

 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进行关联呢?最直接的是给Request添加一个Header的key。Request本身没有提供Api去修改Header。 所以这个时候就需要通过反射来设置, 先获取当前的Header,然后给header新增这个CallId,最后通过反射设置到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。 当然这个key最好能够不带到服务端,所以需要新增一个拦截器,添加到所有拦截器最后,这个这个唯一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())
    }
} 

监控

在生成完唯一Id之后,我们再来看看如何外部是如何添加期望的网络监控的。

基于Client的监控

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() 

基于单个请求的监控

 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) 

在创建EventListener时,按照下面的规则添加。

  1. 添加网络库系统的内部监听
  2. 添加OkHttpClient初始化配置的监听
  3. 添加单个请求配置的监听

基于单个请求的网络监控,需要提前把这个Request和网络监听的listener的关联关系存起来,因为EventListener的设置是针对整个OkHttpClient的生效的,所以需要在EventListener处理的过程中,获取当前的Request设置进去的listener。

网速检测

如果可以获取到当前手机的网速,就可以做很多额外的操作。 比如在图片场景中,可以基于当前的实时网速进行图片的质量的变换,在网速快的场景下,加载高质量的图片,在网速慢的场景下,加载低质量的图片。 我们如何去计算一个比较准确的网速呢,比如下面列举的几个场景

  • 当前app没有发起网络请求,但是存在其他进程在使用网络,占用网速
  • 当前app发起了一个网络请求,计算当前网络请求的速度
  • 当前app并发多个网络请求,导致每个网络请求的速度都比较慢

可能还会存在一些其他的场景,那么在这么复杂的场景,我们通过两种不同的计算方式进行合并计算

  1. 基于当前网络接口的response读取的速度,进行网速的动态计算
  2. 基于流量和时间计算出网速

通过计算出来的两者,取最大值的网速作为当前的网速值。

基于当前接口动态计算

基于前面网络请求的全流程监控,我们可以在全局添加所有网络接口的监听,在ResponseBody这个周期内,基于response的byte数和时间,可以计算每一个网络body读取速度。之所以要选取body读取的时间来计算网速,主要是为了防止把网络建连的耗时影响了最终的网速计算。 不过接口网速的动态计算需要针对不同场景去做不同的计算。

  • 当前只有一个网络请求

在当前只有一个网络请求的场景下, 当前body计算出来请求速度就是当前的网速。

  • 当前同时存在多个网络请求发起时

每一个请求都会瓜分网速,所以在这个场景下,每个网络请求的网速都有其对应的网速占比。比如当前有6个网络请求,每个网络请求的网速近似为1/6。

当然,为了防止网速的短时间的波动,每个网络请求对于当前的网速的影响是有固定的占比的, 比如我们可以设置的占比为5%。

currentSpeed = (requestSpeed * concurrentRequestCount - preSpeed) * ratePercent + preSpeed 

其中

  • requestSpeed:表示为当前网络请求计算出来的网速。
  • concurrentRequestCount:表示当前网络请求的总数
  • preSpeed:表示先前计算出来的网速
  • ratePercent:表示当前计算出来网速对于真正的网速影响占比

为了防止body过小导致的计算出来网速不对的场景,我们选取当前body大小超过20K的请求参与进行计算。

基于流量动态计算

基于流量的计算,可以参照TrafficState进行计算。可以参照facebook的network-connection-class 。可以通过每秒获取系统当前进程的流量变化,网速 = 流量总量 / 计算时间。 它内部也有一个计算公式:

 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++;
  } 

自定义注解处理

假如我们现在有一个需求,在我们的网络库中有一套内置的接口加密算法,现在我们期望针对某几个网络请求做单独的配置,我们有什么样的解决方案呢? 比较容易能够想到的方案是添加给网络添加一个全局拦截器,在拦截器中进行接口加密,然后在拦截器中对符合要求的请求URL的进行加密。但是这个拦截器可能是一个网络库内部的拦截器,在这里面去过滤不同的url可能不太合适。 那么,有什么方式可以让这个配置通用并且简洁呢? 其中一种方式是通过接口配置的地方,添加一个Header,然后在网络库内部拦截器中获取Header中有没有这个key,但是这个这个使用起来并且没有那么方便。首先业务方并不知道header里面key的值是什么,其次在添加到header之后,内部还需要在拦截器中把这个header的key给移除掉。

最后我们决定对于网络库给单接口提供的能力都通过注解来提供。 就拿接口加密为例子,我们期望加密的配置方式如下所示

@Encryption
@POST("xxUrl)
fun testRequest(@Field("xxUrl") nickname: String) 

这个注解是如何能够透传到网络库中的内部拦截器呢。首先需要把在Interface中配置的注解获取出来。CallAdapter.Factory可以拿到网络请求中配置的注解。

override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {} 

我们可以在这里讲注解和同一个url的request的关联起来。 然后在拦截器中获取是否有对应的注解。

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
        ...
} 

调试工具

对于网络能力,我们经常会去针对网络接口进行调试。最简单的方式是通过charles抓包。 通过charles抓包我们可以做到哪些调试呢?

  1. 查看请求参数、查看网络返回值
  2. mock网络数据

看着上面2个能力似乎已经满足我们了日常调试了,但是它还是有一些缺陷的:

  1. 必须要借助PC
  2. 在App关闭了可抓包能力之后,就不能再抓包了
  3. 无法针对于post请求参数区分

所以,我们需要有一个强大的网络调试能力,既满足了charles的能力, 也可以不借助PC,并且不论apk是否开启了抓包能力也能够允许抓包。 可以添加一个专门Debug网络拦截器,在拦截器中实现这个能力。

  1. 把网络的debug文件配置在本地Sdcard下(也可以配置在远端统一的地址中)
  2. 通过拦截器,进行url、参数匹配,如果命中,将本地json返回。否则,正常走网络请求。
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) 

首先日志输出有个全局配置和单个接口的配置,单接口配置优于全局配置。

  • printToConsole表示输出到控制台
  • printToPage表示将接口记录到本地中,可以在本地页面查看请求数据

其次filterInfo就是我们针对接口请求的匹配规则。

  • host表示域名
  • path表示接口请求地址
  • parameter表示请求参数的值,如果是post请求,会自动匹配post请求的body参数。如果是get请求,会自动匹配get请求的query参数。
 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数据。 本地可以提供mock数据展示列表。

组件化上网络库的能力支持

在组件化中,各个组件都需要使用网络请求。 但是在一个App内,都会有一套统一的网络请求的Header,例如AppInfo,UA,Cookie等参数。。在组件化中,针对这几个参数的配置有下面几个比较容易想到的解决方案:

  1. 在各个组件单独配置这几个Header
  • 每个组件都需要但单独配置Header,会存在很多重复代码
  • 通用信息很大概率在各个组件中获取不到
  1. 由主工程实现代理发起网络请求

这种实现方式也有下面几个缺陷

  • 主工程需要关注所有组件,随着集成的组件越来越多,主工程需要初始化网络代理接口会越来越多
  • 由于主工程并不知道组件什么时候会启动,只能App启动就初始化网络代理,导致组件初始化提前
  • 所有直接和间接依赖的模块都需要由主工程来实现代理,很容易遗漏

通用信息拦截器自动注入

正因为上面两个实现方式或多或少都有问题,所以需要从网络库这一层来解决这个问题。 我们可以在网络层通过服务发现的能力,给外部提供一个通用网络信息拦截器注解, 一般由主工程实现, 完成默认信息的Header修改。创建网络Client实例时,自动查找app中被通用网络信息拦截器注解标注的拦截器。

线程池、连接池复用

各个组件都会有自己的网络Client实例,导致在同一个进程中,创建出来网络Client实例过多,同时线程池、连接池并没有复用。所以在网络库中,各个组件创建的网络Client默认会共享网络连接池和线程池,有特殊需要的模块,可以强制使用独立线程池和连接池。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容