[Android基础]WebView的简单使用

[TOC]


[ Demo下载 ]

资源

  1. Web Apps
  2. WebView
  3. Android4.4 webview实现分析
  4. Android WebView使用深入浅出
  5. 深入讲解WebView(上) - 互调,缓存,异常处理等
  6. 深入讲解WebView(下) - session,cookie等
  7. Android WebView Memory Leak WebView内存泄漏 ==! 这个我用leakcanary没检测出来
  8. PHP、Android、iOS 的恩恩怨怨

从Android 4.4(KitKat)开始,WebView组件是基于开源的Chromium项目.包含V8 js引擎并支持新的web标准,新webView也共享Chrome for Android的渲染引擎,另外,从5.0(Lollipop)开始,WebView被移到独立的apk中,因此它可以进行单独更新,可以从 "settings -- Apps -- Android System WebView" 中查看其版本;

用途

默认情况下,webView不启用js交互,并会忽略页面错误,适用于展示静态信息;
也可以启用js功能,实现与用户的交互

辅助类

  • WebChromeClient 当可能影响webView UI的操作发生时会调用到该类,比如进度变化或者js提示框...
  • WebViewClient 当可能影响内容渲染的操作发生时会调用到该类,比如错误等...另外,可以通过重写 shouldOverrideUrlLoading() 来中断url的加载;
  • WebSettings 功能设置,比如可否允许js代码;

基本操作

  • 访问网络的话需要添加网络权限
<uses-permission android:name="android.permission.INTERNET" />
  • 启用js功能
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);

操作localStorage

项目中接到的要求,要传给H5页面添加一些token值,方便其发送非url请求的时候调用
P.S. shouldOverrideUrlLoading() 只能拦截url超链接请求,对于H5页面自己发送其他非跳转请求的话这个方法是没法拦截的
shouldInterceptRequest() 是返回给app端一个response,如果方法返回的是null则走正常网络请求返回,否则就返回给定的response,
想到的方案是调用js代码给localstorage中设定一些值,方便h5调用,当然给出一个原生方法给h5调用也一样

mWebSettings = mWebView.getSettings();
mWebSettings.setJavaScriptEnabled(true);
mWebSettings.setDomStorageEnabled(true);//给权限

mWebView.setWebViewClient(new WebViewClient() {

        // 不在 onPageStart() 中去设置是因为设置完以后又loadUrl(url),之前设定的值就无效了
        // 当然,在 onPageFinished() 设置的话也得H5中在document.ready()之后才能去获取
        // 或者也可以考虑在 WebChromeClient 的 onProgressChanged() 方法中作设定

        @Override
        public void onPageFinished(WebView view, String url) {
                                          LogUtils.d("footTest", "onPageFinished " + url);
                                          view.loadUrl(
                                                  "javascript:" +
                                                          "localStorage.setItem('token', '" + UacDataInstance.getUserTokenWithoutBear() + "');");
        }
    }
);

设置返回键回退功能

mWv.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                // 需要添加 mWv.canGoBack(),不然当返回到初始页面时,可能无法继续通过返回键关闭页面
                if (keyCode == KeyEvent.KEYCODE_BACK && mWv.canGoBack()) {
                    mWv.goBack();
                    return true;
                }
                return false;
            }
        });

也可以通过设置所在Activity的onBackPressed()方法来支持webView回退:

@Override
public void onBackPressed() {
    if (mWv.canGoBack()) {
        mWv.goBack();
    } else {
        super.onBackPressed();
    }
}

设置标题

mWv.setWebChromeClient(new WebChromeClient(){
    @Override
    public void onReceivedTitle(WebView view, String title) {
        // title 是获取到的网页title,可以将之设置为webView所在页面的标题
        MainActivity.this.setTitle(title);
    }
)};

设置加载进度

@Override
protected void onCreate(Bundle savedInstanceState) {
    //requestWindowFeature(Window.FEATURE_PROGRESS);
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_VISIBILITY_ON);
    ......

    mProgressDlg = new ProgressDialog(this);
    mProgressDlg.setMessage("loading...");
    
    mWv.setWebChromeClient(new WebChromeClient() {
        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            //更新进度条示数
            
            //这种方式我没看到效果...
            //MainActivity.this.setProgress(newProgress);
            
            //使用控件ProgressDialog来显示进度
            //但记得这种方式需要在error发生时也进行取消
            if (newProgress <= 90) {
                mProgressDlg.setProgress(newProgress);
            } else {
                mProgressDlg.dismiss();
            }
        }
    });

    mWv.setWebViewClient(new WebViewClient() {
        @Override
        public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
            super.onReceivedError(view, request, error);
            // 加载某些网站的时候会报:ERR_CONNECTION_REFUSED,因此需要在这里取消进度条的显示
            Toast.makeText(MainActivity.this, "error", Toast.LENGTH_SHORT).show();
            if (mProgressDlg.isShowing()) {
                mProgressDlg.dismiss();
            }
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            super.onPageStarted(view, url, favicon);
            if (!mProgressDlg.isShowing()) {
                mProgressDlg.show();
            }
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            if (mProgressDlg.isShowing()) {
                mProgressDlg.dismiss();
            }
        }
    });

