查看原文
其他

京东APP收银台Kotlin化实践

平台研发阮陈辉 京东零售技术 2022-10-18


我们把移动端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,改成纯原生页面。改造后的架构图:



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

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

  • 对Kotlin的理解

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


编程语言的时空观


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



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

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

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

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

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

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

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


摘自网络

随着时间流逝,动态语言加入了静态能力,静态语言加入了动态能力,静态和动态正在相互靠拢,未来的编程语言属于这两者的杂交产物。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结构

  • 柯里化

  • 模式匹配

  • 智能类型推断

  • 递归

  • 并发安全

函数式编程有如下等式:

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

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

不可变程序 = 纯函数 + 组合 (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把基本数据类型统一成对象类型,形成了面向对象的继承体系。 



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系列的接口。


02收银台的空安全实践


原生收银台项目本身的代码,截止目前未出现过空指针崩溃。空指针是客观存在的,收银台是如何避免了空指针呢?借助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 = val(final)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


03遇到的问题


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

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

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

NumberFormatException崩溃日志如下:

---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.InlineOnlypublic 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%。


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

目前急需Java开发工程师、H5开发工程师、Android开发工程师、iOS开发工程师、Flutter开发工程师。欢迎大家加入我们,简历请邮件至:xiasiyong@jd.com


参考文献

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

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

* 《代码的未来》一书

* 《黑客与画家》 一书

* Kotlin核心编程

* 百度百科:hybrid app

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

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

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

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存