Kotlin京东业务实战

238 阅读9分钟

2017年Google开始力推Kotlin,在I/O大会上谷歌宣布Kotlin正式成为Android官方支持开发语言。

2018年4月18号《JAVA编程思想》作者Bruce大神给6967名京东兄弟分享了kotlin语言,认为kotlin为未来语言发展的趋势之一。

Kotlin已经越走越近,抱着“为了让移动开发更简单”的理念,让Kotlin在京东业务中落地。


Kotlin简介

Kotlin是一门运行在JVM之上的语言,由Jetbrains创建。Kotlin是一门非常简单的语言,其主要目标之一就是提供强大语言的同时又保持简单且精简的语法。

为什么选择 Kotlin

  • 简洁:这一点对于Android来说非常重要。项目所需要的库应该尽可能的小。Android对于方法数量有严格的限制,Kotlin依赖库只额外增加了大约6000个方法。同时用Kotlin开发的项目,方法数量也会大幅减少。

  • 安全:Java最大的一个问题就是null。如果没有对变量或是参数进行null判断,那么程序当中就有可能抛出大量的NullPointerException,然而在编码时这些又是难以检测到的。Kotlin使用了显式的null,这会强制我们在必要时进行null检查。

  • 互操作:Kotlin可与Java语言无缝通信。这意味着我们可以在Kotlin代码中使用任何已有的Java库;因此,即便这门语言还很年轻,但却已经可以使用成百上千的库了。除此之外,Kotlin代码还可以为Java代码所用,这意味着我们可以使用这两种语言来构建软件。你可以使用Kotlin开发新特性,同时使用Java实现的其他部分代码。

  • 工具友好:可用任何 Java IDE 或者使用命令行构建,包括常用的IntelliJ IDEA,Android Studio,Eclipse,命令行等。

对比其他语言,Kotlin语法和Java很像,非常容易上手,推荐以循序渐进的方式开发项目;由于项目中允许同时存在Java和Kotlin代码文件,并且允许Java与Kotlin互调,使得开发者可以很方便的在已有项目中引入Kotlin;新模块用Kotlin,稳定模块勿需用Kotlin重写。

Kotlin的一些特性

Kotlin拥有大量非常打动人心的特性,这里无法一一进行介绍,不过我们来看一下其中最为重要的一些。

Null安全

如前所述,Kotlin是null安全的。如果一个类型可能为null,那么我们就需要在类型后面加上一个?。这样,每次在使用该类型的变量时,我们都需要进行null检查。比如说,如下代码将无法编译通过:

var artist: Artist? = null?
artist.print()

第2行会显示一个错误,因为没有对变量进行null检查。

Null曾经被戏称为“十亿美金的错误”,Null虽然好用,但是导致很多错误的元凶往往都是它。在Kotlin中,编译器是可以识别你的引用是否是null,进而提醒你。默认kotlin中所有的对象都是不为Null的。

数据类

在Java中,如果想要创建数据类或是POJO类(只保存了一些状态的类),我们需要创建一个拥有大量字段、getters与setters的类,也许还要提供toString与equals方法:

public class Artist {
    private long id;
    private String name;
    private String url;
    private String mbid;
 
    public long getId() {
        return id;
    }
 