控制url跳转

mWv.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        // 这个方法我没有重写的话也还是使用webView来加载链接
        // 而且我这里测试返回的true/false貌似没什么影响
        
        if (Uri.parse(url).getHost().endsWith("jianshu.com")) {
            //若是指定服务器的链接则在当前webView中跳转
            view.loadUrl(url);
            return false;
        } else if (Uri.parse(url).getHost().length() == 0) {
            // 本地链接的话直接在webView中跳转
            return false;
        }

        // 其他情况则使用系统浏览器打开网址
        Uri uri = Uri.parse(url);
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        startActivity(intent);
        return true;
    }
});

加载页面

1. 加载本地asset文件

 mWv.loadUrl("file:///android_asset/index.html");

2. 加载本地网页2

//index.html文件放置于 src/main/assets 目录中
myWebView.loadUrl("file:///android_asset/index.html");

3. 加载网页

myWebView.loadUrl("http://www.jianshu.com/users/302253a7ed00/latest_articles");

4. 解析html字符串

String summary = "<!DOCTYPE html>\n" +
        "<html lang=\"zh_CN\">\n" +
        "<head>\n" +
        "    <meta charset=\"UTF-8\">\n" +
        "    <title>webViewDemoFromAsset</title>\n" +
        "    <script src=\"js/basic.js\"></script>\n" +
        "</head>\n" +
        "<body>\n" +
        "<div>\n" +
        "    <button id=\"btn\" onclick='showToast()'>调用android toast</button>\n" +
        "</div>\n" +
        "\n" +
        "<label id='label'>js android代码互调测试</label>\n" +
        "\n" +
        "<br>\n" +
        "<a href=\"http://www.jianshu.com/users/302253a7ed00/latest_articles/\">个人主页</a>\n" +
        "</body>\n" +
        "</html>";

// 官网例子给的下面的写法,但是会出现中文乱码,
// 原因:http://blog.csdn.net/top_code/article/details/9163597
// mWv.loadData(summary, "text/html", "utf-8");

mWv.loadData(summary, "text/html;charset=UTF-8", null);

使用android studio的话,项目结构中没有asset目录,需要手动创建 src/main/assets 目录即可;
扩展:

  1. 如果html文件存于sdcard:则加前缀:
    content://com.android.htmlfileprovider/sdcard/
    另外, content 前缀可能导致异常,直接使用 file:///sdcard/ 或者 file:/sdcard 也可以;
  2. 也可使用 locaData() ,先将文件读取出来,在传入字符串到方法中,可以用于展示页面,但不会引用css,js等文件;

js与andorid互调

  1. 通过 addJavaScriptInterface() 来设置接口,传入实例和类名,让js可以调用;

Note: The object that is bound to your JavaScript runs in another thread and not in the thread in which it was constructed.
允许网页调用android功能可以存在风险,比如加载其他网页,默认做法是使用浏览器去加载外部其他网页;

  1. 自定义的js对应andoird实现类
//通过webView按钮调用android toast功能
public class BasicJsAppInterface {
    private Context cxt;
    public BasicJsAppInterface(Context cxt) {
        this.cxt = cxt;
    }
    // 如果targetSDKVersion设置为17以上,这里需要添加该annotation标志
    @JavascriptInterface
    public void showToast() {
        Toast.makeText(this.cxt, "toast in android", Toast.LENGTH_SHORT).show();
    }
}
// 实现js调用android功能
WebView mWv = (WebView) findViewById(R.id.wv);
 WebSettings wvSettings = mWv.getSettings();
wvSettings.setJavaScriptEnabled(true);
wvSettings.setDefaultTextEncodingName("utf-8");
//传入实现js功能的android实例 以及 js调用时使用的名称
mWv.addJavascriptInterface(new BasicJsAppInterface(this), "AndroidApp");
//加载本地asset文件,以 `file:///` 开头
mWv.loadUrl("file:///android_asset/index.html");

1. js 调用 android 功能

