H5 Crash 研究

我们知道,支撑页面在 webview 上良好运转的前提是具备一个高效并且稳定的 webview 容器,而容器的高效稳定不仅仅由容器提供方来保障,也需要容器使用者遵守一些基本准则,否则就有可能出现页面 Crash 的情况,这些准则是什么?什么样的上层代码会引起容器异常退出?这是本文需要阐述的内容。

H5 Crash 问题概况

下图是 H5 Crash 的大致流程图:

H5 Crash 流程图

由于前端没办法捕捉到页面 Crash 的状态和堆栈,但是 H5 页面上发生的错误会传递到 Java 和更底层的 Native 直到容器异常退出,在退出的那一刻,容器会将堆栈写入到日志中,当下次打开容器时(也可能是定时上报)就会上报这些堆栈信息。

H5 Crash 原因初探

测试代码仓库地址:

git clone https://github.com/barretlee/h5crash.git;cd demo;

注意: 代码需要在 Webview 容器中测试,浏览器下不会出现异常。

H5 Crash 的原因不太明显,但是从经验上判断和摸索,大致归类为以下三种:

1. 内存问题

<script>var Closure = function() {  var _cache = [];  var cache = 0;  var add = function(size) {    cache += size;    size = size * 1024 * 1024;    _cache.push(new Array(size).join('x'));    refresh();  };  var refresh = function() {    r.innerHTML = '内存消耗: ' + cache + 'M';  };  return {    cache: _cache + 'M',    add: add,    refresh: refresh  }};var closure = Closure();</script><button onclick="closure.add(1)">增加 1M 内存消耗</button><button onclick="closure.add(10)">增加 10M 内存消耗</button><button onclick="closure.add(20)">增加 20M 内存消耗</button><button onclick="closure.add(50)">增加 50M 内存消耗</button><button onclick="closure.add(100)">增加 100M 内存消耗</button><div id="r">内存消耗:0 M</div>
  • 存在的干扰:这种测试存在比较多的干扰,比如设备类型、系统类型(iOS/Android)、和设备内存运行状态等。

2. Layers 数问题

Layers 数的获取比较麻烦,Chrome Driver 没有提供该数据的接口,目前也没有比较好的办法拿到这个数据。

<style>.transform {  transform: translateZ(0);}.animation {  width:100px;  height:100px;  background:red;  position:relative;  animation:move 5s infinite;}@keyframes move {  from {left:0px;}  to {left:200px;}}</style><script>var Layer = function() {  function getType() {    return document.querySelector('input:checked').value;  };  return {    createOne: function(index) {      var div = document.createElement('div');      div.appendChild(document.createTextNode(index));      switch(getType()) {        case 'opacity':          div.style.cssText = "opacity:" + (index / 1000);          break;        case  'transform':          div.className = 'transform';          break;        case  'animation':          div.className = 'animation';          break;        case  'zindex':          div.style.cssText = "position:relative; z-index:" + index;          break;      }      document.body.appendChild(div);    },    create: function(num) {      [].slice.call(document.querySelectorAll('div')).forEach(function(item) {        item.parentNode && item.parentNode.removeChild(item);      });      while(num--) {        this.createOne(num);      }    }  }};var layer = Layer();</script><strong>层类型: </strong><ul>  <li><label><input type="radio" checked name="type" value="opacity"> <span>通过 opacity 创建层</span></label></li>  <li><label><input type="radio" name="type" value="transform"> <span>通过 transforms 创建层</span></label></li>  <li><label><input type="radio" name="type" value="animation"> <span>通过 animation 创建层</span></label></li>  <li><label><input type="radio" name="type" value="zindex"> <span>通过绝对定位分层</span></label></li></ul><button onclick="layer.create(1)">创建 1 个层</button><button onclick="layer.create(10)">创建 10 个层</button><button onclick="layer.create(20)">创建 20 个层</button><button onclick="layer.create(50)">创建 50 个层</button><button onclick="layer.create(100)">创建 100 个层</button><button onclick="layer.create(200)">创建 200 个层</button><button onclick="layer.create(500)">创建 500 个层</button><button onclick="layer.create(1000)">创建 1000 个层</button><button onclick="layer.create(2000)">创建 2000 个层</button><button onclick="layer.create(5000)">创建 5000 个层</button><button onclick="layer.create(10000)">创建 10000 个层</button>
  • 实际上,创建多个层,也是对内存的巨大消耗,页面 Crash 可能还是因为内存消耗过大