    public void setId(long id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getUrl() {
        return url;
    }
 
    public void setUrl(String url) {
        this.url = url;
    }
 
    public String getMbid() {
        return mbid;
    }
 
    public void setMbid(String mbid) {
        this.mbid = mbid;
    }
 
    @Override public String toString() {
        return "Artist{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", url='" + url + '\'' +
                ", mbid='" + mbid + '\'' +
                '}';
    }
}

在Kotlin中,上述代码可以写成下面这样:

data class Artist (
    var id: Long,
    var name: String,
    var url: String,
    var mbid: String)

使用一行代码创建一个包含 getters、 setters、 equals()、 hashCode()、 toString() 以及 copy() 的 POJO。

互操作

Kotlin提供了一些非常棒的互操作特性,这对于Android开发帮助非常大。其中之一就是拥有单个方法的接口与lambda表达式之间的映射。例如下面这个单击监听器:

viewHolder.mCouponItemBottom?.setOnClickListener(
    object : View.OnClickListener {
      override fun onClick(v: View) {
        Toast.makeText(mContext, "Click", Toast.LENGTH_LONG).show()
      }
    }
)

可以写成这样:

viewHolder.mCouponItemBottom?.setOnClickListener {
    Toast.makeText(mContext, "Click", Toast.LENGTH_LONG).show()
}

Lambda表达式

Lambda表达式会极大程度的精简代码,借助于Lambda表达式,我们可以做到之前无法实现或是实现起来非常麻烦的事情。借助于Lambda表达式,我们可以以一种更加函数式的方式来思考问题。Lambda表达式其实就是一种指定类型,并且该类型定义了一个函数的方式。

lambda的标准形式基本声明满足三个条件: 含有实际参数,含有函数体,以上内部必须被包含在花括号内部。

val sum = { x: Int, y: Int -> x + y }

Kotlin编译分析

Kotlin代码比Java的简洁,更易于编写维护,所以我们认为转换是值得的。 但很多开发者都担心Kotlin编译可能没有Java快,影响开发效率,反而得不偿失。

过程分析

上图是Java编译器的编译过程,Kotlin和Java的编译过程是很相似的,区别在于Kotlin与Java相比重要的细节在编译后端(目标代码生成)环节。

Kotlin编译器在目标代码生成环节做了很多类似于Java封装的事情,比如自动生成Getter/Setter代码的生成、Companion转变成静态类、修改类属性为final不可继承(open修饰即可继承)等等工作。

Kotlin将我们本来在代码层做的一些封装工作转移到了编译后端阶段,使得语言更加简洁。

速度分析

在相同gradle版本,相同设备的情况下,通过重复执行gradle指令,对几个不同的编译场景进行了基准测试,对比Kotlin和Java的编译时间。发现Java在clean构建比Kotlin 快10-15%,增量编译时Kotlin比Java编译速度略快。 对于大多数开发人员来说,更常见的情况是增量编译,Kotlin对增量编译进行了大量改进,保证了编译速度。

由此可见,开发人员不需要担心Kotlin的编译时间,Kotlin的编译速度和Java一样快。

京东业务实现

环境配置

  1. 安装 Kotlin 插件

Android Studio 从 **3.0(preview)**版本开始将内置安装 Kotlin 插件。如果你正在使用的是早期版本, 需要通过 File | Settings | Plugins | Install JetBrains plugin… 搜索并安装 Kotlin 插件。

