本文作者
作者: newki
链接:
Android-WebView的优化与常见问题
其实关于Android的WebView大家使用起来应该都是有过封装,网上林林总总的分析与封装也不少。
我知道只要讲 WebView 一定有同学会说,原生WebView垃圾,我们都用的是腾讯X5 WebView 之类的。但是我们研发的是海外项目,只能使用原生的WebView,所以这里不涉及到TBS服务相关的点。
每一个人的封装可能都不一样,看我抛砖引玉,希望大家可以互相交流学习。
1
自定义WebView
我们需要一个统一管理的WebView,那么我们需要继承WebView,并内部对一些属性开启,对JS的支持,对加载过程与状态的监听,对文件操作的回调等。
publicclassMyWebViewextendsWebView{
privateWebSettings mWebSettings;
privatebooleanisNeedExe = true;
publicMyWebView(Context context){
super(context);
initView;
}
publicMyWebView(Context context, AttributeSet attrs){
super(context, attrs);
initView;
}
publicMyWebView(Context context, AttributeSet attrs, intdefStyleAttr) {
super(context, attrs, defStyleAttr);
initView;
}
@SuppressLint({ "ObsoleteSdkInt", "SetJavaEnabled"})
privatevoidinitView{
mWebSettings = getSettings;
mWebSettings.setSupportZoom( false);
mWebSettings.setBuiltInZoomControls( false);
mWebSettings.setDefaultTextEncodingName( "utf-8");
mWebSettings.setJavaEnabled( 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 = newWebViewClient {
//https ssl证书问题,如果没有https的问题可以注释掉
@Override
publicvoidonReceivedSslError(WebView view, SslErrorHandler handler, SslError error){
// 接受所有网站的证书,Google不通过
//使用下面的兼容写法
finalSslErrorHandler mHandler;
mHandler= handler;
AlertDialog.Builder builder = newAlertDialog.Builder(getContext);
builder.setMessage( "SSL validation failed");
builder.setPositiveButton( "Continue", newDialogInterface.OnClickListener {
@Override
publicvoidonClick(DialogInterface dialog, intwhich) {
mHandler.proceed;
}
});
builder.setNegativeButton( "Cancel", newDialogInterface.OnClickListener {
@Override
publicvoidonClick(DialogInterface dialog, intwhich) {
mHandler.cancel;
}
});
builder.setOnKeyListener( newDialogInterface.OnKeyListener {
@Override
publicbooleanonKey(DialogInterface dialog, intkeyCode, KeyEvent event) {
if(event.getAction == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
mHandler.cancel;
dialog.dismiss;
returntrue;
}
returnfalse;
}
});
AlertDialog dialog = builder.create;
dialog.show;
}
//页面加载完成,展示图片
@Override
publicvoidonPageFinished(WebView view, String url){
if(!mWebSettings.getLoadsImagesAutomatically) {
mWebSettings.setLoadsImagesAutomatically( true);
}
}
//在当前的webview中跳转到新的url
@Override
publicbooleanshouldOverrideUrlLoading(WebView view, String url){
if(mListener != null) mListener.onInnerLinkChecked;
if(Build.VERSION.SDK_INT < 26) {
if(!TextUtils.isEmpty(url)) {
view.loadUrl(url);
}
returntrue;
}
returnfalse;
}
//WebView加载错误的回调
@Override
publicvoidonReceivedError(WebView view, WebResourceRequest request, WebResourceError error){
super.onReceivedError(view, request, error);
if(mListener != null) mListener.onWebLoadError;
}
//拦截WebView中的网络请求
@Nullable
@Override
publicWebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request){
returnsuper.shouldInterceptRequest(view, request);
}
};
WebChromeClient mWebChromeClient = newWebChromeClient {
//获取html的title标签
@Override
publicvoidonReceivedTitle(WebView view, String title){
if(mListener != null) mListener.titleChange(title);
super.onReceivedTitle(view, title);
}
//获取页面加载的进度
@Override
publicvoidonProgressChanged(WebView view, intnewProgress) {
if(mListener != null) mListener.progressChange(newProgress);
super.onProgressChanged(view, newProgress);
if(newProgress > 95&& isNeedExe) {
isNeedExe = !isNeedExe;
if(newProgress == 100) {
//注入js代码测量webview高度
loadUrl( "java:App.resize(document.body.getBoundingClientRect.height)");
}
}
}
// 指定源的网页内容在没有设置权限状态下尝试使用地理位置API。
@Override
publicvoidonGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback){
booleanallow = true; // 是否允许origin使用定位API
booleanretain = false; // 内核是否记住这次制授权
callback.invoke(origin, true, false);
}
// 之前调用 onGeolocationPermissionsShowPrompt 申请的授权被取消时,隐藏相关的UI。
@Override
publicvoidonGeolocationPermissionsHidePrompt{
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
publicbooleanonShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams){
//启动系统相册
YYLogUtils.w( "网页尝试调取Android相机相册");
CommUtils.getHandler.post( -> {
if(mFilesListener != null) mFilesListener.onWebFileSelect(filePathCallback);
});
returntrue;
}
};
//网页状态的回调相关处理
privateOnWebChangeListener mListener;
publicinterfaceOnWebChangeListener{
voidtitleChange(String title);
voidprogressChange( intprogress) ;
voidonInnerLinkChecked;
voidonWebLoadError;
}
publicvoidsetOnWebChangeListener(OnWebChangeListener listener){
mListener = listener;
}
//网页选择图片文件的回调相关处理
privateOnWebChooseFileListener mFilesListener;
publicinterfaceOnWebChooseFileListener{
voidonWebFileSelect(ValueCallback<Uri[]> callback);
}
publicvoidsetOnWebChooseFileListener(OnWebChooseFileListener listener){
mFilesListener = listener;
}
/**
* 暴露方法,是否滑动到底部
*/
publicbooleanisScrollBottom{
if(getContentHeight * getScale == (getHeight + getScrollY)) {
//说明已经到底了
returntrue;
} else{
returnfalse;
}
}
}
都是比较基础的代码,涉及到属性的开启,与监听和回调大家应该都能看懂,下面就是看如何使用了。
privatefuninitWeb{
valparams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
mWebView = MyWebView(applicationContext)
mWebView.layoutParams = params
mWebView.setOnWebChangeListener( object: MyWebView.OnWebChangeListener {
overridefuntitleChange(title: String) {
if(CheckUtil.isEmpty(mWebtitle)) {
easy_title.setTitle(mWebtitle)
}
}
overridefunprogressChange(progress: Int) {
varnewProgress = progress
if(newProgress == 100) {
pb_web_view.setProgress( 100)
CommUtils.getHandler
.postDelayed({ pb_web_view.visibility = View.GONE }, 200) //0.2秒后隐藏进度条
} elseif(pb_web_view.visibility == View.GONE) {
pb_web_view.visibility = View.VISIBLE
}
//设置初始进度10,这样会显得效果真一点,总不能从1开始吧
if(newProgress < 10) {
newProgress = 10
}
//不断更新进度
pb_web_view.setProgress(newProgress)
}
overridefunonInnerLinkChecked{
}
overridefunonWebLoadError{
toast( "Load Error")
}
})
if(!TextUtils.isEmpty(mWeburl))
mWebView.loadUrl(mWeburl!!)
fl_content.addView(mWebView)
}
overridefunonKeyDown(keyCode: Int, event: KeyEvent) : Boolean{
if(keyCode == KeyEvent.KEYCODE_BACK && mWebView!!.canGoBack) {
mWebView!!.goBack
returntrue
}
returnsuper.onKeyDown(keyCode, event)
}
overridefunonPause{
super.onPause
mWebView?.onPause
}
overridefunonResume{
super.onResume
mWebView?.onResume
}
overridefunonDestroy{
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左右。
2
WebView的缓存
其实我们就可以换一个思路,如果说WebView的销毁会内存泄露,那么我们不销毁不就行了吗?我们把WebView缓存起来。每次使用的时候去缓存里面拿,然后销毁的时候回收,这样不就不会内存泄露了吗?
网上找的一个WebViewCacheManager:
/**
* WebView的缓存容器
* obtail获取对象
* recycle回收对象
*/
objectWebViewManager {
privatevalwebViewCache: MutableList<MyWebView> = ArrayList( 1)
privatefuncreate(context: Context) : MyWebView {
returnMyWebView(context)
}
/**
* 初始化
*/
@JvmStatic
funprepare(context: Context) {
if(webViewCache.isEmpty) {
Looper.myQueue.addIdleHandler {
webViewCache.add(create(MutableContextWrapper(context)))
false
}
}
}
/**
* 获取WebView
*/
@JvmStatic
funobtain(context: Context) : MyWebView {
if(webViewCache.isEmpty) {
webViewCache.add(create(MutableContextWrapper(context)))
}
valwebView = webViewCache.removeFirst
valcontextWrapper = webView.context asMutableContextWrapper
contextWrapper.baseContext = context
webView.clearHistory
webView.resumeTimers
returnwebView
}
/**
* 回收资源
*/
@JvmStatic
funrecycle(webView: MyWebView) {
try{
webView.stopLoading
webView.loadDataWithBaseURL( null, "", "text/html", "utf-8", null)
webView.clearHistory
webView.pauseTimers
webView.clearFormData
webView.removeJavaInterface( "webkit")
valparent = webView.parent
if(parent != null) {
(parent asViewGroup).removeView(webView)
}
} catch(e: Exception) {
e.printStackTrace
} finally{
if(!webViewCache.contains(webView)) {
webViewCache.add(webView)
}
}
}
/**
* 销毁资源
*/
@JvmStatic
fundestroy{
try{
webViewCache.forEach {
it.removeAllViews
it.destroy
webViewCache.remove(it)
}
} catch(e: Exception) {
e.printStackTrace
}
}
}
网上很多的这种管理类,原理都大致差不多,这样管理了WebView之后还有一个好处是可以优化启动速度,无需每次New一个WebView然后初始化内核之类的耗时了。
使用之前我们需要初始化。
openclassBaseApplication: Application{
overridefunonCreate{
super.onCreate
//空闲的时候初始化WebView容器
Looper.myQueue.addIdleHandler {
//初始化WebView缓存容器
WebViewManager.prepare( this)
false
}
}
}
初始化完成之后,如果要使用工具类,我们这样修改WebView的使用:
privatefuninitWeb{
valparams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
mWebView = WebViewManager.obtain( this) //管理类获取对象
mWebView.layoutParams = params
mWeburl?.let { mWebView.loadUrl(it) }
mWebView.addJavaInterface(H5CallBackAndroid, "webkit")
mBinding.flContent.addView(mWebView)
}
overridefunonPause{
super.onPause
mWebView.onPause
}
overridefunonResume{
super.onResume
mWebView.onResume
}
overridefunonDestroy{
super.onDestroy
WebViewManager.recycle(mWebView)
}
overridefunonKeyDown(keyCode: Int, event: KeyEvent) : Boolean{
if(keyCode == KeyEvent.KEYCODE_BACK && mWebView!!.canGoBack) {
mWebView!!.goBack
returntrue
}
returnsuper.onKeyDown(keyCode, event)
}
可以看到我们只是修改了WebView的创建与销毁,这么做的好处是当销毁的时候不会泄露内存了。
例如我跳转Web之前的页面-占内存为160左右。
跳转到一个Web,内存飙升至190左右。
返回之前的页面-占用内存依旧是160左右。
如果大家有兴趣,也可以自行测试,如果每次New WebView再destory 那么内存是慢慢上涨的,如果使用WebView缓存之后内存并不会上涨。
3
WebView的返回问题
但是这么做有一个很大的坑,就是每次销毁的时候它的Url并没有清除,我们又不能使用webView的destory方法,那么我们第一个启动Web并返回是正常的,第二次再启动再返回,此时使用的是缓存WebView,是无法一次返回的。
因为之前的WebView已经有一个Url了,因为加载的网页可能是任意网址,我们无法判断,那么我们在回收的方法中手动的设置了指定的url。
webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
那么这样的效果还是有问题,之前我们还需要按2次返回键才能返回Web页面,而现在我们加载了一个空视图之后,现在在Web的栈顶,按一次返回键会返回一个空白的页面,再按返回才能返回,还是需要二次返回。
解决办法是,我们在返回的时候判断一下,上一个url是不是空白的不就行了吗?
我们通过 copyBackForwardList 可以拿到WebView的全部栈顶,和当前的栈索引,我们加上一点判断,就可以正常的返回了。
overridefunonKeyDown(keyCode: Int, event: KeyEvent) : Boolean{
valwebBackForwardList = mWebView.copyBackForwardList
valhistoryOneOriginalUrl = webBackForwardList.getItemAtIndex( 0)?.originalUrl
valcurIndex = webBackForwardList.currentIndex
returnif(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是实战中的一大利器,大大的优化了启动速度,与性能开销。
4
WebView中JS的注入和Java的互调
其实这已经不算优化的点了,但是是我们常用互调的方法,这里就简单说明一下。
当然了很多人喜欢用框架来实现,每个框架的实现步骤不同,这里我不使用框架,用原生的实现。
4.1 Java中调用JS定义的方法
< >
functionchangeContent( data) {
document.getElementById( 'content').innerHTML=data;
}
</ >
有两种方法调用JS:
webView.loadUrl("java:changeContent(' < p> 我是HTML </ p> ')");
webView.evaluateJava("java:changeContent(' < p> 我是HTML </ p> ')");
4.2 JS调用Java的方法
比如JS代码如下:
functionisAndroid_ios{
varu = navigator.userAgent,
app = navigator.appVersion;
varisAndroid = u.indexOf( 'Android') > -1|| u.indexOf( 'Linux') > -1; //android终端或者uc浏览器
varisiOS = !!u.match( /\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
returnisAndroid == true? true: false;
}
functioncheckImage{
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.addJavaInterface(H5CallBackAndroid, "webkit")
inner classH5CallBackAndroid{
//图片的点击
@JavaInterface
funclickImage(obj: String) {
}
}
4.3 JS的手动注入
比如前端工程师没有写一些方法,那要他何用,自己动手,丰衣足食,我们自己手写JS注入到前端代码中,然后自己调用自己的JS。
例如一个前端的网页是新闻展示,我们需要获取新闻的全部图片,而前端代码中并没有定义这样的方法给我们调用。
//js注入调用
view.loadUrl( "java: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( "java:myFunction");
注入了JS之后,我们调用我们注入的JS,注入的JS会回调到Java中来,代码如下:
@ JavaInterface
publicvoidgetAllImgs( String[] imgs) {
mAllImgs.clear;
for( inti = 0; i < imgs.length; i++) {
mAllImgs. add(imgs[i]);
}
}
当然了我们这么玩的机会还是比较少的,因为这种问题一般都是找前端去改的。这里也只是给大家扩展一下思路。
5
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的工具类:
publicclassWebkitCookieUtil{
// 移除指定url关联的所有cookie
publicstaticvoidremove(String url){
CookieManager cm = CookieManager.getInstance;
for(String cookie : cm.getCookie(url).split( "; ")) {
cm.setCookie(url, cookie.split( "=")[ 0] + "=");
}
flush;
}
// sessionOnly 为true表示移除所有会话cookie,否则移除所有cookie
publicstaticvoidremove( booleansessionOnly) {
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;
}
// 写入磁盘
publicstaticvoidflush{
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance.flush;
} else{
CookieSyncManager.getInstance.sync;
}
}
}
同步Cookie:
// 将系统级Cookie(比如`new URL(...).openConnection`的Cookie) 同步到 WebView
public classWebkitCookieHandlerextendsCookieHandler{
private staticfinalStringTAG = WebkitCookieHandler. class.getSimpleName;
private CookieManager wcm;
public WebkitCookieHandler {
this.wcm = CookieManager.getInstance;
}
@Override
public voidput(URI uri, Map< String, List< String>> headers) throws IOException {
if((uri == null) || (headers == null)) {
return;
}
Stringurl = uri.toString;
for( StringheaderKey : headers.keySet) {
if((headerKey == null) || !(headerKey.equalsIgnoreCase( "set-cookie2") || headerKey.equalsIgnoreCase( "set-cookie"))) {
continue;
}
for( StringheaderValue : 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)) {
thrownewIllegalArgumentException( "Argument is null");
}
Stringurl = uri.toString;
Stringcookie = this.wcm.getCookie(url);
Log.e(TAG, "cookie: "+ cookie);
if(cookie != null) {
returnCollections.singletonMap( "Cookie", Arrays.asList(cookie));
} else{
returnCollections.emptyMap;
}
}
}
6
WebView中定位操作
一些Web需要定位的时候,需要我们App提供他们服务,此时需要用到一些权限申请和处理。
先需要配置权限:
< uses-permissionandroid:name= "android.permission.INTERNET"/>
< uses-permissionandroid:name= "android.permission.ACCESS_FINE_LOCATION"/>
< uses-permissionandroid:name= "android.permission.ACCESS_COARSE_LOCATION"/>
设置WebView的服务可用:
settings.setGeolocationEnabled( true);
//申请权限时的回调
@Override
publicvoidonGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback){
booleanallow = true; // 是否允许origin使用定位API
booleanretain = false; // 内核是否记住这次制授权
callback.invoke(origin, true, false);
}
// 申请的授权被取消时,隐藏相关的UI。
@Override
publicvoidonGeolocationPermissionsHidePrompt{
}
当然我们App也是授权给Web,定位的操作还是在Web那边的 Geolocation API,如果想通过App来定位,也是可以的,我们可以通过App的定位完成之后直接把经纬度传递给Web。
7
WebView中图片与文件的获取
首先我们需要定义权限:
< uses-permissionandroid: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
publicbooleanonShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams){
//启动相册
YYLogUtils.w( "网页尝试调取Android相机相册");
if(mFilesListener != null) mFilesListener.onWebFileSelect(filePathCallback);
returntrue;
}
//网页选择图片文件的回调相关处理
privateOnWebChooseFileListener mFilesListener;
publicinterfaceOnWebChooseFileListener{
voidonWebFileSelect(ValueCallback<Uri[]> callback);
}
publicvoidsetOnWebChooseFileListener(OnWebChooseFileListener listener){
mFilesListener = listener;
}
上面是使用了一个回调,让具体的页面来实现具体的需求,我们只需要注意参数 ValueCallback就行了,我们获取到的图片文件数据通过 ValueCallback 回调给Web。
下面看看如何具体使用:
privateValueCallback<Uri[]> filePathCallback1;
//文件与图片的选择回调
mWebView.setOnWebChooseFileListener( newMyWebView.OnWebChooseFileListener {
@Override
publicvoidonWebFileSelect(ValueCallback<Uri[]> callback){
filePathCallback1 = callback;
showPickDialog;
}
});
/**
* 相机相册的选择
*/
privatevoidshowPickDialog{
PickPhotoDialog photoDialog = newPickPhotoDialog(mActivity);
photoDialog.SetOnChooseClickListener( newPickPhotoDialog.OnChooseClickListener {
@Override
publicvoidchooseCamera{
startCamera;
}
@Override
publicvoidchooseAlbum{
startAlbum;
}
});
photoDialog.setCancelable( false);
photoDialog.show;
photoDialog.setOnDismissListener(dialog -> {
cancelFilePick;
});
}
开启相机或者相册大家可以具体的实现,每个人用的框架不同,这里就不做推荐了。
//选择相册
privatevoidstartAlbum{
//自行实现选择相册
...
handlePath(xxx);
}
//选择相机
privatevoidstartCamera{
//自行实现选择相机
...
handlePath(xxx);
}
/**
* 处理图片-转换图片-返回给Web
*/
privatevoidhandlePath( List<String> result) {
YYLogUtils.w( "处理图片-转换图片-返回给Web");
if(!CheckUtil.isEmpty(result)) {
String path = result. get( 0);
Uri fileUri = UriExtKt.getFileUri( this, newFile(path));
if(filePathCallback1 != null) {
//回调给Web
filePathCallback1.onReceiveValue( newUri[]{fileUri});
filePathCallback1 = null;
}
}
}
//取消图片的选择
privatevoidcancelFilePick{
if(filePathCallback1 != null) {
YYLogUtils.w( "取消图片的选择");
filePathCallback1.onReceiveValue( null);
filePathCallback1 = null;
}
}
到此就完成了Web的图片选择了。效果如下:
8
WebView中网络拦截
原理为 WebView内核的shouldInterceptRequest 回调,拦截资源请求由客户端进行下载,并以管道方式填充到内核的WebResourceResponse中。
使用场景是,我们使用Web之前我们已经通过网络把一些JS CSS 图片等资源放入了本地存储,那么我们Web使用的时候就判断如果本地已经有资源了,我们就从本地拿,如果没有我们就使用OkHttp下载到本地再使用。
在 WebView 的WebViewClient中我们加入如下拦截:
@Nullable
@Override
publicWebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
if(view != null&& request != null) {
if(canCacheResource(request)){
returncacheResourceRequest(view.context, request)
}
}
returnsuper.shouldInterceptRequest(view, request)
具体的判断与:
privatefuncanCacheResource(webRequest: WebResourceRequest) : Boolean{
valurl = webRequest.url.toString
valextension = getExtensionFromUrl(url)
//当资源是这些后缀的时候我们都需要拦截
returnextension == "gif"
|| extension == "jpeg"|| extension == "jpg"|| extension == "png"
|| extension == "svg"|| extension == "webp"|| extension == "css"
|| extension == "js"|| extension == "json"|| extension == "eot"
|| extension == "otf"|| extension == "ttf"
}
}
privatefuncacheResourceRequest(context: Context, webRequest: WebResourceRequest) : WebResourceResponse? {
try{
valurl = webRequest.url.toString
valcachePath = CacheUtils.getCacheDirPath(context, "web_cache")
valfilePathName = cachePath + File.separator + url.encodeUtf8.md5.hex
valfile = 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) {
valwebResourceResponse = WebResourceResponse
webResourceResponse.mimeType = getMimeTypeFromUrl(url)
webResourceResponse.encoding = "UTF-8"
webResourceResponse. data= file.inputStream
webResourceResponse.responseHeaders = mapOf( "access-control-allow-origin"to "*")
returnwebResourceResponse
}
} catch(e: Exception) {
e.printStackTrace
}
returnnull
}
这么做可以大大的提升页面的加载速度,特别适用于一些固定样式的页面,如文章的详情之类。但是需要注意的是注意本地磁盘缓存的大小限制,最好是做限时存储(时间戳)或者限量存储(LRUCache)。
9
WebView中点击事件
WebView中图片的点击,或者其他控件的点击我们之前可以通过JS互调的方式来手动的定义,也可以通过WebView自带的一些类型的点击监听。
9.1 使用JS方法自定义
functionisAndroid_ios{
varu = navigator.userAgent,
app = navigator.appVersion;
varisAndroid = u.indexOf( 'Android') > -1|| u.indexOf( 'Linux') > -1; //android终端或者uc浏览器
varisiOS = !!u.match( /\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
returnisAndroid == true? true: false;
}
functionlongClickImage( url) {
if(! window.isClick) {
window.isClick = true;
if(isAndroid_ios) {
window.webkit.longClickImage(url);
} else{
window.webkit.messageHandlers.longClickImage.postMessage(url);
}
}
}
使用
mWebView.addJavaInterface(H5CallBackAndroid, "webkit")
inner classH5CallBackAndroid{
//图片的点击
@JavaInterface
funlongClickImage(url: String) {
Intent i = new Intent(MainActivity. this, ImageActivity. class);
i.putExtra( "imgUrl", url);
startActivity(i);
}
}
9.2 使用WebView的点击监听
mWebView.setOnLongClickListener( newView.OnLongClickListener {
@Override
publicbooleanonLongClick(View v){
WebView.HitTestResult result = ((WebView)v).getHitTestResult;
if( null== result)
returnfalse;
inttype = result.getType;
if(type == WebView.HitTestResult.UNKNOWN_TYPE)
returnfalse;
// 这里可以拦截很多类型,我们只处理图片类型就可以了
switch(type) {
caseWebView.HitTestResult.PHONE_TYPE: // 处理拨号
break;
caseWebView.HitTestResult.EMAIL_TYPE: // 处理Email
break;
caseWebView.HitTestResult.GEO_TYPE: // 地图类型
break;
caseWebView.HitTestResult.SRC_ANCHOR_TYPE: // 超链接
break;
caseWebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
break;
caseWebView.HitTestResult.IMAGE_TYPE: // 处理长按图片的菜单项
// 获取图片的路径
String saveImgUrl = result.getExtra;
// 跳转到图片详情页,显示图片
Intent i = newIntent(MainActivity. this, ImageActivity.class);
i.putExtra( "imgUrl", saveImgUrl);
startActivity(i);
break;
default:
break;
}
}
});
10
总结
其实WebView的细节还是蛮多的,我已经尽量缩减了,但是不知不觉都这么长了,基本的使用应是差不多了。
当然 WebView 还能继续优化,比如使用模板,后端直出等等,如果需要更进一步优化启动速度,还需要前端、后端和我们移动端的配合了,单独我们移动端能优化的点就以上这些了。
本文的部分代码有一些是思路之类的,代码不全,大家可以自行实现,比较基本的封装代码都已经在本文贴出了,大家可以自取。
最后如有讲的不到位或错漏的地方,希望同学们可以评论区指出交流。
如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。
Ok,这一期就此完结。
最后推荐一下我做的网站,玩Android: wanandroid.com,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
:
Android 技术周刊(第1期):38篇技术文章
Android 实现菜单拖拽排序, so easy
货拉拉 Android H5离线包原理与实践
点击关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!返回搜狐,查看更多
责任编辑: