WebView 封装

作者: Justson | 来源:发表于2017-05-30 20:27 被阅读9986次

    WebView 是 Android 最复杂以及最强大的一个控件(最多坑) , 一大堆的 setting 让人摸不着头脑 , 很多时候压根不知道这个设置有什么用 ,加上 WebViewClient 和 WebChromeClient 做为内部类 , 一堆业务逻辑 , 使得 Activity 变得乱糟糟的 ,代码可读性更是糟糕透了 , 最后被逼上梁山 , 走上了封装的道路 。

    WebView 封装思路

    对于 WebView 的封装 , 相信很多人都是抽象在一个基类里面 , 封装成一个 BaseWebActivity , 或者 BaseWebFragment , 对于这种封装还是不能满足像我这种有洁癖有程序员的 , 因为复用性不高 , 而且容易导致 Activity 或者 Fragment 基类膨胀 。 下面向大家分享我的封装思路。

    首先让大家看下我封装的效果

    mAgentWeb = AgentWeb.with(this)//传入Activity
                    .setAgentWebParent(mLinearLayout, new LinearLayout.LayoutParams(-1, -1))//传入AgentWeb 的父控件 ,如果父控件为 RelativeLayout , 那么第二参数需要传入 RelativeLayout.LayoutParams
                    .useDefaultIndicator()// 使用默认进度条
                    .defaultProgressBarColor() // 使用默认进度条颜色
                    .setReceivedTitleCallback(mCallback) //设置 Web 页面的 title 回调
                    .createAgentWeb()//
                    .ready()
                    .go("http://www.jd.com");
    
    

    效果图

    jd.png

    上面已经封装成一个 Web 库了 , 叫 AgentWeb , 欢迎大家使用 。

    可以看到里面没有一句 WebSettings , 甚至 WebChromeClient 和 WebViewClient 都不用配置 , 使用的是简洁链式调用 。

    AgentWeb 封装思路是通过代理 , 将 WebView 从 Activity 或者 Fragment 中代理出来 , 不再需要 Activity 或者 Fragment 内部创建和管理 ,Activity 管理 WebView 需要通过 AgentWeb , 下面通过 UML 图来简单说明下 .

    BaseActivity 封装使用的 UML 关系图

    common_.png

    AgentWeb 封装使用的 UML 关系图

    agentweb结构.png

    BaseWebActivity 直接组合 WebView , 这样做为什么说复用性不高呢 ? 主要还是因为 WebView 依附在 BaseWebActivity 身上 ,要别人直接继承你的 Activity 是很不好的 ,因为 Java 的单继承关系 , 使得使用基类的灵活性受到很大的约束 , 这也是 Effective Java 里面提到的组合优先于继承 。

    AgentWeb 则不同 , AgentWeb 是一个独立的库 , 可以让你很方便一句话就引入 , 不需要依赖 BaseWebActivity , 就像上面一样简简单单一句话引入即可。

    AgentWeb 把 WebView 代理出来 , 将功能细分成一个类去管理 , 比如说的 WebCreator 负责创建 WebView 以及 进度条 、WebSettings 则是对 WebView 进行统一设置 , JsEntraceAccess 是对 Javascript 方法访问进行统一入口 , 这样做使得每一个功能独立 , 相互不影响 , 也使得 AgentWeb 的结构更清晰 , 符合单一职责原则 。 源码太多就不贴了 ,下面分享下封装 WebView 遇到的一些问题 。

    WebView 封装的一些问题与解决思路

    WebView 的封装可谓真是一波三折啊 , WebView 实在太多坑了 , 比如说 常见的泄露 , Js 安全 ,低版本跨源问题 , Context 引致的 onJsAlert 失效 ,Android 4.4 不支持文件选择问题等等。

    内存泄露

    这个问题在低版本不好解决就算类似下面代码通过反射制空 sConfigCallback 该字段, 还是有些手机会出现泄露 ,对于该问题 ,唯一有效方案解决在 AndroidManifest 里面为 Web Activity 添加 android:process=":web" 属性 , 然后在 该 Activity onDestroy 里面 执行 System.exit(0); 下面可以解决一部分泄露

                    Field field = WebView.class.getDeclaredField("mWebViewCore");
                    field = field.getType().getDeclaredField("mBrowserFrame");
                    field = field.getType().getDeclaredField("sConfigCallback");
                    field.setAccessible(true);
                    field.set(null, null);      
    

    addJavascriptInterface API 引起的远程代码执行漏洞

    对于这个问题 ,我相信大家或多或少都有点了解 ,问题是由注入类引起 ,从注入类中找到 Runtime 对象 ,可以通过 Runtime 执行 shell 命令 。 Google 只针对 Android 4.2.2 版本及以上版本给出了解决方法 ,为了解决兼容 4.2.2 以下版本这个问题 AgentWeb 采用 360 大牛给出的方案 , 向 Web 页面注入一段 Js 脚本 ,然后通过脚本弹 Prompt 向 Java 通信 ,解决了 4.2.2 以下版本 addJavascriptInterface 安全通信问题 。 下面给出大致实现

    比如下面注入类

    public class AndroidInterface {   
        public void callAndroid(final String msg) {
                Log.i("Info",""+msg);
        }
    }
    
     mAgentWebView.addJavascriptInterface("android",new AndroidInterface()); //注意 mAgentWebView 是 WebView 子类 , 并重写了 addJavascriptInterface 方法 。
    

    低于 Android 4.2.2 的版本上面的注入对象会经过包装拼接成如下脚本 。

    注入的脚本

    .....省略N多 Js 代码
    android.callAndroid = function() {
            .....省略N多 Js 代码
            var m = prompt('AgentWeb: ' + JSON.stringify({method: l, types: e, args: f}));
            var g = JSON.parse(m);
            if (g.code != 200) {
                throw "Android call error, code:" + g.code + ", message:" + g.result
            }
            return g.result
        };
    
    

    通过 webView.loadUrl(脚本) 注入上面的脚本 。

    Js 执行下面方法

    function sendHelloToAndroid() {    
            window.android.callAndroid("你好,Android! ");
      }
    
    

    就会执行上面的 function 里面的方法体 ,Android 端回调onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) message 参数里面取出 js 要调的目标方法 , 然后通过反射调用该目标方法 。

    对于同源跨域攻击问题

    什么是同源策略吗 ? 同源策略是由 Netscape 提出来的 ,现在主流的浏览器都遵循这种策略 ,同源是一般指http://(协议)www.google.com(主机):8080 (端口) 三要素都相同 ,但实际上并不是那么严格 , 比如 IE 就会忽略对端口的判断 。

    同源有什么作用吗 ?
    同源的数据默认为可以安全访问的 。 比如说 url http://www.google.com:xxxx/login 登录后浏览器就会把返回来的 cookies 保存起来 , 对于 http://www.google.com:xxxx/index.html 对于这个 url 浏览器会默认为跟前者同源 , 那么这个 url 可以无缝的访问到 login 保存下来的 cookies 。

    大家都知道 Android 应用之间的文件和数据一般情况下是不能相互访问的 , 但是不正确的使用 WebView ,会打破这种状况 , 比如说以下这种使用

    public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_webview);
            webView = (WebView) findViewById(R.id.webView1);
            webView.getSettings().setAllowFileAccess(true);                    
            webView.getSettings().setJavaScriptEnabled(true);                   
            webView.getSettings().setAllowFileAccessFromFileURLs(true);       
            webView.getSettings().setAllowUniversalAccessFromFileURLs(true); 
            Intent i = getIntent();
            String url = i.getData().toString(); 
            webView.loadUrl(url);
        }
    
    

    将该 Activity 设置 exported="true" , 其他应用就可以通过隐式启动 ,将 data 作为 url ,启动的该应用 , 让该应用加载脚本 ,遍历该应用内的文件或者私密文件 , 上传服务器 。

    这个问题一直存在 Android 手机中 ,这个问题 Google 并没有修复它 , 只是在 4.2 后的版本把 setAllowFileAccessFromFileURLs 以及 mWebSettings.setAllowUniversalAccessFromFileURLs 设置为 false , 用户没有刻意去开启它 , 高于 Android 4.2 版本默认是安全的 。 对于该问题 AgentWeb 使用的是以下设置 。

            mWebSettings.setAllowFileAccess(true); //允许file 协议 , 加载本地文件  
            mWebSettings.setAllowFileAccessFromFileURLs(false); //禁止通过 file url 加载的文件执行 Javascript 读取其他的本地文件 .
            mWebSettings.setAllowUniversalAccessFromFileURLs(false);//禁止通过 file url 加载的文件执行 Javascript 可以访问其他的源 如  http , https 。
    

    Context 引致的 onJsAlert 失效

    这个问题根本原因在防止泄露时候创建 new WebView(activity.getApplicationContext()); 导致 , 很正常啊 , 因为你创建 WebView 传的是 Application , Application 本身是无法弹 Dialog 的 。 所以只能无反应 !这个问题解决方案只要你创建 WebView 时候传入 Activity , 或者 自己实现 onJsAlert 方法即可。

    Android 4.4 文件访问

    Android 4.4 WebView 内核正式有 WebKit 替换 为 Chromium 使得很多 Api 都废弃掉了 , 这是 Google 正式宣告抛弃 Webkit 的一个句号 。 所以要兼容 Android 4.4 以下的 WebView 的应用特别难受 。 回到正题 , 4.4 文件访问 , 你会发现 4.4 点击 input 标签没反应 , 瞬间一万只曹尼玛在崩腾 , 幸庆的是还有 Js 通信 ,解决方案:可以通能过 Js 访问 Java 然后打开文件选择器 , 拿到文件后 , 将文件转成 Base64 字符串回传给 Js , 因为拿到的文件路径是 Content:// 开头 JS 是无法解析的 。(这个代码跨度有点大 , 就不贴源码了 , 有兴趣可以克隆仓库看下)

    WebView 封装后使用

    App 下载

    AgentWeb 在 Fragment 中使用

        @Override
        public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
            mAgentWeb = AgentWeb.with(this)// Fragment  传入
                    .setAgentWebParent((LinearLayout) view, new LinearLayout.LayoutParams(-1, -1))// 设置 AgentWeb 的父控件 , 这里的view 是 LinearLayout , 那么需要传入 LinearLayout.LayoutParams
                    .useDefaultIndicator()// 使用默认进度条
                    .setReceivedTitleCallback(mCallback) //标题回调
                    .setSecurityType(AgentWeb.SecurityType.strict) //注意这里开启 strict 模式 , 设备低于 4.2 情况下回把注入的 Js 全部清空掉 , 这里推荐使用 onJsPrompt 通信
                    .createAgentWeb()//
                    .ready()//
                    .go(getUrl());
            
        }
    
    

    跟原先 WebFragment 比简洁多了 。

    Js 调用 。

    function callByAndroid(msg1,msg2){
          console.log("callByAndroid")
      }
    

    没有经过封装的

    mWebView.loadUrl("javascript:callByAndroid("+"\"hello\""+","+"\" js\""+")");

    封装后

    mAgentWeb.getJsEntraceAccess().quickCallJs("callByAndroid","Hello","js");

    Github

    相关文章

      网友评论

      • 耀东wang:你这个根本加载不起网页,出现空白页
      • 寒涵:这个怎么去监听H5页面按钮的点击事件呢?
      • faa38d91c9d4:请问下怎么设置 网络错误或无网络页面,WebViewClient中重写onReceivedError时好时坏,一般怎么处理
      • 45e0d6c97522:作者你好,我用UC浏览器和你的AgentWeb打开百度贴吧的网页,弹出的登陆框点击登录后能正常显示验证码,可是用我自己设置的webView去打开,点击登录后,验证码的输入框出来了,但是验证码没出来,你知道我哪里设置错了吗, 求告知 ! :sob:
      • 阿笠卤鸡:怎么加载html啊!?
      • 60f6e371ac98:老哥,前端用vue.js写的页面,该怎么通信啊,头疼了:disappointed_relieved:
      • Colay:AgentWeb可以设置网页一段域名字符不变化吗
      • bad0f36f3ba2:有个译文,当我跳转都首页,需要清楚之前的回退记录怎么办?clearHistory并不好用啊,怎么才能做到清楚历史记录功能呢,canGoBack为false
      • 9dc4f7009d53:Fragment中是不能设置自定义进度条颜色了么?怎样屏蔽掉进度条呢?
      • 13871066fc58:大神你好 我在我的项目里用了你的这个库了 但是发现一个大Bug , 就是这个库如果加载的Url是通过网络后台获取的话 , 然后再初始化mAgentWeb ,在oppo , vivio 机型的手机第一次会白屏 ,第二次才有数据 求解, 求解 :cry: :cry:
      • 766a546ee35d:作为recyclerView 的headerView ,这个显示不了,不知道作者有没有考虑这种情况
        Justson: @lance_0ea7 固定高度了?
      • 神秘的_空指针:您好,我问下我用链式初始化,完成以后 我想重新加载页面 调用什么方法?还是获取webView直接加载?
      • xbase:想了解一下,有没有加载不出来的时候进行重试的机制?
      • 依然范特稀西:赞,js 和原生 之间相互通信这块有封装吗
      • caym:请问可以自定义settings吗
        Justson:可以
      • Xdjm:请问,支持传入一段html吗
        Justson:支持
      • b959449ccd5e:刚导入,发现一个问题,无法设置 android:supportsRtl="false",版本号:1.2.6
      • 付凯强:我想问下webview可以设置代理地址吗?
        付凯强:@ObjectNotFound 实现了, @Override
        public void onCreate() {
        Properties prop = System.getProperties();
        prop.setProperty("proxySet", "true");
        prop.setProperty("proxyHost", "218.241.131.227");
        prop.setProperty("proxyPort", "6100");
        super.onCreate();
        initMainHandler();
        initContext();
        initOKhttpAndCookie();
        }

        你在Application里面调用,按照我上面的代码操作顺序即可。
        88b749685413:请问你实现如何使用代理了么?
      • 335905722282:在哪设置背景透明
      • xbase:clearWebCache方法没有被使用,看到demo没有使用清除缓存的方法,请问应该如何使用?
      • 唐三藏丶:不支持缩放吗?
      • 3866b3f2e166:缓存是如何实现的,我看了源码,没找到:sob:
      • monkey_who:大佬,我是小白,想用你GITHUB上两天前出的AgentWebX5,但是不会接入,您能否在ReadMe中详细告知一下呢
        monkey_who:@Justson 嗯嗯,谢谢了
        Justson:不好意思 , 最近比较忙碌 , 后期会完善说明文档的。
      • 你需要一台永动机:楼主,toolbar的样式可以进行修改吗?
        Justson: 可以的 。 sample 只是演示
      • wo叫天然呆:如果我界面上并非只有AgentWeb,还有其他布局,那这个要怎么添加?
        Justson:传入 WebLayout
      • boboyuwu:这个库多大重吗
        Justson:非常轻量 100多KB 。
      • 无尽bug伴不眠:还是建议用X5内核webView 自带的内存泄漏太严重 再怎么处理还是泄漏
        Justson:https://github.com/Justson/AgentWebX5
      • 张超楠:如果使用eclipse开发APP,有jar包可调用实现这个吗?
        wo叫天然呆:@张超楠 兄弟,那你要尽快切换过来了,不然后续不好开展工作:blush:
        张超楠:@wo叫天然呆 恩,一直都在用eclipse,没有用过Android Studio
        wo叫天然呆:现在还用Eclipse?
      • 山水公子:compile 'com.just.agentweb:agentweb:1.2.2' 这里面找不到addDownLoadResultListener方法,是不是最新代码没提交到仓库?下载的demo里面有
        Justson:@山水公子 兼容的哦 。
        山水公子: @Justson 下载安装不兼容android7.0
        Justson:@山水公子 是的 , 下个版本支持 。
      • ximencx:请问视频全屏 在哪个类实现的??有什么限制么
        ximencx: @Justson 大佬看了源码,是把view替换成全屏的,那个方法能过去播放链接呢
        Justson:VideoImpl
      • AwaitZhang:哦哦,看到了,谢谢您了,表示已经收藏关注
      • 中午晚上吃通心粉:你好,请问怎么注入cookie,还是说自己用CookieManager?
        Justson:AgentWebConfig.syncCookies("http://www.jd.com","ID=XXXX")
      • 668e8902e23b:请问为什么下载总是提示下载失败出错了
      • d0e1ce288d00:如何添加 开始 成功 失败的加载监听啊?
        Justson:@语过添情丶 链式调用里面加入 `setWebChromeClient(mWebChromeClient)`

        或者 `mAgentWeb.getWebCreator().get()`得到 WebView
        d0e1ce288d00:@Justson 现在我获得不到webView的对象 只用了一组链式调用~,求告知如何传入~
        Justson:传入 WebViewClient 。
      • 醉翁之意不在酒也:net::ERR_FILE_NOT_FOUND

        总是报这个错误,权限也都加进去了
        醉翁之意不在酒也:@Justson 恩恩; 找到原因了。 太粗心。 用起来真心不错。
        Justson: 找不到文件,检查下文件路径 。
        醉翁之意不在酒也:加载Aeerts下面的Html的时候 会这样。
      • 40952825d023:4.4及以上的文件上传可以通过重写方法实现。我前几天刚解决,没有使用js调用原生的方法。
      • 小彤花园:在github trending上看到的
        Justson:感谢支持!
      • Shanghai_Liu:因是菜鸟,故问点小白问题
        setReceivedTitleCallback(mCallback) //标题回调 可传null?接口回调?
        Justson:可以为null , 新版本我已加入 @nullable 注解 。
      • jiushiwo:直接github添加依赖,使用最简单的模板api 加载不出网页!
        Justson:@一个叔叔的梦想时光 https://github.com/Justson/AgentWeb/issues/87 ,是否使用了 https ,如果使用了,请检查是否是证书错误, 如果没有使用https , 请检查url是否包含了scheme ,如果没有也会导致空白页面 , 因为WebVIew加载需要完整协议。
        见字如晤一:我的也是,加载出来的是空白的,你解决了吗
        9633a82792da:支不支持js调安卓呢
      • 074d47aebdec:有对应的Jar包吗?我现在还在用eclipse。。。:flushed:
        Chauncey_Chen:我的天......
        haoshr_hada:@我是岳小岳 你咋艾特我了
        haoshr_hada: @尐懒猪_135c 赶快配置android studio好用的不得了
      • 蓝小默:js调用安卓怎么写?
      • PeanutZYH:请问有没有屏蔽广告的功能?
        PeanutZYH:@Justson x5 没有查到主动调用屏蔽广告的方法呢
        Justson:没有哦 , x5 具有这个特性。
      • 27b6276357e8:> Keystore file D:\AndroidStudioProjects\AgentWeb-master\sample\keystore\keystore.jks not found for signing config 'release'.
        请问这个怎么解决?
        jiushiwo:github下载的压缩包 keystore,jks 直接放在了sample下,没有放到指定路径
        可以修改sample下的build.gradle 下的signingConfigs节点下密钥路径,也可以在sample下新建一个keystore文件夹,将sample下的keystore.jks文件拖进去
      • smartapple:下载回调的接口是什么啊?
        Justson:下载回调部分还没完成 , 有时间在整理一下 。 :pensive:
      • 430cbe953065:如何刷新网页?reload?
        Justson:mAgentWeb.getLoader().reload();
      • enlogy:很好

      本文标题:WebView 封装

      本文链接:https://www.haomeiwen.com/subject/zwgrfxtx.html