为什么要学习Android与H5互调?

微信,QQ空间等大量软件都内嵌了H5,不得不说是一种趋势。Android与H5互调可以让我们的实现混合开发,至于混合开发就是在一个App中内嵌一个轻量级的浏览器,一部分原生的功能改为Html 5来开发。

优势:使用H5实现的功能能够在不升级App的情况下动态更新,而且可以在Android或iOS的App上同时运行,节约了成本,提高了开发效率。

原理:其实就是Java代码和JavaScript之间的调用。

开局插入一张文章的目录结构:

WebView简介

要实现Android与H5互调,WebView是一个很重要的控件,WebView可以很好地帮助我们展示html页面,所以有必要先了解一下WebView。

一丶WebView常用方法

loadUrl

加载界面,其次还有LoadData和LoadDataWithBase方法

//加载assets目录下的test.html文件
webView.loadUrl("file:///android_asset/test.html");
//加载网络资源(注意要加上网络权限)
webView.loadUrl(http://blog.csdn.net);

setWebViewClient(如果用户设置了WebViewClient,则在点击新的链接以后就不会跳转到系统浏览器了,而是在本WebView中显示。注意:并不需要覆盖 shouldOverrideUrlLoading 方法,同样可以实现所有的链接都在 WebView 中打开。)

WebViewClient主要用来辅助WebView处理各种通知、请求等事件,通过setWebViewClient方法设置。以下是它的几种常见用法:

实现对网页中超链接的拦截(比如如果是极客导航的主页,则直接拦截转到百度主页):

当点击页面中的链接后,会在WebView加载URL前回调shouldOverrideUrlLoading(WebView view, String url)方法,一般点击一个链接此方法调用一次。

 webView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
 if("http://www.jikedaohang.com/".equals(url)) {
 view.loadUrl("https://www.baidu.com/");
 }
 return true;
 }
 });

关于shouldOverrideUrlLoading返回值的误区:网上很多解释是return true代表在本WebView中打开链接,return false代表调用系统浏览器打开链接。其实只要设置了WebViewClient,则就不会调用系统浏览器。

那么shouldOverrideUrlLoading的返回值到底代表什么呢?return true,则在打开新的url时WebView就不会再加载这个url了,所有处理都需要在WebView中操作,包含加载;return false,则系统就认为上层没有做处理,接下来还是会继续加载这个url的;默认return false。具体的区别展示如下:

加载百度主页,设置WebViewClient后,重写shouldOverrideUrlLoading(WebView view, String url)方法,第一张是返回false的截图(点击后正常跳转),第二章是返回true的截图(点击无反应,如果希望能够跳转,则需要我们自己进行处理):

 

还有一点需要注意的是,如果我们拦截了某个url,那么return false 和 return true区别不大,所以一般建议 return false。

加载网页时替换某个资源(比如在加载一个网页时,需要加载一个logo图片,而我们想要替换这个logo图片,用我们assets目录下的一张图片替代)

我们知道我们在加载一个网页的同时也会加载js,css,图片等资源,所以会多次调用shouldInterceptRequest方法,我们可以在shouldInterceptRequest中进行图片替换。

注意:shouldInterceptRequest有两个重载:

public WebResourceResponse shouldInterceptRequest (WebView view, String url) 【已过时】

public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request)

这两种方法主要是第二个参数的不同,WebResourceRequest 将能够获取更多的信息,提供了getUrl(),getMethod,getRequestHeaders等方法。这里主要是为了展示效果,使用了第一种回调方法。实现方法如下:

mWebView.setWebViewClient(new WebViewClient(){
 @Override
 public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
 WebResourceResponse response = null;
 if (url.contains("logo")) {
 try {
 InputStream logo = getAssets().open("logo.png");
 response = new WebResourceResponse("image/png", "UTF-8", logo);
 } catch (IOException e) {
 e.printStackTrace();
 }
 }
 return response;
 }
 });

设置开始加载网页、加载完成、加载错误时处理

webView.setWebViewClient(new WebViewClient() {

 @Override
 public void onPageStarted(WebView view, String url, Bitmap favicon) {
 super.onPageStarted(view, url, favicon);
 // 开始加载网页时处理 如:显示"加载提示" 的加载对话框
 ...
 }

 @Override
 public void onPageFinished(WebView view, String url) {
 super.onPageFinished(view, url);
 // 网页加载完成时处理 如:让 加载对话框 消失
 ...
 }

 @Override
 public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
 super.onReceivedError(view, errorCode, description, failingUrl);
 // 加载网页失败时处理 如:提示失败,或显示新的界面
 ...
 }
}); 

