7

京东APP收银台Kotlin化实践

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzUyMDAxMjQ3Ng%3D%3D&%3Bmid=2247495587&%3Bidx=1&%3Bsn=c46679215b33dde14ace8de2e6d83af8
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我们把移动端App分为四大类

  • React/Flutter App

  • Web App(纯网页)

  • Native App(纯原生App)

  • Hybrid app (混合App)

o 多View混合型:Native View与WebView交替出现的场景

o 单View混合型:在同一个View内,同时包括Native View和Web View

o Web主体型:移动应用的主体是WebView

过去收银台模块采用单View混合型,我们称之为H5收银台。H5收银台开发快,一次开发,iOS和Android两端通用。在享受开发便利的同时长期使用过程中发现:

  • H5收银台首屏加载时间长,大多数时候超过1秒以上;

  • 弹窗动画生硬,用户体验不够友好;

  • 技术栈链路过长:JDWebView容器,H5前端页面,原生页面和JDWebView统一控件交互,原生和H5页面交互,定位问题、排查问题相对耗时长;

  • 从占用手机大量内存的页面比如游戏网页跳转到收银台时,配置低的手机存在大概率性的黑屏或者白屏问题。

瓶颈是WebView,基于上述认识我们需要对业务较稳定的收银台首页去掉WebView,改成纯原生页面。改造后的架构图:

vyyaMjZ.png!mobile

在原生化改造过程中我们面临一个选择:是继续使用Java还是选用Kotlin,收银台团队认为Kotlin是Andrioid开发的未来,谷歌的全力支持和未来丰富的语言生态让我们有理由相信Kotlin在移动端的远大且光明的前景。

接着来聊聊Kotlin和我们对Kotlin的实践,本文将从下面两部分展开:

  • 对Kotlin的理解

  • Kotlin在收银台里的具体实践

编程语言的时空观

想对Kotlin有全面、深刻的理解,还得从语言的源头入手,溯洄从之,一路探究。纵观过去100年,编程语言经历了三大阶段,分别是机器语言,汇编语言和高级语言。Kotlin隶属于高级语言,从高级语言这一阶段出发来看看编程语言的发展历程:

eumam2q.png!mobile

摘自:Most Popular Programming Languages 1965 - 2019 by youtube

上图为最受欢迎的高级语言的变化过程。 高级语言从20世纪50年代到1983年为早期孕育阶段,按时间先后顺序发展出面向过程的结构化设计,面向对象的分析与设计,函数式编程范式等。随着语言的发展,原本常用的“面向对象”和“函数式”的边界变得越来越模糊。Kotlin于2011年问世,并在2017年得到谷歌官方支持,开始作为Android开发语言。

找到了Kotlin在时间轴线上的位置,Kotlin和其他语言的横向比较的位置在哪儿呢?

根据运行时是否允许隐式变量类型转换,把语言分为强类型和弱类型。Kotlin是强类型语言(隐式类型转换:不需要用户干预,编译器私下进行的类型转换行为)

根据对类型的检测时机是在编译期还是运行期,把语言分为静态语言和动态语言;Kotlin是静态语言。

根据程序的源文件被运行前是否需要提前转化为机器码,把语言分为编译型和解释型;Kotlin和Java类似,需要编译成字节码的解释型语言。

依据动态性和类型强度可以建立一个直角坐标系,如下图所示:沿着X轴正方向,静态性越来越强;沿着Y轴正方向,类型强度越来越强。

ay2mauf.png!mobile

摘自网络

随着时间流逝,动态语言加入了静态能力,静态语言加入了动态能力,静态和动态正在相互靠拢,未来的编程语言属于这两者的杂交产物。Kotlin作为JVM语言,它落在第一象限,动态性比Java强,类型强度比Java弱。

小结:

于是对Kotlin的基本的认识便有了:Kotlin是现代化的编译型、强类型、静态语言。Kotlin相比于Java,更动态性,符合编程语言发展的趋势。上述所展示的Kotlin特点最终是通过语言特性得以落实。

kotlin的语言特性

语言特性分为通用特性,面向过程的特性,面向对象的特性,函数式的特性。

通用的特性,比如:

  • 变量

  • 作用域

  • ...

面向过程的特性,比如:

  • 流程控制:条件语句,分支语句,循环语句

  • 基本数据类型

  • 数据容器(数组&集合)

  • 方法定义与调用

  • 访问权限

  • ...

