Android-WebView的优化与常见问题
其实关于Android的WebView大家使用起来应该都是有过封装,网上林林总总的分析与封装也不少。
我知道只要讲 WebView 一定有同学会说,原生WebView垃圾,我们都用的是腾讯X5 WebView 之类的。但是我们研发的是海外项目,只能使用原生的WebView,所以这里不涉及到TBS服务相关的点。
每一个人的封装可能都不一样,看我抛砖引玉,希望大家可以互相交流学习。
一、自定义WebView
我们需要一个统一管理的WebView,那么我们需要继承WebView,并内部对一些属性开启,对JS的支持,对加载过程与状态的监听,对文件操作的回调等。
public class MyWebView extends WebView {
private WebSettings mWebSettings;
private boolean isNeedExe = true;
public MyWebView(Context context) {
super(context);
initView();
}
public MyWebView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public MyWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
@SuppressLint({"ObsoleteSdkInt", "SetJavaScriptEnabled"})
private void initView() {
mWebSettings = getSettings();
mWebSettings.setSupportZoom(false);
mWebSettings.setBuiltInZoomControls(false);
mWebSettings.setDefaultTextEncodingName("utf-8");
mWebSettings.setJavaScriptEnabled(true);
mWebSettings.setDefaultFontSize(16);
mWebSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
mWebSettings.setGeolocationEnabled(true); //允许访问地址
//允许访问多媒体
mWebSettings.setAllowFileAccess(true);
mWebSettings.setAllowFileAccessFromFileURLs(true);
mWebSettings.setAllowUniversalAccessFromFileURLs(true);
setVerticalScrollBarEnabled(false);
setVerticalScrollbarOverlay(false);
setHorizontalScrollBarEnabled(false);
setHorizontalScrollbarOverlay(false);
setOverScrollMode(OVER_SCROLL_NEVER);
setFocusable(true);
setHorizontalScrollBarEnabled(false);
setDrawingCacheEnabled(true);
//加载https的兼容
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//两者都可以
mWebSettings.setMixedContentMode(mWebSettings.getMixedContentMode());
//mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
//先加载页面再加载图片,这里先禁止图片加载
if (Build.VERSION.SDK_INT >= 19) {
mWebSettings.setLoadsImagesAutomatically(true);
} else {
mWebSettings.setLoadsImagesAutomatically(false);
}
setWebViewClient(mWebViewClient);
setWebChromeClient(mWebChromeClient);
}
WebViewClient mWebViewClient = new WebViewClient() {
//https ssl证书问题,如果没有https的问题可以注释掉
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
// 接受所有网站的证书,Google不通过
//使用下面的兼容写法
final SslErrorHandler mHandler;
mHandler= handler;
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setMessage("SSL validation failed");
builder.setPositiveButton("Continue", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mHandler.proceed();
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mHandler.cancel();
}
});
builder.setOnKeyListener(new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
mHandler.cancel();
dialog.dismiss();
return true;
}
return false;
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
//页面加载完成,展示图片
@Override
public void onPageFinished(WebView view, String url) {
if (!mWebSettings.getLoadsImagesAutomatically()) {
mWebSettings.setLoadsImagesAutomatically(true);
}
}
//在当前的webview中跳转到新的url
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (mListener != null) mListener.onInnerLinkChecked();
if (Build.VERSION.SDK_INT < 26) {
if (!TextUtils.isEmpty(url)) {
view.loadUrl(url);
}
return true;
}
return false;
}
//WebView加载错误的回调
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
if (mListener != null) mListener.onWebLoadError();
}
//拦截WebView中的网络请求
@Nullable
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return super.shouldInterceptRequest(view, request);
}
};
WebChromeClient mWebChromeClient = new WebChromeClient() {
//获取html的title标签
@Override
public void onReceivedTitle(WebView view, String title) {
if (mListener != null) mListener.titleChange(title);
super.onReceivedTitle(view, title);
}
//获取页面加载的进度
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (mListener != null) mListener.progressChange(newProgress);
super.onProgressChanged(view, newProgress);
if (newProgress > 95 && isNeedExe) {
isNeedExe = !isNeedExe;
if (newProgress == 100) {
//注入js代码测量webview高度
loadUrl("javascript:App.resize(document.body.getBoundingClientRect().height)");
}
}
}
// 指定源的网页内容在没有设置权限状态下尝试使用地理位置API。
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
boolean allow = true; // 是否允许origin使用定位API
boolean retain = false; // 内核是否记住这次制授权
callback.invoke(origin, true, false);
}
// 之前调用 onGeolocationPermissionsShowPrompt() 申请的授权被取消时,隐藏相关的UI。
@Override
public void onGeolocationPermissionsHidePrompt() {
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
//启动系统相册
YYLogUtils.w("网页尝试调取Android相机相册");
CommUtils.getHandler().post(() -> {
if (mFilesListener != null) mFilesListener.onWebFileSelect(filePathCallback);
});
return true;
}
};
//网页状态的回调相关处理
private OnWebChangeListener mListener;
public interface OnWebChangeListener {
void titleChange(String title);
void progressChange(int progress);
void onInnerLinkChecked();
void onWebLoadError();
}
public void setOnWebChangeListener(OnWebChangeListener listener) {
mListener = listener;
}
//网页选择图片文件的回调相关处理
private OnWebChooseFileListener mFilesListener;
public interface OnWebChooseFileListener {
void onWebFileSelect(ValueCallback<Uri[]> callback);
}
public void setOnWebChooseFileListener(OnWebChooseFileListener listener) {
mFilesListener = listener;
}
/**
* 暴露方法,是否滑动到底部
*/
public boolean isScrollBottom() {
if (getContentHeight() * getScale() == (getHeight() + getScrollY())) {
//说明已经到底了
return true;
} else {
return false;
}
}
}
复制代码
都是比较基础的代码,涉及到属性的开启,与监听和回调大家应该都能看懂,下面就是看如何使用了。
private fun initWeb() {
val params = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
mWebView = MyWebView(applicationContext)
mWebView.layoutParams = params
mWebView.setOnWebChangeListener(object : MyWebView.OnWebChangeListener {
override fun titleChange(title: String) {
if (CheckUtil.isEmpty(mWebtitle)) {
easy_title.setTitle(mWebtitle)
}
}
override fun progressChange(progress: Int) {
var newProgress = progress
if (newProgress == 100) {
pb_web_view.setProgress(100)
CommUtils.getHandler()
.postDelayed({ pb_web_view.visibility = View.GONE }, 200)//0.2秒后隐藏进度条
} else if (pb_web_view.visibility == View.GONE) {
pb_web_view.visibility = View.VISIBLE
}
//设置初始进度10,这样会显得效果真一点,总不能从1开始吧
if (newProgress < 10) {
newProgress = 10
}
//不断更新进度
pb_web_view.setProgress(newProgress)
}
override fun onInnerLinkChecked() {
}
override fun onWebLoadError() {
toast("Load Error")
}
})
if (!TextUtils.isEmpty(mWeburl))
mWebView.loadUrl(mWeburl!!)
fl_content.addView(mWebView)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK && mWebView!!.canGoBack()) {
mWebView!!.goBack()
return true
}
return super.onKeyDown(keyCode, event)
}
override fun onPause() {
super.onPause()
mWebView?.onPause()
}
override fun onResume() {
super.onResume()
mWebView?.onResume()
}
override fun onDestroy() {
super.onDestroy()
if (mWebView != null) {
mWebView?.clearCache(true) //清空缓存
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
fl_content.removeView(mWebView)
mWebView?.removeAllViews()
mWebView?.destroy()
} else {
mWebView?.removeAllViews()
mWebView?.destroy()
fl_content.removeView(mWebView)
}
mWebView = null
}
}
复制代码
大家大致上应该都是这么使用了,为了优化内存我们手动创建WebView,初始化并lodUrl之后,我们加入到容器中,在销毁的时候我们销毁WebView,并移除掉。
其实老玩家都知道,就算如此还是会有内存泄露与开销的,那么大家使用多进程的方案,让WebView运行在一个单独的进程中,不影响当前进程的内存。
大家可以试试如果是使用这种方式,那么每次退出Web页面,在进入Web,再退出,是可以看到内存是慢慢在涨的。大概一次能涨个2M左右。
二、WebView的缓存
其实我们就可以换一个思路,如果说WebView的销毁会内存泄露,那么我们不销毁不就行了吗?我们把WebView缓存起来。每次使用的时候去缓存里面拿,然后销毁的时候回收,这样不就不会内存泄露了吗?
网上找的一个WebViewCacheManager:
/**
* WebView的缓存容器
* obtail获取对象
* recycle回收对象
*/
object WebViewManager {
private val webViewCache: MutableList<MyWebView> = ArrayList(1)
private fun create(context: Context): MyWebView {
return MyWebView(context)
}
/**
* 初始化
*/
@JvmStatic
fun prepare(context: Context) {
if (webViewCache.isEmpty()) {
Looper.myQueue().addIdleHandler {
webViewCache.add(create(MutableContextWrapper(context)))
false
}
}
}
/**
* 获取WebView
*/
@JvmStatic
fun obtain(context: Context): MyWebView {
if (webViewCache.isEmpty()) {
webViewCache.add(create(MutableContextWrapper(context)))
}
val webView = webViewCache.removeFirst()
val contextWrapper = webView.context as MutableContextWrapper
contextWrapper.baseContext = context
webView.clearHistory()
webView.resumeTimers()
return webView
}
/**
* 回收资源
*/
@JvmStatic
fun recycle(webView: MyWebView) {
try {
webView.stopLoading()
webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
webView.clearHistory()
webView.pauseTimers()
webView.clearFormData()
webView.removeJavascriptInterface("webkit")
val parent = webView.parent
if (parent != null) {
(parent as ViewGroup).removeView(webView)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
if (!webViewCache.contains(webView)) {
webViewCache.add(webView)
}
}
}
/**
* 销毁资源
*/
@JvmStatic
fun destroy() {
try {
webViewCache.forEach {
it.removeAllViews()
it.destroy()
webViewCache.remove(it)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
复制代码
网上很多的这种管理类,原理都大致差不多,这样管理了WebView之后还有一个好处是可以优化启动速度,无需每次New一个WebView然后初始化内核之类的耗时了。
使用之前我们需要初始化
open class BaseApplication : Application() {
override fun onCreate() {
super.onCreate()
//空闲的时候初始化WebView容器
Looper.myQueue().addIdleHandler {
//初始化WebView缓存容器
WebViewManager.prepare(this)
false
}
}
}
复制代码
初始化完成之后,如果要使用工具类,我们这样修改WebView的使用:
private fun initWeb() {
val params = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
mWebView = WebViewManager.obtain(this) //管理类获取对象
mWebView.layoutParams = params
mWeburl?.let { mWebView.loadUrl(it) }
mWebView.addJavascriptInterface(H5CallBackAndroid(), "webkit")
mBinding.flContent.addView(mWebView)
}
override fun onPause() {
super.onPause()
mWebView.onPause()
}
override fun onResume() {
super.onResume()
mWebView.onResume()
}
override fun onDestroy() {
super.onDestroy()
WebViewManager.recycle(mWebView)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK && mWebView!!.canGoBack()) {
mWebView!!.goBack()
return true
}
return super.onKeyDown(keyCode, event)
}
复制代码
可以看到我们只是修改了WebView的创建于销毁,这么做的好处是当销毁的时候不会泄露内存了。
例如我跳转Web之前的页面-占内存为160左右
跳转到一个Web,内存飙升至190左右
返回之前的页面-占用内存依旧是160左右
如果大家有兴趣,也可以自行测试,如果每次New WebView 再 destory () 那么内存是慢慢上涨的,如果使用WebView缓存之后内存并不会上涨。
三、WebView的返回问题
但是这么做有一个很大的坑,就是每次销毁的时候它的Url并没有清除,我们又不能使用webView的destory方法,那么我们第一个启动Web并返回是正常的,第二次再启动再返回,此时使用的是缓存WebView,是无法一次返回的。
因为之前的WebView已经有一个Url了,因为加载的网页可能是任意网址,我们无法判断,那么我们在回收的方法中手动的设置了指定的url
webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
那么这样的效果还是有问题,之前我们还需要按2次返回键才能返回Web页面,而现在我们加载了一个空视图之后,现在在Web的栈顶,按一次返回键会返回一个空白的页面,再按返回才能返回,还是需要二次返回。
解决办法是,我们在返回的时候判断一下,上一个url是不是空白的不就行了吗?
我们通过 copyBackForwardList
可以拿到WebView的全部栈顶,和当前的栈索引,我们加上一点判断,就可以正常的返回了。
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
val webBackForwardList = mWebView.copyBackForwardList()
val historyOneOriginalUrl = webBackForwardList.getItemAtIndex(0)?.originalUrl
val curIndex = webBackForwardList.currentIndex
return if (keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) {
//判断是否是缓存的WebView
if (historyOneOriginalUrl?.contains("data:text/html;charset=utf-8") == true) {
//说明是缓存复用的的WebView
if (curIndex > 1) {
//内部跳转到另外的页面了,可以返回的
mWebView.goBack()
true
} else {
//等于1的时候就要Finish页面了
super.onKeyDown(keyCode, event)
}
} else {
//如果不是缓存复用的WebView,可以直接返回
mWebView.goBack()
true
}
} else {
super.onKeyDown(keyCode, event)
}
}
复制代码
配合返回的完善,缓存的WebView是实战中的一大利器,大大的优化了启动速度,与性能开销。
四、WebView中JS的注入和Java的互调
其实这已经不算优化的点了,但是是我们常用互调的方法,这里就简单说明一下。
当然了很多人喜欢用框架来实现,每个框架的实现步骤不同,这里我不使用框架,用原生的实现。
4.1 Java中调用JS定义的方法
<script>
function changeContent(data){
document.getElementById('content').innerHTML=data;
}
</script>
复制代码
有两种方法调用JS:
webView.loadUrl("javascript:changeContent('<p>我是HTML</p>')");
复制代码
webView.evaluateJavascript("javascript:changeContent('<p>我是HTML</p>')");
复制代码
4.2 JS调用Java的方法
比如JS代码如下:
function isAndroid_ios() {
var u = navigator.userAgent,
app = navigator.appVersion;
var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //android终端或者uc浏览器
var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
return isAndroid == true ? true : false;
}
function checkImage() {
if (!window.isClick) {
window.isClick = true;
if (isAndroid_ios()) {
window.webkit.clickImage(null);
} else {
window.webkit.messageHandlers.clickImage.postMessage(null);
}
}
}
复制代码
在网页中我们定义了Android iOS的回调之后,它的回调方法名是 clickImage 作用域是 webkit ,那么我们在WebView中定义就行了
mWebView.addJavascriptInterface(H5CallBackAndroid(), "webkit")
inner class H5CallBackAndroid {
//图片的点击
@JavascriptInterface
fun clickImage(obj: String) {
}
}
复制代码
4.3 JS的手动注入
比如前端工程师没有写一些方法,那要他何用,自己动手,丰衣足食,我们自己手写JS注入到前端代码中,然后自己调用自己的JS。
例如一个前端的网页是新闻展示,我们需要获取新闻的全部图片,而前端代码中并没有定义这样的方法给我们调用
//js注入调用
view.loadUrl("javascript:function myFunction(){ var imgs = document.getElementsByTagName(\"img\");\n" +
" var imgurls = new Array();\n" +
" for (var i = 0; i < imgs.length; i++) {\n" +
" imgs[i].style.marginTop = '10px';\n" +
" imgs[i].style.marginBottom = '10px';\n" +
" var imgurl = imgs[i].src;\n" +
" if (imgurl.length > 50) {\n" +
" imgurls[i] = imgurl;\n" +
" } else {\n" +
" imgs[i].remove();\n" +
" continue;\n" +
" }\n" +
" (function (e) {\n" +
" imgs[e].onclick = function () {\n" +
" window.App.showImgFromPosition(e);\n" +
" };\n" +
" })(i)\n" +
" }\n" +
" var imgs = function () {\n" +
" window.webkit.getAllImgs(imgurls);\n" +
"\n" +
" };\n" +
" imgs();\n" +
" document.getElementsByTagName(\"aside\")[0].remove();\n" +
" document.getElementsByTagName(\"time\")[0].remove();\n" +
" document.getElementsByClassName('art_title_op')[0].height = '0px';\n" +
" document.getElementsByClassName('art_title_op')[0].lineHeight = '0px';\n" +
" document.getElementsByClassName('art_title_op')[0].remove();\n" +
" var ps = document.getElementsByTagName(\"p\");\n" +
" for (var i = 0; i < ps.length; i++) {\n" +
" var p_text = $(ps[i]).text();\n" +
" if (p_text != null && p_text != undefined && p_text != \"\" && p_text.length > 0) {\n" +
" var pp = function () {\n" +
" window.App.getFirstContent(p_text);\n" +
" };\n" +
" pp();\n" +
" break;\n" +
" }\n" +
" }\n" +
" for (var i = 0; i < ps.length; i++) {\n" +
" ps[i].style.fontSize = '16px';\n" +
" ps[i].style.lineHeight = '1.8';\n" +
" }\n" +
" document.getElementsByTagName(\"body\")[0].style.padding = '10px';\n" +
" document.getElementsByTagName(\"body\")[0].style.background = '#fff'; }");
//注入完成顺便执行注入的JS
view.loadUrl("javascript:myFunction()");
复制代码
注入了JS之后,我们调用我们注入的JS,注入的JS会回调到Java中来,代码如下:
@JavascriptInterface
public void getAllImgs(String[] imgs) {
mAllImgs.clear();
for (int i = 0; i < imgs.length; i++) {
mAllImgs.add(imgs[i]);
}
}
复制代码
当然了我们这么玩的机会还是比较少的,因为这种问题一般都是找前端去改的。这里也只是给大家扩展一下思路。
五、WebView中Cookie的管理
Cookie我们用的也是比较少,一般都是特殊场景下才需要使用到,webkit自带的CookieManager管理
下面是常用的几种方法
// 设置接收第三方Cookie
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().setAcceptThirdPartyCookies(vWeb, true);
}
// 获取指定url关联的所有Cookie
// 返回值使用"Cookie"请求头格式:"name=value; name2=value2; name3=value3"
CookieManager.getInstance().getCookie(url);
// 为指定的url设置一个Cookie
// 参数value使用"Set-Cookie"响应头格式,参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie
CookieManager.getInstance().setCookie(url, value);
// 移除指定url下的指定Cookie
CookieManager.getInstance().setCookie(url, cookieName + "=");
复制代码
Cookie的工具类:
public class WebkitCookieUtil {
// 移除指定url关联的所有cookie
public static void remove(String url) {
CookieManager cm = CookieManager.getInstance();
for (String cookie : cm.getCookie(url).split("; ")) {
cm.setCookie(url, cookie.split("=")[0] + "=");
}
flush();
}
// sessionOnly 为true表示移除所有会话cookie,否则移除所有cookie
public static void remove(boolean sessionOnly) {
CookieManager cm = CookieManager.getInstance();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (sessionOnly) {
cm.removeSessionCookies(null);
} else {
cm.removeAllCookies(null);
}
} else {
if (sessionOnly) {
cm.removeSessionCookie();
} else {
cm.removeAllCookie();
}
}
flush();
}
// 写入磁盘
public static void flush() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().flush();
} else {
CookieSyncManager.getInstance().sync();
}
}
}
复制代码
同步Cookie
// 将系统级Cookie(比如`new URL(...).openConnection()`的Cookie) 同步到 WebView
public class WebkitCookieHandler extends CookieHandler {
private static final String TAG = WebkitCookieHandler.class.getSimpleName();
private CookieManager wcm;
public WebkitCookieHandler() {
this.wcm = CookieManager.getInstance();
}
@Override
public void put(URI uri, Map<String, List<String>> headers) throws IOException {
if ((uri == null) || (headers == null)) {
return;
}
String url = uri.toString();
for (String headerKey : headers.keySet()) {
if ((headerKey == null) || !(headerKey.equalsIgnoreCase("set-cookie2") || headerKey.equalsIgnoreCase("set-cookie"))) {
continue;
}
for (String headerValue : headers.get(headerKey)) {
Log.e(TAG, headerKey + ": " + headerValue);
this.wcm.setCookie(url, headerValue);
}
}
}
@Override
public Map<String, List<String>> get(URI uri, Map<String, List<String>> headers) throws IOException {
if ((uri == null) || (headers == null)) {
throw new IllegalArgumentException("Argument is null");
}
String url = uri.toString();
String cookie = this.wcm.getCookie(url);
Log.e(TAG, "cookie: " + cookie);
if (cookie != null) {
return Collections.singletonMap("Cookie", Arrays.asList(cookie));
} else {
return Collections.emptyMap();
}
}
}
复制代码
六、WebView中定位操作
一些Web需要定位的时候,需要我们App提供他们服务,此时需要用到一些权限申请和处理
先需要配置权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
复制代码
设置WebView的服务可用
settings.setGeolocationEnabled(true);
复制代码
//申请权限时的回调
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
boolean allow = true; // 是否允许origin使用定位API
boolean retain = false; // 内核是否记住这次制授权
callback.invoke(origin, true, false);
}
// 申请的授权被取消时,隐藏相关的UI。
@Override
public void onGeolocationPermissionsHidePrompt() {
}
复制代码
当然我们App也是授权给Web,定位的操作还是在Web那边的 Geolocation API,如果想通过App来定位,也是可以的,我们可以通过App的定位完成之后直接把经纬度传递给Web。
七、WebView中图片与文件的获取
首先我们需要定义权限
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:remove="android:maxSdkVersion" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage"
tools:remove="android:maxSdkVersion" />
复制代码
设置WebView的支持
//允许访问多媒体
mWebSettings.setAllowFileAccess(true);
mWebSettings.setAllowFileAccessFromFileURLs(true);
mWebSettings.setAllowUniversalAccessFromFileURLs(true);
复制代码
在设置的 WebChromeClient 方法中重写此回调
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
//启动相册
YYLogUtils.w("网页尝试调取Android相机相册");
if (mFilesListener != null) mFilesListener.onWebFileSelect(filePathCallback);
return true;
}
//网页选择图片文件的回调相关处理
private OnWebChooseFileListener mFilesListener;
public interface OnWebChooseFileListener {
void onWebFileSelect(ValueCallback<Uri[]> callback);
}
public void setOnWebChooseFileListener(OnWebChooseFileListener listener) {
mFilesListener = listener;
}
复制代码
上面是使用了一个回调,让具体的页面来实现具体的需求,我们只需要注意参数 ValueCallback 就行了,我们获取到的图片文件数据通过 ValueCallback 回调给Web。
下面看看如何具体使用
private ValueCallback<Uri[]> filePathCallback1;
//文件与图片的选择回调
mWebView.setOnWebChooseFileListener(new MyWebView.OnWebChooseFileListener() {
@Override
public void onWebFileSelect(ValueCallback<Uri[]> callback) {
filePathCallback1 = callback;
showPickDialog();
}
});
/**
* 相机相册的选择
*/
private void showPickDialog() {
PickPhotoDialog photoDialog = new PickPhotoDialog(mActivity);
photoDialog.SetOnChooseClickListener(new PickPhotoDialog.OnChooseClickListener() {
@Override
public void chooseCamera() {
startCamera();
}
@Override
public void chooseAlbum() {
startAlbum();
}
});
photoDialog.setCancelable(false);
photoDialog.show();
photoDialog.setOnDismissListener(dialog -> {
cancelFilePick();
});
}
复制代码
开启相机或者相册大家可以具体的实现,每个人用的框架不同,这里就不做推荐了。
//选择相册
private void startAlbum() {
//自行实现选择相册
...
handlePath(xxx);
}
//选择相机
private void startCamera() {
//自行实现选择相机
...
handlePath(xxx);
}
/**
* 处理图片-转换图片-返回给Web
*/
private void handlePath(List<String> result) {
YYLogUtils.w("处理图片-转换图片-返回给Web");
if (!CheckUtil.isEmpty(result)) {
String path = result.get(0);
Uri fileUri = UriExtKt.getFileUri(this, new File(path));
if (filePathCallback1 != null) {
//回调给Web
filePathCallback1.onReceiveValue(new Uri[]{fileUri});
filePathCallback1 = null;
}
}
}
//取消图片的选择
private void cancelFilePick() {
if (filePathCallback1 != null) {
YYLogUtils.w("取消图片的选择");
filePathCallback1.onReceiveValue(null);
filePathCallback1 = null;
}
}
复制代码
到处就完成了Web的图片选择了。效果如下:
八、WebView中网络拦截
原理为 WebView内核的 shouldInterceptRequest 回调,拦截资源请求由客户端进行下载,并以管道方式填充到内核的 WebResourceResponse中。
使用场景是,我们使用Web之前我们已经通过网络把一些JS CSS 图片等资源放入了本地存储,那么我们Web使用的时候就判断如果本地已经有资源了,我们就从本地拿,如果没有我们就使用OkHttp下载到本地再使用。
在 WebView 的 WebViewClient 中我们加入如下拦截
@Nullable
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
if (view != null && request != null) {
if(canCacheResource(request)){
return cacheResourceRequest(view.context, request)
}
}
return super.shouldInterceptRequest(view, request)
复制代码
具体的判断与
private fun canCacheResource(webRequest: WebResourceRequest): Boolean {
val url = webRequest.url.toString()
val extension = getExtensionFromUrl(url)
//当资源是这些后缀的时候我们都需要拦截
return extension == "gif"
|| extension == "jpeg" || extension == "jpg" || extension == "png"
|| extension == "svg" || extension == "webp" || extension == "css"
|| extension == "js" || extension == "json" || extension == "eot"
|| extension == "otf" || extension == "ttf"
}
}
private fun cacheResourceRequest(context: Context, webRequest: WebResourceRequest): WebResourceResponse? {
try {
val url = webRequest.url.toString()
val cachePath = CacheUtils.getCacheDirPath(context, "web_cache")
val filePathName = cachePath + File.separator + url.encodeUtf8().md5().hex()
val file = File(filePathName)
//如果文件不存在,下载到本地
if (!file.exists() || !file.isFile) {
runBlocking {
// 使用工具类下载资源
download(HttpRequest(url).apply {
webRequest.requestHeaders.forEach { putHeader(it.key, it.value) }
}, filePathName)
}
}
//文件存在或下载完成,我们使用管道传递给Web
if (file.exists() && file.isFile) {
val webResourceResponse = WebResourceResponse()
webResourceResponse.mimeType = getMimeTypeFromUrl(url)
webResourceResponse.encoding = "UTF-8"
webResourceResponse.data = file.inputStream()
webResourceResponse.responseHeaders = mapOf("access-control-allow-origin" to "*")
return webResourceResponse
}
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
复制代码
这么做可以大大的提升页面的加载速度,特别适用于一些固定样式的页面,如文章的详情之类。但是需要注意的是注意本地磁盘缓存的大小限制,最好是做限时存储(时间戳)或者限量存储(LRUCache)。
九、WebView中点击事件
WebView中图片的点击,或者其他控件的点击我们之前可以通过JS互调的方式来手动的定义,也可以通过WebView自带的一些类型的点击监听。
9.1 使用JS方法自定义
function isAndroid_ios() {
var u = navigator.userAgent,
app = navigator.appVersion;
var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //android终端或者uc浏览器
var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
return isAndroid == true ? true : false;
}
function longClickImage(url) {
if (!window.isClick) {
window.isClick = true;
if (isAndroid_ios()) {
window.webkit.longClickImage(url);
} else {
window.webkit.messageHandlers.longClickImage.postMessage(url);
}
}
}
复制代码
使用
mWebView.addJavascriptInterface(H5CallBackAndroid(), "webkit")
inner class H5CallBackAndroid {
//图片的点击
@JavascriptInterface
fun longClickImage(url: String) {
Intent i = new Intent(MainActivity.this, ImageActivity.class);
i.putExtra("imgUrl", url);
startActivity(i);
}
}
复制代码
9.2 使用WebView的点击监听
mWebView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
WebView.HitTestResult result = ((WebView)v).getHitTestResult();
if (null == result)
return false;
int type = result.getType();
if (type == WebView.HitTestResult.UNKNOWN_TYPE)
return false;
// 这里可以拦截很多类型,我们只处理图片类型就可以了
switch (type) {
case WebView.HitTestResult.PHONE_TYPE: // 处理拨号
break;
case WebView.HitTestResult.EMAIL_TYPE: // 处理Email
break;
case WebView.HitTestResult.GEO_TYPE: // 地图类型
break;
case WebView.HitTestResult.SRC_ANCHOR_TYPE: // 超链接
break;
case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
break;
case WebView.HitTestResult.IMAGE_TYPE: // 处理长按图片的菜单项
// 获取图片的路径
String saveImgUrl = result.getExtra();
// 跳转到图片详情页,显示图片
Intent i = new Intent(MainActivity.this, ImageActivity.class);
i.putExtra("imgUrl", saveImgUrl);
startActivity(i);
break;
default:
break;
}
}
});
复制代码
总结
其实WebView的细节还是蛮多的,我已经尽量缩减了,但是不知不觉都这么长了,基本的使用应是差不多了。
当然 WebView 还能继续优化,比如使用模板,后端直出等等,如果需要更进一步优化启动速度,还需要前端、后端和我们移动端的配合了,单独我们移动端能优化的点就以上这些了。
本文的部分代码有一些是思路之类的,代码不全,大家可以自行实现,比较基本的封装代码都已经在本文贴出了,大家可以自取。
作者:newki
链接:https://juejin.cn/post/7125680139551113252
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。