// 在src/main/assets 目录(不存在则手动创建)中创建该html文件
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webViewDemoFromAsset</title>
    <script src="js/basic.js"></script>
</head>
<body>
<div>
    <button id="btn" onclick='showToast()'>调用android toast</button>
</div>

<label id='label'>js android代码互调测试</label>

<br>
<a href="http://lucid-lynxz.github.io/">github主页</a>
<a href="http://www.jianshu.com/users/302253a7ed00/latest_articles">简书主页</a>

<video width="400" controls>
    <source src="res/shuai_dan_ge.mp4" type="video/mp4">
    <source src="res/gongsi_de_liliang.flv" type="video/flv">
    <p>不支持该格式视频</p>
</video>
</body>
</html>

注意:这里引入的独立js文件标签不能简写成 <script src="..."/> ,否则解析可能会出错,参见

  1. 自闭合标签;
  2. Whe don't self-closing script tags work
    webview_js
    webview_js
//在 assets/js/ 目录下创建js独立文件basic.js,当然也可以把这些代码直接嵌入到html中
function setLabel(id, label) {
    document.getElementById(id).innerHTML = label;
}

function showToast(){
  AndroidApp.showToast(); //也可以写成window.AndroidApp.showToast();
}

2. android 调用js代码:

//前缀javascript, `setLabel()是网页js文件中定义的方法`
mWv.loadUrl("javascript:setLabel('label','通过android调用js代码')");

缓存/Cookie

webview应用的缓存文件放置于 /data/data/{yourProjectName}/ 下面,之前想提取webview缓存的图片,往上查找的资料大都是通过 webviewcache.db 来获取图片对应的缓存文件,但是我在红米1s4.4以及nexus6p 6.0系统上都没有再发现这个文件了,新的缓存目录结构:

webview缓存的资源文件位置

从上图可以发现 Cookies 文件存在,使用16进制编辑器打开查看,也可在程序中获取:

private String getCookie() {
    CookieManager cm = CookieManager.getInstance();
    String cookie = cm.getCookie(mUrl);
    if (TextUtils.isEmpty(cookie)) {
        cookie = "there is no cookie exist";
    }
    return cookie;
}

另外,图中红色方框内的文件就是缓存的文件了,它们名称是如何跟实际资源文件对应起来的,这个我还没弄懂,不过还是可以获取缓存图片的,我们使用16进制编辑器来查看,可以发现头部有该图片的url地址("?g....d64d.png"):


ff8def31493d3be1_0 文件内容

我们删除该文件的url地址信息,保存后,修改后缀名为png,即可看到实际的图片:


删除图片地址信息

实际的图片(右侧)

播放视频

支持标准MP4,ogg之类的,flash得启用插件进行播放,不考虑
官网 建议播放视频的时候开启硬件加速,不过我在nexus6p上没有开(默认开了吗?)也ok的;

全屏播放

参考

页面适应

Pixel-Perfect UI in the WebView

  • 一个针对移动端优化过的页面带有如下类似的属性:
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

系统将页面显示在一个虚拟的Viewport中,这个视口通常比屏幕大,这样网页就不会被限制在很小的范围内,用户可以通过缩放和平移来查看内容;

  • 对于无法控制内容的线上网页,可以通过代码方式设置ViewPort:
//强制手机使用 desktop-size viewport
wvSettings.setUseWideViewPort(true);
wvSettings.setLoadWithOverviewMode(true);

扩展-响应式

调试

  1. chrome
    需要在电脑上安装Chrome32以上的版本;

  2. 在电脑上启动浏览器打开网址 chrome://inspect ,

  3. android启用webView调试
    条件:

    • android 4.4以上
    • 允许远端调试
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    // 官网: https://developer.chrome.com/devtools/docs/remote-debugging#debugging-webviews
    // 官网说WebView不受manifest的debuggable标签的影响,若需要在该标签启用时才允许调试,则添加如下条件判断(注意:尽量不要在manifest中显式指定debuggable属性,放空即可,这样Android Studio会自动在调试时设置成true,在release版本中设置成false)
    int debuggable = getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE;
    if (0 != debuggable) {
        WebView.setWebContentsDebuggingEnabled(true);
    }
}

注意事项

  1. All WebView methods must be called on the same thread
    wv.loadUrl() 方法放在主线程(根据错误提示来指定)中执行;
    P.S. webview类中有checkThread()方法,跟初始的Lopper.myLooper做比较,想跟踪setContentView看看,结果源码不全,断点跟踪不知道跟到哪里去了...后续得再补补;
  2. html页面应用独立的js文件时,script不能写成自闭合标签,否则浏览器解析可能会出错;
  3. 官方建议WebView的height属性设置为 match_parent 或者指定值,而非 wrap_content ,同事设置为 match_parent 后,其各个父容器不允许设置height为 wrap_content ,否则可能导致异常发生;
  4. android 4.4对webView做了些变化,可以参考 [这篇文章](Migrating to WebView in Android 4.4);
  5. 混淆时,需要设置javaScriptInterface不被混淆
# app/proguard-rules.pro
-keep public class org.lynxz.webviewdemo.BasicJsAppInterface{
    public <methods>;
}
-keepattributes *Annotation*
-keepattributes *JavascriptInterface*

异常

1. 内存泄露

这个我还没测试,Android WebView Memory Leak WebView内存泄漏
==! 然后查了内存检查:
Android最佳性能实践(二)——分析内存的使用情况

这里有人发现android 5.1也有类似的情况,我没有尝试加载很多页面,先记录下来:
Android 5.1 Webview 内存泄漏新场景

2. loadData() 中文乱码

参考这篇

mWv.loadData(yourHtmlString, "text/html;charset=UTF-8", null);

有人说这么设置也可以避免乱码,但是我在nexus 6p上没测试成功:

wvSettings.setDefaultTextEncodingName("utf-8");

3. eglCodecCommon: **** ERROR unknown type 0x73000f (glSizeof,80)

Genymotion模拟器不支持硬件加速,关闭即可:
mWv.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

4. html中含有angular.js,数据获取成功,但是显示空白:

已启用js支持:
mWv.getSettings().setJavaScriptEnabled(true);

logCat报错:

I/xxx: url = http://fep-web.debug.web.nd/#!/report/student/compositive?client=phone&mode=debug&user_id=2079947956
D/dalvikvm: GC_FOR_ALLOC freed 37K, 14% free 21151K/24528K, paused 17ms, total 17ms
I/dalvikvm-heap: Grow heap (frag case) to 25.837MB for 3288976-byte allocation
W/AwContents: nativeOnDraw failed; clearing to background color.
I/Timeline: Timeline: Activity_idle id: android.os.BinderProxy@425bdee8 time:31230137
I/chromium: [INFO:CONSOLE(39)] "Uncaught Error: [$injector:modulerr] http://errors.angularjs.org/1.4.10/$injector/modulerr?p0=app&p1=Error%3A%20%5B%24injector%3Amodulerr%5D%20http%3A%2F%2Ferrors.angularjs.org%2F1.4.10%2F%24injector%2Fmodulerr%3Fp0%3Dapp-theme%26p1%3DTypeError%253...<omitted>...3)", source: http://fep-web.debug.web.nd/bower_components/angular/angular.min.js?v=201604181940 (39)
I/chromium: [INFO:CONSOLE(72)] "error_log:localStorage error", source: http://fep-web.debug.web.nd/js-error.no-ng.js (72)

我也不懂angular.js用到了什么,添加下dom支持就可以了:

settings.setDomStorageEnabled(true);

5. A WebView method was called on thread 'JavaBridge'. All WebView methods must be called on the same thread.

webview加载了网页后,在html中点击重新加载网页,我之前直接在接口类的方法中直接运行,

@JavascriptInterface
public void retriveToUrl(String url) {
    mWv.loadUrl(url);
}

需要将其放置在ui线程中运行:

The JavaScript method is executed on a background (i.e. non-UI) thread. You need to call all Android View related methods on the UI thread.

@JavascriptInterface
public void retriveToUrl(String url) {
    mWv.post(new Runnable() {
        @Override
        public void run() {
            mWebView.loadUrl(...).
        }
    });
}

6. "TypeError: Object [object Object] has no method 'callNative' - 混淆

在release版本中,js代码调用不到我定义的接口类中的方法,从混淆文件中把这个类排除即可;
js interface proguard

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,822评论 25 707
  • 0. 前言 前面有被用户投诉 APP 流量消耗厉害: 于是乎考虑了流量方面的问题。暂时 APP 中涉及流量的几个方...
    zyl06阅读 23,965评论 5 62
  • WebView·开车指南 目录 WebView简介 WebView基本使用 WebView常用方法 WebSett...
    小庄bb阅读 3,493评论 3 25
  • 这篇博客主要来介绍 WebView 的相关使用方法,常见的几个漏洞,开发中可能遇到的坑和最后解决相应漏洞的源码,以...
    Shawn_Dut阅读 7,216评论 3 55
  • 现在在柳巷km帮男友买裤子,分手的想法去一刻都没有离开过我的脑海。这个决定我已经思考了好久,最后还是决定分手吧。 ...
    sometimes_d661阅读 172评论 0 0