面向对象的特性,比如:

  • 类和类层次结构

  • 对象和类型

  • 封装、继承、多态、抽象

  • 接口&抽象方法

  • 混合mixin

  • 并发

  • 异常处理

  • 垃圾回收

  • 递归

  • ...

函数式的特性,比如:

  • 引用透明

  • 高阶函数

  • lambda表达式

  • 模式匹配

  • Monad结构

  • 柯里化

  • 不变性&可变性

  • 闭包

  • ...

Koltin和Java在语言特性上的差异

Kotlin独有的特性有:data类,sealed类,解构,中缀表达,访问范围,操作符重载,主从构造器,内部类/嵌套类,属性访问器(内置setter和getter方法),属性延迟初始化,类型继承体系。如下:

  • 增加类型继承体系

  • 增加默认参数、可变参数

  • 强化了不可变性

  • 强化了空安全

  • 强化了泛型

  • 强化了函数/lambda表达

  • 去掉了可受检异常

  • 增加了运算符重载

  • 修改了权限访问范围

  • 把最佳实践融入到语法

可以看到Kotlin和Java特性差异还是挺多的,这里举两个方面说一说:函数式和类型。

函数式

Kotlin较Java一个很大的特性差异便是对函数式的支持大大提高了。我们知道函数式基本元素有:

  • 不可变性

  • 引用透明

  • 无副作用

  • 高阶函数与lambda表达式

  • Monad结构

  • 柯里化

  • 模式匹配

  • 智能类型推断

  • 递归

  • 并发安全

函数式编程有如下等式:

bUbmuqi.png!mobile

程序 = 可变性程序 + 不可变性程序

可变性程序 = 对象 + 依赖关系

不可变程序 = 纯函数 + 组合 (Monad结构)

Kotlin的语言特性对此都做了支持,相比于Java在组件化和响应式上Kotlin更加简洁、直观。

类型差异

Kotlin和Java类型上的差异,有类型声明差异和类型体系差异。

类型声明差异

val a: String = "I am Kotlin"
val a2 = "I am Kotlin"

这与函数定义时返回类型,类继承,接口实现提供了一致的书写体验

fun sum(x: Int, y: Int): Int {
return x + y
}

Java是类型前置的写法,定义方法时返回类型写在了前面,但是继承和实现是后置的。类型后置的好处:通过类型推导实现类型省略时一致的书写体验;Kotlin做到了三种场景符号一致,书写一致(类型推导)

类型体系差异

下图是Kotlin类型体系,Kotlin把基本数据类型统一成对象类型,形成了面向对象的继承体系。

ABBjUr.png!mobile

Kotlin的瑕疵

kotlin的语言特性丰富而有力,带来了与Java的特性差异。任何事物都有正、反两面,kotlin也不例外。

1)多维数组需要通过嵌套的方式创建

val bytes = Array(3) { ByteArray(4) }
val bytes1 = Array(3) { Array(4) { ByteArray(5) } }

对比一下Java

byte[][] bytes = new byte[3][4];
byte[][][] bytes1 = new byte[3][4][5];

相比Kotlin的,Java清晰简便多了

2)Kotlin没法实现的接口样子

这样的Java接口

interface Itest{
Object get(int index);
Object get(Integer index);
}

在Kotlin里实现该接口,class A会因为实现了两个相同签名方法而报错

class A : Itest{
override fun get(index: Int): Any {
}
override fun get(index: Int?): Any {
}
}

3)抛弃了受检异常(checked exception)

这是颇有争议的瑕疵,有坏处也有好处。

坏处: 对异常不强制要求处理。有时候调用一些方法,特别涉及到硬件或者网络相关的方法,往往不一定知道它可能会抛出什么异常,或者根本不知道它会抛出异常。便会在这块地方遗漏了某些异常的处理或者没做异常处理,埋下一些潜在的问题。

好处: Kotlin不区分checked exception,这样能简化代码书写,符合Kotlin一贯的简洁设计理念。因此Kotlin是没有受检异常的。

Kotlin在收银台具体实践

Kotlin语言所拥有的特性为面向过程特性、面向对象特性和函数式特性的部分总和。从V9.2.2版本开始,安卓收银台模块开始全面使用Kotlin语言。为了稳定,我们采用了Java和Kotlin混合开发的模式。

