坑坑洼洼的WebView

最基础的使用方法

最简单的布局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

在Activity中使用WebView:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WebView webView = (WebView) findViewById(R.id.webview);
        webView.loadUrl(baiduUrl);
    }

但只是这样的话,在模拟器上是会直接调到系统浏览器去的,在手机上(我用的三星N9002,5.0系统)貌似可以直接加载而且点击页面的超链接也可以在当前页面完成跳转。事实上, WebView的默认行为是将链接点击事件作为 Intent 发送给系统,由系统决定如何处理(通常的行为是使用浏览器打开或是弹出浏览器选择对话框),我猜我用的三星测试机的WebView可能被厂商做了一些处理。

所以我们开发使用webView时一般都会给它设置WebViewClient。

If WebViewClient is not provided, by default WebView will ask Activity Manager to choose the proper handler for the url.

    /**
     * Sets the WebViewClient that will receive various notifications and
     * requests. This will replace the current handler.
     *
     * @param client an implementation of WebViewClient
     */
    public void setWebViewClient(WebViewClient client) {
        checkThread();
        mProvider.setWebViewClient(client);
    }

WebViewClient主要帮助WebView处理各种通知、请求事件的,它的方法有:

shouldOverrideUrlLoading
onPageStarted
onPageFinished
onLoadResource
onReceivedError
……

添加如下一行代码:

//WebViewClient中的方法大都是空实现,如果需要处理,则写一个它的子类传入即可
webView.setWebViewClient(new WebViewClient());

另外一个看起来跟WebViewClient很像的类是WebChromeClient,它主要辅助WebView处理Javascript的对话框、网站图标、网站title、加载进度等,它的方法有:

onProgressChanged
onReceivedTitle
onReceivedIcon
onJsAlert
openFileChooser
……

设置方法也差不多:

//传入WebChromeClient或其子类
webView.setWebChromeClient(new WebChromeClient());

运行结果如下:


但会发现怎么页面的百度样式貌似很老很老,跟浏览器加载出来的百度首页怎么不一样呢。那是因为我们百度页面是使用了JS的,所以我们需要对WebView设置JS可执行:

webView.getSettings().setJavaScriptEnabled(true);

另外百度新闻的页面始终只转菊花而加载不出来,试验发现需要设置另外一个属性,使DOM storage API可用:

webView.getSettings().setDomStorageEnabled(true);

这是整个页面就跟浏览器一致了:


当点击进入下一级页面如新闻时,发现点返回键就直接退出了桌面,这里需要我们自己去复写当前Activity的onKeyDown方法实现WebView的返回逻辑:

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) {
            webView.goBack();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

自定义url

  • shouldOverrideUrlLoading
    我的项目中为了使从WebView中点击特定链接(比如我们的自定义链接:yxy://abc?id=1)可以跳转到一个指定的native页面,需要进行特殊处理,这个需求就可以使用shouldOverrideUrlLoading方法来实现。
    /**
     * Give the host application a chance to take over the control when a new
     * url is about to be loaded in the current WebView. If WebViewClient is not
     * provided, by default WebView will ask Activity Manager to choose the
     * proper handler for the url. If WebViewClient is provided, return true
     * means the host application handles the url, while return false means the
     * current WebView handles the url.
     * This method is not called for requests using the POST "method".
     *
     * @param view The WebView that is initiating the callback.
     * @param url The url to be loaded.
     * @return True if the host application wants to leave the current WebView
     *         and handle the url itself, otherwise return false.
     */
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        return false;
    }

从代码注释中可以看出这个方法的作用是,当一个新的url将要在当前webView中加载时,给当前应用程序一个机会,去决定如何处理。有三种情况:

  1. 当前webView没有设置WebViewClient,webView将请求系统去选择合适的处理程序(比如系统浏览器);
  2. 当前webView设置了WebViewClient:
    a. 如果shouldOverrideUrlLoading返回true,则由应用的代码进行处理,webView不处理;
    b.如果shouldOverrideUrlLoading返回false,则由webView去处理,即webView加载url。

上面的第2点的b,其实就是shouldOverrideUrlLoading的默认实现,我们经常见到一些代码这样写:

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    view.loadUrl(url);
    return true;
}

