WebView 图片离线缓存(含图片)

自打去年十一来到掘金,就想着有一点一定会做 WebView 离线缓存,作为一个阅读类 app,不敢想象在没有离线缓存的情况下是怎么撑了这么久的😂。在下个版本中就有大家喜闻乐见的文章离线缓存了。

本文只授权掘金。。。

我还给掘金想了个 slogan : 用掘金的人,技术一定不会太差。

System.out.println("Hello World !");

1

WebView 内容的缓存实际上是把网页的 html、css、js 在本地组合并使用 WebView 的 loadDataWithBaseURL()) 方法来显示缓存的内容

至于图片的缓存,则需要先将图片下载到本地,然后根据 <img> 标签匹配替换 src,最后达到的效果是 WebView 中所有的内容都是本地内容。

2

以知乎日报为例,使用接口获取的 html 内容并未提供网页中图片的 url ,需要使用 JSoup 进行爬取。cundong/ZhihuPaper 当然如过后台可以提供当前文章的图片 url 列表那是再好不过了。

这里我们使用了 RxJava 来同时缓存文章和文章中的图片。(感谢 给 Android 开发者的 RxJava 详解)

public class WebHtml {
    private List<String> imgUrls;
    private String htmlContent;
    //get & set ...
}

首先通过接口获取 WebHtml 数组:

WebHtml[] webhtmls = ...;
Subscriber<String> subscriber = new Subscriber<String>() {
    @Override
    public void onNext(String imgUrl) {
         //TODO 下载图片
        Log.d(tag, "img url : " + imgUrl);
    }
    ...
};
Observable.from(webhtmls)
    .flatMap(new Func1<WebHtml, Observable<String>>() {
        @Override
        public Observable<String> call(WebHtml webHtml) {
            return Observable.from(webHtml.getImgUrls());
        }
    })
    .subscribe(subscriber);

3.显示本地缓存的 html 内容

  • template.html
  • img-replace.js
  • img-header.css

最基本的 html 需要有 <html></html> 标签,<head></head>标签放置 cssjs 文件的引用,<body></body>标签包裹 html 文本。

因为我们手机端需要显示的网页比较简单,有了这三项就可以完成页面完整的展示。需要注意的是

的 id 或者 class 一定要跟 css 的对应,否则找不到对应的 id 则不能得到想要实现的样式。

template.html:

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,user-scalable=no">
    <link href="file:///android_asset/www/header-img.css" type="text/css" rel="stylesheet">
    {juejin-css-js}
    <script src="file:///android_asset/www/img_replace.js"></script>
</head>
<body>
    <div class="container entry-view">
        {juejin-header}
        <div class="entry-content">
            {juejin-content}
        </div>
    </div>
</body>
</html>

细心的同学可能会注意到 html 中有一些比较奇怪的东西:

  • {juejin-css-js}
  • {juejin-header}
  • {juejin-content}

这三个是作为替换 css、js 或者 html 的 key.

本地拼接

详情页图片的替换

替换时机

在 WebView 的 onPageFinished() 方法中将下载的图片的本地路径与 html 中 标签下的 src 替换,WebView 则显示本地图片。

如何替换

上面的 html 代码中有一段 <script src="file:///android_asset/www/img_replace.js"></script>,这里就是我们客户端手动添加的替换图片的 js 代码

Before:

<img src="http://ac-mhke0kuv.clouddn.com/46126f3b0e51085f5fc1.jpg">

After:

<img src="/storage/emulated/0/Android/data/packageName/cache/xxxxxx.jpg">

在 assets/www 目录下 本地创建一个 image_replace.js 来实现详情页图片的替换(网络 url 替换为已下载完成的图片的本地路径),js 代码如下:

function img_replace_by_url(url, localPath) {
    var objs = document.getElementsByTagName("img");
    for(var i=0;i<objs.length;i++) {
        var imgUrl = objs[i].getAttribute("src");
        if (imgUrl == url) {
            objs[i].setAttribute("src", localPath);
        }
    }
}

使用方法也比较简单:

private void replaceImgUrl(String imgUrl, String localPath) {
        String javascript;
        if (headerImageUrl != null && headerImageUrl.equals(imgUrl)) {
            return;
        } else {
            javascript = "img_replace_by_url('" + imgUrl + "', '" + localPath + "')";
        }
        //替换 html 中的 img 图片为本地资源
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            mWebView.evaluateJavascript(javascript, new ValueCallback<String>(){
                @Override
                public void onReceiveValue(String s) {
                    JsonReader reader = new JsonReader(new StringReader(s));
                    // Must set lenient to parse single values
                    reader.setLenient(true);
                    try {
                        if(reader.peek() != JsonToken.NULL) {
                            if(reader.peek() == JsonToken.STRING) {
                                String msg = reader.nextString();
                                if(msg != null) {
                                    Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();
                                }
                            }
                        }
                    } catch (IOException e) {
                        Log.e("TAG", "MainActivity: IOException", e);
                    } finally {
                        try {
                            reader.close();
                        } catch (IOException e) {
                            // NOOP
                        }
                    }
                }
            });
        } else {
            mWebView.loadUrl("javascript:" + javascript);
        }
    }

关于 evaluateJavascript:从 Android 4.4 起,WebView 使用 Chromium 内核,evaluateJavascript 方法可以异步获取 js 的回调。

更多关于 evaluateJavascript: How does evaluateJavascript work?

关于 loadDataWithBaseURL

loadDataWithBaseURL (String baseUrl, String data, String mimeType, String encoding, String historyUrl) 这里的 `baseUrl` 就是一个标志位,用来标志当前页面的Key值的,而historyUrl就是一个value值,在加载时,它会把baseUrl和historyUrl传到List列表中,当作历史记录来使用,当前进和后退时,它会通过baseUrl来寻找historyUrl的路径来加载historyUrl路径来加载历史界面

详见:loadData与loadDataWithBaseURL的区别

这里有个问题,就是在使用 loadUrl() 方法加载网页时,用户点击返回按钮,我们可以根据 WebView.canGoBack() 来判断当前 WebView 的浏览历史用是否有其他记录

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_BACK:
                if (mWebView.canGoBack()) {
                    mWebView.goBack();
                } else {
                    finish();
                }
                return true;
        }
    }
    return super.onKeyDown(keyCode, event);
}

比如我们使用 WebView 打开 a 页面,在 a 页面中点看链接进入到 b 页面,那么我们在 b 页面点击返回按钮时,WebView.canGoBack() 会返回一个 true ,说明 WebView 的浏览历史中有其他页面,然后调用 WebView.goBack() 时系统会调用 WebView.loadUrl() 方法回到 a 页面。

这里有个问题就是,当我们使用 loadDataWithBaseURL() 方法传入 baseUrl 时,在上个例子中的 a 页面的路径即为我们传入的 baseUrl ,但是本地并没有一个 html 文件对应文件,所以当我们调用 WebView.goBack() 加载这个 url 时,会提示找不到当前的 baseUrl。

我们目前使用的解决方案是自己维护一个 WebView 加载 url 的列表,当列表中的 url 数量为 1 时,则手动调用 loadDataWithBaseURL() 来加载内容。

最后

自动清理缓存
加载速度优化:Android webview slow(关于加载速度的优化,我们在网上找了好多解决方案,效果都不理想,最后发现是因为我们的 js 文件太大…)

Dependencies

RxJava
OkHttp

Thanks to

新奇日报:cundong/ZhihuPaper
扔物线:给 Android 开发者的 RxJava 详解