前言
我们会经常碰到一个很恼人的场景:当后端出现访问或数据错误导致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,程序切换到前台的时候,再次显示。
最后
放几张图片给大家看看在项目中应用的效果。
如果您觉得有用,请点个赞吧。