其实这样跟直接return false是一样的处理效果。

  • 处理自定义的url
    我在测试时发现一个问题,当我使用webView加载一个有效的http链接,如:
webView.loadUrl("http://www.baidu.com");

加载时,shouldOverrideUrlLoading会被回调到,而如果直接加载一个无效的自定义链接,如:

webView.loadUrl("yxy://abc");

则shouldOverrideUrlLoading不会被回调,webView会出现一个错误提示:


此时如果直接去点击页面中的蓝色链接,又会发现shouldOverrideUrlLoading被回调了。通过Google,发现官方有一篇文章(Migrating to WebView in Android 4.4)专门提到这一点,从Android 4.4开始,WebView有了一些新的特性,有点方面也跟以前不同了,在处理用户自定义的url时,校验貌似更严格了:

The new WebView applies additional restrictions when requesting resources and resolving links that use a custom URL scheme. For example, if you implement callbacks such as shouldOverrideUrlLoading() or shouldInterceptRequest(), then WebView invokes them only for valid URLs.
If you are using a custom URL scheme or a base URL and notice that your app is receiving fewer calls to these callbacks or failing to load resources on Android 4.4, ensure that the requests specify valid URLs that conform to RFC 3986.

当shouldOverrideUrlLoading被回调时,可以通过下面的方式来实现应用自己的处理:

// The URL scheme should be non-hierarchical (no trailing slashes)
private static final String APP_SCHEME = "example-app:";
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (url.startsWith(APP_SCHEME)) {
       urlData = URLDecoder.decode(url.substring(APP_SCHEME.length()), "UTF-8"); 
       respondToData(urlData); 
       return true;
    } 
    return false;
}
  • 通过Linkify,在TextView中生成超链接
    (……)

WebView中Java和JS交互

我的项目中是使用了这个开源项目,非常好用——https://github.com/lzyzsd/JsBridge

自定义UA

项目中为了方便web端统计分析,我们需要在WebView的UserAgent中加入app的特定标识:

// 获取当前WebView的UA
String ua = webView.getSettings().getUserAgentString();
// 在当前UA字符串的末尾增加app的标识和版本号等信息
webView.getSettings().setUserAgent(ua + " APP_TAG/5.0.1");

输入法设置

为了避免WebView中弹出键盘遮挡住光标,需要在对应的Activity中增加如下配置:

android:windowSoftInputMode="stateHidden|adjustResize"

集成腾讯浏览服务TBS

