vianvio / FE-Companions

山虽高,我心已决要攀登, 路再难,绊不住我的脚跟; 因为我看到生命之路就在这里。 -- 《天路历程》
447 stars 34 forks source link

20200319 - 树星 #57

Open vianvio opened 4 years ago

vianvio commented 4 years ago

问题列表:

  1. 描述一下webview池性能优化的整体方案。配合量化指标说明。
  2. 公链切换导致整体应用切换的时候,内存,cpu这些如何管控的?考虑游戏资源加载对内存要求较高,是否会出现内存激增导致app creash的情况?要如何预防和监控?
  3. 描述一下多应用托管平台的打包更新方案。
  4. 以开发者视角,介绍一下前后端如何与区块链结合的?(请大佬多多指教)
alphacat2018 commented 4 years ago

1.描述一下webview池性能优化的整体方案。配合量化指标说明。

首先说一下项目背景。项目本身是一个游戏平台,主要是H5游戏,同时也支持游戏App通过scheme进行登录授权和支付。

项目中主要有两个地方需要用到webview,一个是H5游戏的容器。另一个是商城中商品的详情页。商品详情页大部分内容都是由App提供的,但是关于商品本身的属性可能不同的商品会有不同的展示形式需求,所以这一部分做成了html页面的形式,方便扩展。只要在webview中插入一段代码,根据html页面的高度动态调整webview组件的高度即可。另外为了优化体验,这些html都由App提前以离线文件的形式缓存到本地,尽量减少属性页面加载耗时。

但是功能开发结束之后发现在iOS上的加载太慢了,从创建webview的实例到页面开始加载中间平均耗时在400ms,首次创建更是高达700ms+。而在android上好多了,平均只有20ms左右。由于游戏的加载一般耗时较长,多几百毫秒并不明显,但是在商城里这个问题就非常大了,亟需优化。

经过分析发现在iOS上WKWebView实例化到页面加载中间最耗时的一步其实是JSBridge的构建,而在RN中,WKWebView的实例都会伴随着创建他的RNWebView组件的销毁而销毁,他们之间的关系类似于RNWebView是WKWebView的代理,所以每次创建RNWebView组件加载网页时候都会创建一个新的WKWebView实例,然后去构建JSBridge。所以想着能不能把实例化的WKWebView缓存到池子里,而不销毁,下次再复用。

正好RN社区已经把RNWebView组件独立出来开发了,所以我也就直接在他们的RNWebView组件之上构建了WKWebViewPool,同时修改RNWebView组件代码,保证创建WKWebView的时候从池子读取,如果池子没有再去实例化,销毁的时候把该解绑的解绑,然后放回池子里。

同时,由于WKWebView中JSBridge需要关联到一个delegate实例上去处理webview中的事件,RN社区的写法是这个delegate就是创建WKWebView的RNWebView实例。

但是再改用缓存池方案的时候为了不重复创建JSBridge,肯定不能解绑delegate,而如果不解绑这个delegate,又会造成两个问题,一是会造成RNWebView的内存泄漏,二是下次从webview中收到的消息只会被上次绑定的RNWebView处理,这样明显不行。

为了保证WKWebView的JSBridge不会在每次销毁RNWebView的时候出上面的问题,比较合理的做法是用WKWebView作为自己的delegate,所以继承了WKWebView给他扩展了一下,姑且称这个类为CustomWKWebView。CustomWKWebView内部也引用一个delegate,当CustomWKWebView收到webview的事件时再delegate出去。而每个RNWebView的实例则通过将自己设置为CustomWKWebView的delegate来间接的处理webview的事件。

同时为了优化首次的加载速度,会在应用启动的时候就创建一个CustomWKWebView的实例,相当于是个预热。

另外android上虽然平均耗时还可以,不过还是用类似的方案稍微优化了一下。

最后展示一下优化前后“从WebView实例化到开始加载的耗时”

开始加载在iOS中指decidePolicyForNavigationAction,在android中指onPageStarted
设备 iOS(iPhone 6s iOS 13.3) android(Samsung Galaxy S6 android 6.0)
优化前 首次 700ms 平均400ms 首次 185ms 平均20ms
优化后 首次 45ms 平均30ms 首次 78ms 平均4ms
alphacat2018 commented 4 years ago