01

与Java的互操作

先来看看Kotlin和Java的互操作。

1)Kotlin调用Java的代码

几乎和Java调用Java代码相同,有几个不相同的点如下:

1.属性前缀

示意代码:

public final class User {
public String getName() { /* … */ }
public boolean isActive() { /* … */ }
public void setName(String name) { /* … */ }
}
val name = user.name // Invokes user.getName()
val active = user.active // Invokes user.isActive()
user.name = "Bob" // Invokes user.setName(String)

2.平台类型

kotlin调用Java代码后返回的类型在Kotlin侧叫做平台类型。平台类型既可当作可空类型也可作为非空类型。换句话说,kotlin编译时,平台类型被认为是非空类型,不需要非空判断顺利编译通过,在运行时被认为是可空类型。平台类型可能触发空指针异常。收银台空安全实践避免了这种空指针,详见后文内容。

2)Java调用Kotlin的代码

在kotlin代码上增加Kotlin注解,Java调用Kotlin便能像Java调用Java代码般

1)@JvmOverloads 默认参数重载

@JvmOverloads
fun setText(textPair: TriggerTextView, moneyFlag: String = ""){
setText(textPair, moneyFlag, true)
}
public final void setText(@NotNull TriggerTextView textPair) {
setText$default(this, textPair, (String)null, 2, (Object)null);
}

public final void setText(@NotNull TriggerTextView textPair, @NotNull String moneyFlag) {
Intrinsics.checkParameterIsNotNull(textPair, "textPair");
Intrinsics.checkParameterIsNotNull(moneyFlag, "moneyFlag");
this.setText(textPair, moneyFlag, true);
}

引入默认参数重载的注解后,只需要一次方法定义就够满足收银台对底部支付文案的内容更新,减少了方法定义的模版代码。

2)@JvmStatic 静态

object Updater {
private var tempSourceList: ArrayList<Entity>? = null




@JvmStatic
fun of(block: () -> ArrayList<Entity>): Updater {
onDestroy()
tempSourceList = block()
return this
}
}

收银台是Java和Kotlin语言混合开发,通过这个注解,原来的Java代码调用Kotlin写的of方法就能成为我们熟知的Updater.of的样子。

3)在接口层面,Java使用Kotlin接口也做到了打通;

Java的接口 <---> Kotlin的接口 <---> Kotlin的lambda表达式

block: () -> ArrayList<Entity>


public interface Function0<out R> : Function<R> {
public operator fun invoke(): R
}


block: (String) -> ArrayList<Entity>


public interface Function1<in P1, out R> : Function<R> {
public operator fun invoke(p1: P1): R
}

收银台kotlin侧定义的lambda表达式,被Java调用时会被识别成Java的Function系列的接口。

0 2

收银台的空安全实践

原生收银台项目本身的代码,截止目前未出现过空指针崩溃。空指针是客观存在的,收银台是如何避免了空指针呢?借助Kotlin主要做了两个方面的工作:

1.通过Kotlin可空类型和空安全调用特性,我们把非空的判断归整到上游的接口数据层,做统一集中式的处理

示意代码如下:

public class Response extends BaseEntity implements ICheckNull {


public List<Plan> list;
public List<Entity> couponList;
public List<Entity> cantUseCouponList;
public Entity selectedCoupon;


@Override
public void checkNullObjAndInit() {
BeanValidator.checkNullList(list);
BeanValidator.checkNullList(couponList);
BeanValidator.checkNullList(cantCouponList);
checkSelectedCoupon();
}


private void checkSelectedCoupon() {
if (selectedCoupon == null) {
selectedCoupon = new CouponEntity();
}
selectedCoupon.checkNullObjAndInit();
}
}

对每个接口返回的实体类,都实现ICheckNull接口,并检查实体类里每个对象是否为null,如果为null我们额外初始化这个对象。这套逻辑借助泛型封装成了收银台的工具类,方便其他实体类复用。

2. Kotlin的平台类型可能造成的空指针异常,因此对Kotlin调用Java的代码返回的类型处理为可空类型,即做一次非空判断。除非Java代码返回能保证非空

Java侧示意代码

   public PayEntity createPayEntity(Payment payment) {
return Utils.getSelectedDefault(payment);
}

