Browse Source

长连接与页面保活优化

yanqiliang 3 months ago
parent
commit
c355658cce

+ 0 - 1
.idea/deploymentTargetDropDown.xml

@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="deploymentTargetDropDown">
-    <multipleDevicesSelectedInDropDown value="true" />
     <targetsSelectedWithDialog>
       <Target>
         <type value="QUICK_BOOT_TARGET" />

+ 2 - 2
app/build.gradle

@@ -8,8 +8,8 @@ android {
         applicationId "com.example.jhpapp"
         minSdk 22
         targetSdk 31
-        versionCode 7
-        versionName "3.1"
+        versionCode 8
+        versionName "3.2"
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         ndk {  abiFilters "armeabi","armeabi-v7a","x86"}

+ 193 - 0
app/src/main/java/com/example/jhpapp/MainActivity.java

@@ -17,6 +17,7 @@ import android.view.KeyEvent;
 import android.view.View;
 import android.webkit.JavascriptInterface;
 import android.webkit.SslErrorHandler;
+import android.webkit.ValueCallback;
 import android.webkit.WebResourceError;
 import android.webkit.WebResourceRequest;
 import android.webkit.WebSettings;
@@ -38,6 +39,8 @@ import com.google.android.exoplayer2.audio.AudioAttributes;
 import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
 import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
 import android.content.Intent;
+import org.json.JSONArray;
+import org.json.JSONObject;
 import pl.droidsonroids.gif.GifImageView;
 
 public class MainActivity extends BaseActivity {
@@ -122,6 +125,20 @@ public class MainActivity extends BaseActivity {
     private Handler handlerWebErr;
     private int pageRetryCount = 0;
     private static final int MAX_PAGE_RETRY = 5;
+    private Handler webHealthHandler;
+    private Runnable webHealthRunnable;
+    private int webHealthFailCount = 0;
+    private int webHealthQuerySeq = 0;
+    private long socketUnhealthySinceAt = 0;
+    private long lastSocketWatchdogAt = 0;
+    private long lastWebReloadAt = 0;
+    private static final long WEB_HEALTH_INTERVAL_MS = 30 * 1000L;
+    private static final long WEB_HEALTH_QUERY_TIMEOUT_MS = 8 * 1000L;
+    private static final int WEB_HEALTH_FAIL_RELOAD_COUNT = 2;
+    private static final long WEB_RELOAD_COOLDOWN_MS = 3 * 60 * 1000L;
+    private static final long SOCKET_WATCHDOG_COOLDOWN_MS = 60 * 1000L;
+    private static final long SOCKET_UNHEALTHY_MS = 45 * 1000L;
+    private static final long PAGE_TICK_STALE_MS = 90 * 1000L;
 
     // ❻ 生命周期观察者(管理播放器暂停/恢复)
     private final LifecycleEventObserver lifecycleObserver = (source, event) -> {
@@ -142,12 +159,14 @@ public class MainActivity extends BaseActivity {
                     player.play();
                 }
                 notifyJsReady();
+                runWebHealthCheck();
                 break;
             case ON_DESTROY:
                 // 销毁时清理Handler
                 if (handlerWebErr != null) {
                     handlerWebErr.removeCallbacksAndMessages(null);
                 }
+                stopWebHealthWatchdog();
                 break;
         }
     };
@@ -193,6 +212,7 @@ public class MainActivity extends BaseActivity {
 
         //初始化
         init();
+        startWebHealthWatchdog();
 
         //版本判断
 //        appVersionCheck();
@@ -349,6 +369,7 @@ public class MainActivity extends BaseActivity {
             webIDs.setVisibility(View.VISIBLE);
             // reload 后通知前端 native 就绪,前端据此复位忙标志并 drain 队列
             notifyJsReady();
+            runWebHealthCheck();
         }
 
         @Override
@@ -383,6 +404,7 @@ public class MainActivity extends BaseActivity {
                     }
                 }, delayMs);
         }
+
     };
 
 
@@ -423,6 +445,176 @@ public class MainActivity extends BaseActivity {
         return value.replace("\\", "\\\\").replace("'", "\\'");
     }
 
