编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • 库的解读

    • 专栏博客

      • 系统启动Zygote
      • Binder通信原理
      • Handler消息机制
      • Activity启动原理
      • 四大组件原理分析
      • AMS与组件管理
      • View绑制与渲染
      • 事件分发机制
      • Surface渲染原理
      • 自定义View设计
      • WMS窗口管理
      • PMS与APK安装
      • 虚拟机与类加载
      • 内存管理与GC
      • 线程与并发编程
      • 性能优化与监控
      • 序列化与数据存储
      • 组件化与路由设计
      • 插件化与热修复
      • NDK开发实践
      • WebView核心设计
        • ADB常见使用操作
      • 智能硬件

    • iOS开发和进阶

    • Web开发和进阶

    • Linux应用开发

    • Apps
    • Android提升进阶
    • 专栏博客
    杨充
    2025-03-19
    目录

    WebView核心设计

    # WebView封装实践

    # 目录介绍

    • 01.整体概述
      • 1.1 项目背景
      • 1.2 遇到问题
      • 1.3 基础概念
      • 1.4 设计目标
      • 1.5 收益分析
    • 02.WebView痛点
      • 2.1 为何WebView难搞
      • 2.2 loadUrl过程复杂
      • 2.4 异常Error场景多
      • 2.6 H5为何加载速度慢
    • 03.技术点分析
      • 3.1 js交互介绍说明
      • 3.2 Android调用Js
      • 3.3 Js调用Android
      • 3.4 Js交互方案对比
      • 3.5 Js调用时机分析
    • 04.封装思路介绍
      • 4.1 封装整体思路
      • 4.2 封装功能介绍
      • 4.4 Native和Js交互
      • 4.5 缓存设计思路
      • 4.6 OkHttp缓存策略
      • 4.7 加载进度条设计
    • 05.方案基础设计
      • 5.1 整体架构图
      • 5.2 UML设计图
      • 5.3 关键流程图
      • 5.4 接口设计图
      • 5.5 模块间依赖关系
    • 07.其他设计说明
      • 7.1 性能设计
      • 7.2 稳定性设计
      • 7.3 灰度设计
      • 7.4 降级设计
      • 7.5 异常设计
      • 7.6 兼容性设计
      • 7.7 安全性设计
    • 08.其他说明介绍
      • 8.1 参考链接

    # 01.整体概述

    # 1.1 项目背景

    # 1.2 遇到问题

    • 重定向导致的问题
      • 加载进度条,重定向(多次调用onPageStarted和onPageFinished)会导致多次加载情况,如何避免多次重定向只会加载一次进度条。
    • WebView加载异常难管理

    # 1.3 基础概念

    • 触发加载网页的行为
      • 主要有两种方式:(A)点击页面,触发标签;(B)调用WebView的loadUrl()方法。这两种方法都会发出一条地址,区别就在于这条地址是目的地址还是重定向地址。
    • 加载h5页面大概流程【请求数据+渲染数据】
      • 1.dns域名解析(将域名解析成ip地址,比较耗时);2.TCP的三次握手;3.建立TCP连接后发起HTTP请求;4.服务器响应HTTP请求
      • 5.浏览器解析html代码;6.同时请求html代码中的资源(如js、css、图片等),注意html中资源是串行;7.最后浏览器对页面进行渲染并呈现给用户

    # 1.4 设计目标

    • 支持处理js的交互逻辑,方便快捷,并且无耦合,操作十分简单,优雅解决重定向回退,白屏等问题;
    • 暴露进度条加载进度,结束,以及异常状态(分多种状态:无网络,404,onReceivedError,sslError异常等)listener给开发者;
    • 支持视频播放,可以切换成全频播放视频,可旋转屏幕,暴露视频操作监听listener给开发者;
    • 集成了腾讯x5的WebView,最新版本,功能强大;支持打开文件的操作,比如打开相册,然后选中图片上传,兼容版本(5.0);
    • 支持加载word,xls,ppt,pdf,txt等文件文档,使用方法十分简单;
    • 支持设置仿微信加载H5页面进度条,完全无耦合,操作简单,极大提高用户体验;
    • 支持用户按照规范自定义WebViewClient和WebChromeClient,不影响js通信;
    • 汇集绝大多数问题,以及解决方案,是学习和深入理解webView的一个比较全面的案例;
    • 除了webView自带缓存外,还添加了资源拦截缓存,交给OkHttp去做,支持设置超时,设置缓存空间大小;
    • 统一处理web页面打电话,发短信,定位,邮件,开启支付宝,微信等scheme拦截处理;
    • 充分运用了面向对象的设计思想,将视频全屏播放,scheme拦截,web进度条,拦截缓存抽成独立的部分,你也可以拿来即用,完全解耦;

    # 1.5 收益分析

    • 提高webView开发效率,大概要节约你百分之六十的时间成本,一键初始化操作;

    # 02.WebView痛点

    # 2.1 为何WebView难搞

    • 繁杂的WebView配置
      • WebView在初始化的时候就提供了默认配置WebSettings,但是很多默认配置是不能够满足业务需求的,还需要进行二次配置
      • 除此之外,使用方还需要根据业务需求实现WebViewClient和WebChromeClient,这两个类所需要覆写的方法更多,用来实现标题定制、加载进度条控制、jsbridge交互、url拦截、错误处理(包括http、资源、网络)等很多与业务相关的功能。
    • 复杂的前端环境
      • html、css、js相应的升级与更新。高版本的语法无法在低版本的内核上识别和渲染,业务上需要使用到新的特性时,开发不得不面对后向兼容的问题。
    • 需要一定的Web知识
      • 使用WebView.loadUrl()来加载一个网页而不了解底层到底发生了什么,那么url发生错误、url中的某些内容加载不出来、url里的内容点击无效、支付宝支付浮层弹不起来、与前端无法沟通等等问题就会接踵而至。要开发好一个功能完整的WebView,需要对Web知识(html、js、css)有一定了解,知道loadUrl,WebView在后台请求这个url以后,服务器做了哪些响应,又下发了哪些资源,这些资源的作用是怎么样的。

    # 2.2 loadUrl过程复杂

    • WebView.loadUrl(url)加载网页做了什么?
      • 加载网页前,重置WebView状态以及与业务绑定的变量状态。WebView状态包括重定向状态(mTouchByUser)、前端控制的回退栈(mBackStep)等,业务状态包括进度条、当前页的分享内容、分享按钮的显示隐藏等。
      • 加载网页前,根据不同的域拼接本地客户端的参数,包括基本的机型信息、版本信息、登录信息以及埋点使用的Refer信息等,有时候涉及交易、财产等还需要做额外的配置。
      • 开始执行页面加载操作时,会回调WebViewClient.onPageStarted(webView,url,favicon)。在此方法中,可以重置重定向保护的变量(mRedirectProtected),当然也可以在页面加载前重置,由于历史遗留代码问题,此处尚未省去优化。
    • 加载页面的过程中回调哪些方法?
      • WebChromeClient.onReceivedTitle(webView, title),用来设置标题。需要注意的是,在部分Android系统版本中可能会回调多次这个方法,而且有时候回调的title是一个url,客户端可以针对这种情况进行特殊处理,避免在标题栏显示不必要的链接。
      • WebChromeClient.onProgressChanged(webView, progress),根据这个回调,可以控制进度条的进度(包括显示与隐藏)。一般情况下,想要达到100%的进度需要的时间较长(特别是首次加载),用户长时间等待进度条不消失必定会感到焦虑,影响体验。其实当progress达到80的时候,加载出来的页面已经基本可用了。事实上,国内厂商大部分都会提前隐藏进度条,让用户以为网页加载很快。
      • WebViewClient.shouldInterceptRequest(webView, request),无论是普通的页面请求(使用GET/POST),还是页面中的异步请求,或者页面中的资源请求,都会回调这个方法,给开发一次拦截请求的机会。在这个方法中,我们可以进行静态资源的拦截并使用缓存数据代替,也可以拦截页面,使用自己的网络框架来请求数据。包括后面介绍的WebView免流方案,也和此方法有关。
      • WebViewClient.shouldOverrideUrlLoading(webView, request),如果遇到了重定向,或者点击了页面中的a标签实现页面跳转,那么会回调这个方法。可以说这个是WebView里面最重要的回调之一,后面WebView与Native页面交互一节将会详细介绍这个方法。
      • WebViewClient.onReceivedError(webView,handler,error),加载页面的过程中发生了错误,会回调这个方法。主要是http错误以及ssl错误。在这两个回调中,我们可以进行异常上报,监控异常页面、过期页面,及时反馈给运营或前端修改。在处理ssl错误时,遇到不信任的证书可以进行特殊处理,例如对域名进行判断,针对自己公司的域名“放行”,防止进入丑陋的错误证书页面。也可以与Chrome一样,弹出ssl证书疑问弹窗,给用户选择的余地。
    • 加载页面结束回调哪些方法
      • 会回调WebViewClient.onPageFinished(webView,url)。这时候可以根据回退栈的情况判断是否显示关闭WebView按钮。通过mActivityWeb.canGoBackOrForward(-1)判断是否可以回退。

    # 2.4 异常Error场景多

    • 对于WebView加载一个网页过程中所产生的错误回调,在WebViewClient中,大致有三种:
      • onReceivedHttpError,任何HTTP请求产生的错误都会回调这个方法,包括主页面的html文档请求,iframe、图片等资源请求。
      • onReceivedSslError,任何HTTPS请求,遇到SSL错误时都会回调这个方法。
      • onReceivedError,只有在主页面加载出现错误时,才会回调这个方法。
    • 加载页面还可能有异常,比如找不到资源,404,在WebChromeClient中,大概有两种:
      • onReceivedTitle,监听标题可能出现 404,网页无法打开,等异常
    • 这些异常中处理什么逻辑
      • onReceivedHttpError,在这个回调中,由于混杂了很多请求,不适合用来展示加载错误的页面,而适合做监控报警。当某个URL,或者某个资源收到大量报警时,说明页面或资源可能存在问题,这时候可以让相关运营及时响应修改。
      • onReceivedSslError,比较正确的做法是让用户选择是否信任这个网站,这时候可以弹出信任选择框供用户选择(大部分正规浏览器是这么做的)。也可以让一些特定的网站,不管其证书是否存在问题,都让用户信任它。
      • onReceivedError,这正是展示加载错误页面最合适的方法。如果不管三七二十一直接展示错误页面的话,那很有可能会误判,给用户造成经常加载页面失败的错觉。

    # 2.6 H5为何加载速度慢

    • webView是怎么加载网页的呢?
      • webView初始化->DOM下载→DOM解析→CSS请求+下载→CSS解析→渲染→绘制→合成
    • 渲染速度慢
      • 前端H5页面渲染的速度取决于 两个方面:
        • Js 解析效率。Js 本身的解析过程复杂、解析速度不快 & 前端页面涉及较多 JS 代码文件,所以叠加起来会导致 Js 解析效率非常低
        • 手机硬件设备的性能。由于Android机型碎片化,这导致手机硬件设备的性能不可控,而大多数的Android手机硬件设备无法达到很好很好的硬件性能
      • 总结:上述两个原因 导致 H5页面的渲染速度慢。
    • 页面资源加载缓慢
      • H5 页面从服务器获得,并存储在 Android手机内存里:
        • H5页面一般会比较多
        • 每加载一个 H5页面,都会产生较多网络请求:
          • HTML 主 URL 自身的请求;
          • HTML外部引用的JS、CSS、字体文件,图片也是一个独立的 HTTP 请求
        • 每一个请求都串行的,这么多请求串起来,这导致 H5页面资源加载缓慢
    • 总结:H5页面加载速度慢的原因:渲染速度慢 & 页面资源加载缓慢 导致。

    # 03.技术点分析

    # 3.1 js交互介绍说明

    • Java调用js方法有两种:
      • 第一种:WebView.loadUrl("javascript:" + javascript);
      • 第二种:WebView.evaluateJavascript(javascript, callback);
    • Java调用js方法方案对比
      • 一般最常使用的就是第一种方法,但是第一种方法获取返回的值比较麻烦,而第二种方法由于是在 4.4 版本引入的,所以局限性比较大。注意问题,记得添加ws.setJavaScriptEnabled(true)代码
    • js调用Java的方法有四种:
      • 第一种:JavascriptInterface
      • 第二种:WebViewClient.shouldOverrideUrlLoading()
      • 第三种:WebChromeClient回调接口
    • js调用Java的方案对比
      • 第一种:这种方式的好处在于使用简单明了,本地和 JS 的约定也很简单,就是对象名称和方法名称约定好即可,缺点就是要提到的漏洞问题。在Js代码中就能直接通过“JSObject”的对象直接调用了该Native的类的方法。
      • 第二种:这个方法可以拦截 WebView 中加载 url 的过程,得到对应的 url,我们就可以通过这个方法,与网页约定好一个协议,如果匹配,执行相应操作。
      • 第三种:onJsAlert,onJsConfirm,onJsPrompt,三个弹窗都是可以处理js交互的。拦截这些方法,得到他们的内容,进行解析,比如如果是 JS 的协议,则说明为内部协议,进行下一步解析然后进行相关的操作即可。
        • 需要注意的是 prompt 里面的内容是通过 message 传递过来的,并不是第二个参数的 url,返回值是通过 JsPromptResult 对象传递。
        • 为什么要拦截 onJsPrompt 方法,而不是拦截其他的两个方法,这个从某种意义上来说都是可行的,但是如果需要返回值给 web 端的话就不行了。
        • 因为 onJsAlert 是不能返回值的,而 onJsConfirm 只能够返回确定或者取消两个值,只有 onJsPrompt 方法是可以返回字符串类型的值,操作最全面方便。

    # 3.2 Android调用Js

    • 第一种方式:native 调用 js 的方法,注意的是名字一定要对应上,要不然是调用不成功的,而且还有一点是 JS 的调用一定要在 onPageFinished 函数回调之后才能调用,要不然也是会失败的。
      //java
      //调用无参方法
      mWebView.loadUrl("javascript:callByAndroid()");
      //调用有参方法
      mWebView.loadUrl("javascript:showData(" + result + ")");
      
      //javascript,下面是对应的js代码
      <script type="text/javascript">
      
      function showData(result){
          alert("result"=result);
          return "success";
      }
      
      function callByAndroid(){
          console.log("callByAndroid")
          showElement("Js:无参方法callByAndroid被调用");
      }
      </script>
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
    • 第二种方式:使用evaluateJavascript,Android4.4新增加了一个新方法,调用Web还有回调。
      • 这个方法比 loadUrl 方法更加方便简洁,效率更高,因为 loadUrl 的执行会造成页面刷新一次,这个方法不会,因为这个方法是在 4.4 版本才引入的,所以使用的时候需要添加版本的判断。思考为何效率更高?高在哪里?
      if (Build.VERSION.SDK_INT < 18) {
          mWebView.loadUrl(jsStr);
      } else {
          mWebView.evaluateJavascript(jsStr, new ValueCallback<String>() {
              @Override
              public void onReceiveValue(String value) {
                  //此处为 js 返回的结果
              }
          });
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

    # 3.3 Js调用Android

    • 第一种方式:通过 addJavascriptInterface 方法进行添加对象映射
      • 这种是使用最多的方式了,首先第一步我们需要设置一个属性:setJavaScriptEnabled(true)
      • 这个函数会有一个警告,因为在特定的版本之下会有非常危险的漏洞,设置完这个属性之后,Native需要定义一个类:
      • 在 API17 版本之后,需要在被调用的地方加上 @addJavascriptInterface 约束注解,因为不加上注解的方法是没有办法被调用的
      public class JSObject {
          private Context mContext;
          public JSObject(Context context) {
              mContext = context;
          }
      
          @JavascriptInterface
          public String showToast(String text) {
              Toast.show(mContext, text, Toast.LENGTH_SHORT).show();
              return "success";
          }
          
          @JavascriptInterface
          public void imageClick(String src) {
              Log.e("imageClick", "----点击了图片");
          }
          
          //网页使用的js,方法无参数
          @JavascriptInterface
          public void startFunction() {
              Log.e("startFunction", "----无参");
          }
      }
      //特定版本下会存在漏洞,第一个是对象,第二个是名称
      mWebView.addJavascriptInterface(new JSObject(this), "javascriptInterface");
      
      //下面是js代码
      function showToast(){
          var result = myObj.showToast("我是来自web的Toast");
      }
      
      function showToast(){
          myObj.imageClick("图片");
      }
      
      function showToast(){
          myObj.startFunction();
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
    • 第二种方式:shouldOverrideUrlLoading
      • 这种方式其实实现也很简单,使用的频次也很高,上面介绍到了 WebViewClient 。其中有个回调接口 shouldOverrideUrlLoading ,就是利用这个拦截 url,然后解析这个 url 的协议。如果发现是我们预先约定好的协议就开始解析参数,执行相应的逻辑。
      public boolean shouldOverrideUrlLoading(WebView view, String url) {
          //假定传入进来的 url = "ycjs://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
          //如果 scheme 为 ycjs,代表为预先约定的 ycjs 协议
          if (Uri.parse(url).getScheme().equals("ycjs")) {
              //代表应用内部处理完成
              return true;
          }
          return super.shouldOverrideUrlLoading(view, url);
      }
      
      //JS 代码调用
      function openActivity(){
          document.location = "js://openActivity?arg1=111&arg2=222";
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      • 存在问题:这个代码执行之后,就会触发本地的 shouldOverrideUrlLoading 方法,然后进行参数解析,调用指定方法。
      //java
      mWebView.loadUrl("javascript:returnResult(" + result + ")");
      
      //javascript
      function returnResult(result){
          alert("result is" + result);
      }
      
      1
      2
      3
      4
      5
      6
      7
    • 第三种方式:利用 WebChromeClient 回调接口的三个方法拦截消息
      • 这个方法的原理和第二种方式原理一样,都是拦截相关接口,只是拦截的接口不一样:
      @Override
      public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
          return super.onJsAlert(view, url, message, result);
      }
      
      @Override
      public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
          return super.onJsConfirm(view, url, message, result);
      }
      
      @Override
      public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
          //假定传入进来的 message = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
          Uri uri = Uri.parse(message);
          if (uri.getScheme().equals("js")) {
              if (uri.getAuthority().equals("openActivity")) {
                  //代表应用内部处理完成
                  result.confirm("success");
              }
              return true;
          }
          return super.onJsPrompt(view, url, message, defaultValue, result);
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      • onJsAlert 方法是弹出警告框,一般情况下在 Android 中为 Toast,在文本里面加入\n就可以换行;
      • onJsConfirm 弹出确认框,会返回布尔值,通过这个值可以判断点击时确认还是取消,true表示点击了确认,false表示点击了取消;
      • onJsPrompt 弹出输入框,点击确认返回输入框中的值,点击取消返回 null。
      • 但是这三种对话框都是可以本地拦截到的,所以可以从这里拦截做一些逻辑处理,prompt 方法调用如下所示:
      function clickprompt(){
          var result=prompt("js://openActivity?arg1=111&arg2=222");
          alert("open activity " + result);
      }
      
      1
      2
      3
      4

    # 3.4 Js交互方案对比

    • 以上三种方案都是可行的,在这里总结一下
    • 第一种方式:使用广泛
      • 是现在目前最普遍的用法,方便简洁,但是唯一的不足是在 4.2 系统以下存在漏洞问题;
    • 第二种方式:拦截约定规范协议
      • 通过拦截 url 并解析,如果是已经约定好的协议则进行相应规定好的操作,缺点就是协议的约束需要记录一个规范的文档,而且从 Native 层往 Web 层传递值比较繁琐,优点就是不会存在漏洞。
      • 它也有一个很繁琐的地方是,如果 web 端想要得到方法的返回值,只能通过 WebView 的 loadUrl 方法去执行 JS 方法把返回值传递回去。
    • 第三种方式:拦截3种对话框
      • 和第二种方式的思想其实是类似的,只是拦截的方法变了,这里拦截了 JS 中的三种对话框方法。
      • 而这三种对话框方法的区别就在于返回值问题,alert 对话框没有返回值,confirm 的对话框方法只有两种状态的返回值,prompt 对话框方法可以返回任意类型的返回值。
      • 缺点就是协议的制定比较麻烦,需要记录详细的文档,但是不会存在第二种方法的漏洞问题。
    • js调用java方法比较和区别分析
      • 1.通过 addJavascriptInterface 方法进行添加对象映射。js最终通过对象调用原生方法
      • 2.shouldOverrideUrlLoading拦截操作,获取scheme匹配,与网页约定好一个协议,如果匹配,执行相应操作
      • 3.利用WebChromeClient回调接口onJsPrompt拦截操作。
        • onJsAlert 是不能返回值的,而 onJsConfirm 只能够返回确定或者取消两个值,只有 onJsPrompt 方法是可以返回字符串类型的值,操作最全面方便。

    # 3.5 Js调用时机分析

    • onPageFinished()或者onPageStarted()方法中注入js代码
      • 做过WebView开发,并且需要和js交互,大部分都会认为js在WebViewClient.onPageFinished()方法中注入最合适,此时dom树已经构建完成,页面已经完全展现出来。但如果做过页面加载速度的测试,会发现WebViewClient.onPageFinished()方法通常需要等待很久才会回调(首次加载通常超过3s),这是因为WebView需要加载完一个网页里主文档和所有的资源才会回调这个方法。
      • 能不能在WebViewClient.onPageStarted()中注入呢?答案是不确定。经过测试,有些机型可以,有些机型不行。在WebViewClient.onPageStarted()中注入还有一个致命的问题——这个方法可能会回调多次,会造成js代码的多次注入。
      • 从7.0开始,WebView加载js方式发生了一些小改变,官方建议把js注入的时机放在页面开始加载之后。
    • WebViewClient.onProgressChanged()方法中注入js代码
      • WebViewClient.onProgressChanged()这个方法在dom树渲染的过程中会回调多次,每次都会告诉我们当前加载的进度。
        • 在这个方法中,可以给WebView自定义进度条,类似微信加载网页时的那种进度条
        • 如果在此方法中注入js代码,则需要避免重复注入,需要增强逻辑。可以定义一个boolean值变量控制注入时机
      • 那么有人会问,加载到多少才需要处理js注入逻辑呢?
        • 正是因为这个原因,页面的进度加载到80%的时候,实际上dom树已经渲染得差不多了,表明WebView已经解析了标签,这时候注入一定是成功的。在WebViewClient.onProgressChanged()实现js注入有几个需要注意的地方:
        • 1 上文提到的多次注入控制,使用了boolean值变量控制
        • 2 重新加载一个URL之前,需要重置boolean值变量,让重新加载后的页面再次注入js
        • 3 如果做过本地js,css等缓存,则先判断本地是否存在,若存在则加载本地,否则加载网络js
        • 4 注入的进度阈值可以自由定制,理论上10%-100%都是合理的,不过建议使用了75%到90%之间可以。

    # 04.封装思路介绍

    # 4.1 封装整体思路

    # 4.4 Native和Js交互

    • java调用js的流程图
      • 第一步操作:mWebView.callHandler("functionInJs", "小杨逗比", new CallBackFunction() {//这里面是回调});
      • 第二步操作:将handlerName,data,responseCallback,封装到Message对象中,然后开始分发数据,最后webView执行_handleMessageFromNative;
      • 第三步操作:去WebViewJavascriptBridge.js类中找到_handleMessageFromNative方法,js根据"functionInJs"找到对应的js方法并且执行;
      • 第四步操作:js把运行结果保存到message对象中,然后添加到js消息队列中;
      • 第五步操作:在_dispatchMessageFromNative方法中,可以看到,js向native发送 "消息队列中有消息" 的通知;
      • 第六步操作:webView执行js的_fetchQueue(WebViewJavascriptBridge.js类)方法;
      • 第七步操作:js把消息队列中的所有消息都一起回传给webView;
      • 第八步操作:webView收到所有的消息,一个一个串行处理,注意其中包括 "functionInJs"方法运行的结果的消息;
    • js调用Android的流程图
      • 第一步操作:mWebView.registerHandler("toPhone", new BridgeHandler() { //回调});
      • 第二步操作:调用messageHandlers.put(handlerName, handler),将名称和BridgeHandler对象放到map集合中
      • 第三步操作:在shouldOverrideUrlLoading方法中拦截url,与网页约定好一个协议,匹配则执行相应操作,也就是利用WebViewClient接口回调方法拦截url
      • 第四步操作:如果是url.startsWith(BridgeUtil.YY_RETURN_DATA)则有数据返回;如果是BridgeUtil.YY_OVERRIDE_SCHEMA则刷新消息队列
      • 第五步操作:通过BridgeHandler对象,将data和callBackFunction返回交给开发者

    # 4.5 缓存设计思路

    • 解决方案
      • 前端H5的缓存机制(WebView 自带)
      • 资源预加载
      • 资源拦截
    • shouldInterceptRequest入口
      • 缓存方案的入口
        • webView在加载网页的时候,用户能够通过系统提供的API干预各个中间过程。我们要拦截的就是网页资源请求的环节。主要是:shouldInterceptRequest,这个方法。
        • 是在调用了WebView#loadUrl()之后,请求网页资源(包括html文件、js文件、css文件以及图片文件)的时候回调。
        • 需要注意:回调不是发生在主线程,因此不能做一些处理UI的事情;接口的返回值是同步的;WebResourceResponse这个返回值可以自行构造,其中关键的属性主要是:代表资源内容的一个输入流InputStream以及标记这个资源内容类型的mMimeType。
      • 替换资源操作
        • 只要在这两个入口构造正确的WebResourceResponse对象,就可以替换默认的请求为我们提供的资源
        • 因此,在每次请求资源的时候根据请求的URL/WebResourceRequest判断是否存在本地的缓存,并在缓存存在的情况下将缓存的输入流返回
    • 方案1:本地资源替换操作,不友好
      • 操作思路
        • 用本地文件js,css,png替换网络请求下来的文件。在shouldInterceptRequest方法中拦截资源,如果是js,png,css资源则直接替换成本地的。操作比较简单
      • 该案例存在问题
        • 需要提前在本地存放大量缓存文件,如果是服务器下发比较麻烦;如果直接放本地缓存文件,则需要app升级;
    • 方案2:处理预加载数据
      • 大概思路介绍
        • 预加载数据就是在客户端初始化WebView的同时,直接由native开始网络请求数据, 当页面初始化完成后,向native获取其代理请求的数据, 数据请求和WebView初始化可以并行进行,缩短总体的页面加载时间。
        • 简单来说就是配置一个预加载列表,在APP启动或某些时机时提前去请求,这个预加载列表需要包含所需H5模块的页面和资源, 客户端可以接管所有请求的缓存,不走webView默认缓存逻辑, 自行实现缓存机制, 原理其实就是拦截WebViewClient的那两个shouldInterceptRequest方法。
      • 存在的问题
        • 待完善
    • 方案3:缓存资源下发替换
      • 大概的思路
        • 通过拦截webView中渲染网页过程中各种资源(包括图片、js文件、css样式文件、html页面文件等)的下载,根据业务的场景考虑缓存的策略
      • 实现步骤
        • 事先将更新频率较低、常用 & 固定的H5静态资源 文件(如JS、CSS文件、图片等) 放到本地
        • 拦截H5页面的资源网络请求 并进行检测
        • 如果检测到本地具有相同的静态资源 就 直接从本地读取进行替换 而 不发送该资源的网络请求 到 服务器获取
      • 拦截处理
        • 在shouldInterceptRequest方法中拦截处理
        • 步骤1:判断拦截资源的条件,即判断url里的图片资源的文件名
        • 步骤2:创建一个输入流,这里可以先从内存中拿,拿不到从磁盘中拿,再拿不到就从网络获取数据
        • 步骤3:打开需要替换的资源(存放在assets文件夹里),或者从lru中取出缓存的数据
        • 步骤4:替换资源
      • 有几个问题
        • 如何判断url中资源是否需要拦截,或者说是否需要缓存
        • 如何缓存js,css等
        • 缓存数据是否有时效性,服务端下发,要是不是要和服务端一起做
    • 方案4:资源请求拦截使用OkHttp缓存,很友好
      • 核心任务就是拦截资源请求,下载资源并缓存资源,因此拦截缓存的设计就分为了下面三个核心点:
        • 请求拦截
        • 资源响应(下载/读取缓存)
        • 缓存,直接使用OkHttp自带缓存
      • 请求拦截
        • 先判断是否需要缓存,然后从Url获取文件扩展名extension,从扩展中获取Mime类型,当mine类型为空时则不缓存
        • 根据对应的资源请求定义是否参与拦截、以及选择性的自定义配置下载和缓存的行为
      • 资源响应(下载/读取缓存)
        • 资源响应有两种情况:
          • 缓存响应
          • 下载响应
        • 当对应的资源缓存不存在的时候,会直接触发资源的下载。

    # 4.6 OkHttp缓存策略

    • 这个是使用方案4,采用OkHttp拦截资源缓存,下面是大概的思路。缓存的入口从shouldInterceptRequest出发
      • 第一步,拿到WebResourceRequest对象中请求资源的url还有header,如果开发者设置不缓存则返回null
      • 第二步,如果缓存,通过url判断拦截资源的条件,过滤非http,音视频等资源,这个是可自由配置缓存内容比如css,png,jpg,xml,txt等
      • 第三步,判断本地是否有OkHttp缓存数据,如果有则直接读取本地资源,通过url找到对应的path路径,然后读取文件流,组装数据返回。
      • 第四步,如果没有缓存数据,创建OkHttp的Request请求,将资源网络请求交给okHttp来处理,并且用它自带的缓存功能,当然如果是请求失败或者异常则返回null,否则返回正常数据
    • 把缓存的复杂逻辑完全交给OkHttp
      • 待完善

    # 4.7 加载进度条设计

    • 进度条场景分析:网页需要在我们加载完成后需要去关闭自定义进度条
      • 如果是一个没有重定向的网页加载这样是没有问题的。如果你的页面重定向了并且还有可能是多次的,我们的在onPageStarted和onPageFinished会回调多次,就会导致进度条出现重复加载
    • 为何会回调多次onPageFinished
      • 重定向导致。举个例子,比如跳转天猫首页,会判断是否登陆,如果没有登陆则重定向到登陆注册页面。主要就要考虑如何避免重定向行为造成的多次加载这种情况。
    • 方案1:不够优雅,且多次重定向不太友好
      • 1、在onPageStarted()中设置为true,若加载样式没有开启,就开启进度条等加载样式,
      • 2、在onPageFinished()中检测,如果为true,就说明已经是目的地址了,可以关闭加载样式了,如果是false,就不做处理,继续等待,
      • 3、在shouldOverrideUrlLoading()中,设置为false,若加载样式没有开启,就开启进度条等加载样式
    • 方案2:不够优雅
      • 1、定义running记录次数,在shouldOverrideUrlLoading记录为running++,
      • 2、在onPageStarted中记录running为Math.max(running, 1),
      • 3、最后在onPageFinished中如果--running等于0则再隐藏进度条加载
    • 方案3:在自定义进度条中,定义一个记录状态(已经开始状态,已经结束状态,不能继续开始状态)的临时变量。
      • 1.在设置进度条进度动画的时候,标记为已经开始状态;
      • 2.当进度条达到95以上时,表明页面几乎加载完成,这时候标记为已经结束状态;
      • 3.当进度条动画监听结束后,将状态标记为不能继续开始状态;
      • 这个时候,即使页面有多次重定向,执行多次onPageStarted->onPageFinished方法,也不会出现一次进度条没跑完又出现第二次进度条。具体代码看lib中的WebProgress代码!

    # 07.其他设计说明

    # 7.1 性能设计

    # 7.1.1 性能现状分析
    • 性能是它目前最大的问题,主要表现在以下两个方面:
      • 启动白屏时间。WebView 是一个非常重量级的控件,无论是 WebView 的初始化,还是整个渲染流程都非常耗时。这导致界面启动的时候会出现一段白屏时间,体验非常糟糕。
      • 响应流畅度。由于单线程、历史包袱等原因,页面的渲染和 JavaScript 的执行效率都不如原生。在一些重交互或者动画复杂的场景,H5 的性能还无法满足诉求。
    • 回顾一下浏览器内核渲染的流程,我们其实可以把整个过程拆成三个部分:
      • Native 时间。主要是 Activity、WebView 创建以及 WebView 初始化的时间。虽然首次创建 WebView 的时间会长一些,但总体 Native 时间是可控的。
      • 网络时间。这里包括 DNS、TCP、SSL 的建连时间和下载主文档的时间。当解析主文档的时候,也需要同步去下载主文档依赖的 CSS 和 JS 资源,以及必要的数据。
      • 渲染时间。浏览器内核构建 Render Tree、Layout 并渲染到屏幕的时间。
    # 7.1.2 性能优化分析
    • 加快请求速度。
      • 整个启动过程中,网络时间是最不可控的。这里的优化方法有很多,例如预解析 DNS、减少域名数、减少 HTTP 请求数、CDN 分发、请求复用、懒加载、Gzip 压缩、图片格式压缩。
    • 代码优化。
      • 主文档的大小越小越好(要求小于 15KB),这里要求我们对 HTML、CSS 以及 JS 进行代码优化。以 JS 为例,前端的库和框架真的太多了,可能一不小心就引入了各种的依赖框架。对于核心页面,我们要求只能使用原生 JS 或者非常轻量级的 JS 框架,例如使用只有几 KB 的 Preact 代替庞大的 React 框架。
    • SSR。
      • 对于浏览器的渲染流程,上面描述的是 CSR 渲染模式,在这种模式下,服务器只返回页面的基本框架。
      • 事实上还有一种非常流行的SSR(Server Side Rendering)渲染模式,服务器可以一次性生成直接进行渲染的 HTML。以做到只有一个网络请求,但是带来的代价就是服务器计算资源的增加。一般来说,我们会在服务器前置 CDN 来解决访问量的问题。
    # 7.1.3 客户端优化分析
    • WebView 预创建。
      • 提前创建和初始化 WebView,以及实现 WebView 的复用,这块大约可以节省 100~200 毫秒。
    • 缓存。
      • H5 是有多级的缓存机制,例如 Memory Cache 存放在内存中,一般资源响应回来就会放进去,页面关闭就会释放。
      • Client Cache 也就是客户端缓存,例如我们最常用的离线包方案,提前将需要网络请求的数据下发到客户端,通过拦截浏览器的资源请求实现加载。
      • Http Cache是我们比较熟悉的缓存机制,而 Net Cache 就是指 DNS 解析结果的缓存,或预连接的缓存等。
    • 从性能上看,Memory Cache > Client Cache >= Http Cache > Net Cache。
      • 所谓的缓存,就是在用户真正点击打开页面之前,提前把数据、资源下载到本地内存或者磁盘中,并放到内核相应的缓存中。
    # 7.1.4 X5内核优化
    • 进一步往底层走,需要有定制修改甚至优化内核的能力。
      • 例如很多接口官方的浏览器内核可能并没有暴露,而腾讯X5的内核里面都会有很多的特殊接口。
    • 托管所有网络请求。
      • 我们不仅可以托管浏览器的 Get 请求,其他的所有 Post 请求也能接管,这样我们可以做非常多的定制化优化。
    • 私有接口。
      • 暴露很多浏览器的一些非公开接口。以预渲染为例,我可以指定在内存直接渲染某个页面,当用户真正打开的时候,只需要直接做刷新就可以了,实现真正的“秒开”。
    • 兼容性和安全。
      • Android 的碎片化导致浏览器内核的兼容性实在令人头疼,而且旧版本内核还存在不少的安全漏洞。
      • 在应用自带浏览器内核可以解决这些问题,而且高版本的内核特性也会更加完善,例如支持 TLS 1.3、QUIC 等。
      • 但是带来的代价是安装包增大 20MB 左右,当然也可以采用动态下载的方式。

    # 7.2 稳定性设计

    # 7.3 灰度设计

    # 7.4 降级设计

    # 7.5 异常设计

    # 7.5.1 自定义error状态页面

    • 在onReceivedTitle方法接收到错误,当WebView加载页面出错时(一般为404 NOT FOUND),安卓WebView会默认显示一个出错界面。
    • 在onReceivedError方法接收异常,网络链接超时,断网,代理,其他异常等,都可以通过listener暴露给开发者。
    • 在onReceivedHttpError方法接收异常,比如404找不到资源,500等,都可以通过listener暴露给开发者。
    • 在onReceivedSslError方法接收异常,webView加载一些别人的url时候,有时候会发生证书认证错误的情况,这时候我们希望能够正常的呈现页面给用户,我们需要忽略证书错误。

    # 7.6 兼容性设计

    # 7.6.1 各个版本特性
    # 7.6.2 部分手机兼容性

    # 7.7 安全设计

    # 7.7.1 WebView白名单校验
    • 说一个现象
      • 有时候,使用公共网络,点击跳转页面。会发现突然跳转到外部某广告的链接页面,这个是怎么回事呢?这就是被劫持……
    • 处理白名单的步骤
      • 1.添加白名单,主要是添加host,通常添加到list集合中。这里主要存放白名单的链接……
      • 2.校验白名单,在哪里检查,在loadUrl之前检查即可
      • 3.如何判断白名单,通过格式化host检查
    • 方案1:使用错误校验方式
      • 通过indexOf简单校验
        • 校验逻辑来判断调用方的域名是否在白名单内,不过这个校验逻辑并没有他当初想象的那么简单。
      • 这种方式存在问题
        • 这个校验逻辑错误比较低级,攻击者直接输入http://www.rebeyond.net/poc.htm?site1.com就可以绕过了。
        • 因为URL中除了代表域名的字段外,还有路径、参数等和域名无关的字段,因此直接判断整个URL是不安全的。
        http://www.site2.com.rebeyond.net/poc.htm
        
        1
        • 上述URL的host中包含site2.com字符串,但是www.site2.com并不是域名,而是rebeyond.net这个域名的一个子域名,所以最终还是指向了攻击者控制的服务器。
    • 方案2:截取域名进行校验
      • 提取域名检验
        • 想要匹配白名单中的域名,首先应该把用户输入的URL中的域名提取出来再进行校验才对,使用字符串substring提取
      • 这种方式存在问题
        • 由于缺乏对URL语法的了解,错误的认为://和第一个/之间的字符串即为域名(host),导致了这个检测逻辑可以通过如下payload绕过。
        • 比如:http://site1.com@www.github.com/yangchong211,打开这个地址,发现竟然是github.com/yangchong211
        • RFC中对URL格式的描述:😕/:@:/
        • 攻击者利用URL不常见的语法,在URL中加入了Authority字段即绕过了这段校验。
        • Authority字段是用来向所请求的访问受限资源提供用户凭证的,比如访问一个需要认证的ftp资源,用户名为test,密码为123456,可以直接在浏览器中输入URL:ftp://test:123456@nju.edu.cn/。
    • 方案3:格式化获取host检验
      • 实现URL的格式化
        • Uri.parse(url).getHost(),使用该种方式即可得到url真正的host地址
    • 可以总结为如下几条开发建议:
      • 不要使用indexOf这种模糊匹配的函数;不要自己写正则表达式去匹配;
      • 尽量使用Java封装好的获取域名的方法,比如java.net.URI,不要使用java.net.URL;
      • 不仅要给域名设置白名单,还要给协议设置白名单,一般常用HTTP和HTTPS两种协议,不过强烈建议不要使用HTTP协议,因为移动互联网时代,手机被中间人攻击的门槛很低,搭一个恶意WiFi即可劫持手机网络流量;
      • 权限最小化原则,尽量使用更精确的域名或者路径。
    • 应该把白名单校验函数放在哪个环节校验?
      • loadUrl之前
      • shouldOverrideUrlLoading中
      • 如果需要对白名单进行安全等级划分,还需要在JavascriptInterface中加入校验函数,JavascriptInterface中需要使用webview.getUrl()来获取webview当前所在域。
    # 7.7.2 密码明文存储漏洞
    • WebView 默认开启密码保存功能 mWebView.setSavePassword(true),如果该功能未关闭,在用户输入密码时,会弹出提示框,询问用户是否保存密码,如果选择”是”,密码会被明文保到 /data/data/com.package.name/databases/webview.db 中,这样就有被盗取密码的危险。
      • 所以需要通过 WebSettings.setSavePassword(false) 关闭密码保存提醒功能。具体代码操作如下所示
      /设置是否开启密码保存功能,不建议开启,默认已经做了处理,存在盗取密码的危险
      mX5WebView.setSavePassword(false);
      
      1
      2

    # 01.概述

    # 1.1 项目背景

    • 通过简要的语言描述项目背景以及要达成的业务目标。

    # 1.2 设计目标

    • 需求的背后往往会带来技术的重构/优化,或者单纯的完成需求,如果有必要,需要从技术角度给出方案设计的目标
    • 比如对于图片下载需求,需要完成相关的功能,那么设计目标主要有完成异步下载、存储、缓存设计、图片解码、渲染等功能。
    • 比如对于优化需求,目标可以是达到一个什么效果?可以是帧率的、Crash率的、卡顿的等。
    • 比如对于重构需求,目标可以是加强扩展、解决问题、提升效率等。

    # 02.方案设计

    • 方案设计是技术文档的最核心部分,主要包括了整体架构和功能设计,这里需要体现:
    • 设计的初衷:概要描述方案设计的思考,可以是为了扩展性的考虑,可以是提升性能 关键技术点的思考:描述关键技术选型的思考,比如要解耦,业内解耦方案能有router、Target-Action等,讲清楚选择的思考
    • 技术上的折中/取舍:在做技术设计的时候,往往要的很多,但时间有限,那么这个需要讲一下折中与取舍,以及接下来的规划、计划

    # 2.1 整体架构

    • 整体架构的组成需要有一张完成的架构设计图,描述清楚具体的分层以及层与层之间的关系
    • 比如传统的开发会分为三层,展示层、逻辑层、数据层
      • 展示层的设计:视图的构成、视图间的耦合关系、具体的交互逻辑
      • 逻辑层的设计:支撑展示层所需要的数据、从数据层获取数据的加工、业务相关逻辑(比如轮询服务)
      • 数据层的设计:数据的获取方式以及存储方式,文件、数据库、本地、网络

    # 2.2 功能设计

    • 功能设计包含但不限于以下几个部分:逻辑流程图、接口设计图、与外部的模块间依赖关系

    # 2.2.1 关键流程图

    • 设计中的最复杂、最关键的逻辑需要画出流程图,实在画不出的流程图需要用语言描述清楚。
    • 关键流程需要有逻辑流程图,帮助其他同学理解功能的关键节点逻辑
    # 2.2.2 接口设计图
    • 通过UML类图来展示类间关系,描述清楚接口设计的一些思考原则
      • 提供的接口,往往接口设计为了完成相关逻辑
    # 2.2.3 模块间依赖关系
    • 描述清楚和哪些模块存在依赖关系以及原因,比如首页依赖于购物车模块,需要解释清楚要强耦合,有没有办法解耦
      • App内部模块间依赖
      • App外部依赖

    # 2.3 UI/动效设计

    • 客户端开发有很大一部分精力在UI/动效上,对于复杂的静态UI和复杂动效,需要给出实现方案和选型逻辑
    • 静态UI
      • 只有复杂的UI才需要给出设计方案,例如核心页面大重构、复杂的协调布局等
    • 复杂动效
      • 复杂的动效是端上容易踩坑的关键点,需要给出实现方案的对比、选型等,为验证动效可行性,可以给出动效Demo

    # 03.其他设计(Optional)

    • 以下部分是可选项,主要是从异常、兼容性、性能、稳定性、灰度、降级等维护来设计。

    # 3.1 性能设计

    • 有些业务项目可能会考虑性能,比如列表页,卡顿、流畅度怎么样?如何评估?
    • 有些技术项目可能也会考虑性能,比如数据库设计,检索性能如何?是否有瓶颈,如何评估?

    # 3.2 稳定性设计

    • 大的项目需要考虑性能如何保障?
      • 比如方案 Review
      • 比如自测Case Review,加强自测
      • 比如单测

    # 3.3 灰度设计

    • 核心关键功能需要有A/B设计
    • 比如UIWebview替换为WKWebview,其中存在很多不确定因素,需要做好灰度设计

    # 3.4 降级设计

    • 在做一些新技术尝试时,需要考虑降级设计
      • 比如RN、swift、weex引入对原有业务造成影响的,需要有兜底,可降级
      • 参考资料
      • 需要列出方案设计过程的文档,包括但不局限于PM需求文档,技术参考文档等。

    # 3.5 异常设计

    • 大部分业务需求都会涉及到异常处理,在关心主流程的同时需要关注异常场景怎么保证正确性?
      • 比如用户操作中途退出、网络异常、数据被清理等

    # 3.6 兼容性设计

    • 业务逻辑一般不会涉及到兼容性,但UI/动效需求容易遇到兼容性问题,也是提测时需要让QA关注的
      • 比如独立端/嵌入端、高低版本API适配等

    # 04.排期与计划

    • 排期计划主要针对周期较长项目的时间补充,对于小型项目不需要,例如:
      • 正常的版本业务需求,5pd以下,不需要给出排期计划;5pd或者以上,可以简单描述一下排期和提测时间
      • 跨版本的大型业务需求、重构专项等,需要给出详细的排期计划
      • 研发自驱的技术优化项目,需要给出详细的排期计划

    # 05.参考资料

    • 需要列出方案设计过程的文档,包括但不局限于PM需求文档,技术参考文档等。

    # 参考

    • Android 黑科技保活实现原理揭秘
      • https://weishu.me/2020/01/16/a-keep-alive-method-on-android/
    • https://www.cnblogs.com/rebeyond/p/10916076.html

    # 03.WebView加载优化

    # 3.1 那些因素影响加载

    • 影响页面加载速度的因素有非常多,在对 WebView 加载一个网页的过程进行调试发现
      • 每次加载的过程中都会有较多的网络请求,除了 web 页面自身的 URL 请求
      • 有 web 页面外部引用的JS、CSS、字体、图片等等都是个独立的http请求。这些请求都是串行的,这些请求加上浏览器的解析、渲染时间就会导致 WebView 整体加载时间变长,消耗的流量也对应的真多。

    # 3.3 DNS采用和客户端API相同的域名

    • 建立连接/服务器处理;在页面请求的数据返回之前,主要有以下过程耗费时间。
      DNS
      connection
      服务器处理
      
      1
      2
      3
    • DNS采用和客户端API相同的域名
      • DNS会在系统级别进行缓存,对于WebView的地址,如果使用的域名与native的API相同,则可以直接使用缓存的DNS而不用再发起请求图片。
      • 举个简单例子,客户端请求域名主要位于api.yc.com,然而内嵌的WebView主要位于 i.yc.com。
      • 当我们初次打开App时:客户端首次打开都会请求api.yc.com,其DNS将会被系统缓存。然而当打开WebView的时候,由于请求了不同的域名,需要重新获取i.yc.com的IP。静态资源同理,最好与客户端的资源域名保持一致。

    # 3.6 提前显示加载进度条

    • 提前显示进度条不是提升性能,但是对用户体验来说也是很重要的一点。
      • WebView.loadUrl("url") 不会立马就回调 onPageStarted 或者 onProgressChanged 。因为在这一时间段,WebView 有可能在初始化内核,也有可能在与服务器建立连接,这个时间段容易出现白屏,白屏用户体验是很糟糕的。

    # 3.8 关于图片显示隐藏

    • 方法一:
      • 无图模式
        mWebView.getSettings().setLoadsImagesAutomatically(boolean enable);
        mWebView.getSettings().setBlockNetworkImage(boolean enable);
        
        1
        2
      • 有图:正常加载显示所有图片
        mWebView.getSettings().setLoadsImagesAutomatically(true)
        mWebView.getSettings().setBlockNetworkImage(false)
        
        1
        2
      • 始终无图:所有图片都不显示
        mWebView.getSettings().setLoadsImagesAutomatically(false)
        mWebView.getSettings().setBlockNetworkImage(true)
        
        1
        2
      • 注:如果是先加载的网页图片,后设置的始终无图,则已加载的图片正常显示
      • 数据网络无图
        mWebView.getSettings().setLoadsImagesAutomatically(true)
        mWebView.getSettings().setBlockNetworkImage(true)
        
        1
        2
        • 注:wifi网络,与有图模式一致;数据网络下,已经下载到缓存的图片正常显示,未下载到缓存的图片不去网络请求显示。
    • 方法二:(新版sdk新加接口,如果在用的sdk中没有该接口需要更新sdk) 设置接口如下:
      mWebView.getSettingsExtension().setPicModel(model);//其中model位于IX5WebSettingsExtension中
      
      1
      • 有图:model设置为IX5WebSettingsExtension.PicModel_NORMAL正常加载显示所有图片;
      • 始终无图:model设置为IX5WebSettingsExtension.PicModel_NoPic不再显示图片(包括已加载出的图片);
      • 数据网络无图:model设置为IX5WebSettingsExtension.PicModel_NetNoPic数据网络下无图(已加载的图片正常显示);
    • 加载webView中的资源时,加快加载的速度优化,主要是针对图片
      • html代码下载到WebView后,webkit开始解析网页各个节点,发现有外部样式文件或者外部脚本文件时,会异步发起网络请求下载文件,但如果在这之前也有解析到image节点,那势必也会发起网络请求下载相应的图片。
      • 在网络情况较差的情况下,过多的网络请求就会造成带宽紧张,影响到css或js文件加载完成的时间,造成页面空白loading过久。解决的方法就是告诉WebView先不要自动加载图片,等页面finish后再发起图片加载。
      //初始化的时候设置,具体代码在X5WebView类中
      if(Build.VERSION.SDK_INT >= KITKAT) {
          //设置网页在加载的时候暂时不加载图片
          ws.setLoadsImagesAutomatically(true);
      } else {
          ws.setLoadsImagesAutomatically(false);
      }
      
      /**
       * 当页面加载完成会调用该方法
       * @param view                              view
       * @param url                               url链接
       */
      @Override
      public void onPageFinished(WebView view, String url) {
          super.onPageFinished(view, url);
          //页面finish后再发起图片加载
          if(!webView.getSettings().getLoadsImagesAutomatically()) {
              webView.getSettings().setLoadsImagesAutomatically(true);
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21

    # 3.9 HttpDns优化

    • HttpDns,使用http协议向特定的DNS服务器进行域名解析请求,代替基于DNS协议向运营商的Local DNS发起解析请求,可以降低运营商DNS劫持带来的访问失败。
    • 阿里云HTTP-DNS是避免dns劫持的一种有效手段,在许多特殊场景如HTTPS/SNI、okhttp等都有最佳实践,事实上很多场景依然可以通过HTTP-DNS进行IP直连,这个方案具体可以看阿里的官方demo和文档,我自己本身也没有实践过,这里只是提一下。
      • 参考链接:Android Webview + HttpDns最佳实践 (opens new window)

    # 04.Js和Native交互优化

    # 4.2 onJsPrompt优化

    • 说一下背景
      • 在js调用window.alert,window.confirm,window.prompt时,会调用WebChromeClient对应方法,可以此为入口,作为消息传递通道,通常会选prompt作为入口。
    • 提出问题
      • 1.原生方法是否可以执行耗时操作,如果有会阻塞通信吗?
      • 2.多线程中调用多个原生方法,如何保证原生方法每一个都会被执行到?
      • 3.js会阻塞等待当前原生函数(耗时操作的那个)执行完毕再往下走,所以js调用java方法里面最好也不要做耗时操作
    • prompt的一个坑导致js挂掉
      • 从表现上来看,onJsPrompt必须执行完毕,prompt函数才会返回,否则js线程会一直阻塞在这里。实际使用中确实会发生这种情况,尤其是APP中有很多线程的场景下,怀疑是这么一种场景:
      • 第一步:js线程在执行prompt时被挂起,
      • 第二步:UI线程被调度,恰好销毁了WebView,调用了 (webView的destroy),destroy之后,导致 onJsPrompt不会被回调,prompt一直等着,js线程就一直阻塞,导致所有webView打不开,一旦出现可能需要杀进程才能解决。
    • 解决方案
      • 由于onJsPrompt是在UI线程执行,所以尽量不要做耗时操作,可以借助Handler灵活处理。利用Handler封装一下,让每个任务自己处理,耗时的话就开线程自己处理。
    • JsPrompt方法message长度限制问题
      • 在调用WebViewChromeClient的onJsPrompt()时,利用message来实现Js与native之间的交互,在不同的手机中,对于message的长度限制不同,华为超长会自动截取,目前发现最长10225 char。

    # 4.3 @JavascriptInterface建议

    • 在js调用Android原生方法时,会用@JavascriptInterface注解标注那些需要被调用的Android原生方法,那么思考一下,这些原生方法是否可以执行耗时操作,如果有会阻塞通信吗?
      • JS会阻塞等待当前原生函数(耗时操作的那个)执行完毕再往下走,所以 @JavascriptInterface注解的方法里面最好也不要做耗时操作,最好利用Handler封装一下,让每个任务自己处理,耗时的话就开线程自己处理,这样是最好的。
    • JavascriptInterface注入的方法被js调用时,可以看做是一个同步调用
      • 虽然两者位于不同线程,但是应该存在一个等待通知的机制来保证,所以Native中被回调的方法里尽量不要处理耗时操作,否则js会阻塞等待较长时间。

    # 4.6 无法释放js耗能

    • 说一下背景
      • 在有些手机你如果webView加载的html里,有一些js一直在执行比如动画之类的东西,如果此刻webView 挂在了后台这些资源是不会被释放用户也无法感知。导致一直占有cpu 耗电特别快,所以如果遇到这种情况,处理方式如下所示。
    • 优化思路如下:
      • 大概意思就是在后台的时候,会调用onStop方法,即此时关闭js交互,回到前台调用onResume再开启js交互。
      //在onStop里面设置setJavaScriptEnabled(false);
      //在onResume里面设置setJavaScriptEnabled(true)。
      
      1
      2

    # 05.WebView缓存优化

    # 5.2 缓存到哪里

    • WebView缓存是在什么地方?
      • 在项目中如果使用到 WebView 控件,当加载 html 页面时,会在/data/data/包名目录下生成 database 与 cache 两个文件夹。
      • 请求的 url 记录是保存在 WebViewCache.db,而 url 的内容是保存在 WebViewCache 文件夹下。
    • 会缓存那些内容?
      • 当我们加载Html时候,会在我们data/应用package下生成database与cache两个文件夹:
      • 请求的Url记录是保存在webViewCache.db里,而url的内容是保存在webViewCache文件夹下
      • WebView中存在着两种缓存:网页数据缓存(存储打开过的页面及资源)、H5缓存(即AppCache)。
    • 页面缓存:
      • 指加载一个网页时的html、JS、CSS等页面或者资源数据。
      • 这些缓存资源是由于浏览器的行为而产生,开发者只能通过配置HTTP响应头影响浏览器的行为才能间接地影响到这些缓存数据。
      • 缓存的索引存放在/data/data/package_name/databases下。
      • 文件存放在/data/data/package_name/cache/xxxwebviewcachexxx下。
    • 数据缓存 :
      • 数据缓存分为AppCache和DOM Storage两种。
      • 这些缓存资源是由开发者的直接行为而产生,所有的缓存数据都由开发者直接完全地掌控。
      • Android中Webkit使用一个db文件来保存AppCache数据(my_path/ApplicationCache.db)
      • Android中Webkit会为DOM Storage产生两个文件(my_path/localstorage/xxx.db和my_path/localstorage/Databases.db)
    • 多种缓存策略分析
      • LOAD_CACHE_ONLY:不使用网络,只读本地缓存。
      • LOAD_NORMAL:在 API Level 17 中已经被废弃,而在API Level 11 开始,策略如 LOAD_DEFALT。
      • LOAD_NO_CACHE:不使用缓存,只从网络获取数据。
      • LOAD_CACHE_ELSE_NETWORK:只要本地有缓存,就从缓存中读取数据。
      • LOAD_DEFAULT:根据 Http 协议,决定是否从网络获取数据。

    # 5.3 浏览器缓存机制

    • response的headers中的参数, 注意到这么几个字段:Last-Modified、ETag、Expires、Cache-Control
      • Cache-Control
        • 例如Cache-Control:max-age=2592000, 表示缓存时长为2592000秒, 也就是一个月30天的时间。如果30天内需要再次请求这个文件,那么浏览器不会发出请求,直接使用本地的缓存的文件。这是HTTP/1.1标准中的字段。
      • Expires
        • 例如Expires:Tue,25 Sep 2018 07:17:34 GMT, 这表示这个文件的过期时间是格林尼治时间2018年9月25日7点17分。因为我是北京时间2018年8月26日15点请求的, 所以可以看出也是差不多一个月有效期。在这个时间之前浏览器都不会再次发出请求去获取这个文件。Expires是HTTP/1.0中的字段,如果客户端和服务器时间不同步会导致缓存出现问题,因此才有了上面的Cache-Control。当它们同时出现时,Cache-Control优先级更高。
      • Last-Modified
        • 标识文件在服务器上的最新更新时间, 下次请求时,如果文件缓存过期,浏览器通过If-Modified-Since字段带上这个时间,发送给服务器,由服务器比较时间戳来判断文件是否有修改。如果没有修改,服务器返回304(未修改)告诉浏览器继续使用缓存;如果有修改,则返回200,同时返回最新的文件。
      • Etag
        • Etag的取值是一个对文件进行标识的特征字串, 在向服务器查询文件是否有更新时,浏览器通过If-None-Match字段把特征字串发送给服务器,由服务器和文件最新特征字串进行匹配,来判断文件是否有更新:没有更新回包304,有更新回包200。Etag和Last-Modified可根据需求使用一个或两个同时使用。两个同时使用时,只要满足基中一个条件,就认为文件没有更新。
    • 浏览器自身的缓存机制是基于http协议层的Header中的信息实现的
      • Cache-control && Expires
        • 这两个字段的作用是:接收响应时,浏览器决定文件是否需要被缓存;或者需要加载文件时,浏览器决定是否需要发出请求
        • Cache-control常见的值包括:no-cache、no-store、max-age等。其中max-age=xxx表示缓存的内容将在 xxx 秒后失效, 这个选项只在HTTP 1.1可用, 并如果和Last-Modified一起使用时, 优先级较高。
      • Last-Modified && ETag
        • 这两个字段的作用是:发起请求时,服务器决定文件是否需要更新。服务端响应浏览器的请求时会添加一个Last-Modified的头部字段,字段内容表示请求的文件最后的更改时间。
        • 而浏览器会在下一次请求通过If-Modified-Since头部字段将这个值返回给服务端,以决定是否需要更新文件
    • 一般设置为默认的缓存模式就可以了。关于缓存的配置, 主要还是靠web前端和后台设置。这些技术都是协议层所定义的,在Android的webView当中我们可以通过配置决定是否采纳这几个协议的头部属性

    # 5.4 本地资源替换操作

    # 07.体验使用优化说明

    # 7.1 设置下载监听优化

    • 添加下载监听操作。跳转外部浏览器进行下载……具体可以看setDownloadListener代码

    # 7.2 设置字体优化

    # 7.8 常见协议处理

    • 网页中tel,sms,mailTo,Intent,Market协议,那么他们分别都是怎么用的呢
    • tel:协议---拨打电话
      • 在html中
      <a href="tel:13667225184">电话给我</a>
      
      1
      • 在java中
      //tel:协议---拨打电话
      if(url.startsWith("tel:")) {
          //直接调出界面,不需要权限
          Intent sendIntent = new Intent(Intent.ACTION_DIAL, Uri.parse(url));
          startActivity(sendIntent);
          //或者
          //直接拨打,需要权限<uses-permission android:name="android.permission.CALL_PHONE"/>
          //Intent sendIntent = new Intent(Intent.ACTION_CALL, Uri.parse(url));
          //startActivity(sendIntent);
          //否则键盘回去,页面显示"找不到网页"
          return true;
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
    • sms:协议---发送短信
      • 在html中
      <a href="sms:">调出发短信界面</a></br>
      <a href="sms:13667225184">调出发短信界面显示号码</a></br>
      <a href="sms:13667225184?body=contents">调出发短信界面显示号码和发送内容</a></br>
      <a href="sms:13667225184&body=contents1">ios调出发短信界面显示号码和发送内容</a></br>
      <a href="sms:13667225184;10010?body=contents2">调出发短信界面给多个号码发内容</a><br/>
      <a href="sms:+13667225184?body=contents3">调出发短信界面显示号码 </a></br>
      <a href="sms:+13667225184;10010?body=contents4">调出发短信界面给多个号码发内容 </a><br/>
      
      1
      2
      3
      4
      5
      6
      7
      • 在java中
      if(url.startsWith("sms:")||url.startsWith("smsto:")||url.startsWith("mms:")||url.startsWith("mmsto:")) {
          //直接调出界面,不需要权限
          Intent sendIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
          startActivity(sendIntent);
      
          //或者
          //打开短信页面,不需要权限
          //Intent sendIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse(url));
          //startActivity(sendIntent);
      
          //或者
          //import android.telephony.SmsManager;
          //SmsManager smsg = SmsManager.getDefault();//----看不到已发送信息。。。
          //smsg.sendTextMessage("10086", null, "tttttt", null, null);
          
          //或者
          //---可以看到已发的信息
          //ContentValues values = new ContentValues(); 
          //values.put("address", "10086");
          //values.put("body", "contents");
          //ContentResolver contentResolver = getContentResolver();
          //contentResolver.insert(Uri.parse("content://sms/sent"), values);
          // contentResolver.insert(Uri.parse("content://sms/inbox"), values);
          //<uses-permission android:name="android.permission.SEND_SMS"/>
          //<uses-permission android:name="android.permission.READ_SMS"/>
          //<uses-permission android:name="android.permission.WRITE_SMS"/>
          //否则键盘回去,页面显示"找不到网页"
          return true;
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
    • mailto:协议---发送邮件
      • 在html中
      <a href="mailto:yangchong211@163.com">邮件</a>
      
      1
      • 在java中
      if (url.startsWith("mailto:")) {
          //打开发邮件窗口
          Intent mailIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse(url));
          startActivity(mailIntent);
          //<uses-permission android:name="android.permission.SEND_TO"/>
          return true;
      }
      
      1
      2
      3
      4
      5
      6
      7

    # 7.9 监控页面卡死

    • https://www.itdaan.com/blog/2017/07/11/f0979eae58b1108710683c0c525adcc0.html

    # 09.一些其他优化

    # 9.1 多次获取web标题title

    • 网上的部分解决思路
      • 网上能查的大部分方法都是在WebChromeClient的onReceivedTitle(WebView view, String title)中拿到title。但是这个方法在网页回退时是无法拿到正确的上一级标题的,网上的处理方法是自己维护一个List去缓存标题,在执行完webView.goBack()后,移除List的最后一条,再将新的最后一条设置给标题栏。
      • 这个方法当然是可行的,但是自己缓存时缓存时机和移除时机都不好确定,onReceivedTitle方法在一个页面打开时并不是仅调用一次,而是多次调用,前面拿到的title都为空。
    • 采用原生的WebBackForwardList获取,不过下面这种仍然也会出现问题。
      webView.setWebChromeClient(new WebChromeClient() {
          @Override
          public void onReceivedTitle(WebView view, String title) {
              getWebTitle();
          }
      });
      
      private void getWebTitle(){
          WebBackForwardList forwardList = webView.copyBackForwardList();
          WebHistoryItem item = forwardList.getCurrentItem();
          if (item != null) {
              setActionBarTitle(item.getTitle());
          }
      }
      
      private void onWebViewGoBack(){
          webView.goBack();
          getWebTitle();
      }
      
      @Override
      public boolean onKeyDown(int keyCode, KeyEvent event) {
          if (webView.canGoBack()) {
              onWebViewGoBack();
              return false;
          }
          return super.onKeyDown(keyCode, event);
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28

    # 9.2 隐藏标签操作

    • 产品需求
      • 要在App中打开xx的链接,并且要隐藏掉H5页面的某些内容,这个就需要在Java代码中操作WebView,让H5页面加载完成后能够隐藏掉某些内容。
    • 需要几个前提条件
      • 页面加载完成
      • 在Java代码中执行Js代码
      • 利用Js代码找到页面中的底部栏并将其隐藏
    • 如何在h5中找隐藏元素
      • 在H5页面中找到某个元素还是有很多方法的,比如getElementById()、getElementsByClassName()、getElementsByTagName()等,具体根据页面来选择
      • 找到要隐藏的h5视图div,然后可以看到有id,或者class。这里用class举例子,比如div的class叫做'copyright'
      • document.getElementsByClassName('copyright')[0].style.display='none'
    • 可能出现的问题
      • 等到页面加载完毕后,执行隐藏div标签方法,会造成控件闪屏,不抬友好。但是如果在onProgressChanged执行到85左右隐藏标签又会导致偶发性没有隐藏成功。
      • 如果有重定向,则会出现执行多次。写了这个隐藏逻辑,会造成所有的页面都会执行,不知道是否会影响性能?待研究……
    • 代码操作如下所示
      /**
       * 可以等页面加载完成后,执行Js代码,找到底部栏并将其隐藏
       * 如何找h5页面元素:
       *      在H5页面中找到某个元素还是有很多方法的,比如getElementById()、getElementsByClassName()、getElementsByTagName()等,具体根据页面来选择
       * 隐藏底部有赞的东西
       *      这个主要找到copyright标签,然后反射拿到该方法,调用隐藏逻辑
       * 步骤操作如下:
       * 1.首先通过getElementByClassName方法找到'class'为'copyright'的所有元素,返回的是一个数组,
       * 2.在这个页面中,只有底部栏的'class'为'copyright',所以取数组中的第一个元素对应的就是底部栏元素
       * 3.然后将底部栏的display属性设置为'none',表示底部栏不显示,这样就可以将底部栏隐藏
       *
       * 可能存在问题:
       * onPageFinished没有执行,导致这段代码没有走
       */
      private void hideBottom() {
          try {
              if (mWebView!=null) {
                  //定义javaScript方法
                  String javascript = "javascript:function hideBottom() { "
                          + "document.getElementsByClassName('copyright')[0].style.display='none'"
                          + "}";
                  //加载方法
                  mWebView.loadUrl(javascript);
                  //执行方法
                  mWebView.loadUrl("javascript:hideBottom();");
              }
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30

    # 10.一些问题bug记录

    # 10.1 shouldOverrideUrlLoading不执行

    • 原因1:shouldOverrideUrlLoading不执行,原因是因为在js里面设置了计时器实现可以判断用户长按的功能,当android遇到html的js代码里面执行有计时器如:setTimeout就不会执行android WebView 里面的 shouldOverrideUrlLoading 。
    • 原因2:
      • https://blog.csdn.net/weixin_37806077/article/details/85488680
      • https://blog.csdn.net/KevinsCSDN/article/details/89598789

    # 01.概述

    # 1.1 项目背景

    • 通过简要的语言描述项目背景以及要达成的业务目标。

    # 1.2 设计目标

    • 需求的背后往往会带来技术的重构/优化,或者单纯的完成需求,如果有必要,需要从技术角度给出方案设计的目标
    • 比如对于图片下载需求,需要完成相关的功能,那么设计目标主要有完成异步下载、存储、缓存设计、图片解码、渲染等功能。
    • 比如对于优化需求,目标可以是达到一个什么效果?可以是帧率的、Crash率的、卡顿的等。
    • 比如对于重构需求,目标可以是加强扩展、解决问题、提升效率等。

    # 02.方案设计

    • 方案设计是技术文档的最核心部分,主要包括了整体架构和功能设计,这里需要体现:
    • 设计的初衷:概要描述方案设计的思考,可以是为了扩展性的考虑,可以是提升性能 关键技术点的思考:描述关键技术选型的思考,比如要解耦,业内解耦方案能有router、Target-Action等,讲清楚选择的思考
    • 技术上的折中/取舍:在做技术设计的时候,往往要的很多,但时间有限,那么这个需要讲一下折中与取舍,以及接下来的规划、计划

    # 2.1 整体架构

    • 整体架构的组成需要有一张完成的架构设计图,描述清楚具体的分层以及层与层之间的关系
    • 比如传统的开发会分为三层,展示层、逻辑层、数据层
      • 展示层的设计:视图的构成、视图间的耦合关系、具体的交互逻辑
      • 逻辑层的设计:支撑展示层所需要的数据、从数据层获取数据的加工、业务相关逻辑(比如轮询服务)
      • 数据层的设计:数据的获取方式以及存储方式,文件、数据库、本地、网络

    # 2.2 功能设计

    • 功能设计包含但不限于以下几个部分:逻辑流程图、接口设计图、与外部的模块间依赖关系

    # 2.2.1 关键流程图

    • 设计中的最复杂、最关键的逻辑需要画出流程图,实在画不出的流程图需要用语言描述清楚。
    • 关键流程需要有逻辑流程图,帮助其他同学理解功能的关键节点逻辑
    # 2.2.2 接口设计图
    • 通过UML类图来展示类间关系,描述清楚接口设计的一些思考原则
      • 提供的接口,往往接口设计为了完成相关逻辑
    # 2.2.3 模块间依赖关系
    • 描述清楚和哪些模块存在依赖关系以及原因,比如首页依赖于购物车模块,需要解释清楚要强耦合,有没有办法解耦
      • App内部模块间依赖
      • App外部依赖

    # 2.3 UI/动效设计

    • 客户端开发有很大一部分精力在UI/动效上,对于复杂的静态UI和复杂动效,需要给出实现方案和选型逻辑
    • 静态UI
      • 只有复杂的UI才需要给出设计方案,例如核心页面大重构、复杂的协调布局等
    • 复杂动效
      • 复杂的动效是端上容易踩坑的关键点,需要给出实现方案的对比、选型等,为验证动效可行性,可以给出动效Demo

    # 03.其他设计(Optional)

    • 以下部分是可选项,主要是从异常、兼容性、性能、稳定性、灰度、降级等维护来设计。

    # 3.1 性能设计

    • 有些业务项目可能会考虑性能,比如列表页,卡顿、流畅度怎么样?如何评估?
    • 有些技术项目可能也会考虑性能,比如数据库设计,检索性能如何?是否有瓶颈,如何评估?

    # 3.2 稳定性设计

    • 大的项目需要考虑性能如何保障?
      • 比如方案 Review
      • 比如自测Case Review,加强自测
      • 比如单测

    # 3.3 灰度设计

    • 核心关键功能需要有A/B设计
    • 比如UIWebview替换为WKWebview,其中存在很多不确定因素,需要做好灰度设计

    # 3.4 降级设计

    • 在做一些新技术尝试时,需要考虑降级设计
      • 比如RN、swift、weex引入对原有业务造成影响的,需要有兜底,可降级
      • 参考资料
      • 需要列出方案设计过程的文档,包括但不局限于PM需求文档,技术参考文档等。

    # 3.5 异常设计

    • 大部分业务需求都会涉及到异常处理,在关心主流程的同时需要关注异常场景怎么保证正确性?
      • 比如用户操作中途退出、网络异常、数据被清理等

    # 3.6 兼容性设计

    • 业务逻辑一般不会涉及到兼容性,但UI/动效需求容易遇到兼容性问题,也是提测时需要让QA关注的
      • 比如独立端/嵌入端、高低版本API适配等

    # 04.排期与计划

    • 排期计划主要针对周期较长项目的时间补充,对于小型项目不需要,例如:
      • 正常的版本业务需求,5pd以下,不需要给出排期计划;5pd或者以上,可以简单描述一下排期和提测时间
      • 跨版本的大型业务需求、重构专项等,需要给出详细的排期计划
      • 研发自驱的技术优化项目,需要给出详细的排期计划

    # 05.参考资料

    • 需要列出方案设计过程的文档,包括但不局限于PM需求文档,技术参考文档等。

    # 参考

    • Android 黑科技保活实现原理揭秘
      • https://weishu.me/2020/01/16/a-keep-alive-method-on-android/

    # 开源项目

    • https://github.com/Ryan-Shz/FastWebView (23star)
    • https://github.com/yale8848/CacheWebView (1.5k star)
    • https://github.com/easilycoder/HybridCache (54star) 文档可以,但是不通
    • https://github.com/NEYouFan/ht-candywebcache-android

    # 技术博客

    • https://www.jianshu.com/p/5e7075f4875f
    • 安卓WebView修改DOM:https://www.jianshu.com/p/e320d6bb11e7
    上次更新: 2026/06/10, 11:13:41
    NDK开发实践
    ADB常见使用操作

    ← NDK开发实践 ADB常见使用操作→

    最近更新
    01
    信号崩溃快速排查
    06-15
    02
    CoreDump破案
    06-15
    03
    perf火焰图实战
    06-15
    更多文章>
    Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式