评论

WebView遇到的各种问题解决方案分享

本文作者

作者: 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 WebViewdestory 那么内存是慢慢上涨的,如果使用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^)┛明天见!返回搜狐,查看更多

责任编辑:

平台声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。
阅读 ()