处理https请求,为WebView处理ssl证书设置WebView默认是不处理https请求的,需要在WebViewClient子类中重写父类的onReceivedSslError函数

webView.setWebViewClient(new WebViewClient() {
 @Override
 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
 handler.proceed(); // 接受信任所有网站的证书
 // handler.cancel(); // 默认操作 不处理
 // handler.handleMessage(null); // 可做其他处理
 }
}); 

setWebChromeClient

WebChromeClient主要用来辅助WebView处理Javascript的对话框、网站图标、网站标题以及网页加载进度等。通过WebView的setWebChromeClient()方法设置。

显示页面加载进度在WebChromeClient子类中重写父类的onProgressChanged函数,progress表示当前页面加载的进度,为1至100的整数

webView.setWebChromeClient(new WebChromeClient() {
 public void onProgressChanged(WebView view, int progress) {
 setTitle("页面加载中,请稍候..." + progress + "%");
 setProgress(progress * 100);

 if (progress == 100) {
 //...
 }
 }
}); 

加快HTML网页加载完成速度(默认情况html代码下载到WebView后,webkit开始解析网页各个节点,发现有外部样式文件或者外部脚本文件时,会异步发起网络请求下载文件,但如果在这之前也有解析到image节点,那势必也会发起网络请求下载相应的图片。在网络情况较差的情况下,过多的网络请求就会造成带宽紧张,影响到css或js文件加载完成的时间,造成页面空白loading过久。解决的方法就是告诉WebView先不要自动加载图片,等页面finish后再发起图片加载。)

//1.首先在WebView初始化时添加如下代码
if(Build.VERSION.SDK_INT >= 19) {
/*对系统API在19以上的版本作了兼容。因为4.4以上系统在onPageFinished时再恢复图片加载时,如果存在多张图片引用的是相同的src时,会只有一个image标签得到加载,因而对于这样的系统我们就先直接加载。*/ webView.getSettings().setLoadsImagesAutomatically(true);
 } else {
 webView.getSettings().setLoadsImagesAutomatically(false);
 }

//2.在WebView的WebViewClient子类中重写onPageFinished()方法添加如下代码:
 @Override
public void onPageFinished(WebView view, String url) {
 if(!webView.getSettings().getLoadsImagesAutomatically()) {
 webView.getSettings().setLoadsImagesAutomatically(true);
 }
} 

setDownloadListener

通常webview渲染的界面中含有可以下载文件的链接,点击该链接后,应该开始执行下载的操作并保存文件到本地中。

创建DownloadListener

class MyDownloadListenter implements DownloadListener{
 @Override
 public void onDownloadStart(String url, String userAgent,String contentDisposition, String mimetype, long contentLength) {
 //下载任务...,主要有两种方式
 //(1)自定义下载任务
 //(2)调用系统的download的模块
 Uri uri = Uri.parse(url);
 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
 startActivity(intent);
 }
}

给webview加入监听

webview.setDownloadListener(new MyDownloadListenter());

goBack()

返回上一浏览页面,通过重写onKeyDown方法实现点击返回键返回上一浏览页面而非退出程序

public boolean onKeyDown(int keyCode, KeyEvent event) {
//其中webView.canGoBack()在webView含有一个可后退的浏览记录时返回true
 if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) {
 webView.goBack();
 return true;
 }
 return super.onKeyDown(keyCode, event);
 }
}

二丶WebSettings配置

获取WebSettings对象

常用设置方法

(1)支持js

(2)设置缓存方式,主要有以下几种:

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,都使用缓存中的数据。

(3)开启DOM storage API功能(HTML5 提供的一种标准的接口,主要将键值对存储在本地,在页面加载完毕后可以通过 JavaScript 来操作这些数据。)

(4)设置数据库缓存路径

(5)设置Application Caches缓存目录

(6)设置默认编码

(7)将图片调整到适合webview的大小

(8)支持缩放

(9)支持内容重新布局

(10)多窗口

(11)设置可以访问文件

(12)当webview调用requestFocus时为webview设置节点

(13)设置支持缩放

(14)支持通过JS打开新窗口

(15)缩放至屏幕的大小

(16)支持自动加载图片

三丶WebViewClient 的回调方法列表

WebViewClient主要用来辅助WebView处理各种通知、请求等事件,通过setWebViewClient方法设置。

(1)更新历史记录

(2)应用程序重新请求网页数据

(3)在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次。

(4)开始载入页面调用,通常我们可以在这设定一个loading的页面,告诉用户程序在等待网络响应。

(5)在页面加载结束时调用。同样道理,我们知道一个页面载入完成,于是我们可以关闭loading 条,切换程序动作。

