Android混合开发之WebView的基础用法介绍
前言
android项目中webview是必不可少的,越来越开的迭代节奏导致越来越多的app采用混合开发,接着我们就介绍一下android中webview的使用。
一、混合开发的优缺点:
优点:
1.开发成本较低:android和ios使用一个地址就可以。
2.自动更新最新的web内容。
3.兼容平台较多。
缺点:
1.用户体验没有原生的炫酷。
2.连接网络等性能较差。
但瑕不掩瑜,对于不需要炫酷动效的简单页面如:用户协议、注册说明、banner跳转的一些页面、图文展示的文章等页面都可以用webview来完成!
二、如何配置webview:
常用的类:
websettings用于管理webview状态配置
webview.getsettings().setdisplayzoomcontrols(false);//是否使用内置缩放机制 webview.getsettings().setsupportzoom(true);// 是否支持变焦 wvsignin.getsettings().setbuiltinzoomcontrols(true);// 设置webview是否应该使用其内置变焦机制,显示放大缩小 webview.getsettings().setusewideviewport(true);//是否开启控件viewport。默认false,自适应;true时标签中指定宽度值生效 webview.getsettings().setloadwithoverviewmode(true); webview.setinitialscale(100);// 初始化时缩放 webview.getsettings().setjavascriptenabled(true);
webviewclient :主要帮助webview处理各种通知、请求事件的。
shouldoverrideurlloading(打开网页时不调用)
onloadresource(在加载页面资源时会调用)
onpagestart(设定加载开始的操作)
onpagefinish(在页面加载结束时调用。我们可以关闭loading 条,切换程序动作)
onreceiveerror(加载出错调用)
//首先选择加载方式 //方式1. 加载一个网页: webview.loadurl("http://www.google.com/"); //方式2:加载apk包中的html页面 webview.loadurl("file:///android_asset/test.html"); //方式3:加载手机本地的html页面 webview.loadurl("content://com.android.htmlfileprovider/sdcard/test.html"); webview.setwebviewclient(new webviewclient() { @override public void doupdatevisitedhistory(webview view, string url, boolean isreload) { super.doupdatevisitedhistory(view, url, isreload); } public boolean shouldoverrideurlloading(webview view, string url) { view.loadurl(url);// 点击超链接的时候重新在原来进程上加载url return true; } @override public void onpagefinished(webview view, string url) { } });
webchromeclien:webchromeclient主要辅助webview处理javascript的对话框、网站图标、网站title、加载进度等
onclosewindow(关闭webview)
oncreatewindow()
onjsalert (webview上alert无效,需要定制webchromeclient处理弹出)
onjsprompt(支持javascript输入框)
onjsconfirm(支持javascript的确认框)
onprogresschanged(获得网页的加载进度并显示)
onreceivedicon(获取webview的icon)
onreceivedtitle(获取webview的标题)
如果你的webview只是用来处理一些html的页面内容,只用webviewclient就行了,如果需要更丰富的处理效果,比如js、进度条等,就要用到webchromeclient。
进度条例子:
webview.setwebchromeclient(webchromeclient) webchromeclient webchromeclient = new webchromeclient() { public void onprogresschanged(webview view, int progress) { super.onprogresschanged(view, progress); wvprogressbar.setmax(100); if (progress < 100) { wvprogressbar.setvisibility(view.visible); if (progress < 10) { wvprogressbar.setprogress(10); } else { wvprogressbar.setprogress(progress); } } else { wvprogressbar.setprogress(100); wvprogressbar.setvisibility(view.gone); } } @override public void onreceivedtitle(webview view, string title) { super.onreceivedtitle(view, title); } };
三、webview和js的交互:
之间的交互无非两种,android调用js,js调用android。
1.android调用js:
- 通过webview的loadurl()
webview.loadurl(url);里面可以配置userid、page等信息。
通过webview的evaluatejavascript()
比第一种方法效率更高,有返回值。但只支持android4.4以上 mwebview.evaluatejavascript(url, new valuecallback() { @override public void onreceivevalue(string value) { //结果 } });
2.js调用android:
- 通过 addjavascriptinterface(存在严重的问题)
被js调用的方法必须加入@javascriptinterface注解 @javascriptinterface public void getdata(string msg) { system.out.println("js调用了android的getdata方法"); }
通过webviewclient 的方法回调拦截,常用的如 shouldoverrideurlloading ()方法回调拦截 url(不存在漏洞,但使用麻烦)
android通过 webviewclient 的回调方法shouldoverrideurlloading ()拦截 url
解析该 url 的协议
如果检测到是预先约定好的协议,就调用相应方法
四、封装一下webview让使用更简单~
public class mywebview extends webview { private int currenty = 0; private context context; public mywebview(context context) { super(context); init(context); } public mywebview(context context, attributeset attrs, int defstyle) { super(context, attrs, defstyle); init(context); } public mywebview(context context, attributeset attrs) { super(context, attrs); init(context); } private void init(context context) { this.context = context; initwebsettings(); } @override public boolean ontouchevent(motionevent ev) { return super.ontouchevent(ev); } @override protected void onscrollchanged(int l, int t, int oldl, int oldt) { currenty = t; super.onscrollchanged(l, t, oldl, oldt); } public int getcurrenty() { return currenty; } public boolean canzoomin() { boolean canzoomin = true; try { field mactualscale = webview.class.getdeclaredfield("mactualscale"); field mmaxzoomscale = webview.class .getdeclaredfield("mmaxzoomscale"); mactualscale.setaccessible(true); mmaxzoomscale.setaccessible(true); canzoomin = mactualscale.getfloat(this) < mmaxzoomscale .getfloat(this); } catch (exception e) { try { field mzoommanager = webview.class .getdeclaredfield("mzoommanager"); if (mzoommanager != null) { mzoommanager.setaccessible(true); object zoommanager = mzoommanager.get(this); if (zoommanager != null) { field membeddedzoomcontrol = zoommanager.getclass() .getdeclaredfield("membeddedzoomcontrol"); if (membeddedzoomcontrol != null) { membeddedzoomcontrol.setaccessible(true); object zoomcontrolembedded = membeddedzoomcontrol .get(zoommanager); if (zoomcontrolembedded != null) { mzoommanager = zoomcontrolembedded.getclass() .getdeclaredfield("mzoommanager"); if (mzoommanager != null) { mzoommanager.setaccessible(true); zoommanager = mzoommanager .get(zoomcontrolembedded); method canzoominmethod = zoommanager .getclass().getdeclaredmethod( "canzoomin"); if (canzoominmethod != null) { canzoominmethod.setaccessible(true); object canzoominobj = canzoominmethod .invoke(zoommanager, new object[0]); if (canzoominobj != null && canzoominobj instanceof boolean) { return (boolean) canzoominobj; } } } } } } } } catch (exception e2) { } } return canzoomin; } public boolean canzoomout() { boolean canzoomout = true; try { field mactualscale = webview.class.getdeclaredfield("mactualscale"); field mminzoomscale = webview.class .getdeclaredfield("mminzoomscale"); field minzoomoverview = webview.class .getdeclaredfield("minzoomoverview"); mactualscale.setaccessible(true); mminzoomscale.setaccessible(true); minzoomoverview.setaccessible(true); canzoomout = mactualscale.getfloat(this) > mminzoomscale .getfloat(this) && !minzoomoverview.getboolean(this); } catch (exception e) { try { field mzoommanager = webview.class .getdeclaredfield("mzoommanager"); if (mzoommanager != null) { mzoommanager.setaccessible(true); object zoommanager = mzoommanager.get(this); if (zoommanager != null) { field membeddedzoomcontrol = zoommanager.getclass() .getdeclaredfield("membeddedzoomcontrol"); if (membeddedzoomcontrol != null) { membeddedzoomcontrol.setaccessible(true); object zoomcontrolembedded = membeddedzoomcontrol .get(zoommanager); if (zoomcontrolembedded != null) { mzoommanager = zoomcontrolembedded.getclass() .getdeclaredfield("mzoommanager"); if (mzoommanager != null) { mzoommanager.setaccessible(true); zoommanager = mzoommanager .get(zoomcontrolembedded); method canzoomoutmethod = zoommanager .getclass().getdeclaredmethod( "canzoomout"); if (canzoomoutmethod != null) { canzoomoutmethod.setaccessible(true); object canzoomoutobj = canzoomoutmethod .invoke(zoommanager, new object[0]); if (canzoomoutobj != null && canzoomoutobj instanceof boolean) { return (boolean) canzoomoutobj; } } } } } } } } catch (exception e2) { } } return canzoomout; } @suppresslint("setjavascriptenabled") @suppresswarnings("deprecation") private void initwebsettings() { websettings websettings = getsettings(); websettings.setrenderpriority(renderpriority.high); websettings.settextsize(textsize.normal);// 设置字体 // 设置支持缩放 websettings.setallowfileaccess(true);// 设置可以访问文件 websettings.setdomstorageenabled(true); // 设置屏幕自适应 if (integer.parseint(build.version.sdk) <= 10) { websettings.setlayoutalgorithm(layoutalgorithm.narrow_columns); } else { websettings.setlayoutalgorithm(layoutalgorithm.normal); } // 启用插件 // websettings.setpluginsenabled(true); // 设置缓存 websettings.setappcacheenabled(true); websettings.setappcachemaxsize(10 * 1204 * 1024); websettings.setdatabaseenabled(true); websettings.setblocknetworkimage(false);//延迟加载图片,首先加载文字 this.setbackgroundcolor(context.getresources().getcolor( r.color.white)); websettings.setcachemode(websettings.load_no_cache); // 去掉缩放按钮 websettings.setdisplayzoomcontrols(false); websettings.setsupportzoom(true);// 是否支持变焦 websettings.setbuiltinzoomcontrols(true);// 设置webview是否应该使用其内置变焦机制,显示放大缩小 websettings.setusewideviewport(true); websettings.setloadwithoverviewmode(true); this.setinitialscale(100);// 初始化时缩放 websettings.setjavascriptenabled(true); this.addjavascriptinterface(new javascriptinterface(context), "android"); this.setwebviewclient(new webviewclient() { @override public void doupdatevisitedhistory(webview view, string url, boolean isreload) { super.doupdatevisitedhistory(view, url, isreload); } public boolean shouldoverrideurlloading(webview view, string url) { dfhewebview.this.loadurl(url);// 点击超链接的时候重新在原来进程上加载url return true; } @override public void onpagefinished(webview view, string url) { } }); } @override public boolean onkeydown(int keycode, keyevent event) { // todo auto-generated method stub return super.onkeydown(keycode, event); } public interface onscrolllistener { void onscroll(); } } 628180
五、webview 踩过的坑:
1.webview内存泄漏问题
解决方案:
1.展示webview的activity可以另开一个进程,这样就能和我们app的主进程分开了,即使webview产生了oom崩溃等问题也不会影响到主程序,在androidmanifest.xml的activity标签里加上android:process=”packagename.web”就可以了,并且当这个 进程结束时,请手动调用system.exit(0)。
2. 如果实在不想用开额外进程的方式解决webview 内存泄露的问题,那么下面的方法很大程度上可以避免这种情况,在webview的 destroy方法里 调用这个方法就行了。
public void releaseallwebviewcallback() { if (android.os.build.version.sdk_int < 16) { try { field field = webview.class.getdeclaredfield("mwebviewcore"); field = field.gettype().getdeclaredfield("mbrowserframe"); field = field.gettype().getdeclaredfield("sconfigcallback"); field.setaccessible(true); field.set(null, null); } catch (nosuchfieldexception e) { if (buildconfig.debug) { e.printstacktrace(); } } catch (illegalaccessexception e) { if (buildconfig.debug) { e.printstacktrace(); } } } else { try { field sconfigcallback = class.forname("android.webkit.browserframe").getdeclaredfield("sconfigcallback"); if (sconfigcallback != null) { sconfigcallback.setaccessible(true); sconfigcallback.set(null, null); } } catch (nosuchfieldexception e) { if (buildconfig.debug) { e.printstacktrace(); } } catch (classnotfoundexception e) { if (buildconfig.debug) { e.printstacktrace(); } } catch (illegalaccessexception e) { if (buildconfig.debug) { e.printstacktrace(); } } } }
2.getsettings().setbuiltinzoomcontrols(true) 引发的crash。
这个方法调用以后 如果你触摸屏幕 弹出的提示框还没消失的时候 你如果activity结束了 就会报错了。3.0以上 4.4以下很多手机会出现这种情况。解决方法是在activity的ondestroy方法里手动的将webiew设置成 setvisibility(view.gone)
3.webview后台耗电问题。
webview会自己开启一些线程,如果没有正确的销毁,这些残留的线程会一直在后台运行,导致耗费电量。还有在有的手机里,你如果webview加载的html里 有一些js 一直在执行比如动画之类的东西,如果此刻webview 挂在了后台,这些资源是不会被释放 用户也无法感知,导致一直占有cpu 耗电特别快。
解决方案:在activity.ondestroy()中直接调用system.exit(0),使得应用程序完全被移出。在activity的onstop和onresume里分别把setjavascriptenabled();给设置成false和true。
六、webview进阶,缓存原理:
原文链接:webview缓存原理分析和应用
减少流量和资源的占用,加载完一次后js没有变化就不再发起网络请求去加载网页
- 浏览器自带的网页数据缓存:浏览器自带
- h5缓存:由web页面的开发者设置
利用app cache 来缓存js文件
浏览器缓存机制是通过http协议header里的cache-control和last-modified等字段来控制的。
接受响应时: 加载文件时,浏览器是否发出请求字段:
cache-control:max-age=36000**,这表示缓存时长为36000秒。如果36000秒内需要再次请求这个文件,那么浏览器不会发出请求,直接使用本地的缓存的文件。这是http/1.1标准中的字段。
发起请求时:服务器决定文件是否需要更新的字段:
last-modified:wed, 28 sep 2016 09:24:35 gmt,表示这个文件最后的修改时间是2016年9月28日9点24分35秒。这个字段对于浏览器来说,会在下次请求的时候作为request header的if-modified-since字段带上。例如浏览器缓存的文件已经超过了cache-control,那么需要加载这个文件时,就会发出请求,请求的header有一个字段为if-modified-since:wed, 28 sep 2016 09:24:35 gmt,服务器接收到请求后,会把文件的last-modified时间和这个时间对比,如果时间没变,那么浏览器将返回304 not modified给浏览器,且content-length肯定是0个字节。如果时间有变化,那么服务器会返回200 ok,并返回相应的内容给浏览器。
webview 如何设置:设置webview的cache mode:
- load_cache_only: 不使用网络,只读取本地缓存数据。
- load_default: 根据cache-control决定是否从网络上取数据。
- load_cache_normal: api level 17中已经废弃,从api level 11开始作用同load_default模式
- load_no_cache: 不使用缓存,只从网络获取数据。
- load_cache_else_network,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。本地没有缓存时才从网络上获取。
例子
websettings settings = webview.getsettings(); settings.setcachemode(websettings.load_default);
浏览器默认缓存的路径
webview自带的浏览器协议支持的缓存,在不同的系统版本上,位置是不一样的。
h5的缓存
这个cache是由开发web页面的开发者控制的,而不是由native去控制的,但是native里面的webview也需要我们做一下设置才能支持h5的这个特性
1.工作原理
写web页面代码时,指定manifest属性即可让页面使用app cache。通常html页面代码会这么写:
xxx.appcache文件用的是相对路径,这时appcache文件的路径是和页面一样的。也可以使用的绝对路径,但是域名要保持和页面一致。
完整的xxx.appcache文件一般包括了3个section,基本格式如下:
cache manifest # 2017-05-13 v1.0.0 /bridge.js network: * fallback: /404.html
cache manifest下面文件就是要被浏览器缓存的文件 network下面的文件就是要被加载的文件
fallback下面的文件是目标页面加载失败时的显示的页面
appcache工作的原理:
当一个设置了manifest文件的html页面被加载时,cache manifest指定的文件就会被缓存到浏览器的app cache目录下面。当下次加载这个页面时,会首先应用通过manifest已经缓存过的文件,然后发起一个加载xxx.appcache文件的请求到服务器,如果xxx.appcache文件没有被修改过,那么服务器会返回304 not modified给到浏览器,如果xxx.appcache文件被修改过,那么服务器会返回200 ok,并返回新的xxx.appcache文件的内容给浏览器,浏览器收到之后,再把新的xxx.appcache文件中指定的内容加载过来进行缓存。
可以看到,appcache缓存需要在每次加载页面时都发出一个xxx.appcache的请求去检查manifest文件是不是有更新(byte by byte)。根据这篇文章(h5 缓存机制浅析 移动端 web 加载性能优化)的介绍,appcache有一些坑的地方,且官方已经不推荐使用了,但目前主流的浏览器依然是支持的。文章里主要提到下面这些坑:
要更新缓存的文件,需要更新包含它的 manifest 文件,那怕只加一个空格。常用的方法,是修改 manifest 文件注释中的版本号。如:# 2012-02-21 v1.0.0
被缓存的文件,浏览器是先使用,再通过检查 manifest 文件是否有更新来更新缓存文件。这样缓存文件可能用的不是最新的版本。 在更新缓存过程中,如果有一个文件更新失败,则整个更新会失败。 manifest 和引用它的html要在相同 host。 manifest 文件中的文件列表,如果是相对路径,则是相对 manifest 文件的相对路径。 manifest 也有可能更新出错,导致缓存文件更新失败。 没有缓存的资源在已经缓存的 html 中不能加载,即使有网络。例如:http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/ manifest 文件本身不能被缓存,且 manifest 文件的更新使用的是浏览器缓存机制。所以 manifest 文件的 cache-control 缓存时间不能设置太长。
2.webview如何设置才能支持appcache
webview默认是没有开启appcache支持的,需要添加下面这几行代码来设置:
websettings websettings = webview.getsettings(); websettings.setappcacheenabled(true); string cachepath = getapplicationcontext().getcachedir().getpath(); // 把内部私有缓存目录'/data/data/包名/cache/'作为webview的appcache的存储路径 websettings.setappcachepath(cachepath); websettings.setappcachemaxsize(5 * 1024 * 1024);
注意:websettings的setappcacheenabled和setappcachepath都必须要调用才行。
3.存储appcache的路径
按照android sdk的api说明,setappcachepath是可以用来设置appcache路径的,但是我实际测试发现,不管你怎么设置这个路径,设置到应用自己的内部私有目录还是外部sd卡,都无法生效。appcache缓存文件最终都会存到/data/data/包名/app_webview/cache/application cache这个文件夹下面,在上面的android 4.4和5.1系统目录截图可以看得到,但是如果你不调用setappcachepath方法,webview将不会产生这个目录。这里有点让我觉得奇怪,我猜测可能从某一个系统版本开始,为了缓存文件的完整性和安全性考虑,sdk实现的时候就吧appcache缓存目录设置到了内部私有存储。
推荐阅读
-
Android图片加载框架之Glide 4的用法介绍
-
Android开发之进程的种类和介绍
-
Android 游戏开发之Canvas画布的介绍及方法
-
Android混合开发之WebView的基础用法介绍
-
Android开发之AlarmManager的用法详解
-
Android开发之StackView用法和遇到的坑分析
-
Android应用开发之(TableLayout中stretchColumns、shrinkColumns的用法)
-
android开发之webview的使用方法
-
Android开发利器之Data Binding Compiler V2 —— 搭建Android MVVM完全体的基础
-
Android开发笔记之如何正确获取WebView的网页Title