Android中的WebView

本文深入解析Android App开发中如何有效结合WebView与H5技术,阐述了WebView的基本概念、属性、功能及用法。文章还讨论了如何在Android界面中合理运用WebView显示网页内容,包括配置WebView、加载网页、处理网络请求和错误处理等关键步骤。此外,文章还介绍了WebView与JavaScript交互的方法、数据传递机制以及如何解决潜在的安全风险,提供了实现跨平台交互的实例和代码,旨在帮助开发者提升App的灵活性和用户体验。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Android基于效率和灵活性的考虑,现在越来越多的开发者采用Hybrid方式开发App,那么如何使android和h5有效结合呢,WebView就可以使网页轻松的内嵌到app里,还可以直接跟js相互调用。这么看来Hybrid开发离不开WebView这个组件了,那就让我们探讨一下Webview的一些属性和功能以及用法

WebView是什么

WebView类是一个扩展Android的视图类,允许将Web页面作为活动布局的一部分。它并不一个完整网络浏览器,它采用了WebKit渲染引擎来显示网页,默认情况下,只显示一个Web页面。WebView可以方便的在线更新内容,不需要发布新版本的app来更新模块。所以使用WebView都需要在android清单文件中加入如下连接网络的权限,除非访问的是本地assets中的html资源

< uses-permission android:name=”android.permission.INTERNET”/>

那么我们如果在android界面中加入一个WebView呢,其实很简单我们可以通过在xml中配置WebView这个组件

<?xml version="1.0" encoding="utf-8"?>
<WebView  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/webview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
/>

同样我们可以在Activity中直接建立一个WebView显示这个网页

WebView mWeb = new WebView(this);
mWeb.loadUrl("http://www.baidu.com");
setContentView(mWeb);

那么我们如何用WebView来显示一个网页呢,其实也是很简单的,我们可以通过loadUrl方法来显示一个网页,上面在Activity中的WebView已经使用了这个方法,如果加载本地文件我们可以使用

webView.loadUrl("file:///android_asset/XX.html");

这个本地的html文件存放于assets 文件中。如果使用xml中配置的WebView我们同样也适用

WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.loadUrl("http://www.example.com");

通过实际运行一下这个WebView,我们可以发现显示效果并不是我们想象的那样,app并没有在这个WebView的控件中显示这个网页,而是启动了手机的浏览器。我们可以通过WebViewClient覆盖WebView默认使用第三方或系统默认浏览器打开网页的行为,使网页用WebView打开

 mWeb = (WebView) findViewById(R.id.mWebView);
 mWeb.loadUrl("http://www.baidu.com");

 mWeb.setWebViewClient(new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            //true 说明事件被webview消费了,不用再向上传播,否则就要上传播
            return true;
        }
 });

这样就网页就显示在我们自己WebView中了 ,


WebChromeClient和WebViewClient

实际使用的话,如果你的WebView只是用来处理一些html的页面内容,只用WebViewClient就行了,如果需要更丰富的处理效果,比如JS、进度条等,就要用到WebChromeClient。

WebViewClient

主要帮助WebView处理各种通知、请求事件。如果希望链接在当前WebView中显示而不是外部浏览器,必须覆盖 webview的WebViewClient对象。

  • shouldOverrideUrlLoading(WebView view, String url)
    在web页面里单击链接的时候,会自动调用android自带的浏览器来打开链接,需要通过这个方法在本页面打开

  • onLoadResource(WebView view, String url)
    通知主程序WebView要通过给定的url来加载资源了,这个方法在加载资源时响应

  • onPageStarted(WebView view, String url, Bitmap favicon)
    通知主程序开始加载界面,在加载页面时响应

  • onPageFinished(WebView view, String url)
    通知主程序界面加载完毕,在加载页面结束时响应

  • onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
    通知主程序加载错误,在加载出错时响应

  • onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)
    通知主程序,在加载资源时发生了SSL错误

WebChromeClient

主要辅助WebView处理Javascript的对话框、网站图标、网站title、加载进度等,一下是一些方法,更多需要看API

  • onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg)
    通知主程序启动一个新窗口

  • onCloseWindow(WebView window)
    通知主机应用程序关闭指定的WebView,如有必要,将其从系统中删除。

  • onJsConfirm(WebView view, String url, String message, JsResult result)
    通知客户端显示一个确认对话框给用户

  • onJsAlert(WebView view, String url, String message, JsResult result)
    通知客户端显示一个警告对话框

  • onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)
    通知客户端显示一个提示对话框

  • onProgressChanged(WebView view, int newProgress)
    通知主应用程序加载页面的当前进展情况。


WebSettings

在创建WebView时,系统有一个默认的设置,我们可以通过WebView.getSettings来得到这个设置。负责管理一个web视图设置,当第一次创建一个web视图时,它获得了一组默认设置,这些默认设置通过所有的getxx方式返回。从WebView.getSettings方法获得的WebSettings对象绑定到WebView的生命周期中,如果这个web视图被破坏,在WebSettings调用任何方法将抛出IllegalStateException。这个对象一般负责管理WebView的缩放、字体、编码等设置。
以下是一些常用方法,具体查询API

  • getAllowFileAccess()
    获取此的WebView是否支持文件访问

  • setBlockNetworkImage(boolean flag)
    设置WebView是否从网络上加载图像资源,是否显示网络图像

  • setBuiltInZoomControls(boolean enabled)
    设置是否显示缩放工具

  • setCacheMode(int mode)
    设置WebView的缓存模式,覆盖默认的缓存模式,有以下几种
    LOAD_NO_CACHE:不要使用缓存,从网络加载
    LOAD_CACHE_ELSE_NETWORK:如果内容已经存在cache 则使用cache,即使是过去的历史记录。如果cache中不存在,从网络中获取。所以加上这句,不仅可以使用cache离线显示用户浏览过的内容,还可以在有网络的情况下优先调用缓存,为用户减少流量
    LOAD_CACHE_ONLY:只从缓存中加载,不使用网络
    LOAD_DEFAULT:不设置时候的默认缓存模式,即不使用缓存

  • setDefaultFontSize(int size)
    设置默认的字体大小(1-72),默认值是16

  • setDefaultTextEncodingName(String encoding)
    设置解码HTML页面时使用的默认文本编码名称,默认值是“UTF-8”。

  • setJavaScriptEnabled(boolean flag)
    通知的WebView可以执行JavaScript, 默认为false。

  • setSupportZoom (boolean support)
    设置是否支持变焦

WebView开发问题

浏览网页的回退

当我们在使用一些浏览器浏览网页的时候,经常会遇到这种功能,点击Android的返回键,就会产生网页回退,这个功能应该如何实现呢

webview.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    if (keyCode == KeyEvent.KEYCODE_BACK && webview.canGoBack()) {
                        webview.goBack();
                        return true;
                    }
                }
                return false;
            }
        });


错误处理

当我们使用浏览器的时候,通常因为加载的页面的服务器的各种原因导致各种出错的情况,最平常的比如404错误,通常情况下浏览器会提示一个错误提示页面。事实上这个错误提示页面是浏览器在加载了本地的一个页面,用来提示用户目前已经出错了。是当我们的app里面使用webview控件的时候遇到了诸如404这类的错误的时候,若也显示浏览器里面的那种错误提示页面就显得很丑陋了,那么这个时候我们的app就需要加载一个本地的错误提示页面,这里就是其实就是webview如何加载一个本地的页面

webview.setWebViewClient(new WebViewClient(){

            @Override
            public void onReceivedError(WebView view, int errorCode,
                    String description, String failingUrl) {
                switch(errorCode)
                {
                case HttpStatus.SC_NOT_FOUND:
                    view.loadUrl("file:///android_assets/error_handle.html");
                    break;
                }
            }
        });

其实,当出错的时候,我们也可以选择隐藏掉WebView,而显示native的错误处理控件,这个时候只需要在onReceivedError里面显示出错误处理的native控件同时隐藏掉webview即可。


WebView和javaScript代码交互


1. WebView调用js
这种方式比较简单,可以通过loadUrl方法实现

webView.loadUrl(“javascript:play()”);
表示webview在调用js中的一个叫做play的方法


2. js调用WebView
下面这个类是用于暴露给js的类


/**
 * 暴露给js的类和方法
 */
class MyJavaScriptInterface {

    private Context context;

    public MyJavaScriptInterface(Context con) {
        this.context = con;
    }

    @JavascriptInterface
    public void clickMe(Context context) {
        Toast.makeText(context, "click", Toast.LENGTH_SHORT).show();
    }

}

那么在js中应该如何调用这个java代码呢,我们需要首先设置WebView允许JavaScript执行,然后将本地的类(用于被js调用的类)映射出去
“myJs”这个名字就是公布出去给JS调用的,那么js就可以直接调用本地的MyJavaScriptInterface类中的方法了

 webview.getSettings().setJavaScriptEnabled(true);
 webview.addJavascriptInterface(new MyJavaScriptInterface(context),"myJs");

//-----js用这个是调用
<body onload="javascript:myJs.clickMe()">  
    ...
</body>

若webview中的js调用了本地的方法,正常情况下发布的debug包时,js调用是没有问题的,但是通常发布release商业版本的apk都是要经过代码混淆,这个时候会发现之前调用正常的js无法正常调用本地方法了。这是因为混淆的时候已经把本地代码的引用给打乱了,导致js中的代码找不到本地方法的地址。
我们可以通过在proguard.cfg文件中加上一些代码来解决,声明本地中被js调用的代码不被混淆

-keep public class com.test.webview.MyJavaScriptInterface{
    public <methods>;
}

js对象注入漏洞解决

问题及解决

在上面我们通过webview.addJavascriptInterface()方法将这个功能类暴露给了js,但是addJavascriptInterface()方法存在安全隐患,在JavaScript中可以反射调用到Class的任意属性,比如可以通过对象取得包名和类名,获取类的结构等,再有甚者可以通过这种方式进行远程挂马,可以通过网页挂马的形式来恶意获取用户信息破坏运行环境等行为。Google官方在android 4.2之后通过加入@JavascriptInterface 注释来选择性的暴露方法,即只有标示了@JavascriptInterface的方法JavaScript才能调到。 所以在新版android中可以通过addJavascriptInterface/@JavascriptInterface这个方式解决这个漏洞, 但是由于目前绝大多数app支持android 4.2以前的版本。那么针对android 4.2之前的版本我们还有什么方法么?
首先,我们肯定不能再调用addJavascriptInterface方法了。那么我们如何交互呢,我们知道JS与Java进行交互,有以下几种,比如prompt, alert等,这样的动作都会对应着WebChromeClient类中相应的方法,对于prompt,它对应的方法是onJsPrompt方法,即WebChromeClient 输入框弹出模式,通过这个方法,JS能把信息传递到Java,而Java同样也能把信息传递给JS

public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) 

我们可以利用这个方式,进行数据传递,在使用时候,我们需要判断系统版本是否在4.2以下,因为在4.2以上,Android修复了这个安全问题,我们只是需要针对4.2以下的系统作修复

原理: 我们让JS调用一个Javascript方法,这个方法调用prompt方法,通过prompt把JS中的信息传递过来,这些信息是我们自己组合的包括方法名称参数等信息,我们最好这个信息做成一个json样式,这样更方便解析。然后在WebView端的onJsPrompt方法中,我们用json的方式去解析传递过来的文本,得到方法名、参数等,然后通过反射机制,调用指定对象的方法,这样就实现了js调用WebView,也就可以解决js注入的漏洞


用法及流程

那么我们以一个实例来看一下如何使用 prompt 方式的传递信息,实现 native 和 webview 两端的详细通信


1. 制定通信协议

数据的传输需要约定双方的通信协议,所以我们需要定制一套通信协议来让 js 和 native 相互联系


1)js传递给native的通信协议

我们可以通过uri的形式传递我们的数据,通过 scheme://host:port/path 来定制传递的信息,我们可以定制如下 “hybird协议”

hybrid://className:port/methodName?jsonObject

hybrid:做为传输协议
className:对应的native类
port:native的执行结果callback 的缓存位置
methodName:native类中提供的方法
jsonObject:用json封装好的方法参数

比如我们要调用android中Toast的makeText方法,我们就可以如下方式传递

hybird://Toast:callbackAddress/makeText ? {“msg”:”native log”}


2) native传递给js的通信协议

native向js的通信协议也需要制定,一个必不可少的元素就是返回值,这个返回值和js的参数做法一样,通过json对象进行传递,该json对象中有状态码code,提示信息msg,以及返回结果result,如果code为非0,则执行过程中发生了错误,错误信息在msg中,返回结果result为null,如果执行成功,返回的json对象在result中,看一下这个jsonObj

//失败的样子 eg:
{
    "code":404,
    "msg":"method is not exist",
    "result":null
}

//成功的样子
{
    "code":0,
    "msg":"success",
    "result":{
        "key1":"value1",
        "key2":"value2",     
    }
}

获取返回值通过 native调用js暴露的方法即可,需要将返回的jsonObj和js层传给native层的port一并带上

webView.loadUrl("javascript:Hybrid.onFinish(port,jsonObj);");


2. 构建 Hybrid.js 和 myHtml.html 文件

首先我们看一下 Hybrid.js 文件

(function (win) {
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    var Hybrid = win.Hybrid || (win.Hybrid = {});
    var Inner = {
            callbacks: {},
            call: function (obj, method, params, callback) {
                var port = Util.getPort();
                this.callbacks[port] = callback;
                var uri=Util.getUri(obj,method,params,port);
                window.prompt(uri, "");
            },
            onFinish: function (port, jsonObj){
                var callback = this.callbacks[port];
                callback && callback(jsonObj);
                delete this.callbacks[port];
            },
        };

    var Util = {

        getPort: function () {
            return Math.floor(Math.random() * (1 << 30));
        },
        getUri:function(obj, method, params, port){
            params = this.getParam(params);
            var uri = 'hybrid://' + obj + ':' + port + '/' + method + '?' + params;
            return uri;
        },
        getParam:function(obj){
            if (obj && typeof obj === 'object') { return JSON.stringify(obj); } else { return ''; }
        }
    };
    for (var key in Inner) {
        if (!hasOwnProperty.call(Hybrid, key)) {
            Hybrid[key] = Inner[key];
        }
    }
})(window);

我们定义了Util类,其中包含三个方法,分别是
getPort:用于随机生成port
getUri:用于生成native需要的协议uri
getParam:用于生成json字符串
Inner类包含call和onFinish方法,在 call 方法中,调用 Util.getPort() 获得了 port 值,然后将 callback 对象存储在了callbacks中的 port 位置,接着调用 Util.getUri() 将参数传递过去,将返回结果赋值给 uri,调用window.prompt(uri, “ ”) 将uri传递到native层。而 onFinish() 方法接受native回传的 port 值和执行结果,根据 port 值从 callbacks 中得到原始的 callback 函数,执行 callback 函数,然后从 callbacks 中删除。最后将Inner类中的函数暴露给外部的JSBrige对象,通过一个for循环一一赋值

然后再看一下我们的 myHtml.html 文件

<html>
<head>
    <meta charset="utf-8">
    <title>myJS</title>
    <!-- 引入JS -->
    <script src="file:///android_asset/Hybrid.js" type="text/javascript">
        <script type="text/javascript">
    </script>

</head>

<body>

<div class="blog-header">
    <h3>JS调用android中的方法</h3>
</div>
<ul class="entry">
    <li>
        弹出气泡提示<br/>
        <!-- function (obj, method, params, callback)-->
        <button onclick="Hybrid.call('hybrid','toast',{'entity':'我是气泡 .。o0'},function(res){alert(JSON.stringify(res))})">
            点击产生气泡
        </button>
    </li>
    <br/>
    <br/>
    <li>
        获得设备IMEI<br/>
        <!-- function (obj, method, params, callback)-->
        <button onclick="Hybrid.call('hybrid','getDeviceVersion',{}, function(res){alert(JSON.stringify(res))})">
            点击获取Imei号
        </button>
    </li>
</ul>

</body>
</html>

我们可以看到有两个按钮有两个方法,和都是都过调用JS中的call方法 “function (obj, method, params, callback)” 执行的


3. native端的 JsCallJava 和 JsCallBack

JsCallJava 类主要提供两个功能,第一个 executeJS 方法
作用是从一个Map中查找key是不是存在,不存在则反射拿到对应的Class中的所有方法,将方法是 public static void 类型的,并且参数是Webview,JSONObject,Callback 类型的三个参数,如果满足条件,则将所有满足条件的方法 put 进去
另一个方法是 callJava方法
callJava方法,就是将js传来的 uri 进行解析,然后根据调用的类名的命名从刚刚的 map 中查找是否存在,存在的话拿到该类所有方法的 methodMap ,然后根据方法名从 methodMap 拿到方法,反射调用,并将参数传进去,参数就是刚才那三个参数:WebView,JSONObject,Callback

public class JsCallJava {

