Android WebView 加載 H5 頁面,CSS 背景圖、大圖眾多,總緩存需求 ~500MB。原生瀏覽器緩存機制 (AppCache/IndexedDB/LocalStorage) 存在配額限制、異步 API 复雜、Android 兼容性差等問題。本文分享「File API 緩存 + 本地 HTTP 服務」組合拳,繞過 WebView 限制,實現圖片毫秒級二次加載。
| 機制 | 容量 | API | Android WebView 支持 | 致命缺點 |
|---|---|---|---|---|
| 瀏覽器緩存 (HTTP Cache) | 無上限 | Header 控制 | 支持 | 不可控、無法預加載、清理策略不可控 |
| LocalStorage | 5MB | 同步 KV | 支持 | 容量極小、同步阻塞主線程、只能存字符串 |
| SessionStorage | 5MB | 同步 KV | 支持 | 同 LocalStorage,頁面關閉失效 |
| IndexedDB | 幾百 MB | 異步 NoSQL | 部分版本 Bug 多 | API 復雜、事務模型難調試、遷移麻煩 |
| AppCache (已廢棄) | 幾十 MB | Manifest 靜態 | 支持 | 標準廢棄、更新機制極其反直覺 |
| File System API | 理論無限 | 異步文件操作 | 默認不支持 | 需原生橋接,但最接近原生文件體驗 |

┌─────────────────────────────────────────────────────────────────┐
│ H5 頁面 (JS) │
│ 1. 檢查文件是否存在 → 2. 不存在 → 呼叫 JsBridge 下載 │
│ 3. 存在 → 返回 file:// 路徑 → 4. 無法直接顯示 → 5. 請求本地 HTTP │
└─────────────────────────────────────────────────────────────────┘
│ JsBridge (addJavascriptInterface)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Android 原生層 │
│ ImgDownloader.save2localHighPerf(url) → 返回本地文件絕對路徑 │
│ LocalHttpServer (127.0.0.1:7788) 服務文件流 │
└─────────────────────────────────────────────────────────────────┘
@JavascriptInterface 暴露下載接口,JS 調用原生下載。file:// 協議 WebView 禁止跨域/顯示;data URI Base64 解碼極慢閃屏。**本地 HTTP Server (OkHttp/NanoHTTPD)** 服務文件流,完美支持 `public class ImgDownloader {
public String save2localHighPerf(String url, String localRoot, String urlHostPart) {
// 1. 解析相對路徑
String relPath = PathUtil.getPathNoHostNoAppRoot(url, urlHostPart);
String sdRoot = PathUtil.getInnerSDCardPath(); // /storage/emulated/0
String absPath = sdRoot + localRoot + "/" + relPath;
File file = new File(absPath);
if (file.exists()) return absPath; // 命中緩存
// 2. 創建目錄
PathUtil.createAllPath(absPath);
// 3. 下載寫入 (流式,不佔記憶體)
try {
url = UrlX.encodeURI(url);
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(7000);
conn.setRequestMethod("GET");
if (conn.getResponseCode() != 200)
throw new RuntimeException("下載失敗: " + url);
try (InputStream in = conn.getInputStream();
FileOutputStream out = new FileOutputStream(file)) {
StreamUtil.copy(in, out); // 8KB buffer 循環寫入
}
return absPath;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class JsBridge {
@JavascriptInterface
public String invoke4(String method, String p2, String p3, String p4) {
return invoke(method, p2, p3, p4);
}
@JavascriptInterface
public String invoke(String method, Object... args) {
try {
String className = RefUtil.getClassName(method); // "com.xxx.ImgDownloader"
String methodName = RefUtil.getMethodName(method); // "save2localHighPerf"
Object instance = Class.forName(className).getDeclaredConstructor().newInstance();
return (String) MethodUtils.invokeMethod(instance, methodName, args);
} catch (Exception e) {
return ExUtil.getTrace(e); // 返回錯誤堆棧給 JS
}
}
}
// WebView 註冊
webView.addJavascriptInterface(new JsBridge(), "NativeBridge");
public class LocalHttpServer {
public static void main(String[] args) {
HTTPServer server = new HTTPServer();
server.open("127.0.0.1", 7788);
server.addRequestListener(new HTTPRequestListener() {
@Override
public void onRequest(HTTPRequest request) {
String filePath = request.getParameterValue("file");
File file = new File(filePath);
if (!file.exists() || file.length() <= 0) {
request.returnBadRequest(); return;
}
HTTPResponse response = new HTTPResponse();
response.setStatus(HTTPStatus.OK);
response.setContentType(FileUtil.getMimeType(filePath));
response.setContentLength(file.length());
response.setContentInputStream(new FileInputStream(file));
request.respond(response); // 發送文件流
}
});
server.start();
}
}
// JS 邏輯
async function loadImage(url) {
// 1. 呼叫原生下載/獲取緩存路徑
const localPath = await NativeBridge.invoke4(
'com.xxx.ImgDownloader.save2localHighPerf', url, '/cache/img', 'https://cdn.xxx.com'
);
// 2. 請求本地 HTTP 服務獲取圖片流
return `https://127.0.0.1:7788/file?file=${encodeURIComponent(localPath)}`;
}
// CSS 使用
element.style.backgroundImage = `url(${await loadImage(imgUrl)})`;
| 指標 | 原生 HTTP Cache | IndexedDB | 本方案 |
|---|---|---|---|
| 容量上限 | 不受控 | ~50-200MB | 磁盤空間 (GB 級) |
| 二次加載速度 | 需校驗/協商 | 異步讀取 + Blob 轉換 | 本地 HTTP 直連,毫秒級 |
| 預加載能力 | 無 | 可編程但複雜 | 原生後台下載,不阻塞 UI |
| 清理策略 | 瀏覽器決定 | 手動管理 | 原生定時掃描 LRU 刪除 |
| Base64 閃屏 | 無 | 無 | 完全避免 |
getExternalFilesDir() / MediaStore,避免直接訪問公共目錄需權限。setMixedContentMode(MIXED_CONTENT_ALWAYS_ALLOW) (API 21+)。setContentInputStream 避免全量讀入內存 OOM。@JavascriptInterface 方法必須捕獲所有異常,返回錯誤信息字符串,否則 JS 端無感知崩潰。H5/WebView 圖片性能優化:**File API 緩存解決容量/持久化,JsBridge 解決原生下載能力,本地 HTTP Server 解決 file:// 協議受限與 Base64 閃屏**。三者組合拳,將 500MB 圖片資源緩存落地,二次加載從秒級降至毫秒級,完美適配 Android 10+ 存儲新政。核心代碼 <100 行,接入成本極低,強烈推薦同類場景採用。