2.公链切换导致整体应用切换的时候,内存,cpu这些如何管控的?考虑游戏资源加载对内存要求较高,是否会出现内存激增导致app crash的情况?要如何预防和监控?

公链切换前后,主要有下面四个模块是不一样的:

  1. 游戏。每条公链都有自己的游戏配置,当切换公链,公链初始化后也会加载这条链的游戏配置。但是由于游戏本身是H5游戏,所以游戏资源的加载只有在进入游戏时才会发生。

  2. 不同公链下的UI。为了确保用户能够清楚的意识到目前操作的是哪条公链,每条公链在UI上都有一些不一致,比如说字体颜色或者图片等等。但是由于切换公链的时候,应用的回退栈中基本上只存在一个主页,而其中需要更新的资源并不多。

  3. 游戏的WebView容器环境。区块链游戏基本上都需要和WebView容器进行交互,以获取钱包信息或者进行交易等等。比如说要在Chrome上玩以太坊的游戏的话,主流的做法是安装Metamask插件,这个插件会在window对象中插入自己的web3以及ethereum对象,供游戏调用。当游戏需要对交易进行签名的时候,Metamask会以弹窗的形式向用户展示交易信息,并询问用户是否同意交易。而不同的主链是需要不同的容器环境的。为了减少已经适配好类似Metamask这类主流插件API的游戏接入我们平台的麻烦,需要我们向WebView中提供与这些主流插件兼容的API,与我们的App进行交互,并随着主链的切换而切换。我们会为每条公链打包一个初始化WebView时需要执行的代码文件。不过这个文件下载和更新的时机是在游戏初次启动的时候,而非切换公链的时候。

  4. 公链的核心逻辑,主要分为钱包的管理和交易的管理。这基本上是整个应用的核心,无论是游戏或者资产的管理最后都会依赖这个模块。但是由于不同的链下的行为大体一致,所以这里采用了委托模式,每条公链实现自己的delegate,切换公链的时候切换不同的delegate就好。虽然在公链切换的时候会有一些I/O操作,但并不会带来大幅的内存和cpu波动。

综上,虽然切换公链会导致应用整体切换,但是可以说大部分资源加载并不会立刻发生,而是在使用时才发生,所以并不会带来内存和cpu的大幅波动。

alphacat2018 commented 4 years ago

3.描述一下多应用托管平台的打包更新方案。

首先说一下因为项目本身是区块链产品,苹果中国商店不允许上架。所以采取的方案是海外用户在苹果海外商店下载,中国用户通过Testflight分发。但是App中有用虚拟货币支付的游戏,这是两种渠道都不允许的,所以不得已在审核的时候只能开启纯净版,审核结束可以分发之后会关闭纯净版。另外,android海外用户使用Google Play,国内用户使用官网进行分发。

应用内大概存在以下四类资源,对应着不同的更新方式:

  1. native层的代码和资源。比如为了支持横屏游戏需要更改native层的代码和配置。为了支持聊天需要导入数据库等。这种级别的更新只能通过强更来分发。不同的渠道在打包的时候就会把各自的Channel写到native的配置中,方便运行时根据自身的Channel去请求版本信息,判断是否需要更新。而这四类资源中也只有这一类是需要根据不同平台去加以区分的,其他的资源很容易在平台间保持一致。

  2. 比较重要的JS层的代码和资源,强制用户热更。大部分功能和bugfix都是这一类资源。不过由于使用的热更服务是微软的CodePush,不能保证中国用户的访问速度,虽然CodePush本身有native层是否需要热更的校验逻辑,但是鉴于访问速度,App启动时会先访问我们的应用服务器进行热更版本的请求,继而判断是否需要请求CodePush服务器。

  3. 不太重要的JS层的代码和资源,后台静默热更。基本同第二点,不过一些不太重要的小优化就偷偷下载了。

  4. 一些可以随时更新的配置和资源。比如本地化的翻译文件,游戏配置,或者WebView容器的插入代码等。为了减少请求,更多地利用缓存,这一类资源的缓存策略采取了类似于index.html + 文件md5的方式。App启动时,会以no-cache的方式下载一个manifest。当需要某个文件时,优先判断manifest中该文件对应的url是否已经缓存过,如果没有缓存则先下载,否则直接使用缓存。而当线上更新某个文件时,也会触发该manifest的更新。