    //保存hybird和方法类的键值对
    private static Map<String, ArrayMap<String, Method>> mInjectNameMethods = new ArrayMap();

    /**
     * 动态注入方法
     *
     * @param exposedName
     * @param clazz
     */
    public static void executeJS(String exposedName, Class<JsMethod> clazz) {
        if (!mInjectNameMethods.containsKey(exposedName)) {
            try {
                mInjectNameMethods.put(exposedName, getAllMethod(clazz));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 通过反射获取类中所有符合规定的方法
     * 以public static 开头的方法
     *
     * @param injectedClass
     * @return
     */
    public static ArrayMap<String, Method> getAllMethod(Class injectedClass) {
        ArrayMap<String, Method> map = new ArrayMap<>();
        Method[] methods = injectedClass.getDeclaredMethods();
        for (Method method : methods) {
            String name;
            if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {
                continue;
            }
            //获取方法参数类型,约定好传参形式
            Class[] parameters = method.getParameterTypes();
            if (null != parameters && parameters.length == 3) {
                if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == JsCallBack.class) {
                    map.put(name, method);
                }
            }
        }
        return map;
    }

    /**
     * 通过Uri资源格式解析返回的数据
     * @param webView
     * @param uriString
     * @return
     */
    public static String callJava(WebView webView, String uriString) {
        String methodName = "";
        String className = "";
        String param = "{}";
        String port = "";
        if (!TextUtils.isEmpty(uriString) && uriString.startsWith("hybrid")) {
            Uri uri = Uri.parse(uriString);
            className = uri.getHost();
            param = uri.getQuery();
            port = uri.getPort() + "";
            String value = uri.getPath();
            if (!TextUtils.isEmpty(value)) {
                methodName = value.replace("/", "");
            }
        }

        if (mInjectNameMethods.containsKey(className)) {
            ArrayMap<String, Method> methodHashMap = mInjectNameMethods.get(className);

            if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
                Method method = methodHashMap.get(methodName);
                if (method != null) {
                    try {
                        method.invoke(null, webView, new JSONObject(param), new JsCallBack(webView, port));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return null;
    }
}

看到 JsCallJava 类中用了 new JsCallBack(webView, port) 进行新建对象,该对象就是用来回调 JS 中回调方法的 java 对应的类。这个类你需要将 JS 传来的port传进来之外,还需要将 WebView 的引用传进来,因为要使用到WebView的loadUrl方法,这里使用弱引用来防止内存泄漏

/**
 * 返回响应
 */
public class JsCallBack {
    private static Handler mHandler = new Handler(Looper.getMainLooper());
    private static final String CALLBACK_JS_FORMAT = "javascript:hybrid.onFinish('%s', %s);";
    private String mPort;
    private WeakReference<WebView> mWebViewRef;

    public JsCallBack(WebView view, String port) {
        mWebViewRef = new WeakReference<>(view);
        mPort = port;
    }


    public void apply(JSONObject jsonObject) {
        final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));
        if (mWebViewRef != null && mWebViewRef.get() != null) {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mWebViewRef.get().loadUrl(execJs);
                }
            });

        }

    }
}

apply方法通过Handler的切换回到主线程中执行,这是因为暴露给js的方法可能会在子线程中调用这个callback,这样的话就会报错


4. 在Activity中注册这个WebView

然后就是通过Activity中加载这个WebView来显示这个界面了,还有别忘了提供提供html中对应的供JS使用的native代码,方法要满足
public static void 开头并且有Webview,JSONObject,Callback三个参数
比如toast方法

public static void toast(WebView webView, JSONObject entity, JsCallBack back)

然后我们在主Acitivity中加入这个WebView

public class MainActivity extends Activity {

    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mWebView = new WebView(this);
        setContentView(mWebView);

        WebSettings settings = mWebView.getSettings();
        settings.setJavaScriptEnabled(true);
        mWebView.setWebChromeClient(new MyWebChromeClient());
        mWebView.setWebViewClient(new MyWebViewClient());
        mWebView.loadUrl("file:///android_asset/myHtml.html");

        JsCallJava.executeJS("hybrid", JsMethod.class);
    }
}

然后我们看一下效果
这里写图片描述

其他细节请看这个小例子:http://download.youkuaiyun.com/detail/hkx_smile/9505711

这就是关于WebView的一些总结 恩恩

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值