Android WebView的http和blob协议的下载

2,058 阅读4分钟

一、WebView是否需要独立进程?

如果浏览的是大型网站,内容很多,占用App的内存很大,建议WebView使用单独的进程,因为如果WebView占用内存过大崩溃不会导致整个App奔溃,但是单独的进程坑很多,因为进程间内存是隔离的很多主进程的东西拿不到,主要的坑有:①多进程Application会多次初始化;②无法获取共享的静态变量(内存隔离);③很多主进程中的东西使用起来都很不方便,可能拿不到,也可能没初始化;④跨进程通讯;⑤其他。

可以参考一下这篇博客:WebView独立进程方案

二、WebView的下载触发

Android中WebView的下载有个自带的下载监听器,接口如下:

package android.webkit;

public interface DownloadListener {

    /**
     * Notify the host application that a file should be downloaded
     * @param url The full url to the content that should be downloaded
     * @param userAgent the user agent to be used for the download.
     * @param contentDisposition Content-disposition http header, if 
     *                           present.
     * @param mimetype The mimetype of the content reported by the server
     * @param contentLength The file size reported by the server
     */
    public void onDownloadStart(String url, String userAgent,
            String contentDisposition, String mimetype, long contentLength);

}

只需要实现这个接口就可以拿到网页上触发的下载动作。这个接口的几个参数需要说明一下:

url: 下载的链接,有blob协议的,有http协议的,也有网页内嵌数据(图片、txt等)data打头的。

举例:blob协议链接:blob:https://www.thingiverse.com/6ccf104b-0a7e-4499-9372-06d0704d855c http协议链接:https://cdn.thingiverse.com/tv-zip/5474093 data内嵌数据链接:略

userAgent:用户代理,属于头的部分,举例一个打印的结果:Mozilla/5.0 (Linux; Android 12; SM-G9910 Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/99.0.4844.88 Mobile Safari/537.36

contentDisposition:内容部署,这个字段很重要,要下载的文件名很多时候会放到这里,举例一个打印的结果:attachment;filename="FBX.zip",通过切割就能拿到文件名,但是有些网站不支持。

mimetypeapplication/octet-stream

contentLength:要下载的文件的字节大小

三、获取下载的文件名

当无法获取文件的扩展名时下载动作就没有意义,那么不同的下载协议应该如何获取下载的文件名?

1、http协议

方式一:首先查看onDownloadStart接口返回的contentDisposition字段是否含有文件名。

方式二:下载链接是否含有文件名,比如下面这个链接:https://object-assets.cdn3.myminifactory.com/604df292c11b7/threedfiles/cats-on-the-fence.stl?response-content-disposition=attachment%3B%20filename%3Dcats-on-the-fence-stl%3B%20filename%2A%3Dutf-8%27%27cats-on-the-fence.stl

把它decode一下,得到如下的链接: https://object-assets.cdn3.myminifactory.com/604df292c11b7/threedfiles/cats-on-the-fence.stl?response-content-disposition=attachment; filename=cats-on-the-fence-stl; filename*=utf-8''cats-on-the-fence.stl

可以很清晰的看到文件名为:cats-on-the-fence.stl

方式三:将下载链接发起一次head请求,看看返回的请求头中是否有文件名。举例查看下面的截图:

image.png

image.png

2、blob协议

目前webview不支持blob协议的文件下载,所以需要采用其他方案,其他方案就是注入JS的代码来实现。这里先考虑拿文件名的问题。目前测试的网站样本有限,但是大致发现WebView在拦截到blob的下载链接前会拿到一条常规的下载链接,如下:

image.png 这个链接一般是可以直接下载的,可以直接用,但我们要先获取真实的文件名,我们可以先对该链接发起一次head请求,同上面http协议的方式三,一般可以拿到文件名,要是拿不到可以尝试下载链接的切割同http协议的方式二。

四、WebView下载文件

1、http协议下载,可以直接使用系统自带的DownloadManager,简单方便

val request = DownloadManager.Request(uri)
    .setTitle(extensionFileName)
    .setMimeType(mimeType)
    .setDescription(host)
    .setDestinationInExternalPublicDir(
        Environment.DIRECTORY_DOWNLOADS,
        extensionFileName
    )
val downloadsDir =
    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
if (!downloadsDir.exists()) {
    downloadsDir.mkdirs()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
} else {
    @Suppress("DEPRECATION")
    request.setShowRunningNotification(true)
}
request.setVisibleInDownloadsUi(true)
val downloadId = dm.enqueue(request)

2、blob下载

如果onDownloadStart里面的url是blob开头的,是不能使用DownloadManager的,会直接报错。下载方式大致如下:

public class DownloadBlobFileJSInterface {

    private Context mContext;
    private static String mFileName;
    private DownloadFileSuccessListener mDownloadFileSuccessListener;

    public DownloadBlobFileJSInterface(Context context) {
        this.mContext = context;
    }

    public void setDownloadFileSuccessListener(DownloadFileSuccessListener listener) {
        mDownloadFileSuccessListener = listener;
    }

    @RequiresApi(api = Build.VERSION_CODES.N)
    @JavascriptInterface
    public void getBase64FromBlobData(String base64Data)  {
        convertToFileAndProcess(base64Data);
    }

    public static String getBase64StringFromBlobUrl(String blobUrl,String fileName) {
        mFileName = fileName;
        if (blobUrl.startsWith("blob")) {
            return "javascript: var xhr = new XMLHttpRequest();" +
                    "xhr.open('GET', '" + blobUrl + "', true);" +
                    "xhr.setRequestHeader('Content-type','image/gif');" +
                    "xhr.responseType = 'blob';" +
                    "xhr.onload = function(e) {" +
                    "    if (this.status == 200) {" +
                    "        var blobFile = this.response;" +
                    "        var reader = new FileReader();" +
                    "        reader.readAsDataURL(blobFile);" +
                    "        reader.onloadend = function() {" +
                    "            base64data = reader.result;" +
                    "            Android.getBase64FromBlobData(base64data);" +
                    "        }" +
                    "    }" +
                    "};" +
                    "xhr.send();";
        }
        return "javascript: console.log('It is not a Blob URL');";
    }

    @RequiresApi(api = Build.VERSION_CODES.N)
    private void convertToFileAndProcess(String base64) {
        File stlFile = new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_DOWNLOADS) + mFileName);
        saveFileToPath(base64, stlFile);
        Toast.makeText(mContext, R.string.save_image_success, Toast.LENGTH_SHORT).show();
        if (mDownloadFileSuccessListener != null) {
            mDownloadFileSuccessListener.downloadFileSuccess(stlFile.getAbsolutePath());
        }
    }

    private void saveFileToPath(String base64, File filePath) {
        try {
            byte[] fileBytes = Base64.decode(base64.replaceFirst(
                    "data:application/octet-stream;base64,", ""), 0);
            FileOutputStream os = new FileOutputStream(filePath, false);
            os.write(fileBytes);
            os.flush();
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public interface DownloadFileSuccessListener {
        void downloadFileSuccess(String absolutePath);
    }
}

参考了以下博客:

WebView独立进程方案

Android应用内多进程分析和研究

Android—在WebView中下载Blob协议文件

DownLoadManager的下载及进度监听