Android爬坑之旅之WebView
不知不觉,HybirdApp已经成了目前比较主流的一种开发方式。
对于用户体验要求较高或者与硬件交互较多的功能我们一般都会采用Native原生的方式来实现。
而用户交互少,偏展示类,活动类的功能我们则通常采用H5的方式来实现,
例如新闻类的app,详情展示页一般就是H5的页面
一方面图文排版上web有着先天的优势,同时纯展示类的页面在目前的移动设备上,性能体验已经很难让用户分辨是网页还是原生了;
另一方面,H5的页面跨平台,方便在原生客户端上实现分享功能,拥有较强的传播性,我们平时常见的活动页面也拥有这样的优势,所以你看到的活动页面也基本都是H5,只需轻轻一点就能分享到各个平台;
同时,H5的页面开发降低了开发成本,一套代码,web,android,ios都能访问。(然而实际开发过程中,H5的适配也都是各种泪)
既然HybirdApp有这么多优势,那在Android中我们通过什么样的方式在原生项目中嵌入H5页面呢?
那就不得不提到我们的WebVew了,作为官方唯一用来显示web的组件,
展示网页这样的任务也只能交给它了。
AViewthatdisplayswebpages.ThisclassisthebasisuponwhichyoucanrollyourownwebbrowserorsimplydisplaysomeonlinecontentwithinyourActivity.ItusestheWebKitrenderingenginetodisplaywebpagesandincludesmethodstonavigateforwardandbackwardthroughahistory,zoominandout,performtextsearchesandmore.
引用官方文档的一句话:
WebView是一个用来在Activity中显示我们网页的视图组件,它通过webkit渲染引擎渲染和显示我们的web页面,并且包含了web的历史导航操法,页面放大缩小,文本搜索等方法。
我们首先来看一下WebView的基本用法:
WebView的基本用法
WebView也是AndroidView的一种,我们通常用它来在应用内部展示网页,和以往一样,我们先来简单看一下它的基本用法。
添加网络权限
在布局中添加WebView
使用WebView加载网页
WebViewmyWebView=(WebView)findViewById(R.id.webview);
myWebView.loadUrl("http://www.example.com");
以上就是WebView的简单用法,相比大家已经十分熟悉,下面我们就来逐一看看WebView的其他特性。
组件">一WebView基本组件
了解了基本用法,我们对WebView就有了大致的印象,下面我们来看看构建Web应用的三个重要组件。
1.1WebSettings
WebSettings用来对WebView做各种设置,你可以这样获取WebSettings:
WebSettingswebSettings=mWebView.getSettings();
WebSettings的常见设置如下所示:
JS处理
setJavaScriptEnabled(true);//支持jssetPluginsEnabled(true);//支持插件setJavaScriptCanOpenWindowsAutomatically(true);//支持通过JS打开新窗口
缩放处理
setUseWideViewPort(true);//将图片调整到适合webview的大小setLoadWithOverviewMode(true);//缩放至屏幕的大小setSupportZoom(true);//支持缩放,默认为true。是下面那个的前提。setBuiltInZoomControls(true);//设置内置的缩放控件。这个取决于setSupportZoom(),若setSupportZoom(false),则该WebView不可缩放,这个不管设置什么都不能缩放。setDisplayZoomControls(false);//隐藏原生的缩放控件
内容布局
setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN);//支持内容重新布局supportMultipleWindows();//多窗口
文件缓存
setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//关闭webview中缓存setAllowFileAccess(true);//设置可以访问文件
其他设置
setNeedInitialFocus(true);//当webview调用requestFocus时为webview设置节点setLoadsImagesAutomatically(true);//支持自动加载图片setDefaultTextEncodingName(“utf-8”);//设置编码格式setPluginState(PluginState.OFF);//设置是否支持flash插件setDefaultFontSize(20);//设置默认字体大小
1.2WebViewClient
WebViewClient用来帮助WebView处理各种通知,请求事件。我们通过继承WebViewClient并重载它的方法可以实现不同功能的定制。具体如下所示:
shouldOverrideUrlLoading(WebViewview,Stringurl)//在网页上的所有加载都经过这个方法,这个函数我们可以做很多操作。比如获取url,查看url.contains(“add”),进行添加操作
shouldOverrideKeyEvent(WebViewview,KeyEventevent)//处理在浏览器中的按键事件。
onPageStarted(WebViewview,Stringurl,Bitmapfavicon)//开始载入页面时调用的,我们可以设定一个loading的页面,告诉用户程序在等待网络响应。
onPageFinished(WebViewview,Stringurl)//在页面加载结束时调用,我们可以关闭loading条,切换程序动作。
onLoadResource(WebViewview,Stringurl)//在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次。
onReceivedError(WebViewview,interrorCode,Stringdescription,StringfailingUrl)//报告错误信息
doUpdateVisitedHistory(WebViewview,Stringurl,booleanisReload)//更新历史记录
onFormResubmission(WebViewview,MessagedontResend,Messageresend)//应用程序重新请求网页数据
onReceivedHttpAuthRequest(WebViewview,HttpAuthHandlerhandler,Stringhost,Stringrealm)//获取返回信息授权请求
onReceivedSslError(WebViewview,SslErrorHandlerhandler,SslErrorerror)//让webview处理https请求。
onScaleChanged(WebViewview,floatoldScale,floatnewScale)//WebView发生改变时调用
onUnhandledKeyEvent(WebViewview,KeyEventevent)//Key事件未被加载时调用
1.3WebChromeClient
WebChromeClient用来帮助WebView处理JS的对话框、网址图标、网址标题和加载进度等。同样地,通过继承WebChromeClient并重载它的方法也可以实现不同功能的定制,如下所示:
publicvoidonProgressChanged(WebViewview,intnewProgress);//获得网页的加载进度,显示在右上角的TextView控件中
publicvoidonReceivedTitle(WebViewview,Stringtitle);//获取Web页中的title用来设置自己界面中的title,当加载出错的时候,比如无网络,这时onReceiveTitle中获取的标题为”找不到该网页”,
publicvoidonReceivedIcon(WebViewview,Bitmapicon);//获取Web页中的icon
publicbooleanonCreateWindow(WebViewview,booleanisDialog,booleanisUserGesture,MessageresultMsg);
publicvoidonCloseWindow(WebViewwindow);
publicbooleanonJsAlert(WebViewview,Stringurl,Stringmessage,JsResultresult);//处理alert弹出框,html弹框的一种方式
publicbooleanonJsPrompt(WebViewview,Stringurl,Stringmessage,StringdefaultValue,JsPromptResultresult)//处理confirm弹出框
publicbooleanonJsConfirm(WebViewview,Stringurl,Stringmessage,JsResultresult);//处理prompt弹出框
二WebView生命周期
2.1onResume()
WebView为活跃状态时回调,可以正常执行网页的响应。
2.2onPause()
WebView被切换到后台时回调,页面被失去焦点,变成不可见状态,onPause动作通知内核暂停所有的动作,比如DOM的解析、plugin的执行、JavaScript执行。
2.3pauseTimers()
当应用程序被切换到后台时回调,该方法针对全应用程序的WebView,它会暂停所有webview的layout,parsing,javascripttimer。降低CPU功耗。
2.4resumeTimers()
恢复pauseTimers时的动作。
2.5destroy()
关闭了Activity时回调,WebView调用destory时,WebView仍绑定在Activity上.这是由于自定义WebView构建时传入了该Activity的context对象,因此需要先从父
容器中移除WebView,然后再销毁webview。
mRootLayout.removeView(webView);
mWebView.destroy();
三WebView页面导航
3.1页面跳转
当我们在WebView点击链接时,默认的WebView会直接跳转到别的浏览器中,如果想要实现在WebView内跳转就需要设置WebViewClient,下面我们先来
说说WebView、WebViewClient、WebChromeClient三者的区别。
WebView:主要负责解析和渲染网页WebViewClient:辅助WebView处理各种通知和请求事件WebChromeClient:辅助WebView处理JavaScript中的对话框,网址图标和标题等
如果我们想控制不同链接的跳转方式,我们需要继承WebViewClient重写shouldOverrideUrlLoading()方法
staticclassCustomWebViewClientextendsWebViewClient{
privateContextmContext;
publicCustomWebViewClient(Contextcontext){
mContext=context;
}
@Override
publicbooleanshouldOverrideUrlLoading(WebViewview,Stringurl){
if(Uri.parse(url).getHost().equals("github.com/guoxiaoxing")){
//如果是自己站点的链接,则用本地WebView跳转
returnfalse;
}
//如果不是自己的站点则launch别的Activity来处理
Intentintent=newIntent(Intent.ACTION_VIEW,Uri.parse(url));
mContext.startActivity(intent);
returntrue;
}
}
关于shouldOverrideUrlLoading()方法的两点说明:
1方法返回值
返回true:Android系统会处理URL,一般是唤起系统浏览器。
返回false:当前WebView处理URL。
由于默认放回false,如果我们只想在WebView内处理链接跳转只需要设置mWebView.setWebViewClient(newWebViewClient())即可
/
Givethehostapplicationachancetotakeoverthecontrolwhenanew
urlisabouttobeloadedinthecurrentWebView.IfWebViewClientisnot
provided,bydefaultWebViewwillaskActivityManagertochoosethe
properhandlerfortheurl.IfWebViewClientisprovided,returntrue
meansthehostapplicationhandlestheurl,whilereturnfalsemeansthe
currentWebViewhandlestheurl.
ThismethodisnotcalledforrequestsusingthePOST"method".
@paramviewTheWebViewthatisinitiatingthecallback.
@paramurlTheurltobeloaded.
@returnTrueifthehostapplicationwantstoleavethecurrentWebView
andhandletheurlitself,otherwisereturnfalse.
/
publicbooleanshouldOverrideUrlLoading(WebViewview,Stringurl){
returnfalse;
}
2方法deprecated问题
shouldOverrideUrlLoading()方法在API>=24时被标记deprecated,它的替代方法是
@Override
publicbooleanshouldOverrideUrlLoading(WebViewview,WebResourceRequestrequest){
view.loadUrl(request.toString());
returntrue;
}
但是publicbooleanshouldOverrideUrlLoading(WebViewview,Stringurl)支持更广泛的API我们在使用的时候还是它,
3.2页面回退
Android的返回键,如果想要实现WebView内网页的回退,可以重写onKeyEvent()方法。
@Override
publicbooleanonKeyDown(intkeyCode,KeyEventevent){
//CheckifthekeyeventwastheBackbuttonandifthere''shistory
if((keyCode==KeyEvent.KEYCODE_BACK)&&myWebView.canGoBack()){
myWebView.goBack();
returntrue;
}
//Ifitwasn''ttheBackkeyorthere''snowebpagehistory,bubbleuptothedefault
//systembehavior(probablyexittheactivity)
returnsuper.onKeyDown(keyCode,event);
}
3.3页面滑动
关于页面滑动,我们在做下拉刷新等功能时,经常会去判断WebView是否滚动到顶部或者滚动到底部。
我们先来看一看三个判断高度的方法
getScrollY();
该方法返回的是当前可见区域的顶端距整个页面顶端的距离,也就是当前内容滚动的距离.
getHeight();
getBottom();
该方法都返回当前WebView这个容器的高度
getContentHeight();
返回的是整个html的高度,但并不等同于当前整个页面的高度,因为WebView有缩放功能,所以当前整个页面的高度实际上应该是原始html的高度
再乘上缩放比例.因此,判断方法是:
if(webView.getContentHeight()webView.getScale()==(webView.getHeight()+webView.getScrollY())){
//已经处于底端
}
if(webView.getScrollY()==0){
//处于顶端
}
以上这个方法也是我们常用的方法,不过从API17开始,mWebView.getScale()被标记为deprecated
ThismethodwasdeprecatedinAPIlevel17.Thismethodispronetoinaccuracyduetoraceconditions
betweenthewebrenderingandUIthreads;preferonScaleChanged(WebView,
因为scale的获取可以用一下方式:
publicclassCustomWebViewextendsWebView{
publicCustomWebView(Contextcontext){
super(context);
setWebViewClient(newWebViewClient(){
@Override
publicvoidonScaleChanged(WebViewview,floatoldScale,floatnewScale){
super.onScaleChanged(view,oldScale,newScale);
mCurrentScale=newScale
}
});
}
三WebView缓存实现
在项目中如果使用到WebView控件,当加载html页面时,会在/data/data/包名目录下生成database与cache两个文件夹。
请求的url记录是保存在WebViewCache.db,而url的内容是保存在WebViewCache文件夹下。
控制缓存行为
WebSettingswebSettings=mWebView.getSettings();
//优先使用缓存
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
//只在缓存中读取
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ONLY);
/不使用缓存
WwebSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
清除缓存
clearCache(true);//清除网页访问留下的缓存,由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序.
clearHistory();//清除当前webview访问的历史记录,只会webview访问历史记录里的所有记录除了当前访问记录.
clearFormData()//这个api仅仅清除自动完成填充的表单数据,并不会清除WebView存储到本地的数据。
四WebViewCookies
添加Cookies
publicvoidsynCookies(){
if(!CacheUtils.isLogin(this))return;
CookieSyncManager.createInstance(this);
CookieManagercookieManager=CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.removeSessionCookie();//移除
Stringcookies=PreferenceHelper.readString(this,AppConfig.COOKIE_KEY,AppConfig.COOKIE_KEY);
KJLoger.debug(cookies);
cookieManager.setCookie(url,cookies);
CookieSyncManager.getInstance().sync();
}
清除Cookies
CookieManager.getInstance().removeSessionCookie();
五WebView本地资源访问
当我们在WebView中加载出从web服务器上拿取的内容时,是无法访问本地资源的,如assets目录下的图片资源,因为这样的行为属于跨域行为(Cross-Domain),而WebView是禁止
的。解决这个问题的方案是把html内容先下载到本地,然后使用loadDataWithBaseURL加载html。这样就可以在html中使用file:///android_asset/xxx.png的链接来引用包里
面assets下的资源了。
privatevoidloadWithAccessLocal(finalStringhtmlUrl){
newThread(newRunnable(){
publicvoidrun(){
try{
finalStringhtmlStr=NetService.fetchHtml(htmlUrl);
if(htmlStr!=null){
TaskExecutor.runTaskOnUiThread(newRunnable(){
@Override
publicvoidrun(){
loadDataWithBaseURL(htmlUrl,htmlStr,"text/html","UTF-8","");
}
});
return;
}
}catch(Exceptione){
Log.e("Exception:"+e.getMessage());
}
TaskExecutor.runTaskOnUiThread(newRunnable(){
@Override
publicvoidrun(){
onPageLoadedError(-1,"fetchhtmlfailed");
}
});
}
}).start();
}
注意
从网络上下载html的过程应放在工作线程中html下载成功后渲染出html的步骤应放在UI主线程,不然WebView会报错html下载失败则可以使用我们前面讲述的方法来显示自定义错误界面
了解完WebView的基本用法,那就来总结下最近项目中遇到的关于WebView的坑
项目中使用WebView遇到的问题
WebView界面的原生标题设置
WebView所在界面
一般情况下,我们WebView所在界面由顶部带标题的原生导航栏跟WebView的内容部分组成,
而WebView中的界面可能在点击后还会再跳其他Web页面(如图点击请假会在当前WebView跳转请假的Web页面)。
由于点击内容的不确定性,所以通常情况下,最简单的做法就是捕获h5页面的标签来进行标题设置。
对于捕获标签内容的方式,WebView也很好地提供了支持,我们可以通过继承WebChromeClient的onReceivedTitle来进行获取:
privateclassWebViewChromeClientextendsWebChromeClient{
@Override
publicvoidonReceivedTitle(WebViewview,Stringtitle){
super.onReceivedTitle(view,title);
mTitleText.setTitle(String.valueOf(view.getTitle()));
}
}
然而这样的方式在实际使用中有一个问题:
当通过webView.goBack()方式返回上一级Web页面的时候不会触发这个方法,因此会导致标题无法跟随历史记录返回上一级页面。
所以在项目中,
我们可以通过重写WebViewClient的onPageFinished方法,在onPageFinished中对界面标题进行设置。
因为不管是历史记录的返回还是点击跳转都会触发页面加载,
当页面加载完成时(不包括js动态创建以及img图片加载完毕)都会触发onPageFinished这个方法,
此时我们去获取的标题内容不会有任何问题,可以确保在页面返回时能够获取到正确的标题。
mWebView.setWebViewClient(newWebViewClient(){
//Web页面每次加载并完成时会触发该方法
@Override
publicvoidonPageFinished(WebViewview,Stringurl){
super.onPageFinished(view,url);
mToolbar.setTitle(String.valueOf(view.getTitle()));
Log.i(LOG_TAG,"onPageFinished");
}
注:这种做法有一个缺陷,就是返回上一个界面的时候,等页面加载完成的时候标题才会显示出来,为了更好地优化,我们可以创建一个集合用来保存我们的标题,加载url的时候把标题添加进集合,当返回上一级页面的时候,从集合中取出标题进行显示,同时从集合中移除标题。
WebView中的Web页面存在标签时无法打开文件选择器
在我们的手机浏览器中,当web页面中有按钮标签的时候点击会自动打开系统的文件选择器,
然而这个功能在主流系统的WebView中没有被默认实现,
因此,为了让点击时能够打开系统的文件选择器,
我们必须通过重写WebChromeClient来实现点击打开系统文件选择器。
代码如下:
publicclassMainActivityextendsAppCompatActivity{
/Android5.0以下版本的文件选择回调/
protectedValueCallbackmFileUploadCallbackFirst;
/Android5.0及以上版本的文件选择回调/
protectedValueCallbackmFileUploadCallbackSecond;
protectedstaticfinalintREQUEST_CODE_FILE_PICKER=51426;
protectedStringmUploadableFileTypes="image/";
privateWebViewmWebView;
@Override
protectedvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initWebView();
}
privatevoidinitWebView(){
mWebView=(WebView)findViewById(R.id.my_webview);
mWebView.loadUrl("file:///android_asset/index.html");
mWebView.setWebChromeClient(newOpenFileChromeClient());
}
privateclassOpenFileChromeClientextendsWebChromeClient{
//Android2.2(APIlevel8)到Android2.3(APIlevel10)版本选择文件时会触发该隐藏方法
@SuppressWarnings("unused")
publicvoidopenFileChooser(ValueCallbackuploadMsg){
openFileChooser(uploadMsg,null);
}
//Android3.0(APIlevel11)到Android4.0(APIlevel15))版本选择文件时会触发,该方法为隐藏方法
publicvoidopenFileChooser(ValueCallbackuploadMsg,StringacceptType){
openFileChooser(uploadMsg,acceptType,null);
}
//Android4.1(APIlevel16)--Android4.3(APIlevel18)版本选择文件时会触发,该方法为隐藏方法
@SuppressWarnings("unused")
publicvoidopenFileChooser(ValueCallbackuploadMsg,StringacceptType,Stringcapture){
openFileInput(uploadMsg,null,false);
}
//Android5.0(APIlevel21)以上版本会触发该方法,该方法为公开方法
@SuppressWarnings("all")
publicbooleanonShowFileChooser(WebViewwebView,ValueCallbackfilePathCallback,WebChromewww.tt951.comClient.FileChooserParamsfileChooserParams){
if(Build.VERSION.SDK_INT>=21){
finalbooleanallowMultiple=fileChooserParams.getMode()==FileChooserParams.MODE_OPEN_MULTIPLE;//是否支持多选
openFileInput(null,filePathCallback,allowMultiple);
returntrue;
}
else{
returnfalse;
}
}
}
@SuppressLint("NewApi")
protectedvoidopenFileInput(finalValueCallbackfileUploadCallbackFirst,finalValueCallbackfileUploadCallbackSecond,finalbooleanallowMultiple){
//Android5.0以下版本
if(mFileUploadCallbackFirst!=null){
mFileUploadCallbackFirst.onReceiveValue(null);
}
mFileUploadCallbackFirst=fileUploadCallbackFirst;
//Android5.0及以上版本
if(mFileUploadCallbackSecond!=null){
mFileUploadCallbackSecond.onReceiveValue(null);
}
mFileUploadCallbackSecond=fileUploadCallbackSecond;
Intenti=newIntent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
if(allowMultiple){
if(Build.VERSION.SDK_INT>=18){
i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE,true);
}
}
i.setType(mUploadableFileTypes);
startActivityForResult(Intent.createChooser(i,"选择文件"),REQUEST_CODE_FILE_PICKER);
}
publicvoidonActivityResult(finalintrequestCode,finalintresultCode,finalIntentintent){
if(requestCode==REQUEST_CODE_FILE_PICKER){
if(resultCode==Activity.RESULT_OK){
if(intent!=null){
//Android5.0以下版本
if(mFileUploadCallbackFirst!=null){
mFileUploadCallbackFirst.onReceiveValue(intent.getData());
mFileUploadCallbackFirst=null;
}
elseif(mFileUploadCallbackSecond!=null){//Android5.0及以上版本
Uri[]dataUris=null;
try{
if(intent.getDataString()!=null){
dataUris=newUri[]{Uri.parse(intent.getDataString())};
}
else{
if(Build.VERSION.SDK_INT>=16){
if(intent.getClipData()!=null){
finalintnumSelectedFiles=intent.getClipData().getItemCount();
dataUris=newUri[numSelectedFiles];
for(inti=0;i dataUris[i]=intent.getClipData().getItemAt(i).getUri();
}
}
}
}
}
catch(Exceptionignored){}
mFileUploadCallbackSecond.onReceiveValue(dataUris);
mFileUploadCallbackSecond=null;
}
}
}
else{
//这里mFileUploadCallbackFirst跟mFileUploadCallbackSecond在不同系统版本下分别持有了
//WebView对象,在用户取消文件选择器的情况下,需给onReceiveValue传null返回值
//否则WebView在未收到返回值的情况下,无法进行任何操作,文件选择器会失效
if(mFileUploadCallbackFirst!=null){
mFileUploadCallbackFirst.onReceiveValue(null);
mFileUploadCallbackFirst=null;
}
elseif(mFileUploadCallbackSecond!=null){
mFileUploadCallbackSecond.onReceiveValue(null);
mFileUploadCallbackSecond=null;
}
注:当用户点击inputfile弹出文件选择器后,点击取消或者返回按钮没有执行选择时,必须在onActivityResult里给valueCallback的onReceiveValue传null,因为valueCallback持有的是WebView,在onReceiveValue没有回传值的情况下,WebView无法进行下一步操作,会导致取消选择文件后,点击inputfile不会再响应:
if(mFileUploadCallbackFirst!=null){
mFileUploadCallbackFirst.onReceiveValue(null);
mFileUploadCallbackFirst=null;
}
elseif(mFileUploadCallbackSecond!=null){
mFileUploadCallbackSecond.onReceiveValue(null);
mFileUploadCallbackSecond=null;
}
示例demo地址:
https://github.com/cjpx00008/FileChooser4WebViewDemo
WebView中的web页面调用系统选择器或者相机导致app进入后台被系统释放
众所周知,WebView基于webkit内核来渲染web页面,因此使用起来相当于一个小型浏览器,即使页面内容不复杂,只要使用WebView也会占用大量的内存。
而Android的内存回收机制,在系统内存不足的情况下会优先释放内存占用较大的app从而回收内存资源,此时正在使用WebView的运行在后台的app肯定是首当其冲被回收的。
因此,当WebView通过inputfile调用系统文件选择器,或者通过文件选择器调用了相机时,我们的app就进入了后台,在部分低端Android设备(尤其红米这类手机,默认的神隐模式会在app进入后台的时候较大概率的释放app)或者系统内存资源不足的情况下,我们的app就会优先被释放掉,导致文件选择完毕后,回到上一界面时,app的界面重新走了onCreate,web页面也因此重建了。
对于部分需要填写大量表单的web页面来说,用户填写的数据会随着界面的销毁重建而丢失,而选择的文件也因为页面的重建而无法回传给inputfile,这对于用户的体验来说肯定是不友好的。
也许你会说,重写onSaveInstance保存数据就是啦。
这也是我一开始考虑的,
我们的WebView也提供了saveState以及restoreState来保存状态。
然而悲催的是,这两个方法并不会保存web页面内的数据,它只保存了WebView加载的页面,前进后退的历史状态等数据。
引用官方文档的描述:
SavesthestateofthisWebViewusedinonSaveInstanceState(Bundle)
.PleasenotethatthismethodnolongerstoresthedisplaydataforthisWebView.ThepreviousbehaviorcouldpotentiallyleakfilesifrestoreState(Bundle)
wasnevercalled.
PleasenotethatthismethodnolongerstoresthedisplaydataforthisWebView
WebView的saveState并不会保存界面的数据。
所以,对于表单数据的恢复,我们只能自己想办法了,我们这里采用了两套方案:
1.通过WebView与JS交互,在onSaveInstance的时候触发界面保存数据,保存数据的方式也大体分为两种,
一种使用H5自带的localStorage来进行数据存储,页面销毁重建的时候H5页面判断本地localStorage数据是否有值,有就将值重新填充到页面表单,提交数据后清除本地localStorage的数据。
这种方式需要给WebView开启对localStorage的支持。
WebSettingssettings=mWebView.getSettings();
settings.setDomStorageEnabled(true);
另一种则提供JS接口将数据传递给原生,通过原生代码将数据保存到本地,在页面重建渲染完成时,web页面通过JS接口调用原生方法拉取数据判断是否有值,有则填充表单,无则不做操作,提交数据后调用JS接口调用原生方法清空本地数据。
由web端自己处理,在表单页面文本输入失去焦点时自动保存数据,页面销毁重建时,自己拉取数据进行判断。
这种方式对原生的依赖较低,个人更倾向这种方式,当然最终由于项目的特殊情况,我们还是采用了第一种方式。
这是表单数据的恢复,
而对于从系统文件选择器选择的文件web页面是无法直接接收并处理了,这里我们提供了一个JS接口在web页面加载完成时,进行触发,并将数据传递给web页面。
说到这里,不得不提另外一个问题
WebView调用服务端页面如何访问本地文件
上面我们提到了通过JS接口将选择的文件数据传递给web页面,
然而由于安全原因,WebView限制了远程url页面访问本地文件,
如果我们加载的url是服务端的页面,那我们没有任何办法直接通过文件地址来访问客户端本地的文件
我们知道,WebView用来加载网页的方式主要有三种:
loadUrl(Stringurl)
loadUrl(Stringurl,MapadditionalHttpHeaders)
loadData(Stringdata,StringmimeType,Stringencoding)
loadDataWithBaseURL(StringbaseUrl,Stringdata,StringmimeType,Stringencoding,StringhistoryUrl)
loadData()和loadDataWithBaseURL()都是直接将数据加载进WebView中,相当于显示的一个本地Web
loadUrl也可以通过访问本地的文件地址(例如本地asset目录下的存放了index.html页面,可以通过loadUrl("file:///android_asset/index.html")来显示web页面)
对于这样的三种加载本地内容的方式,我们可以使用多种方式来传递路径供web页面传递,这里以图片为例(相册目录下test/IMG_20170105_093405.jpg):
1.直接通过文件的绝对地址来提供给页面显示:
2.通过媒体库查询出来的contenturi地址展示
3.通过FileProvider转换的contenturi地址展示
可当你使用loadUrl(Stringurl)加载服务端的http地址时,以上三种方法将均无法使用,经过各种尝试,目前找到两种方案来提供给web端进行图片显示:
由原生代码处理,将文件流转换为Base64之后通过JS接口回传给web;
重写WebViewClient里的shouldInterceptRequest方法,每当页面发生资源请求的时候就会触发这个方法,我们可以过滤请求,判断请求是否为本地文件,通过拦截请求转换为二进制流回传回去,
示例代码如下:
mWebView.setWebViewClient(newWebViewClient(){
@Override
publicWebResourceResponseshouldInterceptRequest(WebViewview,Stringurl){
if(url.startsWith("http://")&&url.endWith(".jpg"){
returngetWebRwww.baiyuewang.netesourceResponse("/storage/emulated/0/dcim/trinaic/IMG_20170105_093405.jpg","image/jpeg",".jpg");
}
returnsuper.shouldInterceptRequest(view,url);
}
}
privateWebResourceResponsegetWebResourceResponse(Stringurl,Stringmime,Stringstyle){
WebResourceResponseresponse=null;
try{
response=newWebResourceResponse(mime,"UTF-8",newFileInputStream(newFile(url)));
}catch(FileNotFoundExceptione){
e.printStackTrace();
}
returnresponse;
}
WebViewJS注入漏洞
要想让原生跟JS进行交互,按照官方提供的方法就得使用addJavaScriptInterface
classJsObject{
@JavascriptInterface
publicStringtoString(){return"injectedObject";}
}
webView.addJavascriptInterface(newJsObject(),"injectedObject");
webView.loadData("","text/html",null);
webView.loadUrl("javascript:alert(injectedObject.toString())");
InjectsthesuppliedJavaobjectintothisWebView.TheobjectisinjectedintotheJavaScriptcontextofthemainframe,usingthesuppliedname.ThisallowstheJavaobject’smethodstobeaccessedfromJavaScript.ForapplicationstargetedtoAPIlevelJELLY_BEAN_MR1
andabove,onlypublicmethodsthatareannotatedwithJavascriptInterface
canbeaccessedfromJavaScript.ForapplicationstargetedtoAPIlevelJELLY_BEAN
orbelow,allpublicmethods(includingtheinheritedones)canbeaccessed,seetheimportantsecuritynotebelowforimplications.
引用官方api的说明,在Android4.2以下,会有被注入的风险,4.2以上版本可以通过@JavascriptInterface的注解来处理这个问题。
具体的注入方式,我找了篇博客,如果有不清楚的同学可以了解下:
AndroidWebView的Js对象注入漏洞解决方案
在之前乌云平台报出的漏洞中,
android/webkit/webview中默认内置的一个searchBoxJavaBridge_接口同时存在远程代码执行漏洞
在于android/webkit/AccessibilityInjector.java中,调用了此组件的应用在开启辅助功能选项中第三方服务的安卓系统中会造成远程代码执行漏洞。这两个接口分别是”accessibility”和”accessibilityTraversal”,此漏洞原理与searchBoxJavaBridge_接口远程代码执行相似,均为未移除不安全的默认接口,不过此漏洞需要用户启动系统设置中的第三方辅助服务,利用条件较复杂。
因此,一般情况下我们通过removeJavaScripteInterface来移除这几个接口
if(Build.VERSION.SDK_INT<17){
mAdvanceWebView.removeJavascriptInterface("searchBoxJavaBridge_");
mAdvanceWebView.removeJavascriptInterface("accessibility");
mAdvanceWebView.removeJavascriptInterface("accessibilityTraversal");
}
除此之外也有通过onJsPrompt的方式来实现WebView原生跟JS交互功能的,github上的开源项目JSBridge就是采用这种方法:
https://github.com/lzyzsd/JsBridge
之前拜读过大名鼎鼎的cordova的源码,它内部的原生JS交互也是采用onJsPrompt的方式,不过在此基础上做了更强大的封装。
WebView后台耗电问题
当我们的WebView的web页面在解析或者播放视频再或者有js定时器在执行的时,
如果我们把应用退到后台,不做任何处理的情况下,以上的操作还会在后台继续执行,导致WebView在后台持续耗电,因此一般我们会做以下处理
@Override
protectedvoidonPause(){
super.onPause();
mWebView.onPause();//暂停部分可安全处理的操作,如动画,定位,视频播放等
mWebView.pauseTimers();//暂停所有WebView的页面布局、解析以及JavaScript的定时器操作
}
@Override
protectedvoidonResume(){
super.onResume();
mWebView.onResume();
mWebView.resumeTimers();
}
对于WebView的使用,在处理问题的过程中发现一个不错的开源库:
|
|