(6)报告错误信息

(7)获取返回信息授权请求

(8)重写此方法可以让webview处理https请求。

(9)WebView发生改变时调用

(10)Key事件未被加载时调用

(11)重写此方法才能够处理在浏览器中的按键事件。

(12)在网页跳转时调用,这个函数我们可以做很多操作,比如我们读取到某些特殊的URL,于是就可以不打开地址,取消这个操作,进行预先定义的其他操作,这对一个程序是非常必要的。

(13)在加载某个网页的资源的时候多次调用(已过时)

(14)在加载某个网页的资源的时候多次调用

注意:

  • shouldOverrideUrlLoading在网页跳转的时候调用,且一般每跳转一次只调用一次。
  • shouldInterceptRequest只要是网页加载的过程中均会调用,资源加载的时候都会回调该方法,会多次调用。

四丶WebChoromeClient的回调方法列表

WebChromeClient主要用来辅助WebView处理Javascript的对话框、网站图标、网站标题以及网页加载进度等。通过WebView的setWebChromeClient()方法设置。

(1)监听网页加载进度

(2)监听网页标题 : 比如百度页面的标题是“百度一下,你就知道”

(3)监听网页图标

Java和JavaScript互调

为方便展示,使用addJavascriptInterface方式实现与本地js交互(存在漏洞)。也可通过其他方式实现,比如拦截ur进行参数解析l等。

Java调JS

首先是JS的一段代码:

function javaCallJs(arg){
 document.getElementById("content").innerHTML =
 ("欢迎:"+arg );
 }

然后是在java中调用JS中的方法

webView.loadUrl("javascript:javaCallJs("+"'"+name+"'"+")");

以上代码就是调用了JS中一个叫javaCallJs(arg)的方法,并传入了一个name参数。(具体效果下面有展示)

JS调java

配置Javascript接口

webView.addJavascriptInterface(new JSInterface (),"Android");

实现Javascript接口类

class JSInterface {
 @JavascriptInterface
 public void showToast(String arg){
 Toast.makeText(MainActivity.this,arg,Toast.LENGTH_SHORT).show();
 }
}

JS中调用java代码

<input type="button" value="点击Android被调用" onclick="window.Android.showToast('JS中传来的参数')"/>

window.Android.showToast(‘JS中传来的参数')”中的”Android”即addJavascriptInterface()中指定的,并且JS向java传递了参数,类型为String。而showToast(String arg)会以Toast的形式弹出此参数。

java与JS互调代码示例

先看效果图:

代码非常简单,并且加了注释,直接看代码就可以了。

首先是本地的JavaAndJavaScriptCall.html文件,放在asstes目录下

<html>
<head>
 <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
 <script type="text/javascript">

 function javaCallJs(arg){
 document.getElementById("content").innerHTML =
 ("欢迎:"+arg );
 }

 </script>
</head>
<body>
 <div id="content"> 请在上方输入您的用户名</div>
 <input type="button" value="点击Android被调用" onclick="window.Android.showToast('JS中传来的参数')"/>
</body>
</html>

javaCallJs是java调用JS的方法,showToast方法是JS调用java的方法

接下来是布局文件,activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:id="@+id/ll_root"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:orientation="vertical"
 tools:context=".MainActivity">
 <LinearLayout
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 android:padding="20dp"
 android:background="#000088">
 <EditText
 android:id="@+id/et_user"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:hint="输入WebView中要显示的用户名"
 android:background="#008800"
 android:textSize="16sp"
 android:layout_weight="1"/>
 <Button
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_marginLeft="40dp"
 android:layout_marginRight="20dp"
 android:textSize="16sp"
 android:text="确定"
 android:onClick="click"/>
 </LinearLayout>
</LinearLayout>

很简单,就是一个输入框和一个确定按钮,点击按钮会调用JS中的方法。

MainActivity

package com.wangjian.webviewdemo;
import android.annotation.SuppressLint;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
 private WebView webView;
 private LinearLayout ll_root;
 private EditText et_user;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 ll_root = (LinearLayout) findViewById(R.id.ll_root);
 et_user = (EditText) findViewById(R.id.et_user);
 initWebView();
 }

 //初始化WebView
 private void initWebView() {
 //动态创建一个WebView对象并添加到LinearLayout中
 webView = new WebView(getApplication());
 LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
 webView.setLayoutParams(params);
 ll_root.addView(webView);
 //不跳转到其他浏览器
 webView.setWebViewClient(new WebViewClient() {
 @Override
 public boolean shouldOverrideUrlLoading(WebView view, String url) {
 view.loadUrl(url);
 return true;
 }
 });
 WebSettings settings = webView.getSettings();
 //支持JS
 settings.setJavaScriptEnabled(true);
 //加载本地html文件
 webView.loadUrl("file:///android_asset/JavaAndJavaScriptCall.html");
 webView.addJavascriptInterface(new JSInterface(),"Android");
 }

 //按钮的点击事件
 public void click(View view){
 //java调用JS方法
 webView.loadUrl("javascript:javaCallJs(" + "'" + et_user.getText().toString()+"'"+")");
 }

 //在页面销毁的时候将webView移除
 @Override
 protected void onDestroy() {
 super.onDestroy();
 ll_root.removeView(webView);
 webView.stopLoading();
 webView.removeAllViews();
 webView.destroy();
 webView = null;
 }

 private class JSInterface {
 //JS需要调用的方法
 @JavascriptInterface
 public void showToast(String arg){
 Toast.makeText(MainActivity.this,arg,Toast.LENGTH_SHORT).show();
 }
 }
}