3. 并发过多问题

<script>var Request = function() {  function getType() {    return document.querySelector('input:checked').value;  };  function getResource() {    var type = getType();    var resource = {      fetch: '/',      xhr: '/',      script: '//g.alicdn.com/sd/data_sufei/1.5.1/aplus/index.js',      css: '//g.alicdn.com/kg/global-util/1.0.3/index-min.css'    };    return resource[type];  };  return {    emitOne: function() {      var url = getResource() + "?_t=" + (new Date * 1 + Math.random());      switch(getType()) {        case 'fetch':          return fetch('/');        case 'xhr':          with(new XMLHttpRequest) {            open('GET', url);            send();          }          return;        case 'script':          var s = document.createElement('script');          s.src = url;          document.body.appendChild(s);          return;        case 'css':          var s = document.createElement('link');          s.href = url;          document.body.appendChild(s);      }    },    emit: function(num) {      [].slice.call(document.querySelectorAll('script,link')).forEach(function(item) {        item.parentNode && item.parentNode.removeChild(item);      });      while(num--) {        this.emitOne();      }    }  }};var request = Request();</script><strong>请求类型: </strong><ul>  <li><label><input type="radio" checked name="type" value="fetch"> <span>使用 Fetch 发送请求</span></label></li>  <li><label><input type="radio" name="type" value="xhr"> <span>使用 XHR 发送请求</span></label></li>  <li><label><input type="radio" name="type" value="script"> <span>并发请求脚本资源</span></label></li>  <li><label><input type="radio" name="type" value="css"> <span>并发请求样式资源</span></label></li></ul><button onclick="request.emit(1)">并发 1 个请求</button><button onclick="request.emit(10)">并发 10 个请求</button><button onclick="request.emit(20)">并发 20 个请求</button><button onclick="request.emit(50)">并发 50 个请求</button><button onclick="request.emit(100)">并发 100 个请求</button><button onclick="request.emit(500)">并发 500 个请求</button><button onclick="request.emit(1000)">并发 1000 个请求</button>
  • 存在的干扰:设备的种类、设备的 CPU 使用情况和网络状况等。

H5 Crash 测试结果

测试结果:

  • 通过 opacity、animation、positon 等方式创建层,即便是 1w 个,页面也没有明显变化;但是使用 transform 创建 2k~5k 个层,页面会卡顿几秒后立即闪退;
  • 内存是条红线,测试发现,一次性消耗 20M 的内存,会导致客户端立即闪退;
  • 并发请求也是存在响应问题的,Fetch API 和 CSS Resource 并发 1k 请求没有出现问题,但是 XHR 和 Script Resource 请求,问题特别明显,虽然没有导致页面闪退,但是页面已经进入了假死状态。

以上临界值还可以继续精确。

小结

本文主要是对 H5 Crash 做了一个预研,测试可能存在诸多误差,测试方法也需要改进,不过沿着这些的思路考究会比较容易找到结论。

后续会给出比较有意义的边界数据已经探测工具。

本文同步自 小胡子哥的个人网站,原文地址: http://www.barretlee.com/blog/2016/05/30/h5-crash-research/

Advertisements
H5 Crash 研究

Kindle 电子书生成工具

花了两个晚上把 OPF 和 epub 格式整明白了,准备把订阅的 RSS 内容抓取下来做成电子书推到 kindle 中阅读。后续也会把自己博客整成电子书,提供给习惯 kindle 阅读的朋友。研究这些东西目的还是想回到比较纯粹的阅读设备上,毕竟手机屏小干扰多,看久了眼睛也有点不舒服。