TBS的官网:http://x5.tencent.com/index
在使用Android的WebView时,遇到过很多问题。比如支持html页面内点击按钮打开本地图片上传到服务器这个需求,Android各个版本WebView的api都不同,而且更奇葩的是有的手机竟然完全不支持这个功能,原因竟然是Android在WebView版本迭代时造成的历史遗留bug。诸如此类大坑小坑还是挺多的,碎片化太严重。(可以看看知乎这个提问:http://www.zhihu.com/question/31316646?sort=created

目前移动端系统内置浏览器的常见内核有 Webkit,Blink,Trident,Gecko 等,其中 iPhone 和 iPad 等苹果 iOS 平台主要是 WebKit,Windows Phone 8 系统浏览器内核是 Trident,Android 4.4 之前的 Android 系统浏览器内核是 WebKit,Android4.4 系统浏览器切换到了Chromium,内核是 Webkit 的分支 Blink。所以Android4.4之前与之后存在一些WebView的兼容问题。

提到兼容问题,我们肯定会想到一些三方的SDK可以完美地解决这些问题。所以去年时我们项目尝试接入腾讯的X5(TBS的官网)。当时X5还是1.x的版本,集成后发现很不好用,因为它是要共享微信或者手Q的内核,但是app启动后老是加载不到X5内核,自己去调系统内核了,10次只有1、2次能调用到X5内核,SDK提供的demo也一样有问题,而且文档很简陋,很多地方写得不清楚,论坛提问题也没人解决,所以只好放弃了。今年听说X5发布了2.x的版本,功能也相对稳定了,所以现在再次尝试。

下载SDK,发现有两种:

第一种说明了只共享微信或者手Q的内核,那就是说如果用户手机上没有微信和手Q,就不能使用X5内核了,虽然现在基本每台手机都有微信或手Q,但还是自带X5内核比较好,而且下载了两个skd对比,发现只是jar包大小上的区别。

开始集成了!
1.把SDK解压后,找出里面的jar包导入到工程,我使用的AS,所以直接粘贴到lib包下面:


2.根据集成文档,把项目中所有用到的WebView及其相关的类都替换为“com.tencent.smtt”包下同名的类:


必须要替换完全了,包括java代码和xml中使用到的地方,否则会发生错误;

3.如果使用的是“Android SDK(With download)”的SDK,即我们现在使用的可独立下载X5内核的SDK,则需要使用下面代码允许第三方app下载X5内核:

QbSdk.allowThirdPartyAppDownload(true);

因为文档中强调这句代码要在创建WebView之前调用,xml中的WebView会再执行Activity中的代码之前被创建,所以我觉得最好将这句代码放在Application中。

4.加入必要的权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />

我将文章开头讲到的测试代码中WebView相关的类替换为SDK提供的同名类并加入第3点的代码后,运行代码,WebView正常加载,那么如何判断使使用了X5内核呢,有一个小技巧,就是长按文字,唤出复制菜单:


使用系统内核时WebView的复制样式

使用X5内核时WebView的复制样式

如果复制的样式改变了,则说明X5内核启动了。当我多实验了几次后,发现并不一定每次app一打开,X5内核就会加载成功,可能是下载X5内核的工作不一定可以立马完成吧,有时还要等一段时间多启动几次才加载上X5的内核。但比1.x的SDK感觉要好很多,至少没出现一直加载不出X5内核的情况。

【首次加载即能使用X5内核的方法】
原来官方在这里有解决方案:http://x5.tencent.com/doc?id=1002_1
真心建议X5的接入文档能够更详细更完善一点,否则很多地方接入时真得很费劲,要来回看各个角落里的信息才能解决一些基础问题,论坛上也有很多遗留问题其实可以集中放在文档中的。
如何首次启动WebView就能加载到X5内核呢,SDK中其实直接提供了对应的API,只需要在Application中加上下面的代码就可以了:

public class MainApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        QbSdk.initX5Environment(this, QbSdk.WebviewInitType.FIRSTUSE_AND_PRELOAD, new QbSdk.PreInitCallback() {
            @Override
            public void onCoreInitFinished() {
                Log.d("MainApplication", "x5 core load success");
            }

            @Override
            public void onViewInitFinished(boolean b) {

            }
        });
    }
}

initX5Environment 内部会创建一个线程向后台查询当前可用内核版本号,这个函数内是异步执行所以不会阻塞 App 主线程,这个函数内是轻量级执行所以对 App 启动性能没有影响,当 App 后续创建 webview 时就可以首次加载 x5 内核了。首界面就使用tbs webview的,因为 initX5Environment 需要做初始化操作,不适用首次加载 x5的方案。

虽然是预加载,如果在没有加载完成前就启动了含有WebView的界面,还是会有一点卡顿。

5.API使用的调整

  • 在使用WebView时,我们获取它的宽度是使用:
webView.getWidth();

但因为SDK所提供的WebView类,是对系统WebView的聚合包装,所以获取宽度时需要用:

webView.getView().getWidth();
  • 调整cookie的使用

