Mobile Development 18 min read

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.

21CTO
21CTO
21CTO
Why Choose a Hybrid Framework? Architecture, WebView Loading, and Caching Explained

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;
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

nativeAndroidWebViewHybrid
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.