fix(network): 修复重复请求拦截器去重失效问题

在请求发出前拦截重复请求,复用进行中请求的结果,
并在完成后保留 500ms 结果窗口;同时新增 DX11、G12NL
平台标识与构建变体,更新版本号至 1.6.0914。
This commit is contained in:
2026-09-18 16:20:03 +08:00
parent 151e488b6f
commit 8e37bca0c3
6 changed files with 329 additions and 76 deletions

View File

@@ -75,8 +75,8 @@ android {
minSdkVersion 26
targetSdkVersion 28
versionCode 206
versionName "1.6.0731"
versionCode 209
versionName "1.6.0914"
ndk {
//选择要添加的对应 cpu 类型的 .so 库。
@@ -256,13 +256,36 @@ android {
v1SigningEnabled true
v2SigningEnabled true
}
MT6789 {
storeFile file("keystore/MT6789.keystore")
storePassword "123456"
keyAlias "mt6789"
keyPassword "123456"
v1SigningEnabled true
v2SigningEnabled true
}
}
buildTypes {
G12NLDebug.initWith(debug)
G12NLDebug {
buildConfigField "String", "platform", '"G12NL"'
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.MT6789
}
G12NLRelease.initWith(release)
G12NLRelease {
buildConfigField "String", "platform", '"G12NL"'
signingConfig signingConfigs.MT6789
}
DX11Debug.initWith(debug)
DX11Debug {
buildConfigField "String", "platform", '"DX11"'
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.DX11
}
@@ -276,7 +299,7 @@ android {
G122Debug.initWith(debug)
G122Debug {
buildConfigField "String", "platform", '"MT6765"'
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.G122
}
@@ -290,7 +313,7 @@ android {
QualcommDebug.initWith(debug)
QualcommDebug {
buildConfigField "String", "platform", '"QualcommA"'
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.zhanRui
}
@@ -304,7 +327,7 @@ android {
G128TDebug.initWith(debug)
G128TDebug {
buildConfigField "String", "platform", '"G128T"'
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.G128T
}
@@ -318,7 +341,7 @@ android {
QZ15Debug.initWith(debug)
QZ15Debug {
buildConfigField "String", "platform", '"QZA15"'
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.U1302
}
@@ -331,7 +354,7 @@ android {
seewoDebug.initWith(debug)
seewoDebug {
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.seewo
buildConfigField "String", "platform", '"seewo"'
@@ -345,7 +368,7 @@ android {
P50Debug.initWith(debug)
P50Debug {
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.P50
buildConfigField "String", "platform", '"P50TD"'
@@ -359,7 +382,7 @@ android {
T40AirDebug.initWith(debug)
T40AirDebug {
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.T40Air
buildConfigField "String", "platform", '"HL500"'
@@ -373,7 +396,7 @@ android {
// AiUduDebug.initWith(debug)
// AiUduDebug {
// versionNameSuffix "-debug"
// versionNameSuffix "_debug"
// debuggable true
// signingConfig signingConfigs.G10P
// buildConfigField "String", "platform", '"AUDG104"'
@@ -387,7 +410,7 @@ android {
Huaruian8768Debug.initWith(debug)
Huaruian8768Debug {
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.Huaruian8768
buildConfigField "String", "platform", '"MT8768"'
@@ -401,7 +424,7 @@ android {
G11Debug.initWith(debug)
G11Debug {
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.G11Userdebug
buildConfigField "String", "platform", '"MTKG11"'
@@ -415,7 +438,7 @@ android {
YXPD1Debug.initWith(debug)
YXPD1Debug {
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.G10P
buildConfigField "String", "platform", '"YXPD1"'
@@ -429,7 +452,7 @@ android {
c2Debug.initWith(debug)
c2Debug {
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.teclast8183
buildConfigField "String", "platform", '"MTK8183"'
@@ -443,7 +466,7 @@ android {
G10PDebug.initWith(debug)
G10PDebug {
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.G10P
buildConfigField "String", "platform", '"G10P"'
@@ -458,7 +481,7 @@ android {
G104Debug.initWith(debug)
G104Debug {
buildConfigField "String", "platform", '"MTK11"'
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.G10P
}
@@ -471,7 +494,7 @@ android {
zhanRuiDebug.initWith(debug)
zhanRuiDebug {
versionNameSuffix "-debug"
versionNameSuffix "_debug"
buildConfigField "String", "platform", '"ZhanRui"'
debuggable true
signingConfig signingConfigs.zhanRui
@@ -486,7 +509,7 @@ android {
//userdebug rom使用这个版本
zhanRuiUserdebug.initWith(zhanRuiDebug)
zhanRuiUserdebug {
versionNameSuffix "-debug"
versionNameSuffix "_debug"
buildConfigField "String", "platform", '"ZhanRui"'
debuggable true
signingConfig signingConfigs.zhanRuiUserdebug
@@ -494,7 +517,7 @@ android {
zhanRui12Debug.initWith(debug)
zhanRui12Debug {
versionNameSuffix "-debug"
versionNameSuffix "_debug"
buildConfigField "String", "platform", '"AH6016"'
debuggable true
signingConfig signingConfigs.zhanRui12
@@ -509,7 +532,7 @@ android {
TeclastP20SDebug.initWith(debug)
TeclastP20SDebug {
debuggable true
versionNameSuffix "-debug"
versionNameSuffix "_debug"
buildConfigField "String", "platform", '"P20S"'
signingConfig signingConfigs.TeclastP20S
}
@@ -523,7 +546,7 @@ android {
zhanruiG10ZDebug.initWith(debug)
zhanruiG10ZDebug {
buildConfigField "String", "platform", '"ZR6016"'
versionNameSuffix "-debug"
versionNameSuffix "_debug"
debuggable true
signingConfig signingConfigs.zhanruiG10Z
}
@@ -537,7 +560,7 @@ android {
debug {
buildConfigField "String", "platform", '"MTK"'
debuggable true
versionNameSuffix "-debug"
versionNameSuffix "_debug"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.mtk

View File

@@ -15,6 +15,10 @@ android {
}
buildTypes {
MX11Debug {}
MX11Release {}
G12NLDebug {}
G12NLRelease {}
DX11Debug {}
DX11Release {}
G122Debug {}

BIN
keystore/MT6789.keystore Normal file

Binary file not shown.

View File

@@ -208,6 +208,10 @@ public class NetInterfaceManager {
set.add(JgyUtils.AUDG104Tag);
} else if (platform == JgyUtils.seewoPlatform) {
set.add(JgyUtils.seewoTag);
} else if (platform == JgyUtils.DX11Platform) {
set.add(JgyUtils.DX11_TAG);
} else if (platform == JgyUtils.G12NLPlatform) {
set.add(JgyUtils.G12NL_TAG);
}
}
});

View File

@@ -8,9 +8,13 @@ import com.aoleyun.os.uiuiutils.MD5Util;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.RequestBody;
@@ -19,89 +23,292 @@ import okhttp3.ResponseBody;
import okio.Buffer;
/**
* v1.0 2022-07-15 16:16:52
* 重复请求拦截器。
*
* <p>原实现是在 {@code chain.proceed()} 之后才判断是否为重复请求,此时请求早已发到服务器,
* 只能丢弃返回结果,所以服务端依然会收到两次(甚至多次)请求;并且记录在 finally 中立即删除,
* 去重窗口几乎为零,重复请求根本拦不住。</p>
*
* <p>优化后的策略:</p>
* <ul>
* <li>发请求之前先判断是否已经有相同请求:有则等待它的结果并复用,不再访问服务器;</li>
* <li>请求完成后在 {@link #REUSE_WINDOW_MS} 内保留结果,短时间内相同的请求直接复用结果;</li>
* <li>复用时使用真实的状态码、响应头和响应体,调用方无感知(原来的空 {@link Response} 会让
* 解析层直接抛异常);</li>
* <li>无响应体或响应体过大(超过 {@link #MAX_DEDUP_BODY_BYTES})的请求不参与去重,直接透传,
* 避免占用大量内存。</li>
* </ul>
*
* <p>v1.0 2022-07-15 16:16:52</p>
*/
public class RepeatRequestInterceptor implements Interceptor {
private static final String TAG = RepeatRequestInterceptor.class.getSimpleName();
private static final String TAG = "RepeatRequestInterceptor";
private final ConcurrentHashMap<String, Long> requestIdsMap = new ConcurrentHashMap<>();
public static final String REPEAT_REQUEST_PROTOCOL = "OKHTTP_REPEAT_REQUEST_PROTOCOL";
/**
* 等待进行中请求返回结果的最长时间,超时后本次请求自己访问服务器。
* 需要大于 OkHttpClient 的超时时间(当前为 5 秒)。
*/
private static final long WAIT_IN_FLIGHT_TIMEOUT_MS = 10_000L;
/**
* 请求完成后结果保留的时间。在该时间内收到相同请求,直接复用结果,不再访问服务器。
* 需要小于业务侧的刷新间隔(ConnectMode 最小为 1 分钟)。
*/
private static final long REUSE_WINDOW_MS = 500L;
/** 超过该大小的响应体不参与去重(直接透传),避免内存占用 */
private static final long MAX_DEDUP_BODY_BYTES = 1024 * 1024L;
/** 参与去重 key 计算的请求体最大字节数 */
private static final int MAX_BODY_BYTES_FOR_KEY = 8 * 1024;
/** 日志中请求体、响应体的最大长度 */
private static final int MAX_LOG_BODY_LENGTH = 10240;
private final ConcurrentHashMap<String, RepeatRecord> recordMap = new ConcurrentHashMap<>();
@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
ResponseBody responseBody = response.body();
String bodyText = requestBodyToString(request.body());
String requestKey = buildRequestKey(request, bodyText);
//会消费请求,导致请求多次
String content = responseBody.string();
// Response copy = response.newBuilder().body(responseBody).build();
ResponseBody copy = ResponseBody.create(responseBody.contentType(), content);
if (BuildConfig.DEBUG) {
Log.e(TAG, "请求体返回:| Response: " + request.url().encodedPath() + "\t body: " + content);
clearExpiredRecords();
RepeatRecord record = acquireRecord(requestKey);
if (record == null) {
// 已有相同请求,但没有拿到可用结果(上一次失败、超时或结果过大),本次请求直接访问服务器
return chain.proceed(request);
}
//相同的请求
String requestKey = MD5Util.getUpperMD5Str(request.method() + request.url().toString() + requestBodyToString(request.body()));
long time = System.currentTimeMillis();//请求时间
if (!record.isOwner()) {
// 复用相同请求的结果:服务端只会收到一次请求
log("复用请求:", requestKey, request, bodyText);
return record.newResponse(request);
}
log("注册请求:", requestKey, request, bodyText);
try {
if (requestIdsMap.size() > 0 && requestIdsMap.containsKey(requestKey)) {
log("重复请求:", requestKey, request);
//下面这行写了不会抛出onerror
// chain.call().cancel();
return new Response.Builder()
.protocol(Protocol.get(REPEAT_REQUEST_PROTOCOL))
.request(request) //multi thread
.build();
Response response = chain.proceed(request);
ResponseBody responseBody = response.body();
if (responseBody == null || responseBody.contentLength() > MAX_DEDUP_BODY_BYTES) {
// 无响应体或响应体过大:不消费 body,也不参与去重,直接透传
logResponse(request, null);
return response;
}
requestIdsMap.put(requestKey, time);
log("注册请求:", requestKey, request);
// RepeatRequestInterceptor.Builder builder = request.newBuilder();
// builder.addHeader("header", jsonObject.toString());
return response.newBuilder().body(copy).build();
// body 只能被消费一次,读出来之后重建一个新的 body 返回给调用方
MediaType contentType = responseBody.contentType();
byte[] bytes = responseBody.bytes();
logResponse(request, bytes);
if (bytes.length <= MAX_DEDUP_BODY_BYTES) {
// 缓存结果,供等待中的、以及短时间内后续的相同请求复用
record.complete(response, bytes, contentType);
}
return response.newBuilder().body(ResponseBody.create(contentType, bytes)).build();
} catch (IOException e) {
Log.e(TAG, "intercept: " + e.getMessage());
throw e;
} finally {
if (requestIdsMap.containsKey(requestKey) && requestIdsMap.containsValue(time)) {//请求任务完成删除map中的数据
requestIdsMap.remove(requestKey);
log("移除请求:", requestKey, request);
// 兜底:无论成功、失败还是异常,都结束记录,避免等待中的相同请求被一直阻塞
record.finish();
}
}
/**
* 获取请求对应的去重记录。
*
* @return 返回记录表示本次请求要么成为 owner 去访问服务器、要么直接复用已有结果;
* 返回 null 表示已有相同请求但结果不可用,本次请求自己访问服务器(不参与去重)
*/
private RepeatRecord acquireRecord(String requestKey) {
while (true) {
long now = System.currentTimeMillis();
RepeatRecord record = recordMap.get(requestKey);
if (record == null) {
RepeatRecord created = new RepeatRecord();
if (recordMap.putIfAbsent(requestKey, created) == null) {
created.markOwner();
return created;
}
continue;// 其他线程抢先注册,重新判断
}
if (record.isExpired(now)) {
// 结果已过期,替换为本次请求的记录并成为 owner
RepeatRecord created = new RepeatRecord();
if (recordMap.replace(requestKey, record, created)) {
created.markOwner();
return created;
}
continue;
}
// 已有相同请求:等待它的结果,拿到结果直接复用
if (record.await(WAIT_IN_FLIGHT_TIMEOUT_MS)) {
return record;
}
return null;
}
}
/** 清理过期记录,释放其缓存的响应体 */
private void clearExpiredRecords() {
if (recordMap.isEmpty()) {
return;
}
long now = System.currentTimeMillis();
for (Map.Entry<String, RepeatRecord> entry : recordMap.entrySet()) {
RepeatRecord record = entry.getValue();
if (record.isExpired(now)) {
recordMap.remove(entry.getKey(), record);
}
}
}
private void log(String action, String requestKey, Request request) {
private static String buildRequestKey(Request request, String bodyText) {
return MD5Util.getUpperMD5Str(request.method() + "|" + request.url() + "|" + bodyText);
}
/**
* 请求体转字符串,用于计算去重 key 和输出日志。
* 只读取可重复读写的请求体,避免把一次性(one-shot)请求体读空。
*/
private static String requestBodyToString(RequestBody body) {
if (body == null || body.isOneShot()) {
return "";
}
try {
Buffer buffer = new Buffer();
body.writeTo(buffer);
long size = buffer.size();
if (size > MAX_BODY_BYTES_FOR_KEY) {
// 请求体过大时只取前一部分参与去重,避免不必要的内存开销
return size + ":" + buffer.readByteString(MAX_BODY_BYTES_FOR_KEY).hex();
}
return buffer.readUtf8();
} catch (Exception e) {
return "";
}
}
private static void log(String action, String requestKey, Request request, String bodyText) {
if (BuildConfig.DEBUG) {
Log.e("REPEAT-REQUEST", action + requestKey + " Method @" + request.method() + " --- " + " URL = " + request.url().encodedPath() + "\t" + bodyToString(request));
String body = bodyText.length() > MAX_LOG_BODY_LENGTH ? "-too long" : bodyText;
Log.e("REPEAT-REQUEST", action + requestKey + " Method @" + request.method()
+ " --- " + " URL = " + request.url().encodedPath() + "\t" + body);
} else {
Log.e("REPEAT-REQUEST", action + requestKey + " Method @" + request.method());
}
}
private static String bodyToString(final Request request) {
try {
final Request copy = request.newBuilder().build();
final Buffer buffer = new Buffer();
copy.body().writeTo(buffer);
if (buffer.size() > 4096) {
return "-too long";
}
return buffer.readUtf8();
} catch (Exception e) {
return "-";
private static void logResponse(Request request, byte[] bytes) {
if (!BuildConfig.DEBUG) {
return;
}
if (bytes == null || bytes.length > MAX_LOG_BODY_LENGTH) {
Log.e(TAG, "请求体返回:| Response: " + request.url() + "\t body: too long");
} else {
Log.e(TAG, "请求体返回:| Response: " + request.url()
+ "\t body: " + new String(bytes, StandardCharsets.UTF_8));
}
}
private static String requestBodyToString(RequestBody body) {
try {
final Buffer buffer = new Buffer();
body.writeTo(buffer);
if (buffer.size() > 4096) {
return "-too long";
/**
* 一次请求的去重记录:owner 线程负责访问服务器,其他相同请求等待并复用它的结果
*/
private static final class RepeatRecord {
private final Object lock = new Object();
private final long createdTime = System.currentTimeMillis();
private boolean owner;
private boolean finished;
private boolean succeed;
private long finishedTime;
private byte[] body;
private MediaType contentType;
private Headers headers;
private Protocol protocol;
private int code;
private String message;
void markOwner() {
owner = true;
}
boolean isOwner() {
return owner;
}
/**
* 等待 owner 请求结束
*
* @return true 表示已拿到可复用的结果
*/
boolean await(long timeoutMs) {
synchronized (lock) {
if (!finished) {
try {
lock.wait(timeoutMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return finished && succeed;
}
}
/** 请求成功,缓存结果并唤醒等待的请求 */
void complete(Response response, byte[] body, MediaType contentType) {
synchronized (lock) {
if (finished) {
return;
}
this.body = body;
this.contentType = contentType;
this.headers = response.headers();
this.protocol = response.protocol();
this.code = response.code();
this.message = response.message();
this.succeed = true;
finished = true;
finishedTime = System.currentTimeMillis();
lock.notifyAll();
}
}
/** 请求结束(失败或异常时没有可复用的结果);重复调用忽略 */
void finish() {
synchronized (lock) {
if (finished) {
return;
}
finished = true;
finishedTime = System.currentTimeMillis();
lock.notifyAll();
}
}
/** 进行中的记录超过等待超时时间、已完成的记录超过结果保留时间即视为过期 */
boolean isExpired(long now) {
synchronized (lock) {
if (!finished) {
return now - createdTime > WAIT_IN_FLIGHT_TIMEOUT_MS;
}
return now - finishedTime > REUSE_WINDOW_MS;
}
}
/** 用缓存的结果构建响应,状态码、响应头、响应体都是真实数据 */
Response newResponse(Request request) {
synchronized (lock) {
return new Response.Builder()
.request(request)
.protocol(protocol)
.code(code)
.message(message)
.headers(headers)
.body(ResponseBody.create(contentType, body))
.build();
}
return buffer.readUtf8();
} catch (Exception e) {
return "-";
}
}
}

View File

@@ -45,6 +45,9 @@ public class JgyUtils {
public static final int MT8768Platform = 17;
public static final int AUDG104Platform = 18;
public static final int seewoPlatform = 25;
public static final int DX11Platform = 31;
public static final int G12NLPlatform = 32;
public static final String Other = "其他";
public static final String MTKTag = "MTK";
@@ -62,6 +65,8 @@ public class JgyUtils {
public static final String MT8768Tag = "MT8768";
public static final String AUDG104Tag = "AUDG104";
public static final String seewoTag = "seewo";
public static final String DX11_TAG = "DX11";
public static final String G12NL_TAG = "G12NL";
private JgyUtils(Context context) {
@@ -120,6 +125,12 @@ public class JgyUtils {
} else if (seewoTag.equalsIgnoreCase(platform)) {
Log.i(TAG, "checkAppPlatform: " + "seewo");
return seewoPlatform;
} else if (DX11_TAG.equalsIgnoreCase(platform)) {
Log.i(TAG, "checkAppPlatform: " + "DX11");
return DX11Platform;
} else if (G12NL_TAG.equalsIgnoreCase(platform)) {
Log.i(TAG, "checkAppPlatform: " + "G12NL");
return G12NLPlatform;
} else {
Log.i(TAG, "checkAppPlatform: " + "没有数据");
return UnknowPlatform;
@@ -154,6 +165,10 @@ public class JgyUtils {
getAppPlatformCallback.AppPlatform(AUDG104Platform);
} else if (seewoTag.equalsIgnoreCase(platform)) {
getAppPlatformCallback.AppPlatform(seewoPlatform);
} else if (DX11_TAG.equalsIgnoreCase(platform)) {
getAppPlatformCallback.AppPlatform(DX11Platform);
} else if (G12NL_TAG.equalsIgnoreCase(platform)) {
getAppPlatformCallback.AppPlatform(G12NLPlatform);
} else {
getAppPlatformCallback.AppPlatform(UnknowPlatform);
}