本项目旨在写一个 Kindle 电子书的构建工具,从互联网上抓取数据,合并整合都生成一本小巧的 .mobi 电子书。而使用这个工具,你只需要编辑下配置文件,或者直接运行命令行工具。

项目结构

数据有这么几个来源:

  • 通过抓取单个 uri 的内容,配合 title 和 content DOM 选择器,获取文章的标题和内容
  • 通过抓取 RSS 源获取内容
  • 使用本地数据,比如 hexo build 目录下的 html 文件

下图为该工具的一个结构图:

Kindle Book Maker

抓到数据后,工具会帮助分析过滤数据,尤其对 hexo 生成的文件做了特殊的处理,后续也会添加几个扩展功能(比如之间转换 markdown 文件),如果 html 中包含了远程内容——CSS、图片等——程序会全部抓取过来。

最后,使用官方提供了 kindleGen 工具构建,我已经把这个文件放到了 /bin/kindlegen 下,大约 28M,有点大。

使用方法

可以下载代码之后,尝试运行下已经提供了一个 DEMO(封面图片就懒得换了,是我自己的头像):

git clone https://github.com/barretlee/kindleBookMaker.git;cd kindleBookMaker;npm install;node index;open build/*.mobi;

提供了很多方法可以调用,不过都通过命令行的方式简化了:

  • 从 RSS 源构建:

    node index --rss http://barretlee.com/rss2.xml# node index -r http://barretlee.com/rss2.xml
  • 从单个 URI 构建,-u URL titleQuery ContentQuery FilterRegExp, 其中 titleQuery 为文章标题的 css query,ContentQuery 为文章主要内容的 css query,FilterRegExp 为正则过滤:

    node index --uri \    http://www.barretlee.com/blog/2016/04/28/mini-query/ \    .post-title \    .post-content \    /<div class="shit-spider"[\s\S]+?<\/div>/
  • 从本地构建

    node index --dirctory ./src/demo/# node index -d ./src/demo/

还有另外三个参数:

  • --verbose, -v, 查看 kindle 构建的详细细节,因为编译也可能出错
  • --help, -h, 帮助说明
  • -push2kindle, -p, 将构建的 .mobi 文件推送你设定的 kindle 账户上

配置

var moment = require('moment');module.exports = {  // entry: './src/KF8-Demo',  entry: {    base: './src/KF8-Demo',    list: []  },  bookInfo: {    title: "Barret Lee's Personal Website",    lang: "zh",    creator: "Barret Lee",    copyright: "Barret Lee",    publisher: "",    coverImage: 'coverImage.png'  },  /*option*/  output: {    base: './build',    format: '[name]-' + moment().format('YYYYMMDD')  },  /*option for uri*/  singlePage: {    title: 'div.title',    description: 'div.content',    reg: function(data) {      return data.replace(/<div class="shit-spider"[\s\S]+?<\/div>/, '');    }  },  /*option*/  push2kindle: {    email: 'barret.china@gmail.com',    password: 'your-email-password',    kindle: 'barretlee.com@kindle.cn'  }};
  • entry, 可以为一个 String 或者 Object
    • base, 入口地址,下载的文件都会放在这里
    • list, list 参数,会影响最后生成的电子书的文章排序
  • bookInfo, 注意设置 coverImage,它为书籍封面图片
  • ouput, 可选参数, 默认值为 ./build[name]
  • singlePage, 可选参数, 从 URI 爬取数据时会用到
  • push2kindle, 可选参数, kindle 参数为你设备对应的推送邮箱, 可以在 这里 设置

Todo

  • 直接从 Markdown 文件生成内容
  • 找到 kindle 帐号偶尔不接受我推送 .mobi 文件的原因,意思就是有的时候推送未成功

参考文献

本文同步自 小胡子哥的个人网站,原文地址: http://www.barretlee.com/blog/2016/05/20/kindle-book-maker/

Kindle 电子书生成工具