一、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"
,通过切割就能拿到文件名,但是有些网站不支持。
mimetype
:application/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
请求,看看返回的请求头中是否有文件名。举例查看下面的截图:
2、blob协议
目前webview不支持blob协议的文件下载,所以需要采用其他方案,其他方案就是注入JS的代码来实现。这里先考虑拿文件名的问题。目前测试的网站样本有限,但是大致发现WebView在拦截到blob的下载链接前会拿到一条常规的下载链接,如下:
这个链接一般是可以直接下载的,可以直接用,但我们要先获取真实的文件名,我们可以先对该链接发起一次
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);
}
}