ReactNative/BLE 的使用体会

我们从15年底开始使用 RN,版本号为0.16,到2月底为止,我们完成了三次版本的升级,最新版本为0.35。不得不说升级 RN 真是一件很费时费力的事情,期间版本变动较大。

我下面说一下我和我的团队在使用 RN 的一些问题,很可能我要说的这么问题,现在已经被解决啦。

基础知识

React 的生命周期

RN 中的每一个控件都是单独的,都有自己的生命周期,如图所示:

如图,可以把组件生命周期大致分为三个阶段:

  • 第一阶段:是组件第一次绘制阶段,如图中的上面虚线框内,在这里完成了组件的加载和初始化;
  • 第二阶段:是组件在运行和交互阶段,如图中左下角虚线框,这个阶段组件可以处理用户交互,或者接收事件更新界面;
  • 第三阶段:是组件卸载消亡的阶段,如图中右下角的虚线框中,这里做一些组件的清理工作。

生命周期回调函数

下面来详细介绍生命周期中的各回调函数。

getDefaultProps

在组件创建之前,会先调用 getDefaultProps(),这是全局调用一次,严格地来说,这不是组件的生命周期的一部分。在组件被创建并加载候,首先调用 getInitialState(),来初始化组件的状态。

componentWillMount

然后,准备加载组件,会调用 componentWillMount(),其原型如下:

1
void componentWillMount()

这个函数调用时机是在组件创建,并初始化了状态之后,在第一次绘制 render() 之前。可以在这里做一些业务初始化操作,也可以设置组件状态。这个函数在整个生命周期中只被调用一次。

componentDidMount

在组件第一次绘制之后,会调用 componentDidMount(),通知组件已经加载完成。函数原型如下:

1
void componentDidMount()

这个函数调用的时候,其虚拟 DOM 已经构建完成,你可以在这个函数开始获取其中的元素或者子组件了。需要注意的是,RN 框架是先调用子组件的 componentDidMount(),然后调用父组件的函数。从这个函数开始,就可以和 JS 其他框架交互了,例如设置计时 setTimeout 或者 setInterval,或者发起网络请求。这个函数也是只被调用一次。这个函数之后,就进入了稳定运行状态,等待事件触发。

componentWillReceiveProps

如果组件收到新的属性(props),就会调用 componentWillReceiveProps(),其原型如下:

1
2
3
void componentWillReceiveProps(
object nextProps
)

输入参数 nextProps 是即将被设置的属性,旧的属性还是可以通过 this.props 来获取。在这个回调函数里面,你可以根据属性的变化,通过调用 this.setState() 来更新你的组件状态,这里调用更新状态是安全的,并不会触发额外的 render() 调用。如下:

1
2
3
4
5
componentWillReceiveProps: function(nextProps) {
this.setState({
likesIncreasing: nextProps.likeCount > this.props.likeCount
});
}

shouldComponentUpdate

当组件接收到新的属性和状态改变的话,都会触发调用 shouldComponentUpdate(...),函数原型如下:

1
2
3
boolean shouldComponentUpdate(
object nextProps, object nextState
)

输入参数 nextProps 和上面的 componentWillReceiveProps 函数一样,nextState 表示组件即将更新的状态值。这个函数的返回值决定是否需要更新组件,如果 true 表示需要更新,继续走后面的更新流程。否者,则不更新,直接进入等待状态。

默认情况下,这个函数永远返回 true 用来保证数据变化的时候 UI 能够同步更新。在大型项目中,你可以自己重载这个函数,通过检查变化前后属性和状态,来决定 UI 是否需要更新,能有效提高应用性能。

componentWillUpdate

如果组件状态或者属性改变,并且上面的 shouldComponentUpdate(...) 返回为 true,就会开始准更新组件,并调用 componentWillUpdate(),其函数原型如下:

1
2
3
void componentWillUpdate(
object nextProps, object nextState
)

输入参数与 shouldComponentUpdate 一样,在这个回调中,可以做一些在更新界面之前要做的事情。需要特别注意的是,在这个函数里面,你就不能使用 this.setState 来修改状态。这个函数调用之后,就会把 nextPropsnextState 分别设置到 this.propsthis.state 中。紧接着这个函数,就会调用 render() 来更新界面了。

componentDidUpdate

调用了 render() 更新完成界面之后,会调用 componentDidUpdate() 来得到通知,其函数原型如下:

1
2
3
void componentDidUpdate(
object prevProps, object prevState
)

因为到这里已经完成了属性和状态的更新了,此函数的输入参数变成了 prevPropsprevState

componentWillUnmount

当组件要被从界面上移除的时候,就会调用 componentWillUnmount(),其函数原型如下:

1
void componentWillUnmount()

在这个函数中,可以做一些组件相关的清理工作,例如取消计时器、网络请求等。

总结一下:

到这里,RN 的组件的完整的生命都介绍完了,在回头来看一下前面的图,就比较清晰了,把生命周期的回调函数总结成如下表格:

生命周期 调用次数 能否使用 setSate()
getDefaultProps 1(全局调用一次)
getInitialState 1
componentWillMount 1
render >=1
componentDidMount 1
componentWillReceiveProps >=0
shouldComponentUpdate >=0
componentWillUpdate >=0
componentDidUpdate >=0
componentWillUnmount 1

父子控件

父子控件之间传值,如果是父控件给子空间传值,那么就利用子控件的props值;如果子空间向父控件传值,我想到的是 事件分发,这可是一项很重要的技能,我们的不同控件传值主要用的就是它。

State/Props

每一个控件都有自己的 state 和 props 值,我们利用不同 state 值去实现页面的变化,利用 props 值去执行特定的操作。

ReactNative 的优缺点

优点

  • 跨平台特性,一套代码可以实现两个平台的效果
  • 原生的用户体验,这是它可以击败 H5 的地方。
  • 开发静态页面效率高
  • 网络请求封装的很好,fetch 、promise 使用起来很方便

缺点

  • 不稳定,还处于完善的阶段。
  • 调试Debug时,提示错误比较晦涩难懂。

下面讲讲我在利用 RN 开发应用的一下体会,

  1. ListView 的多状态显示

    现在可能已经不是问题了,但当时对我来说确实遇到困难了,这是我请示大神给予我的方案:将不同状态的元素放在不同的数组里,针对不同的数组去选择不同的 item 去渲染,这样就实现了不同的状态显示不同的item 的目的。

  2. 生命周期和数组的处理

    我在开发统计分享界面的时候,需要加载三个数组,每一个数组的条目都很多,大约30条左右吧,最初做这个界面的时候,就直接 Var 三个变量,这样测试在测试的时候发现,不停的进进出出这个页面,应用程序会卡顿;最后我们分析可能是数组的加载导致的。我们的方式是:(1)将数组放在组件的内部去加载,同时在组件被卸载之后呢,将数组晴空;(2) 将绘图的时机进行调整,当有状态值发生改变时才会去重画图片。

  3. 自定义 SliderBar

    刚开始使用RN 时,并没有 sliderBar 这个控件,但实际项目中还需要有这么一个控件,只能自定义啦。我们利用 PanResponse 的特性,获取手指点击区域的 XY value 来改变 View 的 width 属性实现动态的延展和缩放。

  4. 数据不一致的队列处理

    我们的app 在使用中需要于硬件设备进行交互,有时候我发给设备的值,设备并没有执行,这就出现了页面显示数据和设备数据不一致的情况。我们想到的办法是:利用队列。将要传输的数据放到队列中,逐次向下传输,同时设备会把它现在的值进行上报,我们会对队列的最后一个数据和设备的数据进行比较,如果不一致,再重新发一次命令;如果一致,将队列晴空。在多说一句,队列需要自己用 js 去实现。

  5. Map 处理不同设备数据

    在开发过程中,某一个功能需要针对不同的设备设置不同的值,这个时候就需要 Map 的属性,还好 ES6 为我们提供了 Map,不需要我们自己去实现。

  6. 水波纹动画

    我们原本想用前后两张图片朝着同一个方法移动的方式去实现,但是实现之后发现问题较多,最后我们利用 ART 控件去画了正弦曲线,效果很好,详细的原理单独找一篇去写。

RN 在两个平台的差异性

  • flex 属性实现

    ios 针对 View 必须加上 Width 属性才能实现,Android 不需要。

  • Text 背景色

    如果不设置背景色,Android默认是透明色,ios 默认是白色,ios 需要添加 backgroundColor 属性。

  • ListView 控件

    ios需添加removeClippedSubviews={false}属性

BlE

我确实弄了一年多的低功耗蓝牙开发,但也仅仅限于应用层开发,没有深入到底层,我就说一下我在应用层开发遇到的问题。

  1. 关闭手机蓝牙,开启时需要执行一次搜索功能。主要针对三星设备
  2. 自动连接功能的实现,再执行连接之前先执行一次搜索的操作,如果搜索到设备就发起连接,否则不发起连接。
  3. 在连接的过程中,如果发生 133 错误,这个问题主要是手机本身蓝牙出现了问题。针对这个问题,我们做了多次连接的处理,当出现在这个状态码时,执行3次连接,如果3次连接还是没有成功,那么关闭 Gatt 端。
  4. 进行DFU 升级时,如果发生失败,需要调用 refreshGatt(…) 方法来清空 Gatt 的缓存,否则升级失败的可能性很大
  5. 搜索的时候,我们可以将公司的 UUID 放在一个 Bundle 里做一个集合,但是这种方式在 三星 S6 上出现了无法搜索到设备情况,所以暂时停滞了,但确实很简便。
  6. 针对哪些在设备连接成功之后需要发送的命令,我们可以用一个单独的线程去执行命令的发送。这个线程的生命周期和应用一致。

总结

最后写一下,自己这一年半的感悟吧,我的头的一句话还是挺触发我的,如下:

你完成的只是半成品

选择一个点深挖进去

逻辑的提升才是最重要的