com.tencent.smtt.sdk.CookieManager和com.tencent.smtt.sdk.CookieSyncManager的相关接口的调用,在接入SDK后,需要放到创建X5的WebView之后(也就是X5内核加载完成)进行;否则,cookie的相关操作只能影响系统内核。

6.获取异常上报信息
可将以下API返回的信息携带进异常上报的附加信息里

WebView.getTbsCoreVersion(); // 返回内核版本信息
WebView.getTbsSDKVersion(); // 返回浏览器SDK版本信息
WebView.getCrashExtraMessage; // 返回crash线索信息

7.兼容视频播放

  • 享受页面视频的完整播放体验需要在WebView所在的Activity中增加下面的声明:
android:configChanges="orientation|screenSize|keyboardHidden"

加上之后,就支持了视屏播放的横竖屏切换和全屏非全屏的切换。

  • 视频为了避免闪屏和透明问题,需要如下设置:
    • 网页中的视频,上屏幕的时候,可能出现闪烁的情况,需要如下设置:Activity在onCreate时需要设置:
getWindow().setFormat(PixelFormat.TRANSLUCENT);  //这个对宿主没什么影响,建议声明
  • 在非硬绘手机和声明需要controller的网页上,视频切换全屏和全屏切换回页面内会出现视频窗口透明问题,需要如下设置:
声明当前Activiy的<item name="android:windowIsTranslucent">false为不透明。
特别说明:这个视各app情况所需,不强制需求,如果声明了,对体验更有利
  • 以下接口禁止(直接或反射)调用,避免视频画面无法显示:
webview.setLayerType()
webview.setDrawingCacheEnabled(true);

X5对视频的兼容很不错,在使用原生WebView时,播放视频的效果是下面这样:


样式丑,全屏和横屏的效果也很不好。使用X5内核的WebView时,播放视频效果如下:


播放进度、功能按钮等的样式美观了,而且横屏和全屏的支持也很赞,还可以锁定屏幕和调整画面比例,唯一美中不足的是有广告:


8.混淆
TBS jar 已经混淆过,所以 App 混淆时可以不再混淆。也可以添加集成文档中给出的混淆策略。

9.接入TBS视频播放器

TBS不仅提供了强大的网页浏览功能,更提供了强大的页面H5视频播放支持,播放器同时支持页面,小窗,全屏播放体验,强大的解码能力,包括mp4,rmvb,flv,avi等26种视频格式支持。
TBS播放器的播放场景不仅局限于H5页面播放,也可以接入一般的视频流链接,比如本地文件,网络的视频流链接。开发者如果想播放一个视频链接,在不自己开发播放器的前提下,一般做法是将视频的播放链接放到一个Intent里面,抛给系统的播放器进行播放,那么当你集成了TBS后,你只需要通过简单的方式接入视频播放调用接口,这样你不需要写任何一句关于播放器的代码,就可以享受一个本地播放器体验,播放视频再不需要Intent来跨App、跨进程的调用了。

  • 在AndroidManifest中注册VideoActivity
        <activity
            android:name="com.tencent.smtt.sdk.VideoActivity"
            android:alwaysRetainTaskState="true"
            android:configChanges="orientation|screenSize|keyboardHidden"
            android:exported="false"
            android:launchMode="singleTask">
            <intent-filter>
                <action android:name="com.tencent.smtt.tbs.video.PLAY" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
  • 调用播放视频的接口

//判断当前Tbs播放器是否已经可以使用。
public static boolean canUseTbsPlayer(Context context)
//直接调用播放接口,传入视频流的url
public static void openVideo(Context context, String videoUrl)
//extraData对象是根据定制需要传入约定的信息,没有需要可以传null
public static void openVideo(Context context, String videoUrl, Bundle extraData)

直接调用以上接口,就可以打开VideoActivity播放传入url对应的视频,相当于集成了一个内部播放器,非常方便:

TbsVideo.openVideo(context, "http://www.huixuedu.com/uploads/media/151201/1-151201112136.mp4");

推薦閱讀更多精彩內容