• Android OKHttp 可能你从来没用过的拦截器 【实用推荐】


    前言

    在平时开发中,你有没有下面这样的困扰呢?

    场景一

    明明是服务端的接口数据错误,而QA(测试)第一个找到的可能是客户端开发的你,为什么这个页面出现错误了?

    而作为客户端开发的你,可能要拿出测试机连上电脑,打一下Log,看一下到底返回了什么数据,导致页面错误。

    或者高级一点的QA,会自己打Log或者连接抓包工具看一下服务端返回的具体数据,然后把Bug提给对应的人,而大多数公司的业务测试,都仅仅是测试业务,不管技术层的。我司的大部分QA,属于外派来的,一般也只测试业务,每次有问题,都先找客户端。

    场景二

    你现在正在外面做地铁,产品或者你领导突然给你反馈,你之前做的那块业务,突然线上跑不起来了,不行了。你一想,这肯定是服务端的问题啊,但是怎么证明呢?

    场景三

    服务端上个线,每次都需要客户端加班配合,说有问题,可以及时帮助排查问题。

    推荐一个小工具

    说了这么多,就是缺少一个端上的抓包小工具,来查看服务端的数据是否有问题,今天推荐的是一个基于OKHttp的抓包工具。 部分截图如下

    支持功能

    • 自带分类接口
    • 抓包数据以时间为纬度,默认存储到手机缓存下 /Android/Data/包名/Cache/capture/ 下
    • 支持Http/Https协议的抓包,分类请求方式/请求URL/请求Header/请求体/响应状态/响应Header/响应体
    • 支持一键复制对应的状态
    • 响应体如果是JSON,支持自动格式化
    • 抓包数据,默认缓存一天

    Github地址

    代码已经托管到Github 地址:github.com/DingProg/Ne…

    快速接入

    allprojects {
    	repositories {
    	   maven { url 'https://jitpack.io' }
    	}
    }
    
    dependencies {
        debugImplementation 'com.github.DingProg.NetworkCaptureSelf:library:v1.0.1'
        releaseImplementation 'com.github.DingProg.NetworkCaptureSelf:library_no_op:v1.0.1'	 
    }
    复制代码

    在你的全局OkHttp中添加 Interceptor

    new OkHttpClient.Builder()
            .addInterceptor(new CaptureInfoInterceptor())
            .build();
    复制代码

    原理及涉及知识详解

    作为Android开发,说到OKHttp的Interceptor,肯定熟悉不过了。那么你对 Interceptor 又了解多少呢?你都使用过那些OKHttp的 Interceptor呢?

    我们先来看一下最近滴滴很火的哆啦A梦

    DoraemonKit

    长下面这个样子

    其中关于网络模块OK Http的监听如下

    OkHttpClient client = new OkHttpClient().newBuilder()
                    //用于模拟弱网的拦截器
                    .addNetworkInterceptor(new DoraemonWeakNetworkInterceptor())
                    //网络请求监控的拦截器 ,用于网络流量监听等
                    .addInterceptor(new DoraemonInterceptor()).build();
    复制代码

    这里举例说一下弱网模拟

    弱网模拟

    看一下他的实现代码

    public class DoraemonWeakNetworkInterceptor implements Interceptor {
    
        @Override
        public Response intercept(Chain chain) throws IOException {
            if (!WeakNetworkManager.get().isActive()) {
                Request request = chain.request();
                return chain.proceed(request);
            }
            final int type = WeakNetworkManager.get().getType();
            switch (type) {
                case WeakNetworkManager.TYPE_TIMEOUT:
                    //超时
                    final HttpUrl url = chain.request().url();
                    throw WeakNetworkManager.get().simulateTimeOut(url.host(), url.port());
                case WeakNetworkManager.TYPE_SPEED_LIMIT:
                    //限速
                    return WeakNetworkManager.get().simulateSpeedLimit(chain);
                default:
                    //断网
                    throw WeakNetworkManager.get().simulateOffNetwork(chain.request().url().host());
            }
        }
    }
    复制代码

    实现一个OkHttp的Intercepter,根据不同的状态来进行延迟,例如如下的模拟超时

    /**
         * 模拟超时
         *
         * @param host
         * @param port
         */
        public SocketTimeoutException simulateTimeOut(String host, int port) {
            SystemClock.sleep(mTimeOutMillis);
            return new SocketTimeoutException(String.format("failed to connect to %s (port %d) after %dms", host, port, mTimeOutMillis));
        }
    复制代码

    根据Interceptor 可以干很多事情,那么Interceptor到底是什么样的原理呢?

    Interceptor原理

    先看一下Interceptor的原型

    public interface Interceptor {
      Response intercept(Chain chain) throws IOException;
    }
    复制代码

    再看一下OkHttp源码,可以知道,我们的请求最终都会被调用到RealCall中,并执行到如下代码

     @Override protected void execute() {
        boolean signalledCallback = false;
        try {
            Response response = getResponseWithInterceptorChain();
        }
        ...
    }
    
     Response getResponseWithInterceptorChain() throws IOException {
        // Build a full stack of interceptors.
        List<Interceptor> interceptors = new ArrayList<>();
        interceptors.addAll(client.interceptors());
        interceptors.add(retryAndFollowUpInterceptor);
        interceptors.add(new BridgeInterceptor(client.cookieJar()));
        interceptors.add(new CacheInterceptor(client.internalCache()));
        interceptors.add(new ConnectInterceptor(client));
        if (!forWebSocket) {
          interceptors.addAll(client.networkInterceptors());
        }
        interceptors.add(new CallServerInterceptor(forWebSocket));
    
        Interceptor.Chain chain = new RealInterceptorChain(
            interceptors, null, null, null, 0, originalRequest);
        return chain.proceed(originalRequest);
      }
    复制代码

    在getResponseWithInterceptorChain 添加了很多OkHttp自定义的拦截器,其中有重定向,Cache,连接请求,发起请求到服务端等。我们来看一下最后几行 代码,RealInterceptorChain是一个Interceptor.Chain类型,并执行chain.proceed,接着看一下proceed方法

    //RealInterceptorChain
     public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
          RealConnection connection) throws IOException {
        ...
        if (index >= interceptors.size()) throw new AssertionError();
        calls++;
        // Call the next interceptor in the chain.
        RealInterceptorChain next = new RealInterceptorChain(
            interceptors, streamAllocation, httpCodec, connection, index + 1, request);
        Interceptor interceptor = interceptors.get(index);
        Response response = interceptor.intercept(next);
        ....
        return response;
      }
    复制代码

    重点看一下Call the next interceptor in the chain 下面几行代码,他把当前的interceptor.intercept()时,传入的是下一个interceptor的包装类,RealInterceptorChain 这样就实现了,链式递归调用了,直到最后一个response返回,才会依次返回到第一个interceptor。

    可以用如下图大致描述

    讲了那么多相关的知识点,我们来回到正题,上述推荐小工具的实现步骤介绍

    抓包工具实现主要步骤介绍

    添加一个抓包入口

    在Manifest中注册即可,如下

      <activity
            android:name="com.ding.library.internal.ui.CaptureInfoActivity"
            android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|locale"
            android:launchMode="singleInstance"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme.NoActionBar" />
    
        <activity-alias
            android:label="抓包入口"
            android:name="CaptureInfoActivity"
            android:targetActivity="com.ding.library.internal.ui.CaptureInfoActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity-alias>
    复制代码

    暴露Interceptor

    public final class CaptureInfoInterceptor implements Interceptor{
        @Override public Response intercept(Chain chain) throws IOException {
        //获取request 所有信息
        ...
        //获取response 所有信息
        ...
        
        //存储抓包数据
        CacheUtils.getInstance().saveCapture(request.url().toString(),captureEntity);
        }
    }
    复制代码

    这里其中有两种方式,添加到OkHttp的Interceptor,一种硬编码,如下

    new OkHttpClient.Builder()
       .addInterceptor(new CaptureInfoInterceptor())
       .build();
    复制代码

    另一种方式 采用字节码注入的形式,关于字节码注入,可以简单参考我的另一篇Gradle学习笔记,自定义 Transform部分。

    存储和读取抓包数据 效率问题

    存储时,为了不影响到主APP的网络请求效率,需要在单独的线程中执行IO操作,这里使用了单线程池

    public class DiskIOThreadExecutor implements Executor {
       private final Executor mDiskIO;
       public DiskIOThreadExecutor() {
           mDiskIO = Executors.newSingleThreadExecutor();
       }
       @Override
       public void execute(@NonNull Runnable command) {
           mDiskIO.execute(command);
       }
    }
    
    复制代码

    存储

       public void saveCapture(final String url, final CaptureEntity value) {
           Runnable runnable = new Runnable() {
               @Override
               public void run() {
                   String saveUrl = url;
                   if (url.contains("?")) {
                       saveUrl = saveUrl.substring(0, saveUrl.indexOf("?"));
                   }
                   String key = urlMd5(saveUrl);
                   sp.edit().putString(key, saveUrl).apply();
                   checkOrCreateFilePath(key);
                   File file = new File(captureFilePath + "/" + key + "/" + getCurrentTime() + ".txt");
                   BufferedSink bufferedSink = null;
                   try {
                       file.createNewFile();
                       bufferedSink = Okio.buffer(Okio.sink(file));
                       bufferedSink.writeString(JSON.toJSONString(value), StandardCharsets.UTF_8);
                       bufferedSink.flush();
                   } catch (Exception e) {
                       e.printStackTrace();
                   } finally {
                       if (bufferedSink != null) {
                           try {
                               bufferedSink.close();
                           } catch (IOException e) {
                               e.printStackTrace();
                           }
                       }
                   }
               }
           };
           diskIOThreadExecutor.execute(runnable);
       }
    复制代码

    读取

    读取抓包数据时,不直接读取全部的数据,只读取当前抓包的目录,数据,点击时,在去加载对应的数据

    public List<String> getCapture() {
       File file = new File(captureFilePath);
       return getFileList(file);
    }
    
    public List<String> getCapture(String key) {
       File file = new File(captureFilePath + "/" + key);
       return getFileList(file);
    }
    复制代码

    好了,关于这个小工具,就介绍那么多了,具体细节代码,可以直接查看Github代码仓库,github.com/DingProg/Ne…

    总结

    其实关于抓包工具,有一些成熟的方案。

    • 电脑端的有Fiddler、Charels,Wireshark等,但是不是特别方便。
    • APP可以抓其他包的工具,如NetWorkPacketCapture/抓包精灵/AndroidHttpCapture,但是都一些限制条件,要么代码没开源,广告多。要么就是只能在WIFI下,或者要么就是需要Root等,不太好定制。

    本文,主要是介绍OkHttp的拦截器,并从中发现可以干很多事情。如文中有错误,还忘指正,感谢。

    最后也感谢你的点赞及Github的Star NetworkCaptureSelf


    作者:北斗星_And
    链接:https://juejin.im/post/5ddddd2a6fb9a07161483fb2
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    每周总结13
    每周总结12
    每周总结11
    每周总结10
    每周总结9
    启明5-1总结
    启明4-30团队进度博客
    期末总结
    每日日报2021.6.11
    每日日报2021.6.10
  • 原文地址:https://www.cnblogs.com/yizijianxin/p/12000283.html
Copyright © 2020-2023  润新知