Why Choose a Hybrid Framework? Architecture, WebView Loading, and Caching Explained
This article explains the motivations behind adopting a hybrid framework for Android apps, outlines the overall architecture, details the WebView loading flow, jump protocol, resource interception, asynchronous image handling, cache version management, and interaction mechanisms, providing practical code examples.
Hybrid Framework Overview
Reasons for Using Hybrid Mode
Pure native development iterates slowly, cannot update dynamically, and lacks cross‑platform capability.
Pure web pages cannot implement some functions and have poor animation experience.
Overall Framework Structure
WebView Loading Process
Step 1 : Intercept HTML requests, whitelist domains, and forward special schemes such as tel:.
Only requests from allowed domains pass.
Forward phone‑call requests.
Step 2 : Show a loading indicator.
Step 3 : shouldInterceptRequest() (available from API 11). It cannot intercept resources on Android 2.x.
Used to cache native resources.
Step 4 : onPageFinished() is called after all resources are loaded, but the UI may already be rendered.
Loading dismissal :
When the HTML page finishes rendering, JavaScript calls a PageFinished action to hide the loading view early.
If the action is not received, hide the loading view in onPageFinished().
Jump Protocol
Current jump protocol is a JSON object:
{
"action":"loadpage",
"pagetype":"link",
"url":"http://xxxx",
"title":"标题",
"xxx":""
}The web page title is implemented natively, so it must be obtained from the jump protocol.
Recommended URL‑based protocol: jump://action/pagetype?url=xxx&title=xxx Benefit: unified external invocation.
HTML Interception Mechanism
Native caching is achieved by intercepting HTML requests in shouldInterceptRequest().
JS, CSS, Image Interception
Interception works the same way as HTML, using shouldInterceptRequest(). Two caching rules apply:
Standard method: append cachevers parameter to the URL, e.g., http://xxx/xxx?cachevers=xx.
CDN method: URLs following the CDN pattern, e.g., http://xxx/xxx_v版本号.xx. The CDN format is converted to the standard format inside shouldInterceptRequest().
Asynchronous Image Loading
Because shouldInterceptRequest() runs on a background thread, loading images synchronously would block rendering. The solution is to create a new thread and return a pipe immediately:
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); // one end for input, one for output
new TransferThread(context, uri, new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])).start();
AssetFileDescriptor afd = new AssetFileDescriptor(pipe[0], 0, AssetFileDescriptor.UNKNOWN_LENGTH);
FileInputStream in = afd.createInputStream();
return new WebResourceResponse(type, "utf-8", in);
}Cache Resource Version Management
Cache updates are driven by version numbers. The best practice is to store the version together with the cached file. The following ExtraDiskCache class demonstrates how to embed version information as a header in the file:
/**
* Created by maolei on 2015/9/8.
*/
public class ExtraDiskCache {
private static final String FUNCTION = "diskCache";
/** Magic number for current version of cache file format. */
private static final int CACHE_MAGIC = 0x20150908;
private static final String NO_VALUE = "null";
private final File mRootDirectory;
public ExtraDiskCache(File rootDirectory) {
mRootDirectory = rootDirectory;
if (!mRootDirectory.exists()) {
mRootDirectory.mkdirs();
}
}
private File getFile(String fileName) {
return new File(mRootDirectory, fileName);
}
public boolean save(String fileName, Map<String, String> extraInfo, InputStream in) {
BufferedOutputStream fos = null;
File tempFile = getFile(fileName + "_temp");
try {
fos = new BufferedOutputStream(new FileOutputStream(tempFile));
if (extraInfo != null && extraInfo.size() > 0) {
boolean success = writeHeader(fos, extraInfo);
if (!success) {
throw new IOException();
}
}
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
fos.write(buf, 0, len);
}
fos.flush();
File cacheFile = getFile(fileName);
if (cacheFile.exists()) {
cacheFile.delete();
}
tempFile.renameTo(cacheFile);
return true;
} catch (IOException e) {
LOGGER.k(FUNCTION, "write data error", e);
} finally {
try {
if (in != null) in.close();
if (fos != null) fos.close();
if (tempFile.exists()) tempFile.delete();
} catch (IOException e) {
LOGGER.k(FUNCTION, "close stream error", e);
}
}
return false;
}
public Map<String, String> getInfo(String fileName) {
BufferedInputStream bis = null;
try {
bis = new BufferedInputStream(new FileInputStream(getFile(fileName)));
return readHeader(bis);
} catch (IOException e) {
LOGGER.k(FUNCTION, "getInfo error", e);
} finally {
try { if (bis != null) bis.close(); } catch (IOException e) {}
}
return null;
}
public InputStream getContentStream(String fileName) {
try {
File file = getFile(fileName);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
if (readHeader(bis) != null) {
return bis; // file has extra info
}
bis.close();
return new BufferedInputStream(new FileInputStream(file));
} catch (IOException e) {
}
return null;
}
private Map<String, String> readHeader(InputStream in) {
try {
int magic = readInt(in);
if (magic != CACHE_MAGIC) {
throw new IOException();
}
return readStringStringMap(in);
} catch (IOException e) {}
return null;
}
private boolean writeHeader(OutputStream out, Map<String, String> extraInfo) {
try {
writeInt(out, CACHE_MAGIC);
writeStringStringMap(extraInfo, out);
return true;
} catch (IOException e) {
return false;
}
}
private static int read(InputStream is) throws IOException {
int b = is.read();
if (b == -1) throw new EOFException();
return b;
}
static void writeInt(OutputStream os, int n) throws IOException {
os.write((n >> 0) & 0xff);
os.write((n >> 8) & 0xff);
os.write((n >> 16) & 0xff);
os.write((n >> 24) & 0xff);
}
static int readInt(InputStream is) throws IOException {
int n = 0;
n |= (read(is) << 0);
n |= (read(is) << 8);
n |= (read(is) << 16);
n |= (read(is) << 24);
return n;
}
static void writeLong(OutputStream os, long n) throws IOException {
os.write((byte) (n >>> 0));
os.write((byte) (n >>> 8));
os.write((byte) (n >>> 16));
os.write((byte) (n >>> 24));
os.write((byte) (n >>> 32));
os.write((byte) (n >>> 40));
os.write((byte) (n >>> 48));
os.write((byte) (n >>> 56));
}
static long readLong(InputStream is) throws IOException {
long n = 0;
n |= ((read(is) & 0xFFL) << 0);
n |= ((read(is) & 0xFFL) << 8);
n |= ((read(is) & 0xFFL) << 16);
n |= ((read(is) & 0xFFL) << 24);
n |= ((read(is) & 0xFFL) << 32);
n |= ((read(is) & 0xFFL) << 40);
n |= ((read(is) & 0xFFL) << 48);
n |= ((read(is) & 0xFFL) << 56);
return n;
}
static void writeString(OutputStream os, String s) throws IOException {
byte[] b = s.getBytes("UTF-8");
writeLong(os, b.length);
os.write(b, 0, b.length);
}
static String readString(InputStream is) throws IOException {
int n = (int) readLong(is);
byte[] b = streamToBytes(is, n);
return new String(b, "UTF-8");
}
static void writeStringStringMap(Map<String, String> map, OutputStream os) throws IOException {
if (map == null || map.isEmpty()) return;
writeInt(os, map.size());
for (Map.Entry<String, String> e : map.entrySet()) {
writeString(os, e.getKey());
String v = e.getValue();
if (v == null || v.isEmpty()) {
writeString(os, NO_VALUE);
} else {
writeString(os, v);
}
}
}
static Map<String, String> readStringStringMap(InputStream is) throws IOException {
int size = readInt(is);
if (size <= 0) return null;
Map<String, String> result = new HashMap<>(size);
for (int i = 0; i < size; i++) {
String key = readString(is).intern();
String value = readString(is).intern();
if (NO_VALUE.equals(value)) value = "";
result.put(key, value);
}
return result;
}
private static byte[] streamToBytes(InputStream in, int length) throws IOException {
byte[] bytes = new byte[length];
int pos = 0, count;
while (pos < length && (count = in.read(bytes, pos, length - pos)) != -1) {
pos += count;
}
if (pos != length) throw new IOException("Expected " + length + " bytes, read " + pos + " bytes");
return bytes;
}
}Related Classes
WebResLoader – handles asynchronous and synchronous resource loading.
WebResCacheManager – manages resource saving, loading, and version control.
Interaction Framework
Two interaction methods are used:
addJavascriptInterface() : simple, supports return values from JS (available from API 1) but is insecure.
shouldInterceptRequest() : secure, available from API 11, but JS cannot receive return values.
Interaction protocol is JSON‑based, e.g.:
{
"action":"xxx",
"xxx":"xxx"
}WebView Host Pages
Multiple host pages (subclasses of MessageBaseFragment) cause maintenance overhead and incompatibility because each page supports different actions. The recommended approach is to consolidate functionality into a single host page and decouple features via wrappers.
Cookie and Header Handling
Loading a URL can be done with webview.loadUrl(String url) or with additional HTTP headers using
webview.loadUrl(String url, Map<String, String> additionalHttpHeaders)(available from Android 2.2). Data can be passed via cookies or headers; headers are more reliable, but JavaScript‑initiated requests cannot include custom headers, so cookies are still needed.
Whitelist
Requests not in the whitelist are blocked or trigger a dialog. Implementation steps:
Maintain a local whitelist of domain names that can be updated.
In shouldOverrideUrlLoading(), check the request against the whitelist, considering sub‑domains.
Additional WebView Features
Image upload (via gallery or camera) – can be delegated to native actions.
File download – redirect download URLs to the system browser.
Phone calls and other system component invocations – handled through native actions.
Example of handling generic URLs in shouldOverrideUrlLoading():
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
if (url.startsWith("http:") || url.startsWith("https:") || url.startsWith("file:")) {
// HTML request handling
}
// Other generic handling
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
} catch (Exception e) {
LOGGER.e(TAG, null, e);
}
return false;
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
21CTO
21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
