欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android混合开发之WebView的基础用法介绍

程序员文章站 2022-12-25 17:27:12
前言 android项目中webview是必不可少的,越来越开的迭代节奏导致越来越多的app采用混合开发,接着我们就介绍一下android中webview的使用。 一、混合开发的优缺点: 优点: 1...

前言

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缓存目录设置到了内部私有存储。