+    private void startWebHealthWatchdog() {
+        if (webHealthHandler == null) {
+            webHealthHandler = new Handler(Looper.getMainLooper());
+        }
+        if (webHealthRunnable != null) {
+            webHealthHandler.removeCallbacks(webHealthRunnable);
+        }
+        webHealthRunnable = new Runnable() {
+            @Override
+            public void run() {
+                runWebHealthCheck();
+                if (webHealthHandler != null) {
+                    webHealthHandler.postDelayed(this, WEB_HEALTH_INTERVAL_MS);
+                }
+            }
+        };
+        webHealthHandler.postDelayed(webHealthRunnable, WEB_HEALTH_INTERVAL_MS);
+    }
+
+    private void stopWebHealthWatchdog() {
+        if (webHealthHandler != null) {
+            webHealthHandler.removeCallbacksAndMessages(null);
+        }
+        webHealthRunnable = null;
+    }
+
+    private String normalizeEvaluateResult(String value) {
+        if (value == null) {
+            return "";
+        }
+        String text = value.trim();
+        if (text.isEmpty() || "null".equals(text) || "undefined".equals(text)) {
+            return "";
+        }
+        if (text.length() >= 2 && text.startsWith("\"") && text.endsWith("\"")) {
+            try {
+                return new JSONArray("[" + text + "]").optString(0, "");
+            } catch (Exception e) {
+                Log.e("webhealth", "解析 JS 字符串结果失败: " + e.getMessage());
+                return "";
+            }
+        }
+        return text;
+    }
+
+    private void runWebHealthCheck() {
+        if (isFinishing() || isDestroyed() || webIDs == null) {
+            return;
+        }
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            runOnUiThread(this::runWebHealthCheck);
+            return;
+        }
+        final int querySeq = ++webHealthQuerySeq;
+        if (webHealthHandler == null) {
+            webHealthHandler = new Handler(Looper.getMainLooper());
+        }
+        webHealthHandler.postDelayed(() -> {
+            if (querySeq == webHealthQuerySeq) {
+                handleWebHealthFailure("health query timeout");
+            }
+        }, WEB_HEALTH_QUERY_TIMEOUT_MS);
+        try {
+            webIDs.evaluateJavascript(
+                "(function(){try{return window.__CALLAPP_HEALTH__?window.__CALLAPP_HEALTH__():null;}catch(e){return {error:String(e&&e.message||e)};}})()",
+                new ValueCallback<String>() {
+                    @Override
+                    public void onReceiveValue(String value) {
+                        if (querySeq != webHealthQuerySeq) {
+                            return;
+                        }
+                        webHealthQuerySeq++;
+                        handleWebHealthResult(value);
+                    }
+                }
+            );
+        } catch (Exception e) {
+            webHealthQuerySeq++;
+            handleWebHealthFailure("health query exception: " + e.getMessage());
+        }
+    }
+
+    private void handleWebHealthResult(String value) {
+        try {
+            String text = normalizeEvaluateResult(value);
+            if (text.isEmpty()) {
+                handleWebHealthFailure("health empty");
+                return;
+            }
+            JSONObject health = new JSONObject(text);
+            if (health.has("error")) {
+                handleWebHealthFailure("health js error: " + health.optString("error"));
+                return;
+            }
+            long now = System.currentTimeMillis();
+            long pageTickAt = health.optLong("pageTickAt", health.optLong("aliveAt", now));
+            boolean visibleLogin = health.optBoolean("visibleLogin", false);
+            if (!visibleLogin && pageTickAt > 0 && now - pageTickAt > PAGE_TICK_STALE_MS) {
+                handleWebHealthFailure("page tick stale");
+                return;
+            }
+            webHealthFailCount = 0;
+
+            boolean socketConnected = health.optBoolean("socketConnected", false);
+            long lastConnectAt = health.optLong("lastConnectAtMs", 0);
+            long lastPongAt = health.optLong("lastPongAtMs", 0);
+            long lastHeartbeatAt = health.optLong("lastHeartbeatAtMs", 0);
+            long serverAliveAt = Math.max(Math.max(lastPongAt, lastHeartbeatAt), lastConnectAt);
+            boolean serverHeartbeatStale = socketConnected && serverAliveAt > 0 && now - serverAliveAt > SOCKET_UNHEALTHY_MS;
+            if (!visibleLogin && (!socketConnected || serverHeartbeatStale)) {
+                if (socketUnhealthySinceAt == 0) {
+                    socketUnhealthySinceAt = now;
+                }
+                if (now - socketUnhealthySinceAt >= SOCKET_UNHEALTHY_MS) {
+                    requestFrontendSocketRecreate(serverHeartbeatStale ? "android watchdog heartbeat stale" : "android watchdog socket disconnected");
+                }
+            } else {
+                socketUnhealthySinceAt = 0;
+            }
+        } catch (Exception e) {
+            handleWebHealthFailure("health parse failed: " + e.getMessage());
+        }
+    }
+
+    private void handleWebHealthFailure(String reason) {
+        webHealthFailCount++;
+        Log.w("webhealth", "健康检查失败(" + webHealthFailCount + "): " + reason);
+        if (webHealthFailCount >= WEB_HEALTH_FAIL_RELOAD_COUNT) {
+            reloadWebViewSafely(reason);
+        }
+    }
+
+    private void requestFrontendSocketRecreate(String reason) {
+        long now = System.currentTimeMillis();
+        if (now - lastSocketWatchdogAt < SOCKET_WATCHDOG_COOLDOWN_MS) {
+            return;
+        }
+        lastSocketWatchdogAt = now;
+        if (webIDs == null) {
+            return;
+        }
+        String script = "window.__CALLAPP_FORCE_RECREATE_SOCKET__ && window.__CALLAPP_FORCE_RECREATE_SOCKET__('" + escapeJsArg(reason) + "')";
+        try {
+            webIDs.evaluateJavascript(script, null);
+            Log.w("webhealth", "已请求前端重建 Socket: " + reason);
+        } catch (Exception e) {
+            Log.e("webhealth", "请求前端重建 Socket 失败: " + e.getMessage());
+            handleWebHealthFailure("socket recreate call failed");
+        }
+    }
+
+    private void reloadWebViewSafely(String reason) {
+        long now = System.currentTimeMillis();
+        if (now - lastWebReloadAt < WEB_RELOAD_COOLDOWN_MS) {
+            Log.w("webhealth", "跳过 WebView reload,仍在冷却期: " + reason);
+            return;
+        }
+        lastWebReloadAt = now;
+        webHealthFailCount = 0;
+        socketUnhealthySinceAt = 0;
+        try {
+            if (webIDs != null) {
+                Log.e("webhealth", "WebView watchdog reload: " + reason);
+                webIDs.reload();
+            }
+        } catch (Exception e) {
+            Log.e("webhealth", "WebView reload 失败: " + e.getMessage());
+        }
+    }
+
     private void notifyJs(String method, String param, String playId) {
         // 检查Activity状态,避免销毁后调用
         if (isFinishing() || isDestroyed() || webIDs == null) {
@@ -620,6 +812,7 @@ public class MainActivity extends BaseActivity {
             audioHandler = null;
             audioTimeoutRunnable = null;
         }
+        stopWebHealthWatchdog();
 
         // 4. 释放WebView(避免泄漏)
         if (webIDs != null) {