最后上个流程图: flowchart

alphacat2018 commented 4 years ago

4.以开发者视角,介绍一下前后端如何与区块链结合的?

首先描述一下一个玩家开始玩区块链应用时直接或间接牵涉到的过程。

  1. 下载一个钱包应用。钱包应用可以有多种形式,应用最广泛的是浏览器插件,手机App。

  2. 创建一个钱包。钱包最核心的就是私钥和地址了。地址相当于是每个钱包的标识,类似银行卡号。私钥是我们在发送交易时用来对交易加密的,确保交易是本人发出的。可以由私钥间接推算出地址,所以只要保存好各种形式的私钥就可以了。如果之前已经有钱包了,那这里就需要将钱包导入钱包应用中。

  3. 进入游戏。游戏可能会要求使用钱包对一段信息进行签名,然后对签名后的信息进行校验,如果校验通过则登录成功。

  4. 氪金。氪金当然是用加密货币支付了。当游戏调用钱包应用在window对象中的支付方法时,钱包应用会将订单相关的信息展示给用户,等用户同意并设置好交易手续费之后,会使用私钥对订单签名,然后发送给节点。所谓节点就是运行着特定的挖矿程序,可以与区块链网络中的其他节点通信的服务器。当订单抵达节点之后,并不会立刻执行,而是放入交易池中,并广播给其他节点。节点基本上是依据交易费的高低对交易进行优先级排序,最终这笔交易消耗的时间受用户设置的手续费高低,网络的相对繁忙情况,公链本身使用的协议和性能等等因素影响,从几秒到几天都有可能。

  5. 交易最终得到确认。应用服务器监测到这笔交易之后,会根据交易信息给玩家的账户打装备或者打钱。

虽然中间还会有很多小细节,比如支付的时候有可能基于合约,或者直接往一个钱包地址打钱。除了使用平台币,也可能使用在平台上发行的token等,但是大概就是这样一个流程.

根据我上面的描述,大家应该都知道区块链应用的两大问题就是交易慢和交易成本高。比如说在现在主流的以太坊上进行一笔交易的话,交易时间比较快的是在一两分钟,成本大概是RMB几毛钱,慢的话成本在几分钱,但是时间就不确定了。而这只是最简单的转账,如果业务是基于区块链的智能合约的话,那根据合约代码的复杂度不同,玩家跟合约交互一次的成本可能高达几块甚至十几块,几十块钱。另外还有一点就是数据安全以及隐私相关的问题,可能有些人觉得区块链一大特点是安全,因为完成的交易无法被篡改,但其实这点并不绝对。而且每个人都可以成为公链的节点,每笔交易别人都可能知道,或许有人认为自己暴露在外的只是钱包地址,但是当你结合大数据去分析每一笔交易的时候,最终推算出来一个人的真实身份并非难事。有一个数据是美国一家公司据称掌握了80%以上比特币交易人员的真实身份,仅供参考。所以在进军区块链业务时,需要先考虑好这种完全不确定的交易延迟造成的业务连续性问题、玩家是否能够接受这种相对传统游戏应用而言高昂的交易成本,还有就是数据完全公开透明是否可以接受。

所以这里虽然问怎么结合,但是我得说并不建议结合。如果一定要结合,那我觉得目前来说比较合理的做法是尽量减少和区块链的交互,只拿区块链作为结算层用。最典型的就是各大加密货币交易所。如果需要交易加密货币的话首先需要将钱打到交易所为你指定的地址中,之后你在交易所发生的所有交易都是中心化的交易,只有当你最后要将钱提现的时候交易所再通过区块链将钱转到你的钱包地址下。

这种方式是最简单的,也是我觉得现有用户最能接受,开发难度最小的,只需要在游戏里有一个充值点,然后等用户充值后,应用服务器监测指定的钱包地址下收到交易之后再给玩家账户里打游戏币,之后所有的交易都发生在应用服务器上。