Android:告别Log,在手机上显示网络请求

前言

我们会经常碰到一个很恼人的场景:当后端出现访问或数据错误导致App出现Bug时,测试人员首先会给前端开发提一个bug,这时候我们得连上手机看下网络日志,看到底是前端bug还是后端问题。如果是release版的话就更麻烦了,因为log被屏蔽,还得自己重新Run程序。
基于此,我考虑能不能把网络请求的Request和Response直接在手机屏幕上显示,这样方便调试。

定位网络Request和Response的地方

绝大部分的App开发都会自己定制的一套网络请求框架,因此第一步就是定位Request的和Response具体在哪生成。
一般而言,我们请求的传的Url都是拼接而成,BaseUrl+RequestType。不同的请求就是RequestType不同。因此我们可以把RequestType作为请求的key,来对应每个请求的Request和Response。

另外:有些请求不是BaseUrl+RequestType的形式,而是完成的一个URL。根据入参不同来表示不同的请求,那么这种情况选key就因需求而定了。

如下代码是我自己封装的一个类,用于保存Request的和Response,供各位参考。

public class NetHelper {
    //max size
    private static final int MAX_SIZE = 5;

    private static NetHelper sInstance;
    //save net console information
    private static final List<ConsoleInfo> sConsoleList = new ArrayList<>();

    private OnNetConsoleListener mListener;

    private static final ArrayMap<String, ConsoleInfo> sConsoleArrayMap = new ArrayMap<>();

    private NetHelper() {

    }

    synchronized public static NetHelper getInstance() {
        if (sInstance == null) {
            sInstance = new NetHelper();
        }
        return sInstance;
    }

    public void setOnRequestListener(OnNetConsoleListener listener) {
        this.mListener = listener;
    }


    public synchronized void putRequest(String key, String value) {
        if (sConsoleArrayMap.containsKey(key)) {
            sConsoleArrayMap.remove(key);
        }

        if (sConsoleArrayMap.size() > MAX_SIZE) {
            sConsoleArrayMap.remove(sConsoleArrayMap.keyAt(0));
        }

        ConsoleInfo consoleInfo = new ConsoleInfo();
        CacheInfo cacheRequest = new CacheInfo();
        cacheRequest.setTime(getDayTime());
        cacheRequest.setValue(value);
        cacheRequest.setRequestType(key);
        consoleInfo.setRequestType(key);
        consoleInfo.setRequest(cacheRequest);
        sConsoleArrayMap.put(key, consoleInfo);
        if (mListener != null) {
            mListener.onNetConsole(getConsoleInfoList());
        }
    }

    public synchronized void putResponse(String key, String value) {
        CacheInfo cacheResponse = new CacheInfo();
        cacheResponse.setTime(getDayTime());
        cacheResponse.setValue(value);
        cacheResponse.setRequestType(key);
        if (sConsoleArrayMap.containsKey(key)) {
            sConsoleArrayMap.get(key).setResponse(cacheResponse);
        }
        if (mListener != null) {
            mListener.onNetConsole(getConsoleInfoList());
        }
    }

    public List<ConsoleInfo> getConsoleList() {
        return sConsoleList;
    }

    private synchronized List<ConsoleInfo> getConsoleInfoList() {
        sConsoleList.clear();
        sConsoleList.addAll(sConsoleArrayMap.values());
        return sConsoleList;
    }

    private static String getDayTime() {
        SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
        Date currentTime = new Date();
        return formatter.format(currentTime);
    }

    public interface OnNetConsoleListener {
        void onNetConsole(List<ConsoleInfo> consoleInfoList);
    }

    public static class CacheInfo {

        String value;

        String time;

        String requestType;

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }

        public String getTime() {
            return time;
        }

        public void setTime(String time) {
            this.time = time;
        }

        public String getRequestType() {
            return requestType;
        }

        public void setRequestType(String requestType) {
            this.requestType = requestType;
        }
    }


    public static class ConsoleInfo {

        private String requestType;

        private CacheInfo request;

        private CacheInfo response;

        public CacheInfo getResponse() {
            return response;
        }

        public void setResponse(CacheInfo response) {
            this.response = response;
        }

        public CacheInfo getRequest() {
            return request;
        }

        public void setRequest(CacheInfo request) {
            this.request = request;
        }

        public String getRequestType() {
            return requestType;
        }

        public void setRequestType(String requestType) {
            this.requestType = requestType;
        }
    }
}

App上显示网络请求

App上要显示网络请求,肯定是要用一个独立且常驻的Window来显示,即与Activity的生命周期无关。

系统类型的Window就满足条件(Window相关的概念我准备在后续章节进行详细说明,绝对干货)。我们能够使用的系统类型Window常见有两种,分别是TYPE_TOAST、TYPE_SYSTEM_ALERT,但是TYPE_SYSTEM_ALERT类型Window需要权限(某些国产ROM不仅仅需要在manifest中声明权限,同时需要用户允许悬浮框权限)。到这基本上确定TYPE_TOAST类型Window就是最优解。

    public void generateTypeToast() {
        WindowManager wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        params.type = WindowManager.LayoutParams.TYPE_TOAST;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        params.width = WindowManager.LayoutParams.MATCH_PARENT;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.gravity = Gravity.CENTER;
        final View view = LayoutInflater.from(this).inflate(R.layout.window_toast, null);
        wm.addView(view, params);
    }

以上代码是创建TYPE_TOAST类型Window。
本来这一切都是美好的,但是测试的发现,小米手机的TYPE_TOAST类型Window也需要用户允许悬浮框。
后来搜索资料的时候找到Android中通过反射来设置Toast的显示时间这篇文章,我们可以通过改变Toast时间来达到常驻Window的目的。
本来这一切都是美好的,但后来测试发现,又TM是小米,在MIUI8中,对“反射改变Toast显示时间方案”进行了限制。在该方案中,Window是能正常显示,但是Window的位置不能再改变(即我们不能移动Window,只能固定在某个位置),否则会报错“java.lang.IllegalArgumentException: Window type can not be changed after the window is added.”。这个错误报的莫名其妙!

当然我们也可以对miui8进行特殊处理,其他ROM手机按照反射toast方案来解决。但是毕竟反射的方案不稳定,以防万一,最终我采取了常规方案。毕竟这只是开发工具,并不需要所有手机都兼容,当需要使用该工具的时候,提示下需要申请对应的权限就行。
最后,我们可以创建service来显示网络请求输出工具。因为系统类型的Window的生命周期是和应用程序进程生命相关,所以为了更友好的交互,当应用程序进入后台的时候,我们应该关闭网络请求输出Window,程序切换到前台的时候,再次显示。

最后

放几张图片给大家看看在项目中应用的效果。


网络请求输出工具1
网络请求输出工具2
网络请求输出工具3

如果您觉得有用,请点个赞吧。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,664评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,178评论 25 707
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,988评论 6 13
  • 文║抚心少年 原创发布 1. 找到一个相爱的人不容易,找到你爱的人又爱你的人,并且对你好的人,是很难的。 我有个朋...
    治愈话阅读 1,438评论 0 1
  • ――肖肖 都说缘分天注定,或许得不到的才显得更加弥足珍贵。 没有更多的...
    肖肖最潇洒阅读 373评论 0 0