注册

做一个具有高可用性的网络库(上)

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

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

7ec1a37b4c724b68af2763bd776c97aa~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.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。


续 做一个具有高可用性的网络库(下) 

作者:谢谢谢_xie
来源:juejin.cn/post/7074493841956405278

0 个评论

要回复文章请先登录注册