需要注意的地方

参考链接:安卓webview的一些坑

  • webView.addJavascriptInterface()方法在API 17之前有一些漏洞(有兴趣的可以参考本篇文章,WebView 远程代码执行漏洞浅析),所以在API 17以后,需要在JavaScript接口类的方法加上@JavascriptInterface注解。
  • 仔细看的话你会发现我们上面的WebView对象并不是直接写在布局文件中的,而是通过一个LinearLayout容器,使用addview(webview)动态向里面添加的。另外需要注意创建webview需要使用applicationContext而不是activity的context,销毁时不再占有activity对象,最后离开的时候需要及时销毁webview,onDestory()中应该先从LinearLayout中remove掉webview,再调用webview.removeAllViews();webview.destory();
  • 如果想要webView在产生OOM的时候不影响主进程,可以另开一个进程,在androidmanifest.xml的activity标签里加上Android:process属性就可以了。
  • 在activity被杀死之后,依然保持webView的状态,方便用户下次打开的时候可以回到之前的状态。webview支持saveState(bundle)restoreState(bundle)方法。

保存状态

@Override
protected void onSaveInstanceState(Bundle outState) {
 super.onSaveInstanceState(outState);
 wv.saveState(outState);
 Log.e(TAG, "save state...");
} 

恢复状态(在activity的onCreate(bundle savedInstanceState)里)

if(null!=savedInstanceState){
 wv.restoreState(savedInstanceState);
 Log.i(TAG, "restore state");
}else{
 wv.loadUrl("http://3g.cn");
} 

其他一些常见问题:

1. WebViewClient.onPageFinished()。

你永远无法确定当WebView调用这个方法的时候,网页内容是否真的加载完毕了。当前正在加载的网页产生跳转的时候这个方法可能会被多次调用,StackOverflow上有比较具体的解释(How to listen for a Webview finishing loading a URL in Android?), 但其中列举的解决方法并不完美。所以当你的WebView需要加载各种各样的网页并且需要在页面加载完成时采取一些操作的话,可能WebChromeClient.onProgressChanged()比WebViewClient.onPageFinished()都要靠谱一些。

2. WebView后台耗电问题。

当你的程序调用了WebView加载网页,WebView会自己开启一些线程(?),如果你没有正确地将WebView销毁的话,这些残余的线程(?)会一直在后台运行,由此导致你的应用程序耗电量居高不下。对此我采用的处理方式比较偷懒,简单又粗暴(不建议),即在Activity.onDestroy()中直接调用System.exit(0) ,使得应用程序完全被移出虚拟机,这样就不会有任何问题了。

3. 切换WebView闪屏问题。

如果你需要在同一个ViewGroup中来回切换不同的WebView(包含了不同的网页内容)的话,你就会发现闪屏是不可避免的。这应该是Android硬件加速的Bug,如果关闭硬件加速这种情况会好很多,但无法获得很好的浏览体验,你会感觉网页滑动的时候一卡一卡的,不跟手。

4. 在某些手机上,Webview有视频时,activity销毁后,视频资源没有被销毁,甚至还能听到在后台播放。即便是像刚才那样各种销毁webview也无济于事,解决办法:在onDestory之前修改url为空地址。

5.WebView硬件加速导致页面渲染闪烁问题

关于Android硬件加速 开始于Android 3.0 (API level 11),开启硬件加速后,WebView渲染页面更加快速,拖动也更加顺滑。但有个副作用就是容易会出现页面加载白块同时界面闪烁现象。解决这个问题的方法是设置WebView暂时关闭硬件加速 代码如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

02-07 09:02