Android Weex 自定义 Component 指北

Android Weex 自定义 Component 指北

Weex 自定义 Component 开发这块,官方文档和网上示例都较少涉及。工作所需有所研究,总结此文以飨读者。

基础定义与注册

如下述代码所示,从 WXComponent 继承出来以后,复写四个构造器方法,就可以完成一个最简单可跑当然也显示不了任何东西的 WXComponet。

需要说明的是 WXComponent 可以指定泛型 T,T extends View,用于指定WXComponent hostView 根布局的类型。这个还是指定的比较好,在某些进阶用法中会需要这个类型。

使用这个 Component 之前,还需要把他注册进 WXSDKEngine。如下所示:
一次注册后,在Android程序销毁之前,可以一直使用这个 Component。无需 unregister,WXSDKEngine 也没有提供 unregister 方法,这是显而易见的,因为当前还未产生任何实例。

如果想要设定 props 怎么办,如

那就在 Componet 中增加如下:

weex 会根据外部传入的 props,根据注解调用对应 props 的 set 方法。

生命周期

显然,看了第一节,只能保证链路上 weex 自定义 Component 能跑起来,没有做其他任何事情。那么,为了能实现我们需要的渲染和其他逻辑,就需要了解 Weex Componet 的生命周期。这里的生命周期,实质就是了解可 Override 的几个 WXComponent 方法,和他们的被调用的时机。这一块官方没有任何文档,全靠去 github 源码中看和试。

必需 Override

initComponentHostView()

用于生成根 View 返回给 Weex 来渲染。注意,不要在这里进行任何响应外界设入的 props 的渲染,因为此时极大可能 props 还没有被传入。

可选 Override

bindData()

需要注意的是,直接把 void 改成返回值比如 boolean 然后试图 return 是没有用的,weex js 侧收不到。因此,必须要去使用回调来给返回值。如上所示。

DOM

Weex 新内核(WeexCore)将 Dom 层和 Layout 引擎下沉到 C++ 层实现,移除 Java 层的 DomObject,提升渲染性能和内核的可通用性。因此,github 最新版不再可以获取到 WXComponent 中的 DomObject。

Tricks:强转

如果发现自定义 Component 的逻辑需要用到 Activity,而 WXComponent 只给你提供了 Context 的时候,不要慌,Weex 传入的 Context 其实可以强转 Activity。当然,以防万一,记得用 instance of 保护一下。

同理,如果你想要弹出一个 Fragment,结果发现自己需要一个 FragmentActivity 来getSupportFragmentManager(),不要慌,weex 传入的这个 Activity 也可以强转为
FragmentActivity,同样记得加 instance of 保护,否则业务挂了不算我的,因为这毕竟是文档中的未定义行为。

面向 EventBus 开发

什么叫面向 EventBus 开发?

即项目为了某些难解的引用找不到的问题,引入了 EventBus,然后后来的开发出于项目工期紧张,或者开发的主观偷懒等原因,滥用 EventBus,大量使用 subscrib – eventbus – observer 的形式来解依赖问题。

危害是什么?

  1. 逻辑零乱,到处跳转,可读性极差。
  2. 代码层级和实际业务层级分离。代码结构连形式上的合理性都没有。
  3. observer 的生命周期极其复杂。时常因为 observer 未被销毁而导致不该有的逻辑。如在 ListView 中,向下滚动,没有销毁 Observer,最终导致通知到已经被滚出屏幕的 Item 中的 Observer。这一点可以通过底层设计来确保销毁,但仍然是一种一旦发生就很隐蔽,难以排查的问题。
  4. 无条件滥用的事件中心,最终等同于 goto。Dijkstra 的那篇经典的《goto statement considered harmful》可以一字不动地抄过来喷事件中心。

考量另外一个点,其实 React Redux 状态容器设计中,Dispatcher 和 Reducer 实质上也是组成了一种事件中心。

  1. 他会不会膨胀?会,怎么办,拆分,Reducer 可以被细粒度地拆解。可读性怎么解决?统一管理的Reducer 可读性比无序要好。
  2. 代码层级和实际业务会不会层级分离?不会,因为只有两层,一层 UI 层,在视图层面上天然地结构合理。子 View,父 View,父父 View。Redux 层,根据事件来分离。
  3. 生命周期管理,会不会有问题?一部分会,因为 Redux 以外的代码足够薄,只有 UI 层。UI 是根据屏幕来渲染的。何况,还有 pure render 的约束,多次渲染、冗余渲染并不会产生什么问题。
  4. 滥用问题导致退化为 goto。事实上,goto 的真正问题在于,你不知道逻辑将会跳往何方。但是 Redux 的真正价值就在于,Redux dispatch (or goto)以后的事件处理机制,包含了统一的 Reducer,异步过程解决方案 aysnc,promise 等,中间件 middleware …

所以这里的问题很清楚,在传统的程序设计中,就不应当有事件中心的存在。只是因为一些特殊的客户端上难解的 Case,比如产品上突发要求的之前互不从属的组件的联动设计,多个 aar 之间的组件相互通信,等业务问题的时候,才会需要引入事件中心,但这绝对不是滥用事件中心的理由。或者就像 Redux 一样,把 UI 层以外的所有逻辑都交付给一套逻辑自洽的 Reducer,Dispacher,Middleware 等等来管理。

写到最后,我觉得我自己都变成 Redux 无脑吹了。然而事实就是如此,就像传统武术对阵 MMA 一样,一个还在使用着玄学气功穴位的概念,另一个已经应用了现代医学、力学、科学地总结了搏击经验,显然是前者被摁在了地板上。

Android Activity间通信的序列化过程中深浅拷贝的讨论

问题的背景是,视频互动业务需要增加弹幕功能,但是播放器的视图是伪横屏的,即,他是一种类似于使用 rotate(90.0)的方式,旋转横屏的,在 Activity 层面上还是一个竖屏的状态。那么弹幕输入的时候的键盘,也是竖屏的。这会带来比较严重的用户体验问题。

由于屏幕旋转状态在 android 下,是一个 Activity 层面上的事情,而且相当的底层,无从 hook,多方调研以后,决定采拉起一个横屏的 Activity 作为键盘输入的专用 Activity。这里的代码很快就可以写好,如下所示。

DTO 的代码定义如下所示:

那么现在问题来了,怎么把这个 Activity 获取到的 String 带回去?

最自然的想法是 onActivityResult,然而,播放器是一个 sdk,写不了 Activity 里的代码,也不可能通知许多业务方一一做改动。

那就只能抛开 android 原生的 Activity 间拉起结束中的通信机制了,思考其他可以通信的方法。很自然地,我们想到了 Callback 。结构如下图。但是 Callback 这样的一个非基本数据类型的对象怎么在 Activity 间传递呢?
danmaku_structure

尝试通过存入 Intent 的 Extras的方式,然而 putExtra 方法并不能 put 一个 object,只能 put 一个 serializable。那就让这个 DTO(Data Transfer Object)implements serializable 接口吧。没有问题。

然而无法启动 Activity,会有一个 crash 抛出:

java.lang.NullPointerException: Expected to unbox a ‘int’ primitive type but was returned null

报错堆栈如下:
$Proxy1.startActivity(Unknown Source)
android.app.Instrumentation.execStartActivity(Instrumentation.java:1520)
android.taobao.atlas.runtime.InstrumentationHook$2$1.execStartActivity(InstrumentationHook.java:299)

如果把这个 DTO 的成员变量改为 static 类型,则可以启动 Activity。

背后的原因是因为,在常规的序列化过程中,浅拷贝其实是没什么意义的。浅拷贝意味着复制一个引用的地址,是一个内存地址,但是常规序列化,要么跨进程,要么就是网络传输,序列化为 JSON,在这些常规场景里内存地址没有意义。因此 Java 序列化没有浅拷贝的选项,也往往是针对一个 POJO 或者 Bean 进行序列化,而不会对一个一般的含有很多引用的类进行序列化。

然而 Android 中的 Activity 与 Activity 间的传递对象又有所不同,理论上,都在同一个 Dalvik VM 中运行,相互的类引用都是可以访问到的。但是由于 Android Intent 设计为序列化传递,序列化过程中没有设计浅拷贝的机制,因此就无法浅拷贝地传递引用过去。

那么为什么设为 static 以后就可以传递,不会导致 crash 了呢?是因为静态成员属于类级别的,虽然不能序列化,但是因为我是在同一个机器(而且是同一个进程),我的jvm已经把这个类连带着他的静态变量一起加载进来了,所以获取到的是类层面上的静态变量地址,故,功能正常。

那么就决定是使用public static WeakReference<DWDanmakuWriteController.DanmakuWriteCallback> callback;了。但是事实上遇到了另一个问题:

在第一次 startActivity 的时候,观察到 Android 做了一次 GC,然后该 WeakReference 就被释放了,因此 Callback 的业务功能也不能正常执行。引入 WeakReference,原本是为了避开 static cakllback 导致的可能的内存泄漏,然而在这种主动 GC 的情况下,WeakReference 失效了。如果改用 SoftReference,和强引用并没有什么区别,都不能避免内存的泄漏。

最终,采用 AtomReference 来持有这个 static callback,在 Activity 退出的时机去将 AtomicReference 置空。之所以使用 AtomicReference,是因为考虑到视频 sdk 有并发场景的可能性,避免一边置 null 另一边准备使用的可能。

深度定制 RecyclerView 的几个例子

最近手头上接连接了几个比较复杂的需求。一个,是 github 上 star 数较多的 RecyclerViewPager,我去年深度定制了它,将 RecyclerView.Adapter 作为 ViewModel,内嵌 RecyclerView 在 ViewModel 之中,增加生命周期管理和 Utils注入,使之能够成为一个通用组件。然而,UED 要求的滑动效果,比起正常的 RecyclerView 的滑动距离,要多出半个 Item 的 Width。

考虑到 RecyclerViewPager 自身为了让 RecyclerView 的能够实现类 ViewPager 的效果,它自身就需要做一个触发滑动 => 自发滑动 => 惯性停止的事情,因此就去看 RecyclerViewPager 的自身实现。最终发现,RecyclerView 是通过 LayoutManager 来控制滑动、显示、隐藏等一系列逻辑的,其中就有 LayoutManager 的内部类 SmoothScroller 的 startSmoothScroll 方法,用来处理平滑滚动。hook 此方法,即可实现多滚动一段距离的功能。代码如下所示:

可以看到逻辑非常简单,只是判断了一下滑动方向,对于 dx 做了加减半个 item width 的功能。此外,就是看了一下 Action 这个内部类的结构,发现,如果对于不会触发滑动的 Action,会标示 onChanged field,遗憾的是 private,外部拿不到。那再 debug,发现不触发的情况下,duration 也会为一个负值。那就用 getDuration() 替代 onChanged 作为判断条件。细想也很自然,如果动作无需触发,也没有必要为动作设置持续时间 duration 了。

然而,以上简单的修改花了我两天的工作量,因为难度的本质在于,找到 hook 的地方,知道这么改会发生什么情况。那么就需要去读 Android 的源代码,看 LayoutManager 中的 SmoothScroller 是干什么用的,有哪些方法,哪个方法是滑动的关键方法。

同理,第二个案例是,如果使用横向 RecyclerView 代替 ViewPager 会有一个问题。RecyclerView 一次滑动是可以滑好几个 Item 的,但 ViewPager 始终只能一次滑一个。那么如何让 RecyclerView 实现 ViewPager 的滑动效果呢?

分析得知,滑动分成两种,滑动和甩动,前者针对于手指在屏幕上实际的触摸滑动距离,后者则是触摸滑动的速度,由速度计算出惯性滑动需要的距离。速度的处理在 fling 函数之中,因此,实现效果的代码如下:

这样就阻断了 super.fling() 中对甩动速度的处理。

推荐一个搜索 Android 源码的方法,google 关键词 + site:android.googlesource.com,这里带了大量的源码,相比使用 IDE 更为快捷。

Android 开发中的猥琐与底层

做 sku 的过程中,使用了很多猥琐的技巧。起因是因为,我需要在这个需求中解决几个问题:

  1. 如何使用 RecyclerView 做出 ViewPager 的效果。
  2. 如何在 RecyclerView 中 hook 到几个可 Override 的生命周期。
  3. 如何解决通用容器和专用业务容器的界限划定。

所谓的猥琐,就是阅读一遍相关的 Android 系统源码,然后去 hook Android api 的方法,然后去各种调教他的入参出参,做出自己的私活。

比如每次滑动距离要增加一个固定距离,比如关掉惯性滑动,比如尝试失败的 Recycler 回收 View做出单/多切换功能,比如之前一直没有解决的头图容器二次渲染白色的问题,等等。

说猥琐,其实也还好。Android 中很多现在大家约定俗称的东西,Google 文档里并没有这么写,只是一开始摸索 Android 的人,都这么写了,在网上的文章里也大多这么分享了,大家也就很自然地去做了。比如为什么 remove View 再重新 add View 就不行? 而要重新 new 一个 View ?的确,Android 上这样会有闪烁的问题。但是在有些不需要 care 闪烁的场景,就这么用,有他的好处。节约内存,省掉 GC,无需状态同步,等等。猥琐的方法用多了,也就变成了堂堂正正的路。

真正的问题大概是,对于框架层的东西,没有必要保持敬畏之情,上去干就好了。阅读一遍 google 的源码,就能搞清楚他的一个惯性滑动是怎么处理的。即便是一个 private 的 field,也可以丧心病狂地使用反射把他的值给改了。我们所需要 care 的点始终只是:

  1. 产品上做这个点有没有价值
  2. 如果有,工程上多猥琐的技巧都能把他给做了。
  3. 万一有风险怎么办?try catch,Tlog,orange,abtest,一堆兜底方案,哪怕98%的场景 ok,2%的场景挂了,只要能兜住,新业务能跑起来,2%的场景兜住不 crash,事后慢慢分析就可以了。没准还没分析完,业务先死了,那就更没有分析的必要了。

开发的层面上,没有解决不了的问题。因为开发始终是用快速学习、工程经验、与产品和视觉的沟通,游走在各种我可能不熟悉的领域,变成比较熟悉,做出东西来验证价值。如果有价值,再交给更专业的人去深入深化。如果解决不了,说明这是算法、当前技术水平、等等一系列的问题,不应当到开发层面。否则,只要无节操,多猥琐都能干了。

另一个有所领悟的点是,认清楚什么是自己的底层。过去认为系统的 API 就是自己的底层,那么一旦找不到一个 API 解决自己的功能,就会手足无措。但是,一旦迈出第一步去阅读 Android 源码,hook 系统公用 API 来做私活了,就会觉得,自己的底层又往下了一步。等到用反射去修改 private field 了,就会觉得,节操更低了,但也更强大了。因此,时刻怀疑自己的底层,是否真的足够底层了,也许再低一点,又会有新的突破。

enum、static final 与 IntDef:Android 中实现枚举的方案选择

前述

曾经有一段时间,许多网上的 Android 性能调优的文章都提到,要尽量避免在 Android 中使用 enum,因为使用 enum 会引入较大的性能损失。

然而,最新的 Android 文档已经改变了这一说法。根据 Android VM 的开发者Elliot Hugues 的博客所述,过去的 Android 官网的性能优化指南并不准确,混杂了许多臆断。因此如今他们严格地依据事实,重写了Android 性能优化指南,开发者也应当以最新的文档为准。当然比较窘迫的是,Android 文档的更新并不是同时改口,事实上就在同个 training 目录下的 管理应用内存一文中,就仍然保留了过时的避免使用 enum 的说法。

最新的解释

之所以重新鼓励使用 enum ,其解释是:

  1. Android 2.2 及以下系统上,使用 enum 的确会引发较大的性能损耗。主要是内存上的消耗,enum 远大于使用 static final int。
  2. 在 Android 2.3 及以后的系统中,之前的一些 enum 的性能问题已被 JIT 所优化。此时,虽然 enum 相比于 static final int,内存仍然有所增加,但已经是可以接受的了。加之 Android 2.2 到如今的 Android 7.0,Android 手机的内存配置突飞猛进,从256MB跃升至6GB,enum 所带来的内存增加已经可以忽略。

强内存依赖的应用的枚举实现

尽管如此,在实际开发中仍然有可能遇到内存消耗较大的应用开发问题,那么此时,该如何优化枚举值的实现,以节约内存消耗呢?方案如下:

直接使用 static final int

然而,其问题在于,直接使用无法实现枚举变量赋值的类型安全。且无法把多个枚举值归纳到同一个枚举类型下。比如:

显然,此时 int 就未能实现赋值的类型检查,也未能把多个枚举值归纳到同一个枚举类型下。

Android Proguard 优化

在 Android Proguard 中,可以在 proguard.cfg 中加入参数 -Doptimization class/unboxing/enum,从而自动将 enum 替换为 static final int。这样,也就无需担心多余的内存问题了。

使用 IntDef 注解替代 int

IntDef 可以用于替代 int,其价值在于用@IntDef int var限定赋值范围,实现类型安全。还用@IntDef({SUNDAY, MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY})归集了散乱的 static final int 变量,如下代码所示:

然而,IntDef 的缺点在于无法优雅地把 int 转为 IntDef,尤其在一个枚举值是服务端下发的时候。强行的实现会变的极为尴尬:

此时,在枚举值较少的时候还能忍,较多的时候代码就会变得非常丑陋。本质是因为,@IntDef 缺少像 Enum.values() Enum.ordinal() 等等 int 与 enum 与 String 互转的方法,因此在想换转换较多的场景下,不如采取第二种优化方法。

Node.js + React Native毕设:农业物联网监测系统的开发手记

毕设大概是大学四年里最坑爹之一的事情了,毕竟一旦选题不好,就很容易浪费一年的时间做一个并没有什么卵用,又不能学到什么东西的鸡肋项目。所幸,鄙人所在的硬件专业,指导老师并不懂软件,他只是想要一个农业物联网的监测系统,能提供给我的就是一个Oracle 11d数据库,带着一个物联网系统运行一年所保存的传感器数据…That’s all。然后,因为他不懂软件,所以他显然以结果为导向,只要我交出一个移动客户端和一个服务端,并不会关心我在其中用了多少坑爹的新技术。

那还说什么?上!我以强烈的恶搞精神,决定采用业界最新最坑爹最有可能烂尾的技术,组成一个 Geek 大杂烩,幻想未来那个接手我工作的师兄的一脸懵逼,我露出了邪恶的笑容,一切只为了满足自己的上新欲。

全部代码在 GPL 许可证下开源:
服务端代码:https://github.com/CauT/the-wall
客户端代码:https://github.com/CauT/NightWatch

由于数据库是学校实验室所有,所以不能放出数据以供运行,万分抱歉~。理论上应该有一份文档,但事实上太懒,不知道什么时候会填坑~。

总体架构

OK,上图说明技术框架。
总体结构

该物联网监测系统整体上可分为三层:数据库层,服务器层和客户端层。

数据库和代码层

数据库层除了原有的Oracle 11d数据库以外,还额外增加了一个Redis数据库。之所以增加第二个数据库,原因为:
1. Node.js 的 Oracle 官方依赖 node-oracledb 没有ORM,也就是说,所有的对数据库的操作,都是直接执行SQL语句,简单粗暴,我担心自己孱弱的数据库功底(本行是 Android 开发)会引发锁表问题,所以通过限制只读来避开这个问题。
2. 由于该系统服务于农业企业的内部管理人员,因此其账号数量和总体数据量必然有限,因此使用 redis 这种内存型数据库,可以不必考虑非关系型数据库在容量占用上的劣势。读取速度反而较传统的 SQL 数据库有一定的优势。
3. 使用非关系型数据库比关系型数据库好玩多了(雾
4. 之所以写了右边的Git部分,是因为原本打算利用docker技术搞一个持续集成和部署的程序,实现提交代码=>自动测试=>更新服务器部署更新=>客户端自动更新 这样一整套持续交付的流程,然而最后并没有时间写。

服务器层

服务器层,采用 Node.js 的 Express 框架作为客户端的 API 后台。因为 Node.js 的单线程异步并发结构使之可以轻松实现较高的 QPS,所以非常适合 API 后端这一特点。其框架设计和主要功能如下图所示:
服务端结构

像网关层:鉴权模块这么装逼的说法,本质也就是app.use(jwt({secret: config.jwt_secret}).unless({path: ['/signin']}));一行而已。因为是直接从毕业论文里拿下来的图,毕业论文都这尿性你们懂的,所以一些故弄玄虚敬请谅解。

客户端层

客户端层绝大部分是 React Native 代码,但是监控数据的图表生成这一块功能(如下图),由于 React Native 目前没有开源的成熟实现;试图通过 Native 代码来画图表,需要实现一个 Native 和 React Native 互相嵌套的架构,又面临一些可能的困难;故而最终选择了内嵌一个 html 页面,前端代码采用百度的 Echarts 框架来绘制图表。最终的结构就是大部分 React Native + 少部分 Html5 的客户端结构。

另外就是采用了 Redux 来统一应用的事件分发和 UI 数据管理了。可以说,React Native 若能留名青史,Redux 必定是不可或缺的一大原因。这一点我们后文再述。

细节详述

服务端层

服务端接口表:
服务端接口表

服务端程序的编写过程中,往往涉及到了大量的异步操作,如数据库读取,网络请求,JSON解析等等。而这些异步操作,又往往会因为具体的业务场景的要求,而需要保持一定的执行顺序。此外,还需要保证代码的可读性,显然此时一味嵌套回调函数,只会使我们陷入代码几乎不可读的回调地狱(Callback Hell)中。最后,由于JavaScript单线程的执行环境的特性,我们还需要避免指定不必要的执行顺序,以免降低了程序的运行性能。因此,我在项目中使用Promise模式来处理多异步的逻辑过程。如下代码所示:

这是生成指定N个传感器在一段时间内的折线图的逻辑。显然,剖析业务可知,我们需要在数据库中查询N次传感器,获得N个值对象数组,然后才能去用N组数据渲染出图表的HTML页面。 可以看到,外部核心的Promise控制的流程只集中于下面的几行之中:Promise.all(queryPromises()).then(renderGraph()).catch()。即,只有获取完N个传感器的数值之后,才会去渲染图表的HTML页面,但是这N个传感器的获取过程却又是并发进行的,由Promise.all()来实现的,合理地利用了有限的机器性能资源。
然而,推入queryPromises数组中的每个Promise对象,又构成了自己的一条Promise逻辑链,只有这些子Promise逻辑链被处理完了,才可以说整个all()函数都被执行完了。子Promise逻辑链大致地可以总结为以下形式:

其中的难点在于:

  1. 合理地切分整套业务逻辑到不同的then()函数中,且一个then()中只能有一个异步过程。
  2. 函数体内的异步过程所产生的新的Promise逻辑链必须被通过return的方式挂载到父函数的Promise逻辑链中,否则即可能形成一个有先有后的控制流程。
  3. catch()函数必须要做好捕捉和输出错误的处理,否则代码编写过程中的错误即不可能被发现,异步编程的整个过程也就无从继续下去了。
  4. 值得一提的是Promise模式的引入。Node.js 自身不带有Promise,可以引入标准的ECMAScript的Promise实现,然而其功能较为简陋,对于各种API的实现过于匮乏,因此最后选择了bluebird库来引入Promise模式的语言支持。

由此我们可以看到,没有无缘无故的高性能。Node.js 的高并发的优良表现,是用异步编程的高复杂度换来的。当然,你也可以选择不要编程复杂度,即不采用 Promise,Asnyc 等等异步编程模式,任由代码沦入回调地狱之中,那么这时候的代价就是维护复杂度了。其中取舍,见仁见智。

客户端层

客户端主要功能如下表所示:
功能设计表

接下来简单介绍下几个主要页面。可以发现 iOS 明显比 Android 要来的漂亮,因为只对 iOS 做了视觉上的细调,直接迁移到 Android 上,就会由于屏幕显示的色差问题,显得非常粗糙。所以,对于跨平台的 React Native App 来说,做两套色值配置文件,以供两个平台使用,还是很有必要的。

当前数据界面

上图即是土壤墒情底栏的当前数据页面,分别在Android和iOS上的显示效果,默认展示所有当前的传感器的数值,可以通过选择传感器种类或监测站编号进行筛选,两个条件可以分别设置,选定后再点击查找,即向服务器发起请求,得到数据后刷新页面。由于React Native 的组件化设计,刷新将只刷新下侧的DashBoard部分,且,若有上次已经渲染过的MonitorView,则会复用他们,不再重复渲染,从而实现了降低CPU占用的性能优化。MonitorView,即每一个传感器的展示小方块,自上至下依次展示了传感器种类,传感器编号,当前的传感器数值以及该传感器显示数值的单位。MonitorView和Dashboard均被抽象为一个一般化,可复用的组件,使之能够被利用在气象信息、病虫害监测之中,提升了开发效率,降低了代码的重复率。

查询历史界面
上图是土壤墒情界面的历史数据界面,分别在Android和iOS上的展示效果,默认不会显示数据,直到输入了传感器种类和监测站编号,选择了年月日时间后,再点击查找,才会得到结果并显示出来。该界面并非如同当前数据界面一样,Android和iOS代码完全共用。原因在于选择月日和选择时间的控件,Android和iOS系统有各自的控件,它们也被封装为React Native中不同的控件,因此,两条绿色的选择时间的按钮,被封装为HistoricalDateSelectPad,分别放在componentIOS和componentAndroid文件夹中。界面下侧的数据监测板,即代码中的Dashboard,是复用当前数据中的Dashboard。

图表界面

上图是土壤墒情界面的图表生成界面,分别在Android和iOS上的展示效果。时间选择界面,查找按钮,选择框,均可复用前两个界面的代码,因此无需多提。值得说的是,生成的折线图,事实上是通过内嵌的WebView来显示一个网页的。图表网页的生成,则依靠的百度Echarts 第三方库,然后服务端提供了一个预先写好的前端模板,Express框架填入需要的数据,最后下发到移动客户端上,渲染生成图表。图表支持了多曲线的删减,手指选取查看具体数据点,放大缩小等功能。

Screen Shot 2016-10-06 at 8.54.14 P

上图则是实际项目应用中的Redux相关文件的结构。stores中存放全局数据store相关的实现。

actions中则存放根据模块切割开的各类action生成函数集合。在 Redux 中,改变 State 只能通过 action。并且,每一个 action 都必须是 Javascript Plain Object。事实上,创建 action 对象很少用这种每次直接声明对象的方式,更多地是通过一个创建函数。这个函数被称为Action Creator。

reducers中存放许多reducer的实现,其中RootReducer是根文件,它负责把其他reducer拼接为一整个reducer,而reducer就是根据 action 的语义来完成 State 变更的函数。Reducer 的执行是同步的。在给定 initState 以及一系列的 actions,无论在什么时间,重复执行多少次 Reducer,都应该得到相同的 newState。

性能测试

服务端

测试工具:OS X Activity Monitor
http_load
serve

客户端

iOS

测试工具:Xcode 7.3
iOS

Android

测试工具:Android Studio 1.2.0
Android

代码量相关

code

简单总结

React Native 尽管在开发上具有这样那样的坑,但是因其天生的跨平台,和优于 Html5的移动性能表现,使得他在写一些不太复杂的 App 的时候,开发速度非常快,自带两倍 buff。

非全屏 Weex 页面开发中的 Android 适配

weex代码中的高度和宽度的单位均为px,然而,在手机屏幕上显示的高宽却不一定与代码指定的相同。原因是weex框架在底层做了针对不同屏幕的适配工作,具体计算公式为 实际高宽 = 代码高宽 * (屏幕宽度 / 750)。

举个例子,假设代码中是这么写的:

那么,在一款屏幕分辨率为1920*1280的Android手机上,此时的计算过程为:
height: 100 * (1080 / 750) = 144;
width: 200 * (1080 / 750) = 288。

如果我们开发的weex页面是全屏幕的,那么这个高宽的转换过程对我们而言是透明的,无需做额外的工作。然而一旦有一个业务场景,weex容器并非是全屏幕的,而是需要从外部传入weex容器的高度,那么,就不得不考虑这个转换的过程。

举一个我在开发weex弹窗时的例子。该weex弹窗的样式如下:

weex-blog

可以看到,如果不考虑多屏幕适配,顶栏和底栏都是一个固定值,那么只需要用总容器高度 – 两个定高组件就可以了。那么需要解决的第一个问题,就是如何获取外部容器的高度。由于weex可以通过$getConfig().env.deviceHeight$getConfig().env.deviceWidth的形式来获取手机屏幕的高度,因而,很自然地就想到,是否能在安卓中以屏幕的3/5的比例,约定容器高度,然后在weex代码中,同样通过3/5来计算容器高度。这样就避免了去写 Native Module 和 Method。

然而,这样的思路是不可行的。因为Android Native的总高度,事实上是可供显示的全屏高度,而不一定是物理屏幕的高度,因为有状态栏,虚拟按键栏,Smartbar等等安卓碎片化引入的额外显示元素,实际全屏高度很有可能小于物理屏幕高度。所以,仍然需要开发和注册Native Module,以获取外部容器高度。

再来看上文的计算公式:总容器高度 – 两个定高 = scroller高度。因为多屏幕适配的原因,上面的公式是不可行的,需要改为:

外部传入的总容器高度 – 两个定高组件的高度字面量 * 转换比例 = scroller实际高度

也就是说:外部传入的总容器高度 / 转换比例 – 两个定高组件的高度字面量 = scroller实际高度 / 转换比例 = scroller的字面量高度。

所以,最终的业务代码如下所示:

这个坑非常的隐蔽,本质是因为:weex 默默做了A参考系转换到B参考系的过程,然而一旦我们自力更生,试图从B参考系获得一个测量得到的高度,用在A参考系,而没意识到这个隐蔽的转换过程的时候,就会陷入到一台机子上调好了,另一台又跪了的尴尬局面。而且,这种情况在Android上远较iOS要来的严重。因为iOS上,除了4S以外,5,5s,6,6p,6s,6sp,屏幕尺寸均为同一长宽比。因此,在一台上调整好后,可无缝等比例放大到其他机型上。然而在Android上,毋论碎片化的屏幕尺寸,光status bar,navigation bar,smartbar等等虚拟的占用实际显示区域的各类bar,就足够让weex的默认适配喝一壶的。因此,weex这种隐蔽适配的处理方式,在Android生态上是否真的合理方便,尚待商榷。

React Native 蛮荒开发生存指南

引言

React Native,在过去一年大红大紫,一下子成为了许多人追捧的新兴技术热点。然而,除却蜻蜓点水般运行一个 Hello World 式的 React Native 小 App,真正想要用 React Native 写一个商用的 App,却要面临很多困难。其中最主要的,就是缺少技术资料,缺少像 Android 这样发展七八年的技术一般,在博客和技术社区上存留的大量的技术资料。因而,面向百度编程,面向 Google 编程,面向 StackOverFlow 编程这三大杀手锏,均对 React Native 开发中遇到困惑表示无可奈何。加之 Facebook 开发组的文档更新速度远远跟不上开发的速度,使得 React Native 的工程化应用之路,恍若在蛮荒生存。笔者不才,为某一商业项目开发 React Native App 已近半年,以自己的踩坑和爬坑经验撰写此文,取名蛮荒开发生存指南。

文档

1. 立足 React Native 英文文档,参考中文翻译的文档。再简陋的文档好歹也是文档。

官方英文文档地址在这里。官方文档需要注意的是,左上角有一个蓝色的版本号,点击可以翻阅过去版本号的文档。文档中有写到的技术点肯定都已经在这个版本实现了,但文档没写的技术点,则有可能也实现了,只是没写上去。

目前找到的 React Native 中文文档有两份:一份是 React Native 中文网的,另一份是极客学院上的。前者一直在持续更新,后者似乎已经很久没有更新了。可在拿不准英文文档意思的时候作为参考。

2. React 文档同样重要。

由于 React Native 实质上是 React.js 的开发思想在移动端的实现,因此,许多如 Component, Props, State, flux 等等概念,在 React Native 的文档中均没有提及,相反在 React 的文档中有着详细的讲解。关于 React.js 和 React Native 之间的关系,知乎上这篇回答讲的鞭辟入里,值得深思。React 官方英文文档地址在这里,国内志愿者翻译的中文文档在这里

3. 搜索文档的方法
由于上述文档中除 React Native 文档以外,其他文档均无配置文档搜索框。因此,有必要使用 Google 或 Baidu 加上 site:url 来全局搜索文档。

社区

4. 像对待官方文档一样认真阅读 React Native 所有版本的 release note。
React Native 文档更新速度缓慢,且不能保证覆盖所有的 feature。与之相反,release note 则会告知你新的 feature 和 bug fix,虽然很多只有一句话,但是真正有帮助的是,release note 会给出相关的 commit 链接,从而我们可以阅读代码和注释,以此来了解该 feature 或 bug fix 的内容。但是同理,总会有一些改动没有统计到 release note 中。深深的怨念…

5. 不要对 StackOverFlow 抱太大期望。
截至目前,StackOverFlow 上 React Native 相关的,且得到了满意回答的问题寥寥无几,而且大多集中于 React Native 开发环境搭建等入门踩坑问题上。这种情况是完全可以预料到的,因为从本质来说,类似 StackOverFlow 这种问答社区的优质问答积累需要漫长的时间,何况技术的细节无穷无尽,非数年之功不能处处兼顾。这是针对搜索现有问答来说,StackOverFlow 无法满足 React Native 的开发问题。同理,如果自己发帖提问,同样不能保证快速地得到满意的解答,我认为最关键的问题在于,现有的 React Native 开发者的活跃社区,不在 StackOverFlow,而在 Github 上。因此,引出第六条指南。

6. 在 Github issue 中搜索出现的问题的关键字。
react native 的Github Issue中的问题和解决极为丰富,迄今已有4000多条 issue,与 StackOverFlow 判若云泥。然而由于其是论坛的性质,因此需要耐心阅读英文对话内容,才可能找到解决的方法。此外,如不能找到相关内容,另开一个 issue 寻求帮助也不失为一种良策。

6. 遇到无法解决的问题,就升级 React Native 版本。
虽然很无脑,但的确有时候很有奇效。比如笔者在实现 React Native 内嵌 WebView 时,React Native for Android v0.18.0中 WebView 的 javaScriptEnabled 属性即便设置为 true 也依然无效。升级到 v0.22.0 即解决该问题。然而,从v0.19.0 到 v0.22.0 的 release note 对该 bug fix 根本没提 T_T。

7. 阅读源代码,是求生的最后工具。
8. 源代码中的注释往往透露了非常关键的信息。
与文档相比,React Native 的源代码结构非常清晰,代码风格干净,其注释也往往包含了使用的说明,而这些说明又往往是文档中未曾包含的。因此,阅读源代码,不失为无计可施情况下的一种解决方法。拙作初窥基于 react-art 库的 React Native SVG
即是通过阅读 React Native 源代码而有所收获的。

工具

8. 快速运行他人代码的神器 – iOS RNPlayNative
https://rnplay.org 实现了令人惊叹的 React Native 实时运行的效果,即,你可以在网页上输入 React Native 代码,然后在网页上的模拟器中直接运行代码。你也可以在 iPhone 上安装RNPlayNative 应用,扫描网站上的二维码,然后直接就可以在自己的 iPhone 上运行该代码了,完全免除了 NPM 的依赖下载 和 Xcode 编译的冗长时间。此外,该网站还提供了 React Native 框架版本的切换,Amazing!

rnplay 可以帮助 React Native 开发者快速地运行和体验他人的代码,同时也可以用于排除自身环境的错误,还可以用于快速排除旧版本引入的 bug。如此神器,然而国内却很少有人知道,希望经笔者介绍后,能被更多的 React Native 开发者所用。

和 Web 一样的代码部署速度,却有着远超 Web 的流畅手感,既让人感到不可思议,仔细想想 React Native 的早已宣传的快速部署特性,却又在情理之中。只能感慨老外们应用新技术的速度太快了。

9. react-native-logcat
一个开源的 React Native Android Log输出工具,免去了繁杂的adb命令。

调试

10. 注释调试法:虽然很 Low 但是很有效。
这里不得不提一个 React Native 在捕捉错误上的一个缺陷。如果错误是在 ComponentDidMount 之前出现的,那么 backtrace 上只会有一堆神神叨叨的 React Native 库函数,完全无法定位到你的代码中,即便只是一些小语法错误。

那么此时,除了肉眼复查代码,唯一的方法也就是注释调试了。逐行注释掉新加入的代码,观察 bug 是否会复现。

探究 React Native 中 Props 驱动的 SVG 动画和 Value 驱动动画

引言

一直以来,动画都是移动开发中极为特殊的一块。一方面,动画在交互体验上有着不可替代的优越处,然而另一方面,动画的开发又极为的耗时,需要消耗工程师大量的时间用于开发和调试。再来看前端,前端的动画实现,经过多年的发展,已分为 CSS3 动画和 JavaScript 动画。

React Native 作为一个复用前端思想的移动开发框架,并没有完整实现CSS,而是使用JavaScript来给应用添加样式。这是一个有争议的决定,可以参考这个幻灯片来了解 Facebook 做的理由。自然,在动画上,因为缺少大量的 CSS 属性,React Naive 中的动画均为 JavaScript 动画,即通过 JavaScript 代码控制图像的各种参数值的变化,从而产生时间轴上的动画效果。

React Native 的官方文档已经详细地介绍了 React Native 一般动画的使用方法和实例,在此不再赘述。然而阅读官方文档后可知,官方的动画往往是给一个完整的物体添加各种动画效果,如透明度,翻转,移动等等。但是对于物体的自身变化,比如如下这个进度条,明显是在旋转的同时也在伸缩,则缺乏必要的实现方法。这是因为,动画的本质既是图形的各种参数的数值变化的过程,文档中的 Animated.Value 就是用作被驱动的参数,可以,想要让一个圆环能够伸缩,就必须让数值变化的过程,深入到图形生成的过程中,而不是如官方文档的例子一样,仅仅是施加于图形生成完毕后的过程,那么也就无法实现改变图形自身的动画效果了。

拙作初窥基于 react-art 库的 React Native SVG已讨论了 React Native 中静态 SVG 的开发方法,本文则致力于探究 React Native 中 SVG 与 Animation 结合所实现的 SVG 动画。也就是可以改变图形自身的动画效果。此外还探究了 Value 驱动动画在实现方法上的不同之处。

Props 驱动的 SVG 动画

本节即以实现一个下图所示的旋转的进度条的例子,讲述 React Native SVG 动画的开发方法。

rotating-wedge

Wedge.art.js 位于 react-art 库下 lib/ 文件夹内,提供了 SVG 扇形的实现,然而缺乏对 cx, cy 属性的支持。另外拙作之前也提到了,Wedge中的扇形较为诡异,只有一条半径,为了实现进度条效果我把另一条半径也去掉了。我将 Wedge.art.js 拷贝到工程中,自行小修改后的代码如下。

然后就是实现的主体。其中值得关注的点是:

  1. 并非任何 Component 都可以直接用 Animated.Value 去赋值 Props,而需要对 Component 做一定的改造。AnimatedAnimated.createAnimatedComponent(Component component),是 Animated 库提供的用于把普通 Component 改造为 AnimatedComponent 的函数。阅读 React Native 源代码会发现,Animated.Text, Animated.View, Animated.Image,都是直接调用了该函数去改造系统已有的组件,如Animated.createAnimatedComponent(React.Text)
  2. Easing 库较为隐蔽,明明在react-native/Library/Animated/路径下,却又需要从React中直接引出。它为动画的实现提供了许多缓动函数,可根据实际需求选择。如 linear() 线性,quad() 二次(quad明明是四次方的意思,为毛代码实现是t*t….),cubic() 三次等等。官方文档中吹嘘 Easing 中提供了 tons of functions(成吨的函数),然而我数过了明明才14个,233333。
  3. 该动画由起始角度和终止角度两个变化的参数来控制,因此,两个Animated.Value需要同时启动,这涉及到了动画的组合问题。React Native 为此提供了 parallelsequencestaggerdelay 四个函数。其主要实现均可在react-native/Library/Animated/Animate中找到,官方文档中亦有说明。这里用的是Animated.parallel

开发中遇到的问题有:

  1. 该动画在 Android 上可以运行,但是刷新频率看上去只有两帧,无法形成一个自然过渡的动画,笔者怀疑是 React Native Android 对 SVG 的支持仍有缺陷。
  2. SVG 图形和普通 React Native View 的叠加问题,目前我还没有找到解决方法。感觉只能等 React Native 开发组的进一步支持。
  3. 动画播放总会有一个莫名其妙的下拉回弹效果,然而代码上没有任何额外的控制。

Value 驱动的动画

接下来看 Value 驱动的 SVG 动画。先解释一下 Value 和 Props 的区别。<Text color='black'></Text>,这里的 color 就是 Props,<Text>black</Text>这里的 black 就是 value。

为什么要特意强调这一点呢,如果我们想要做一个如下图所示的从1到20变动的数字,按照上节所述的方法,直接调用 Animated.createAnimatedComponent(React.Text)所生成的 Component ,然后给 Value 赋值一个Animated.Value(),然后Animated.timing…,是无法产生这样的效果的。

rising-number

必须要对库中的createAnimatedComponent()函数做一定的改造。改造后的函数如下:

为了获取必须要用到的AnimatedProps,笔者甚至违背了道德的约束,访问了双下划线前缀的变量Animated.__PropsOnlyForTests,真是罪恶啊XD。

言归正传,重要的修改有:

  1. 修改了 attachProps 函数。对于任何变动的 props,原来的代码会试图使用 setNativeProps 函数进行更新,若 setNativeProps 函数为空,才会使用 forceUpdate() 函数。对于 props,setNativeProps 函数是可行的,然而对 value 无效。我猜测,setNativeProps 方法在 Android 底层可能就是 setColor() 类似的 Java 方法,然而并没有得到实证。目前这种 forceUpdate,由注释知,是彻底更新了整个 Component,相当于先从 DOM 树上取下一个旧节点,再放上一个新节点,在性能的利用上较为浪费。
  2. 使用 PropTypes.xxx.isRequired 来进行参数的类型检查。PropTypes 检查支持的类型可在 react-native/node_modules/react/lib/ReactPropTypes.js 中看到,在此不再赘述。
  3. Animated.value() 从1到20变化的过程是一个随机采样的过程,并不一定会卡在整数值上,因此还需要做一些小处理。

值得注意的是,该动画在 Android 上虽然可以正常运行,但也存在丢帧的问题,远远不能如 iOS 上流畅自然。对于这一点,只能等待 Facebook 的进一步优化。

全部的代码如下: