[关闭]
@flyouting 2014-03-20T08:01:47.000000Z 字数 3700 阅读 4717

WebView探索

最近一段时间都在捣腾 Webview,对于之前的Jelly Bean中与KitKat中基于Chromium实现的不同,有点有意思的发现。如果你不知道我在说什么,可以理解,毕竟,除了一些很好的补充,API大多仍保持原状,我们可以从这两个地址了解更多相关资料,地址一地址二

免责声明:这篇文章我不打算描述所有不同的API,我只是想强调我经历的一些问题/解决方案/在使用WebView API开发应用程序时的发现。

API实现上的差异

Hit test result

我最近才发现这个类,它非常有用,在我目前所进行的项目中,用户可以通过点击链接或者列表项,去到他网站的不同页面,这些动作有不同的影响,在后者的情况下,应用程序必须明确地请求加载新的URL,否则就会再次加载相同的页面,但在前者中不用这样。

WebView.getHitTestResult()方法返回一个HitTestResult对象。对象中包含类型,url,这个类型用来表示被点击元素的类型,如果是个有HTTP src属性标签的HTML,类型会是WebView.HitTestResult.SRC_ANCHOR_TYPE。在Webview中,如果有一个不支持的元素被点击,4.4之前的api会返回一个null4.4的api会返回一个非空对象

WebView 历史记录

Webview把所有的访问记录都存在一个称之为WebBackForwardList的数据结构中。可以通过WebView.copyBackForwardList()方法来获取列表检索,正如名字一样,它会返回一个Webview中保存的数据列表的副本。调用WebView.goBack()方法之后,应用需要检索之前页面的url,我们可以很简单的通过WebBackForwardList.getCurrentItem()方法得到。之前的实现是在返回当前页面,4.4的做法是返回之前的页面。

这一点有一点点模糊:如果一个页面包含一个这样的链接:<a href="javascript:;">...</a>,这种做法其实很常见如果你想把某个元素显示成链接,但是又没有链接任何资源时。4.4中当元素被触发,会触发WebViewClient.onPageFinished()回调。虽然不确定这里发生了什么,但这么做很好,因为如果你在页面完成了加载时注入javascript,它有可能注入两次,这会导致一些意想不到的事情发生。

增加的API

Remote debugging:非常不错,如果你工作用到Webview很多,你会喜欢这个。链接里有很详细的细节,可以看看。
WebView.evaluateJavascript():这个方法可以直接从注入的javascript得到一个回调结果。只是需要在方法调用时提供一个ValueCallback。4.4之前也可以实现类似效果,但是需要更多的样板。

保持代码健壮,封装差异

因为处理不同的API实现而导致代码混乱的情况很常见,我发现避免这种情况的一个方式是创建一个适配器接口,隐藏了这些差异。如此,我最后实现的代码如下:

  1. public interface IWebViewCompatibility {
  2. void injectWebView(WebView webView);
  3. void evaluateJavascript(String script, ValueCallbackAdapter callback);
  4. boolean httpLinkHit();
  5. String getPreviousPageUrl();
  6. // Adapter interface for legacy WebView API
  7. public static interface ValueCallbackAdapter {
  8. void evaluateResult(String value);
  9. String javascriptInterfaceMethodName();
  10. }
  11. }

如果应用使用了什么注入框架,比如 Dagger,在注入之前,在适配器初始化时,我们需要添加一个if语句。

  1. @Module(injects = WebViewFragment.class)
  2. public class WebViewModule {
  3. ...
  4. @Provides @Singleton
  5. public IWebViewCompatibility provideWebViewCompatibility() {
  6. return SUPPORTS_KITKAT ? new ChromiumWebViewCompatibility()
  7. : new LegacyWebViewCompatibility();
  8. }
  9. }

我们看看之前的api是如何获取javascript的回调结果的。我们可以利用 JavascriptInterface 机制在页面添加helper接口,从这会接收到的javascript返回的结果(我的建议:在webview加载任何东西之前确保这一点,否则为了把接口加入页面,需要重新加载页面),现在我们实现传统的适配器接口:

  1. public class LegacyWebViewCompatibility implements IWebViewCompatibility {
  2. private WebView webView;
  3. private ValueCallbackAdapter callback;
  4. @Override public void injectWebView(WebView webView) {
  5. this.webView = webView;
  6. this.webView.addJavascriptInterface(new LegacyCallbackInterfaceHelper(), NAME);
  7. }
  8. @Override
  9. public void evaluateJavascript(final String script, ValueCallbackAdapter callback) {
  10. this.callback = callback;
  11. if (callback != null) {
  12. String js = String.format("javascript:{var res=%s;%s.%s(res);};void(0);", script, NAME,
  13. callback.javascriptInterfaceMethodName());
  14. webView.loadUrl(js);
  15. } else {
  16. webView.loadUrl("javascript:{" + script + "};void(0);"));
  17. }
  18. }
  19. ...
  20. class LegacyCallbackInterfaceHelper {
  21. static final String NAME = "legacyAndroidCallbackInterfaceHelper";
  22. @JavascriptInterface @SuppressWarnings("unused") // Called from js
  23. public void jimdoDefined(String result) {
  24. ((Activity) webView.getContext()).runOnUiThread(new Runnable() {
  25. @Override public void run() {
  26. LegacyWebViewCompatibilityDelegate.this.callback.evaluateResult(result);
  27. }
  28. });
  29. }
  30. }
  31. }

正如你所看到的,我们有很多方式来实现相同的结果。这里需要注意的是:回调应该运行在主线程——JavascriptInterface方法在WebView线程上运行,否则你会在log中得到一个警告。

从这点出发,我们可以用这点来检查是否javascript已经被注入,避免其被注入两次。

  1. javascriptInjector.injectFunction(screen, "typeof jimdo === \'undefined\'",
  2. new WebViewCompatibilityDelegate.ValueCallbackAdapter() {
  3. @Override public void evaluateResult(String jimdoUndefined) {
  4. if (Boolean.valueOf(jimdoUndefined)) {
  5. javascriptInjector.injectScript(screen, "my_script.js", null);
  6. }
  7. }
  8. @Override public String javascriptInterfaceMethodName() {
  9. // This corresponds to LegacyCallbackInterfaceHelper.jimdoDefined()
  10. return "jimdoDefined";
  11. }
  12. });

首先,程序注入一个javascript方法去检查有脚本定义的一个变量是否被定义,然后根据返回的结果来决定是否进行javascript注入。

翻译:@flyouting
时间:2014/03/20
源地址

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注