摘要
最近做需求时,碰到几个和网络相关的问题,特此记录,也算是对错误的一个总结。
1.多线程执行顺序问题
现象:
考拉商品详情页请求时,需要多个接口同时发出请求,出现请求丢失或者重复请求的状况。
现状:
目前考拉网络库底层改造,采用了aidl的方式,将网络请求抛到另一个进程里,减少主进程的CPU、内存等消耗,并且可以有效的防止网络进程的崩溃影响到主进程使用。
为了解决问题,我们先查看代码的调用顺序:
- HttpManager#executeRequest
- RemoteCallHelper.mNetServiceProxyWrapper(IKaolaNetAidlInterface)#execute
- RemoteCallHelper#postToNetService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| private IKaolaNetAidlInterface mNetServiceProxyWrapper = new IKaolaNetAidlInterface.Stub() { @Override public void execute(String method, String host, String path, String jsonBody, IKaolaRequestListener listener) throws RemoteException { postToNetService(() -> { try { mNetServiceProxy.execute(method, host, path, jsonBody, listener); } catch (Exception e) { ExceptionUtils.uploadCatchedException(e); } }); } }; public void postToNetService(final Runnable onConnectRunnable) { mOnConnectRunnables.add(onConnectRunnable); if (mNetServiceProxy == null) { boolean bound = mContext.bindService(new Intent(mContext, mRemoteNetCallEnable ? KaolaNetService.class : KaolaNetService.KaolaNetLocalService.class), mServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT); if (!bound) { mHandler.postDelayed(this::testNetService, BIND_TO_NET_SERVICE_RETRY_DELAY); } } else { runAndFlushOnConnectRunnables(); } } private void runAndFlushOnConnectRunnables() { for (Runnable r : mOnConnectRunnables) { r.run(); } mOnConnectRunnables.clear(); }
|
不了解通过AIDL实现Binder流程的,可以查看Android中AIDL的创建流程。
通过下面这张Binder机制图,
我们知道生成的IKaolaNetAidlInterface文件中onTransact方法是在运行在线程池中。但在以上代码中没有看到关于线程同步的任何代码。
因为没有线程同步,无法保证mOnConnectRunnables集合add、遍历、clear的执行顺序:
- add后立刻被clear,导致请求丢失;
- 遍历后再次遍历,导致请求重复;
找到问题原因,那么解决起来就很容易了。考虑到使用synchronized关键字可能导致的阻塞问题,我们创建只有1个线程的线程池,来保证对mOnConnectRunnables集合的有序执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| private ExecutorService mThreadExecutor = Executors.newSingleThreadExecutor(); public void postToNetService(final Runnable onConnectRunnable) { mThreadExecutor.execute(new Runnable() { @Override public void run() { mOnConnectRunnables.add(onConnectRunnable); } }); if (mNetServiceProxy == null) { boolean bound = mContext.bindService(new Intent(mContext, mRemoteNetCallEnable ? KaolaNetService.class : KaolaNetService.KaolaNetLocalService.class), mServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT); if (!bound) { mHandler.postDelayed(this::testNetService, BIND_TO_NET_SERVICE_RETRY_DELAY); } } else { runAndFlushOnConnectRunnables(); } } private void runAndFlushOnConnectRunnables() { mThreadExecutor.execute(new Runnable() { @Override public void run() { for (Runnable r : mOnConnectRunnables) { r.run(); } mOnConnectRunnables.clear(); } }); }
|
2.POST请求重定向问题
charles抓https请求偶尔抽风,为了开发方便,我们一般会把https请求强制变为http请求,如下图所示,是一个https的post请求:
这个请求要求必须使用https,所以当它变成http请求时就会报301,在请求的Response的header里,Location给我们指定了一个新的https的请求路径,发出请求后,这个post请求竟然变成了get请求:
再三确定代码无误后,开启debug之旅,最终发现,在okhttp中存在一个RetryAndFollowUpInterceptor
拦截器,其中关键方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| private Request followUpRequest(Response userResponse, Route route) throws IOException { if (userResponse == null) throw new IllegalStateException(); int responseCode = userResponse.code(); final String method = userResponse.request().method(); switch (responseCode) { .... case HTTP_MULT_CHOICE: case HTTP_MOVED_PERM:// 301 case HTTP_MOVED_TEMP: case HTTP_SEE_OTHER: .... if (HttpMethod.permitsRequestBody(method)) { final boolean maintainBody = HttpMethod.redirectsWithBody(method); if (HttpMethod.redirectsToGet(method)) { requestBuilder.method("GET", null); } else { RequestBody requestBody = maintainBody ? userResponse.request().body() : null; requestBuilder.method(method, requestBody); } if (!maintainBody) { requestBuilder.removeHeader("Transfer-Encoding"); requestBuilder.removeHeader("Content-Length"); requestBuilder.removeHeader("Content-Type"); } } // When redirecting across hosts, drop all authentication headers. This // is potentially annoying to the application layer since they have no // way to retain them. if (!sameConnection(userResponse, url)) { requestBuilder.removeHeader("Authorization"); } return requestBuilder.url(url).build(); .... default: return null; } }
|
我们可以看到就是requestBuilder.method("GET", null);
这行代码将post请求变成了get请求,我们可以看看它的两个前置条件:
1 2 3 4 5 6 7 8
| public static boolean permitsRequestBody(String method) { return !(method.equals("GET") || method.equals("HEAD")); } public static boolean redirectsToGet(String method) { // All requests but PROPFIND should redirect to a GET request. return !method.equals("PROPFIND"); }
|
很明显,此次请求的method为post,两个方法都返回true导致method从post变成了get。
3.POST请求body设置问题
在环境、参数、请求都相同的情况下,请求依然一直报错。使用charles做对比,发现body的内容和content-type类型无法匹配,导致出错。
我们默认使用content-type为application/json; charset=utf-8
作为请求响应头。
因为这次请求并没有设置body内容,所以调用底层网络框架时,传入参数null。但底层框架库将传入的参数直接通过json转换为字符串null导致出错:
找到问题,只要将body内容为空的请求默认设置为{}
即可。