Kotlin调用Java

val payEntity: PayEntity? = Manager.getInstance().createPayEntity(payment)

除此之外利用Kotlin语言特性,我们更一步地提高了空安全。具体地说通过不可变性,属性读写分离,Elvis操作符等来实现。

1)可变与不可变性

variable = var ; value = valfinal
class PayViewModel : AbsViewModel<State>() {
val payLiveData: PayLiveData by lazy {
PayLiveData()
}
}

2)分离属性的读和写操作

Java的setter方法和getter方法内置到属性,方便分离读&写操作,有弹性。

示意代码

var age: Int = 0
private set


val currentAge: Int
get() = age

3)空安全调用 & Elvis操作

Kotlin语言层面提供了便捷的空判断表达,避免了类似写Java代码通过大量if语句判空嵌套的情况。

val currentPlan: String? = defaultCard?.recommendId ?: DEFAULT_PLAN
// Elvis操作
val activity = (this.activity as? PayActivity) ?: return

0 3

遇到的问题

  • 反序列化:fastjson框架反序列化Kotlin定义的的Int、Float、Double类型时,变成null而不是Java的基本数据类型值,用String类型替换原来的字段类型。

  • 平台编译失败:通过增加中间变量,变换写法而得到解决。

  • 遗漏的异常处理:NumberFormatException崩溃

NumberFormatException崩溃日志如下:

bUbmuqi.png!mobile

---java.lang.NumberFormatException: empty String sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842) sun.misc.FloatingDecimal.parseFloat(FloatingDecimal.java:122) java.lang.Float.parseFloat(Float.java:451)

...

问题出在parseFloat(Float.java:451)方法上,看下toFloat的Kotlin实现:

@kotlin.internal.InlineOnly
public actual inline fun String.toFloat(): Float = java.lang.Float.parseFloat(this)

Kotlin的toFloat方法内部调用了Java的parseFloat方法,Java的parseFloat源码如下:

    /**
* @param s the string to be parsed.
* @return the {@code float} value represented by the string
* argument.
* @throws NullPointerException if the string is null
* @throws NumberFormatException if the string does not contain a
* parsable {@code float}.
* @see java.lang.Float#valueOf(String)
* @since 1.2
*/
public static float parseFloat(String s) throws NumberFormatException {
return FloatingDecimal.parseFloat(s);
}

从源码上发现parseFloat方法会抛出NullPointerException和NumberFormatException两种异常,漏做了NumberFormatException异常处理,传入了空字符串。因此调用不是自己写的方法的时候要关注可能会抛出的异常。

小结

从编程语言的历史进程看Kotlin,它是现代化的编译型、强类型、静态语言,符合语言多编程范式的发展潮流。语言特性相比Java更加函数式,并把一些Java编程的最佳实践沉淀到语言特性中。

收银台原生化对编程语言的选型,是基于对运行效率和开发效率之间的权衡;Kotlin是工具,正确的使用工具有助于我们更好地表达收银台的业务逻辑。使用Kotlin的过程是对过去Java编程思维进化的过程,这是Kotlin带来的额外收益。

收银台落地Kotlin过程中,我们消除了空指针异常,截止目前收银台自身业务代码没有空指针的异常。Kotlin原生化改造后,进入首页加载完成耗时由原来H5收银台1180ms左右到现在的360ms左右,渲染时间缩短了820ms,首屏加载时间缩短了69.5%。

byAbmaJ.gif!mobile

接下来是广告时间,我们是京东零售平台业务中心基础业务研发部(京东零售-平台业务中心-基础业务研发部),拥有最具挑战的技术场景。这里有广阔的技术上升空间、融洽的团队氛围、良好的薪资待遇和一群为梦想而奋斗的小伙伴。

目前急需Java开发工程师、H5开发工程师、Android开发工程师、iOS开发工程师、Flutter开发工程师。欢迎大家加入我们,简历请邮件至: [email protected]

参考文献

* 编程语言的发展趋势及未来方向

* 最受欢迎的编程语言 (1965-2019)

* 《代码的未来》一书

* 《黑客与画家》 一书

* Kotlin核心编程

* 百度百科:hybrid app

* 百度百科:结构化程序设计

* 阮一峰的网络日志:Pointfree 编程风格指南

* 王垠:如何掌握所有的程序语言


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK