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 了,就会觉得,节操更低了,但也更强大了。因此,时刻怀疑自己的底层,是否真的足够底层了,也许再低一点,又会有新的突破。

解决OS X 10.11上Ducky 2108s机械键盘无响应的问题

我的Macbook Pro mid 2013在升级到OS X 10.11后,ducky 2108s插上去后无反应。在StackExchange中找到了该问题的解决方法,总结如下:

  1. 怀疑OS X 10.11进一步削弱了USB口的供电能力,致使部分机械键盘可能会因为供电不足而没有反应。笔者在之前版本的OS X的使用中也遇到过,机械键盘用着用着可能就突然没有响应了,通常都是重启解决。解决办法:购买独立供电的USB Hub,出门左拐万能的淘宝
  2. ducky 2108和2108s两款机械键盘都在OS X下无法使用,无论是否使用了外接供电的USB Hub。联系support@duckychannel.com.tw 后得知,需要刷新键盘的固件。刷新只能在Windows下进行,但刷新完成后,键盘即可在OS X上正常使用。本文后附固件文件(仅适配2108和2108s)、固件刷新程序和固件刷新帮助手册。其他版本固件请联系 support@duckychannel.com.tw 。

点击下载 ducky_2108_firmware_and_tool.zip

点击下载 firmware update流程Chinese.pdf