react-native-navigation 简单分析和跨页跳转

目录 APP 端, Javascript

虽然 react-native-navigation 是 Facebook React Native 官方文档推荐的导航库之一 ,但我也不得不说使用它做 APP 导航主框架的体验简直糟糕透了。当然,这本身可能就是 React Native 自身的问题。

1 react-native-navigation 简单分析

使用 react-native-navigation 首先得理解下它的实现。它独立于 RN Component 的 componentWillMount/componentWillUnmount 接口实现了一套自己的事件机制,最重要的可能是 willAppear/willDisappear。它提供了一套页面堆栈操作和切换动画, push 可以将目标页面切换到最上方,pop 可以返回上一页。

可能是为了性能或者设计使然,push 的时候不会销毁当前页。也就是说,在 A 页面里 push 跳转到B 页面,不会 Unmount A 页面的Component。 不过在 B 页面 pop 回 A 页面时,的确会 Unmount B 页面的Component。这也意味着,整个导航路径是一个页面堆栈,只要在堆栈里页面的 Component,都不会被 Unmount。

2 页面堆栈的问题

这有时候会导致一些很严重的问题。有些情况下,特定的 Component 可能会占用唯一的系统资源,比如:麦克风、照相机等。这些 Component 在实现的时候往往只考虑了 React Native 的接口,在 componentWillUnmount 的时候释放占用的资源。它们不会预料到与 react-native-navigation 的结合,专门提供一个 willDisappear 时释放资源的接口,而且有些情况下也未必能这样做。

如果 A 页面在使用这些 Component 已经占用了麦克风或者相机,B 页面也要使用这些 Component,那么从 A push 跳转到 B 时,A 页面的资源不会被释放,B 页面就可能会遇到麦克风不可用,或者相机无法初始化等问题。

解决这个问题,最简单的办法是调整页面交互顺序,保证使用这些独占系统资源的页面永远在堆栈的最顶端,或者使用 Modal Stack,把独占资源的 Component 放到 Modal 里去 present 然后 dismiss。

3 跨页跳转实现

react-native-navigation 只能支持页面堆栈,而且看起来只能支持 push/pop 一个页面,也就是说整个切换过程是串行的,push 顺序是 A->B->A->D ,那么 pop 顺序也只能是 D->A->B->A。

但很可惜地是,在产品经理眼中,是不存在串行页面切换这种限制的。TA 们有时候要求跳转的过程中没 A,但返回的时候要有 A;或者要求跳转的过程中有 A,但返回的时候可以跳过 A,或者甚至直接返回到堆栈最底端。

直接返回栈底很容易,react-native-navigation 提供了 popToRoot 接口,但它没有提供一下子 push 多个页面,或者一下子 pop 多个页面的功能。它也没有类似于 HTML5 的 history API,我们直接对堆栈进行操作,是不太可能的。只能通过它现有的接口想办法。

3.1 跨页 push

跳转的过程没有 A,但返回的时候要有 A,这只是一个产品需求。在实现上,是可以变成跳转过程中有 A,但是 A 被快速跳过,返回的时候才会被真正渲染。这样从用户体验上来看,并没有看到 A。代码实现上,可以考虑两种方法:

willAppear 结合 didDisappear 做状态控制

在 A 的 state 里放一个 isFirstEntry 状态,默认是 truewillAppear 里判断 isFirstEntry 则直接跳转到下个页面,render 里判断 isFirstEntry 则只渲染一个背景 View ,否则才渲染正常页面。这样就实现了在页面切换过程中跳过 A。在 的 didDisappear 里将 isFirstEntry 置为 false 。这样在返回的时候 willAppearrender 表现就和正常返回一样了。

  willAppear = () => {
    if (this.state.isFirstEntry) {
      this.props.navigator.push(...);
      return;
    }
    ...
  };
  render() {
    if (this.state.isFirstEntry) {
      // 返回背景 View
    } else {
      // 返回正常 View
    }
  }
  didDisappear = () => {
    this.setState({isFirstEntry: false});
  };

willAppear 页面计数

在需要更复杂逻辑的地方,可以在 state 里放一个 appearTimes 计数器。在 willAppear 里给计数器加一,这样每次进入页面都会增加计数。通过判断计数器的值,来决定如何 render 或者跳转。

  willAppear = () => {
    this.setState({appearTimes: this.state.appearTimes + 1});
    if (this.state.appearTimes === 1) {
      this.props.navigator.push(...);
      return;
    }
    ...
  };
  render() {
    if (this.state.appearTimes === 1) {
      // 返回背景 View
    } else {
      // 返回正常 View
    }
  }

3.2 跨页 pop

跳转的过程中有 A,但返回的时候要跳过 A,相当于可以自己操作 pop 的步长。很遗憾,react-native-navigation 没有提供这样的接口。不过我们可以采用一个 trick 的手段,来实现这个逻辑。

假设从 Root->A->B,在 A 的 state 里放一个 relayPop ,默认是 false。 在 A 跳转到 B 时,通过 props 传入一个回调:setParentRelayPop,B 可以通过这个回调修改 A 的 state relayPoptrue; 在 A 的 willAppear 中,首先判断 relayPop 是否为真,如果是真的话,代表是从 B 返回且 B 要求接力返回,那么 A 就直接 pop 返回到 A 的上级。 B 在返回时,首先通过回调设置 relayPoptrue,然后再调用 pop 接口,就实现了跨页返回。

// Screen A
  willAppear = () => {
    if (this.state.relayPop) {
      this.props.navigator.pop();  // 接力返回
      return;
    }
    ...
  };
  ...
    // 跳转逻辑某处
    this.props.navigator.push({..., passProps: {
                                  setParentRelayPop: () => this.setState({relayPop: true}) 
                                }});
// Screen B
    // 返回逻辑某处
    this.props.setParentRelayPop();
    this.props.navigator.pop();

ES/Redis/SSDB/BRPC 的 Open-Falcon 监控脚本

目录 Python, 服务端

前些天想监控不同机房的多个 ElasticSearch 集群,结果网上找到的监控脚本都不太好用。我希望这个脚本能够并发获取多个 ES 集群的状态,而且监控的目标和上报的地址可以通过配置文件修改,不需要去脚本中查找修改位置。

了解到 Open-Falcon 的上报接口非常简单,于是就自己写了一个同时查询多个 ES 集群信息并上传到 Open-Facon Agent 的监控脚本。能够将多个集群的索引文档数、查询请求数、查询时间等关键信息收集到 Open-Falcon 中。

用了一段时间,感觉还挺不错的。后来又头疼 Redis 内存占用太高,分析困难等问题,又以同样的思路写了 Redis 的监控脚本,都是通过 info 命令获取集群的状态,把 KEY 数量,内存占用,命令数,过期的 KEY 数量等等相关的信息都收集到了 Open-Falcon 里。这样就能通过 Open-Falcon 的报表看到 Redis 使用情况的变化。

SSDB 虽然兼容 Redis 命令,但 info 命令的返回跟 Redis 差异实在是太……大了点儿。内容不一样也就算了,格式也太随意了,用纯文本画了几个表格,真让人无力吐槽。没法复用 Redis 的监控,只能自己给 info 写个 parser,将信息提取成可用的字典。

最后说一下 BRPC。BRPC 内建了一个 HTTP 服务,把内部的各种状态用 WEB 页面的形式展示出来。关键的是又提供了一套 BVAR 机制,可以用于统计内部的各种指标,自动显示到页面上。最有意思的是,它这个内建服务会识别 User-Agent,如果请求是通过 curl 发起的,返回的是一个完全不包含任何 HTML 标签的纯文本界面,可以用 yaml 解析成字典。这样就可以用跟监控 ES 完全类似的方式,通过外部请求 BVAR 页面,获取所有状态上报监控系统了。

这四种系统的监控脚本,我已经整理放到 GitHub 上了,希望能对同样需求的朋友也有所帮助:

手机 APP 应该选用哪个加密算法 - 兼吐槽 TEA

目录 APP 端, Java, Objective-C

很多 APP 产品都有通信加密的需求,一部分出于市场的要求,比如苹果对于“ATS”的强制性规定,一部分出于自身安全的考虑,比如对账号和密码的保护。这些需求大部分都可以用简单的 HTTP -> HTTPS 升级来搞定,而且几乎不用付出什么成本(除加解密的计算开支外),例如使用我之前文章介绍到的 Let's Encrypt 免费证书

但还有一类特殊的需求,HTTPS 解决不了,也就是防协议分析的需求。很多 APP 开发者应该知道,只要在手机里安装一个代理 CA 证书,就可以实现中间人攻击,通过代理软件抓到 HTTPS 包的明文内容。虽然这样的攻击很难在公开网络上进行,但对自己的手机进行抓包分析,作为 APP 和服务端通信的调试手段是被广泛使用的。

协议分析能做什么呢?可以猜想到一定的 APP 内部逻辑,可以对产品数据进行作弊攻击。举个例子:你的 APP 通过某个渠道进行推广,为了统计渠道安装、注册或者日活,你往往会在 APP 中埋一个点,当 APP 启动时,发送一些信息到服务器。如果这个协议被破解了,渠道商根本不需要真正进行推广,只需要构造一些假消息发送到你的服务器就行了。仅看数据你可能会以为这个渠道推广效果特别好,其实只是骗局而已。

这类情况下,就要求对敏感协议内容进行额外的数据保护。最常用的做法,就是对协议内容进行一次额外的加密,为了性能,往往选用对称加密算法。那么问题来了,手机 APP 开发时,应该选用哪个加密算法?

关于这个选型,国内互联网圈有个怪现状值得谈一下,那就是 TEA 算法。因为该算法在腾讯有着广泛的应用,因而被很多客户端开发人员推崇。典型推荐理由往往是:“TEA加密算法不但比较简单,而且有很强的抗差分分析能力,加密速度也比较快,还可以根据需求设置加密轮数来增加加密强度”。这是真的吗?算法安全性可以直接看维基百科上 TEA 算法的介绍,我的理解是不够安全。但其实大部分用户也不那么地在乎它的安全强度,那么性能呢?加密速度真的很快吗?

这就要从历史的角度去看了。作为曾经手撸过 “DES 差分密码攻击” 代码的程序员,表示 TEA 算法的确足够简单。在 QQ 诞生的那个年代,TEA 在计算上的确有着不小的优势。但 QQ 已经 18 岁了啊同学们,18 年来中国发生了多大的变化,世界发生了多大的变化啊!

2008 年,Intel 就发布了 x86 的 AES 指令集扩展,近几年的服务器 CPU 应该都支持,不相信你
grep aes /proc/cpuinfo 就能看到 ;2011 年 ARM 也在 ARMv8 架构下直接提供了 AES 和 SHA-1/SHA-256 指令 。这意味着什么?意味着服务端和客户端在硬件上直接支持 AES,意味着原来 N 条汇编指令只需要一条 AES 指令就完成了。其实也意味着,在绝大多数情况下 AES 才应该是你的首选

口说无凭,咱们可以看一下测试数据,x86 服务器 CPU 测试可以直接看 Crypto++ 的 benchmark 。可以看到 AES/CTR (128-bit key) 与 TEA/CTR (128-bit key) 的加密速度比是:4499 MB/s 比 72 MB/s,62 倍的差异!这就是硬件实现的威力。

ARM 手机 CPU 加密算法的 Benchmark,我没有找到。但为了更有说服力,我自己实现了两个测试 APP,一个 Android 版,一个 iOS 版。写技术文章多不容易啊,写博客之前先写三个晚上代码,泪目!!!代码在 https://github.com/solrex/cipher-speedAndroid 版可以直接在 Release 里扫码安装

首先介绍一下目前的旗舰 CPU,骁龙 835 (MSM8998) 的表现,测试机型是小米 6:

# Speed Test of 10MB Data Enc/Decryption #
# AES: 
* [AES/CBC/PKCS5Padding] ENC: 1146.9 KB/ms
* [AES/CBC/PKCS5Padding] DEC: 692.4 KB/ms
* [AES/CBC/NoPadding] ENC: 1118.8 KB/ms
* [AES/CBC/NoPadding] DEC: 1343.5 KB/ms
* [AES/ECB/PKCS5Padding] ENC: 990.4 KB/ms
* [AES/ECB/PKCS5Padding] DEC: 703.2 KB/ms
* [AES/ECB/NoPadding] ENC: 973.4 KB/ms
* [AES/ECB/NoPadding] DEC: 988.9 KB/ms
* [AES/GCM/NOPADDING] ENC: 13.9 KB/ms
* [AES/GCM/NOPADDING] DEC: 14.7 KB/ms
# DES: 
* [DES/CBC/PKCS5Padding] ENC: 20.1 KB/ms
* [DES/CBC/PKCS5Padding] DEC: 20.7 KB/ms
* [DES/CBC/NoPadding] ENC: 21.3 KB/ms
* [DES/CBC/NoPadding] DEC: 21.6 KB/ms
* [DES/ECB/PKCS5Padding] ENC: 26.3 KB/ms
* [DES/ECB/PKCS5Padding] DEC: 26.2 KB/ms
* [DES/ECB/NoPadding] ENC: 25.9 KB/ms
* [DES/ECB/NoPadding] DEC: 26.8 KB/ms
# 3DES: 
* [DESede/CBC/PKCS5Padding] ENC: 23.6 KB/ms
* [DESede/CBC/PKCS5Padding] DEC: 23.2 KB/ms
* [DESede/CBC/NoPadding] ENC: 23.6 KB/ms
* [DESede/CBC/NoPadding] DEC: 23.5 KB/ms
* [DESede/ECB/PKCS5Padding] ENC: 8.5 KB/ms
* [DESede/ECB/PKCS5Padding] DEC: 8.5 KB/ms
* [DESede/ECB/NoPadding] ENC: 8.5 KB/ms
* [DESede/ECB/NoPadding] DEC: 8.6 KB/ms
# TEA: 
* [TEA] ENC: 16.0 KB/ms
* [TEA] DEC: 18.1 KB/ms

可以看到,TEA:AES=16:990,这是多少倍?我都懒得算了。然后是 2 年前的中低端 CPU,联发科 Helio P10 (MT6755),测试机型是魅蓝 Note 3:

# Speed Test of 10MB Data Enc/Decryption #
# AES: 
* [AES/CBC/PKCS5Padding] ENC: 358.8 KB/ms
* [AES/CBC/PKCS5Padding] DEC: 267.9 KB/ms
* [AES/CBC/NoPadding] ENC: 438.8 KB/ms
* [AES/CBC/NoPadding] DEC: 515.0 KB/ms
* [AES/ECB/PKCS5Padding] ENC: 310.6 KB/ms
* [AES/ECB/PKCS5Padding] DEC: 222.1 KB/ms
* [AES/ECB/NoPadding] ENC: 312.4 KB/ms
* [AES/ECB/NoPadding] DEC: 319.5 KB/ms
* [AES/GCM/NOPADDING] ENC: 5.1 KB/ms
* [AES/GCM/NOPADDING] DEC: 5.7 KB/ms
# DES: 
* [DES/CBC/PKCS5Padding] ENC: 7.5 KB/ms
* [DES/CBC/PKCS5Padding] DEC: 7.7 KB/ms
* [DES/CBC/NoPadding] ENC: 7.7 KB/ms
* [DES/CBC/NoPadding] DEC: 7.8 KB/ms
* [DES/ECB/PKCS5Padding] ENC: 9.3 KB/ms
* [DES/ECB/PKCS5Padding] DEC: 9.2 KB/ms
* [DES/ECB/NoPadding] ENC: 9.3 KB/ms
* [DES/ECB/NoPadding] DEC: 9.5 KB/ms
# 3DES: 
* [DESede/CBC/PKCS5Padding] ENC: 12.5 KB/ms
* [DESede/CBC/PKCS5Padding] DEC: 12.3 KB/ms
* [DESede/CBC/NoPadding] ENC: 12.3 KB/ms
* [DESede/CBC/NoPadding] DEC: 12.5 KB/ms
* [DESede/ECB/PKCS5Padding] ENC: 3.1 KB/ms
* [DESede/ECB/PKCS5Padding] DEC: 3.1 KB/ms
* [DESede/ECB/NoPadding] ENC: 3.1 KB/ms
* [DESede/ECB/NoPadding] DEC: 3.1 KB/ms
# TEA: 
* [TEA] ENC: 6.2 KB/ms
* [TEA] DEC: 8.0 KB/ms

然后是 3 年前的旗舰 CPU,Apple A8,测试机型是 iPhone6。别问我为啥不用今年的苹果旗舰 CPU...

# Speed Test of 10MB Data Enc/Decryption #
# AES
* [AES/CBC/PKC7Padding] ENC: 76.0 KB/ms
* [AES/CBC/PKC7Padding] DEC: 111.3 KB/ms
* [AES/CBC/NoPadding] ENC: 138.2 KB/ms
* [AES/CBC/NoPadding] DEC: 450.7 KB/ms
* [AES/ECB/PKC7Padding] ENC: 305.6 KB/ms
* [AES/ECB/PKC7Padding] DEC: 735.9 KB/ms
* [AES/ECB/NoPadding] ENC: 330.0 KB/ms
* [AES/ECB/NoPadding] DEC: 673.6 KB/ms
# DES
* [DES/CBC/PKC7Padding] ENC: 23.1 KB/ms
* [DES/CBC/PKC7Padding] DEC: 24.5 KB/ms
* [DES/CBCPadding] ENC: 23.1 KB/ms
* [DES/CBCPadding] DEC: 22.8 KB/ms
* [DES/ECB/PKC7Padding] ENC: 19.4 KB/ms
* [DES/ECB/PKC7Padding] DEC: 20.8 KB/ms
* [DES/ECBPadding] ENC: 22.2 KB/ms
* [DES/ECBPadding] DEC: 22.2 KB/ms
# 3DES
* [3DES/CBC/PKC7Padding] ENC: 9.7 KB/ms
* [3DES/CBC/PKC7Padding] DEC: 9.8 KB/ms
* [3DES/CBC/NoPadding] ENC: 9.8 KB/ms
* [3DES/CBC/NoPadding] DEC: 9.8 KB/ms
* [3DES/ECB/PKC7Padding] ENC: 9.4 KB/ms
* [3DES/ECB/PKC7Padding] DEC: 9.1 KB/ms
* [3DES/ECB/NoPadding] ENC: 9.2 KB/ms
* [3DES/ECB/NoPadding] DEC: 9.4 KB/ms
# TEA
* [TEA] ENC: 10.9 KB/ms
* [TEA] DEC: 11.1 KB/ms

关于 Apple A8 的测试多说两句。我上面的 AES 性能,离 GeekBench 发布的 A8 AES Single Core 还有不少差距,不知道是不是测试方法差异导致。但总的来说,不影响结论,那就是 TEA 跟 AES 差距巨大

看到这里,可能大部分人心里已经做出选择了。即使还没做出选择的读者,我想你也可以考虑看看我的代码实现是否存在问题。不过最后还是回答一下开头提出的问题吧:

  • 如果你使用平台语言来实现对称加密,也就是 Android 上用 Java,iOS 上用 OC 或者 Swift,AES 是不二选择。这样能充分利用硬件提供的能力,安全性+性能肯定是最优,不要再想其他选项了。
  • 如果你使用 Native 语言来实现对称加密,在 Android 上使用 JNI 调用 C 编译的代码,的确不少人认为原生指令更难逆向。可能要在 ARM 架构上做个取舍,是取悦 v8 用户,还是取悦 v7 以下的用户,这可能影响到选型。不过我认为 AES 依然是一个好的选项,起码在服务器端,你肯定会节省成本。