  1. Jdlib工程中配置 Kotlin

新增 apply plugin: 'kotlin-android' 及其依赖。

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
dependencies {
        classpath "org.jetbrains.kotlin:kotlin-android-extensions:1.2.41"
    }
dependencies {
        ...
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
  1. 插件工程中配置 Kotlin

配置方式与Jdlib相同,注意需要将compile修改为provided,防止类库重复引用。如果不配置,插件代码不能打到apk中,调用时报ClassNotFoundException异常。

provided "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
  1. 添加混淆配置

如果调试Kotlin代码,总是报ClassNotFoundException异常,或者NoSuchMethod异常,你要考虑加入混淆配置。缺失混淆配置,Kotlin类库代码会被优化掉,编译正常,但无法运行。

# Kotlin
-dontwarn kotlin.**
-keep class kotlin.** {*;}
-keepclassmembers class **$WhenMappings {
    <fields>;
    <methods>;
}

业务实现

业务重写

将整个模块改为Kotlin语言实现,修复语法方面的编译错误。Android Studio提供将Java转为Kotlin的插件,可以转换大部分代码。

常见编译问题:

  1. 代码编译出错,定义变量时,没有赋初始值,变量可以为空,需要加?修饰符
var mCouponItemLayout: RelativeLayout
var mCouponItemTop: LinearLayout
var mCouponIcon: SimpleDraweeView
var mCouponName: TextView
var mCouponItemTopTv1: TextView
var mCouponItemTopTv2: TextView
var mCouponItemMiddle: LinearLayout

以下为修改后的代码:

var mCouponItemLayout: RelativeLayout? = null
var mCouponItemTop: LinearLayout? = null
var mCouponIcon: SimpleDraweeView? = null
var mCouponName: TextView? = null
var mCouponItemTopTv1: TextView? = null
var mCouponItemTopTv2: TextView? = null
var mCouponItemMiddle: LinearLayout? = null
  1. 代码编译出错,变量可以为空时,使用变量需要加?修饰符,自动判断是否为空,防止出现空指针异常
viewHolder.mCouponItemValueDiscount.text = entity.quota
viewHolder.mCouponItemValuePrice.text = entity.minOrderAmount

以下为修改后的代码:

viewHolder.mCouponItemValueDiscount?.text = entity.quota
viewHolder.mCouponItemValuePrice?.text = entity.minOrderAmount
  1. 代码编译出错,由于FontsUtil的参数有@NonNull标签,需要保证只有在确保变量不是null的情况下才能这么调用,否则它会抛出异常
FontsUtil.changeTextFont(viewHolder.mCouponItemMoneyTag, FontsUtil.MULTI_BOLD)
FontsUtil.changeTextFont(viewHolder.mCouponItemValueDiscount, FontsUtil.MULTI_REGULAR)

以下为修改后的代码:

FontsUtil.changeTextFont(viewHolder.mCouponItemMoneyTag!!, FontsUtil.MULTI_BOLD)
FontsUtil.changeTextFont(viewHolder.mCouponItemValueDiscount!!, FontsUtil.MULTI_REGULAR)

运行调试

编译成功后进行代码调试,修改运行时异常问题,可以正常使用debug工具,Kotlin模块与Java模块互相直接调用,显示效果和交互效果与Java模块没有差别。

Kotlin语言提供了类型的自动判断,自动拆装箱,字符串拼接,lambda表达式,空判断等一系列功能,功能精简了很多,语法与js有相似,同时去掉了findViewById(),省去了很多if try等语句,业务代码量减少很多。

统计业务模块的Java实现和Kotlin实现的代码量,不包含xml布局文件,代码量减少超过20%;代码减少最多的为pojo类,减少比例甚至超过80%;业务逻辑代码减少10%,同时代码会更加简洁直观,有助于提高代码可维护性。

ABTest上线

由于业务量级比较大,为防止新技术对业务稳定性产生影响,计划通过Java实现代码,Kotlin 实现代码两套代码并存,使用ABTest方式逐渐放量,待稳定后切到Kotlin

Kotlin在设计上避免了常见的编程错误,从而减少了应用程序崩溃和系统故障。此外,由于Kotlin 是快速失败机制,可以立即报告任何可能导致失败的问题。因此Kotlin在降低应用崩溃率上有很大作用,非常值得期待!

常见问题

  • 环境配置出错,出现无法编译或编译正常但apk中无kotlin代码,运行时报kotlin代码找不到; 解决方法:在插件代码中配置kotlin环境,在gradle中增加classpath,compile等,如果不配置环境,kotlin代码不会做编译;

  • 正常出包后,无法运行,调试kotlin代码,总是报Intrinsics的ClassNotFoundException异常,通过反编译分析apk,发现丢失部分代码; 解决方法:在jdlib代码中,加入遗漏的kotlin-android-extensions依赖,同时修改proguard,解决ClassNotFoundException异常;

  • 运行过程中报checkExpressionValueIsNotNull NoSuchMethod异常,分析apk与kotlin源码,发现kotlin中Intrinsics类部分方法打包后丢失; 解决方法:在主站代码中配置属性方法混淆,保证kotlin类中属性方法全部不做混淆优化;

  • 为了保证插件包大小,并且与主站代码不重复,引用类库时使用provided; 解决方法:使用provided引入类库,在编译时使用,最终不会被编译到apk;后期将配置环境放到aura中更方便;

参考资料