澳门新葡亰信誉平台游戏《Kotin 极简教程》第12章 使用 Kotlin 集成Gradle 开发

by admin on 2020年4月22日

把一个Java应用程序转换为Kotlin,编译时间要多久?

写在文前

本文将展示在Android中会遇到的实际问题,并且使用Kotlin怎么去解决它们。一些Android开发者在处理异步、数据库或者处理Activity中非常冗长的listener时发现了很多的问题。通过一个个真实的场景,我们一边解决问题一边学习Kotlin的特性。

第12章 使用 Kotlin 集成Gradle 开发


这是关于Kotlin的一系列文章。分为三个部分。
第一部分讨论了从Java转换到Kotlin。第二部分是我对Kotlin的看法。

快速上手

如果不知道如何在Kotlin中写一个相当简单的Java表达式。这里有一个简单的诀窍,就是在AndroidStudio的Java文件中编写一段代码,然后将其粘贴到kt文件中,它会自动转换为Kotlin。

《Kotlin极简教程》正式上架:

点击这里 > 去京东商城购买阅读

点击这里 > 去天猫商城购买阅读

在前面的文章中, 我讨论了把Android 应用从Java 100%转换为Kotlin 。
Kotlin代码比Java的简洁,更易于维护,所以我认为转换是值得的。
但有些人不想试用Kotlin,因为他们担心它编译可能没有Java快。
这个关注点绝对是正确的,如果变得编译很慢,没有人愿意转换他们的代码。
所以,让我们编译Lock
App试一下
,然后我把它转换成Kotlin。 我不会试图比较一行代码的编译速度;
相反,我将尝试回答将代码从Java转换为Kotlin是否会影响其总体构建的时间。

Kotlin优势

  1. 它更加易表现:这是它最重要的优点之一。你可以编写少得多的代码。

  2. 它更加安全:Kotlin是空安全的,也就是说在我们编译时期就处理了各种null的情况,避免了执行时异常。你可以节约很多调试空指针异常的时间,解决掉null引发的bug。

  3. 它可以扩展函数:这意味着,就算我们没有权限去访问这个类中的代码,我们也可以扩展这个类的更多的特性。

  4. 它是函数式的:Kotlin是基于面向对象的语言。但是就如其他很多现代的语言那样,它使用了很多函数式编程的概念,比如,使用lambda表达式来更方便地解决问题。其中一个很棒的特性就是Collections的处理方式。我稍后会进行介绍。

  5. 它是高度互操作性的:你可以继续使用所有用Java写的代码和库,甚至可以在一个项目中使用Kotlin和Java两种语言混合编程。一行Java一行Kotlin,别提有多风骚了。

非常感谢您亲爱的读者,大家请多支持!!!有任何问题,欢迎随时与我交流~


由于 Kotlin 具有丰富的功能,如一等函数和扩展方法等,因此它可以保留和改进
Gradle 构建脚本的最佳部分——包括简明的声明式语法以及轻松制作 DSL 的能力。

Gradle 团队与 Kotlin 团队密切合作,为 Gradle 开发了新的基于 Kotlin
脚本的构建配置语言,我们称之为 Gradle Script Kotlin,支持使用 Kotlin
编写构建和配置文件。同时,还支持在 IDE
中实现自动完成和编译检查等功能。有了Gradle Script Kotlin,我们可以使用
Kotlin 来写配置文件,就跟写普通代码一样。

我们在前面的章节中,已经有很多示例项目使用了 Gradle 来构建我们的 Kotlin
工程。本章我们将系统地来介绍一下使用 Kotlin 集成Gradle 开发的相关内容。

我如何测试构建时间

我写了一个shell来重复执行gradle。 所有测试连续进行10次。
该项目的每个场景之前clean,并使用Gradle daemon ,daemon之前停止一次。
本文中的所有测试都在运行于3.4 GHz的Intel Core
i7-6700上,使用32GB的DDR4内存和三星850 Pro SSD。 源代码是用Gradle
2.14.1构建的。

详细实例

1. 易表现和简洁性

通过Kotlin,可以更容易地避免模版代码,因为大部分的典型情况都在语言中默认覆盖实现了。

举个例子,在Java中,如果我们要典型的数据类,我们需要去编写(至少生成)这些代码:

public class User{
    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 "User{" +
          "id=" + id +
          ", name='" + name + ''' +
          ", url='" + url + ''' +
          ", mbid='" + mbid + ''' +
          '}';
    }
}

我们在不使用第三方框架的基础上,需要大量的set get方法和复写基础方法。

而使用Kotlin,我们只需要通过data关键字:

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

这个数据类,它会自动生成所有属性和它们的访问器, 并自动生成相应的
equals、hashcode、toString 方法。

空口无凭,我们验证一下:

首先建立一个kt文件,新建一个简单的User类:

data class User(var name: String)

这时候在命令行使用kotlinc编译,得到一个class文件,反编译成Java文件,可以看到:

public final class User {
   @NotNull
   private String name;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.name = var1;
   }

   public User(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
   }

  // 解构声明
   @NotNull
   public final String component1() {
      return this.name;
   }

   @NotNull
   public final User copy(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      return new User(name);
   }

   // $FF: synthetic method
   // $FF: bridge method
   @NotNull
   public static User copy$default(User var0, String var1, int var2, Object var3) {
      if((var2 & 1) != 0) {
         var1 = var0.name;
      }

      return var0.copy(var1);
   }

   public String toString() {
      return "User(name=" + this.name + ")";
   }

   public int hashCode() {
      return this.name != null?this.name.hashCode():0;
   }

   public boolean equals(Object var1) {
      if(this != var1) {
         if(var1 instanceof User) {
            User var2 = (User)var1;
            if(Intrinsics.areEqual(this.name, var2.name)) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}

事实说明在kotlin中 data 修饰符 = java中 private + getter + setter +
toString + equals + hashCode

2. 空安全

当我们使用Java开发的时候,如果我们不想遇到NullPointerException,我们就需要在每次使用它之前,不停地去判断它是否为null。

而Kotlin是空安全的,我们通过一个安全调用操作符?来明确地指定一个对象是否能为空。

我们可以像这样去写:

// 这里不能通过编译. User对象不能是null
var notNullUser: User= null

// User可以是 null
var user: User? = null

// 无法编译, user可能是null,我们需要进行处理
user.print()

// 只要在user != null时才会打印
user?.print()

// 使用Elvis操作符来给定一个在是null的情况下的替代值
val name = user?.name ?: "empty"

/** 
如果user为可空类型,又一定要调用它的成员函数和变量,可以用!!操作符
两种可能,要么正确返回name,要么抛出空指针异常
当user为null,你不想返回null,而是抛出一个空指针异常,你就可以使用它。
*/
var name = user!!.name

3. 扩展方法

我们可以给任何类添加函数(View,Context等)。比起Java的继承机制,更加简洁和优雅。举个例子,我们可以给fragment增加一个显示toast的函数:

fun Fragment.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) { 
    Toast.makeText(getActivity(), message, duration).show()
}

我们现在可以这么做:

fragment.toast("Hello world!")

此处duration已经赋了默认值,所以这个参数可传可不传。

包括扩展属性,可以直接 类名.属性名:类型

注意:Kotlin 的方法扩展并不是真正修改了对应的类文件,而是在编译器和
IDE 方面做了处理。使我们看起来像是扩展了方法。

4. 函数式支持

  • Collections迭代

Kotlin使用lambda表达式来更方便地解决问题。体现最好的就是Collections的处理方式。

list.map(
  println(it) //it表示迭代的对象
)

查看源码,我们可以看到实际上map就是一个扩展方法,给所有可以迭代的集合提供该方法,map方法接收的参数是一个lambda表达式,类型为T,返回值为R类型(意味着任意类型),那这里T类型实际上就是list的元素类型。

map方法源码.png

甚至于可以

list.map(::println)

::表示方法或类的引用。为什么可以直接传方法引用呢?

我们看看println方法源码,可以看到println接收一个Any类也就是任意类型,而且返回值为空(Kotlin中空类型为Unit类,此处源码省略了返回值类型声明),所以完全符合map方法的要求。

println方法源码.png

注:类似于RxJava对数组的处理,Kotlin也提供了flatMap方法,具体可以自己了解。

  • 事件

在Java中,每次我们去声明一个点击事件,都不得不去实现一个内部类,而在Kotlin中,可以直接声明我们要做什么。

view.setOnClickListener { toast("Hello world!") }
//注:此处的toast方法是Kotlin默认已经提供的扩展方法

5. 互操作性

Kotlin调用Java和Java调用Kotlin与之前的Java
类之间调用方式没有太大差别,不详细介绍。

就举个Java调用Kotlin的小例子:

//Kotlin
class Overloads {
    fun overloaded(a: Int, b: Int = 0, c: Int = 1){
        println("$a, $b, $c")
    }
}

//Java
public class AccessToOverloads {
    public static void main(String... args) {
        Overloads overloads = new Overloads();
        overloads.overloaded(1, 2, 3);
    }
}

可以看到非常简单,这里要多介绍一个Kotlin注解@JvmOverloads。仍然定义了一个overloaded方法,加上注解后,Kotlin会自动重载成n个方法(n表示参数个数)

//Kotlin
class Overloads {
    @JvmOverloads
    fun overloaded(a: Int, b: Int = 0, c: Int = 1){
        println("$a, $b, $c")
    }
}

/**
在Java可以调用3个overloaded方法,分别是:
overloaded(a,b,c)
overloaded(a,b)
overloaded(a)
*/
public class AccessToOverloads {
    public static void main(String... args) {
        Overloads overloads = new Overloads();
        overloads.overloaded(1, 2, 3);
        overloads.overloaded(1);
        overloads.overloaded(1,3);
    }
}

6. 其他

  • 单例

首先说说单例的实现方式,在之后的实战中,将会经常接触到object这个关键字。

先看Java,在Java中,实现一个单例,我们需要:

  1. 保留一个单例对象的静态实例

  2. 提供一个类方法让外界访问唯一的实例

  3. 构造方法采用private修饰符

而在Kotlin中,一个修饰符就解决了。

object PlainOldSingleton {

}

怎么做到的?我们看看反编译的结果:

单例

可以看到写法和Java是完全一样的,又有一个新问题,在类加载的时候就初始化了实例,这种方式很糟糕,我们最好选择懒加载。那么在Kotlin中懒加载的2种实现方式如下:

class LazyNotThreadSafe {
      //方式一
    companion object{
        val instance by lazy(LazyThreadSafetyMode.NONE) {
            LazyNotThreadSafe()
        }

        //方式二,实际是Java的直译
    private var instance2: LazyNotThreadSafe? = null

        fun get() : LazyNotThreadSafe {
            if(instance2 == null){
                instance2 = LazyNotThreadSafe()
            }
            return instance2!!
        }
    }
}

如果想要实现线程安全,可以加上@Synchronized注解,这和Java中给类加上Synchronized修饰符是一样的。同样@Volatile注解和Java的Volatile修饰符作用也是一样的。

或者使用静态内部类的单例方法:

class LazyThreadSafeStaticInnerObject private constructor(){
    companion object{
        fun getInstance() = Holder.instance
    }

    private object Holder{
        val instance = LazyThreadSafeStaticInnerObject()
    }
}
  • 委托

Kotlin中,委托的实现依靠于关键字 by
by表示将抽象主题的实例(by后边的实例)保存在代理类实例的内部。

比如下面这个例子中:BaseImpl类继承于Base接口,并可以Base接口的所有的
public 方法委托给一个指定的对象。

interface Base {
    fun display()
}

class BaseImpl : Base {
    override fun display() {
        print("baseimpl display")
    }
}

class ProxyClass(base: Base) : Base by base

//程序入口
fun main(args: Array<String>) {
    var base = BaseImpl()
    var proxy = ProxyClass(base)
    proxy.display()
}
  • 泛型

在Java中,一般使用Gson库来解析Json。调用方法的时候,我们需要传入想要转成的类的Class。我们都知道Java的泛型实际上是伪泛型,对泛型支持的底层实现采用的是类型擦除的方式(只有在编译期才有)。

所以当使用Gson.fromJson(String json , Class<T> classOf)方法时,虽然传入了类型参数,当实际上这个T仍然是个Object。

而在Kotlin中,可以使用reified,告别Class。

reified的意思是具体化。作为Kotlin的一个方法泛型关键字,它代表你可以在方法体内访问泛型指定的JVM类对象。

inline fun <reified T: Any> Gson.fromJson(json: String): T{
//封装了`Gson.fromJson(String json , Class<T> classOf)`方法
    return fromJson(json, T::class.java)
}

这里需要指定T类型为Any,即Object类。

接着可以不需要传入Class,直接调用

fun main(args: Array<String>) {
    val json = "{state:0,result:'success',name:'test'}"
    var result : ReifiedBean =  Gson().fromJsonNew(json)
    println(result.name+","+result.result)
}

这要归功于inline,inline
意味着编译的时候真正要编译到调用点。那么哪个方法调用了它,参数的类型都是确定的。也就不需要传入Class了

** 7. 摆脱不必要的依赖**

Kotlin替换了许多第三方库,如ButterKnife、Google
Autovalue、Retrolambda、Lombok和一些RxJava代码。

但是也是可以100%兼容RxJava的,举个读取本地文本逐个字打印的例子。

Kotlin中使用RxJava

好了,言归正传。

普通的获取View方法,需要一个个去findViewById

普通的获取View方法

而使用Kotlin后

使用Kotlin获取View

可能有人注意到了,还是需要findViewById啊!!骗子!说好的优雅呢?完全没觉得更加简洁啊!!别急,Kotlin常用的获取控件方式不是这样的,容我介绍个Kotlin库——Anko。

12.1 使用 Gradle 构建 Kotlin工程

测试

我想在几种常见的使用场景中运行基准:使用和不使用Gradle
daemon+clean,没有文件更改的增量编译,以及更改的文件的增量编译。
在转换之前,App Lock的Java代码有5,491个方法和12,371行代码。
改写后,这些数字下降到4,987方法和8,564行Kotlin代码。
在重写期间没有发生大的架构更改,因此在重写之前和之后测试编译时间应该很好地了解Java和Kotlin之间的构建时间的差异。

3. Kotlin库——Anko

简介
Anko是Kotlin官方开发的一个让开发Android应用更快速更简单的Kotlin库

1. 再也不用findViewById

做过Android开发的人都知道,布局文件写的多了,findViewById也是一个很大的工作量,而且还要先声明变量,在findViewById然后再强转成我们的控件,使用方式一般如下

TextView username;
username=(TextView)findViewById(R.id.user);

username.setText("我是一个TextView");

有时候写的是不是想吐,可能有些人说现在不是有一些注解的库,如butterknife,当我们使用注解时可以不用findViewById了,使用方式如下

@BindView(R.id.user)
TextView username;

username.setText("我是一个TextView");

确实是这样,使用注解后确实给我们少了一些工作量,不过这依然没有最简单化,最简单的就是我们可以直接给id为user的控件直接赋值,或许你会感觉这有点不可思议。不过Kotlin确实做到了。我们可以直接这样写

user.text="我是一个TextView"

user就是我们布局文件声明的id,.text就相当于setText(),在Kotlin语言中,我们看不到了像Java中的set/get方法了。

当我们想这样使用的时候(不用findViewById,直接使用xml控件id)
我们需要在gradle加入apply plugin: ‘kotlin-android-extensions’,需要加入下面一句代码

import kotlinx.android.synthetic.main.activity_login.*
注:activity_login就是我们的布局

import org.jetbrains.anko.toast
import org.jetbrains.anko.onClick

class Main2Activity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main2)
        my_textView.text = "kotline test"
        my_textView.textColor = Color.BLUE
        my_button.text = "Click"
        my_button.onClick { toast("aa") }
    }
}  

为什么Anko不需要.setText可以直接.text呢?其实这是通过扩展函数实现的,我们看下内部的实现细节:

public var TextView.text: CharSequence        
  get() = getText()           
  set(v) = setText(v)

2. Anko Layout

通常我们使用xml文件写我们的布局,但是存在有一些缺点:如不是类型安全,不是空安全,解析xml文件消耗更多的CPU和电量等等。

而Anko
Layout可以使用DSL动态创建我们的UI,并且它比我们使用Java动态创建布局方便很多。主要是更简洁,它拥有类似xml创建布局的层级关系,能让我们更容易阅读。

 verticalLayout {
            val textView = textView("textview")
            val name = editText()
            val button=button()
                    button.onClick {
                toast("${name.text}")
            }
        }

我们在OnCreate方法中可以去掉setContentView,然后加入上面代码就可以显示如下图的效果,即一个垂直的线性布局中,放了一个TextView,一个EditText,和一个Button。并且Button中有一个点击事件,当点击时将EditText的内容以toast显示。

Anko Layout.png

在上面创建UI过程中,我们直接把创建UI的代码写在onCreate方法中了,当然,还有一种写法。我们创建一个内部类实行AnkoComponent接口,并重写createView方法,该方法返回一个View,也就是我们创建的布局。修改如下

inner class UI : AnkoComponent<LoginActivity> {
        override fun createView(ui: AnkoContext<LoginActivity>): View {
           return with(ui){
               verticalLayout {
                   val textView=textView("我是一个TextView"){
                       textSize = sp(17).toFloat()//自定义字体大小
                       textColor=context.resources.getColor(R.color.red)//自定义颜色
                   }.lparams{
                       margin=dip(10)//它表示将10dp转换为像素
                       height= dip(40)
                       width= matchParent
                   }
                   val name = editText("EditText")
                   button("Button") {
                        onClick { view ->
                            toast("Hello, ${name.text}!")
                        }
                   }
               }
           }
        }
    }

然后在onCreate方法中加一句代码,即可创建我们的布局页面了。如下

UI().setContentView(this@LoginActivity)

其中,dip(10),表示将10dp转换为像素的意思,是Anko的扩展函数,说到扩展函数,我发现Kotlin源码里大量地使用扩展函数,这也是Kotlin语言的优势之一。确实很强大,例如dip扩展(摘取View扩展)

inline fun View.dip(value: Int): Int = context.dip(value)
fun Context.dip(value: Int): Int = (value * resources.displayMetrics.density).toInt()

就如我们之前说的toast、text也是拓展函数一样

inline fun AnkoContext<*>.toast(message: CharSequence) = ctx.toast(message)
fun Context.toast(message: CharSequence) = Toast.makeText(this, message, Toast.LENGTH_SHORT).show()

但是为了界面和逻辑分离,界面还是建议使用xml,所以这里就不对Anko
Layout多做介绍了。

3. 其他方面

比如网络请求AsyncTask

 doAsync {
            //后台执行代码

            uiThread { 
            //UI线程
            toast("线程${Thread.currentThread().name}")

         }
      }

其他内容可以直接访问Anko

12.1.1 kotlin-gradle 插件

为了用 Gradle 构建 Kotlin工程,我们需要设置好 kotlin-gradle 插件:

buildscript {
    ext {
        kotlinVersion = '1.1.3-2'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
                ...
    }
}

apply plugin: 'kotlin'

并且添加 kotlin-stdlib 依赖:

dependencies {
    compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}")
    compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
}

当然,这些操作在我们新建项目的时候,通常我们只需要选择相应的选项,
IntelliJ IDEA 就会直接帮我们完成了基本的配置了。

我们使用 kotlin-gradle-plugin 编译 Kotlin 源代码和模块。使用的 Kotlin
版本通常定义为 kotlinVersion 属性。

针对 JVM,我们需要应用 Kotlin 插件:

apply plugin: "kotlin"

clean + 不用Gradle daemon Build

这是两种语言中构建时间最差的情况:从冷启动运行一个clean的构建。
对于这个测试,我禁用了Gradle daemon。
这里是十个构建所花费的时间:

澳门新葡亰信誉平台游戏 1

在这种情况下的结果是,Java构建时间平均为15.5秒,而Kotlin平均为18.5秒:增加了17%。
这对Kotlin来说并不是一个好的开始,但是大部分人不会这么编译他们的代码。

对于没有Gradle daemon 并且clean构建,Java编译比Kotlin快17%

Kotlin的缺点

尽管 Kotlin 非常棒,但是它并不完美。我列举了一些我不喜欢的部分。

12.1.2 Kotlin 与 Java 混合编程

Kotlin 源代码可以与同一个文件夹或不同文件夹中的 Java
源代码混用。默认约定是使用不同的文件夹:

sourceSets {
    main.kotlin.srcDirs += 'src/main/kotlin'
    main.java.srcDirs += 'src/main/java'
}

如果使用默认的目录,上面的配置可以省去不写。

如果不使用默认约定,那么应该更新相应的 sourceSets 属性

sourceSets {
    main.kotlin.srcDirs += 'src/main/myKotlin'
    main.java.srcDirs += 'src/main/myJava'
}

clean +Gradle daemon Build

这个JIT编译器的问题
,就像JVM中,是它们需要时间来编译对报告的执行的代码,等等的处理随时间增加的性能,因为它运行。
如果停止JVM进程,那么性能增益会丢失。
在构建Java代码时,通常在每次构建时启动和停止JVM。
这迫使JVM每次构建时重做工作。
为了解决这个问题,Gradle附带了一个守护进程,它将在构建之间保持活跃,以便保持JIT编译的性能提升。
你可以通过在gradle命令行加参数–daemon或者在gradle.properties文件添加一句org.gradle.daemon=true。

澳门新葡亰信誉平台游戏 2

可以看到,第一次运行所花费的时间与没有daemon的时间相同,但后续运行的性能提高,直到第四次运行。
在这种情况下,查看第三次运行后的平均构建时间更有用,其中daemon已工作过了。
对于热运行,在Java中执行clean构建的平均时间为14.1秒,而Kotlin以16.5秒的速度运行时间:多了13%。

对于clean + Gralde daemon 编译,Java编译比Kotlin快13%。

Kotlin正在赶上Java,但仍然稍微落后。 但是,无论使用什么语言,Gradle
daemon都会将构建时间减少40%以上。 如果你还没有使用它,你应该用上。

所以Kotlin编译在完整代码情况下比Java慢一点。
但是你通常只会对几个文件进行更改后编译,增量构建将有不同的性能。
所以,让我们来看看Kotlin在增量编译是否可以赶上。

1. 没有命名空间

Kotlin 允许你在文件中定义顶级的函数和属性,但是这会带来困扰——所有从
Kotlin
引用的顶级声明无法区分。这让我们有时候在读代码时很难快速确定用的是哪一个函数。

例如,你定义这样一个顶级函数:

fun foo() {...}

你可以通过 foo() 调用。

如果你在不同的包里面也存在同样的方法,在调用时就不能明显区分出是调用的哪个方法。你可以通过在前面添加包名的方式去调用,但是如果
Java 约定的包名很深,似乎不太友好。

一种近似的解决方案是使用单例的 object 类。

object FooActions { fun foo() {...}}

这样你在 Kotlin 中可以通过 FooActions.foo() 调用,但是在 Java
中你必须要这样 FooActions.INSTANCE.foo()这样调用,这看起来很麻烦。

你也可以使用 @JvmStatic 去注解该方法,从而省掉INSTANCE

其实没有命名空间并不是什么大不了的事,但是如果 Kotlin
能够提供的话,能省不少事。

12.1.3 配置 Gradle JavaScript 项目

当针对 JavaScript 时,须应用不同的插件:

apply plugin: "kotlin2js"

除了输出的 JavaScript 文件,该插件默认会创建一个带二进制描述符的额外 JS
文件。

如果是构建其他 Kotlin
模块可以依赖的可重用库,那么该文件是必需的,并且与转换结果一起分发。

二进制描述符文件的生成由 kotlinOptions.metaInfo 选项控制:

compileKotlin2Js {
    kotlinOptions.metaInfo = true
}

提示:示例工程可以参考
https://github.com/EasyKotlin/chapter2_hello_world_kotlin2js

增量构建

编译器最重要的性能特性之一是使用增量编译。
正常构建将重新编译项目中的所有源文件,但是增量构建将跟踪自上次构建以来哪些文件已更改,并且只重新编译这些文件和依赖它们的文件。
这可能对编译时间有巨大的影响,特别是对于大型项目。
增量构建在kotlin1.0.2以后版本支持
,你可以在你的gradle.properties文件添加kotlin.incremental = true实现。
那么当使用增量编译时,Kotlin与Java的编译时相比如何?
以下是没有更改文件时使用增量编译的基准:

澳门新葡亰信誉平台游戏 3

接下来,我们将使用修改后的源文件测试增量编译。
为了测试这个,我在每次构建之前改变了一个java文件,Kotlin也一样。
在这个基准测试中,源文件是没有其他文件依赖的UI文件:澳门新葡亰信誉平台游戏 4

最后,让我们看看使用修改的源文件进行增量编译,其中文件导入到项目中的许多其他文件

澳门新葡亰信誉平台游戏 5

你可以看到Gradle
daemon仍需要两三次运行来预热,但是之后两种语言的性能是非常相似的。
没有更改,Java每个热建立4.6秒,而Kotlin平均4.5秒。
当我们更改一个没有被任何其他文件使用的文件时,Java平均需要7.0秒来做一个热构建,Kotlin是6.1秒。
最后,当我们更改项目中许多其他文件导入的文件时,Java需要7.1秒才能在Gradle
daemon加热后执行增量构建,而Kotlin平均6.0秒。

在最常见的情况下 – 启用增量编译的部分构建 –
Kotlin编译速度快或略快于Java。

2. 没有静态修饰符

Kotlin为静态函数和属性提供了一个和 Java
不一样的处理方式。并不是说有多烂,只是觉得让代码变得不干净而且没有必要。

例如,在 Android 的 View 类中定义的静态属性 View.VISIBLE 和静态函数
View.inflate

public class View { 
  public static final int VISIBLE = 0x00000000; 
  public static final int INVISIBLE = 0x00000004;
  public static View inflate(Context context, int resource) {...}
}

这个定义是简单的。然而,在 Kotlin 代码中:

class View { 
  companion object { 
    @JvmField 
    val VISIBLE: Int = 0x00000000 
    @JvmField 
    val INVISIBLE: Int = 0x00000004 
    @JvmStatic 
    fun inflate(context: Context, resource: Int) {...} 
  }
}

注:companion object为伴生对象

尽管 Kotlin
的版本并没有那么恐怖,但是它的复杂程度超过了我对这门语言的预期。如果去掉注解,你在
Java 中就不得不使用这样可怕的语法去调用:

// With annotations:
View.VISIBLE;
//Without annotations:
View.Companion.getVISIBLE();

12.1.4 配置 Gradle Android项目

Android 的 Gradle 模型与普通 Gradle 有点不同,所以如果我们要构建一个用
Kotlin 编写的 Android 项目,我们需要用 kotlin-android 插件取代
kotlin 插件:

buildscript {
    ext.kotlin_version = '1.1.2-4'
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

通常我们使用 Android Studio,都是生成一个带 app
子项目的工程。多项目配置的实现通常是在一个根项目路径下将所有项目作为子文件夹包含进去。例如我们在项目根路径下面的settings.gradle中如下配置:

include ':app'

每一个子项目都拥有自己的build.gradle文件来声明自己如何构建。

例如,我们在子项目app的构建配置文件 build.gradle 中一个完整的配置如下:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.kotlin.easy.kotlinandroid"
        minSdkVersion 14
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}
repositories {
    mavenCentral()
}

其中,

apply plugin: ‘kotlin-android’ 是 Kotlin Android 插件。
compile “org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version” 是
Kotlin 运行标准库。

另外, Android Studio 默认加载源码的目录是 src/main/java,如果想指定
Kotlin 代码在src/main/kotln目录,可以在 android 下添加以下内容:

android {
  ……
  sourceSets {
    main.java.srcDirs += 'src/main/kotlin'
  }
}

提示: 关于 Kotlin Android 的 Gradle 完整配置实例可参考
https://github.com/EasyKotlin/KotlinAndroid

结论

我们对几个不同的场景进行了基准测试,看看Kotlin在编译时间是否可以跟上Java。
虽然Java在clean构建比Kotlin 快10-15%,但这些情况很少。
对于大多数开发人员来说,更常见的情况是部分构建,其中增量编译进行了大量改进。
随着Gradle daemon运行和增量编译的开启,Kotlin编译速度快或略快于Java。
这是一个我完全没有想到并且令人印象深刻的结果。
我必须赞扬Kotlin团队设计一种不仅具有很多优秀功能,而且能够快速编译的语言。
如果你因为编译时试图使用Kotlin,你不必担心:Kotlin的编译速度和Java一样快。

英语原文:Kotlin vs Java Compilation
Speed

编译:掘金

(文/开源中国)    

3. 编译方法数量

Kotlin
肯定会减少项目中的代码行数,但是它也会提高代码在编译以后的方法数。主要原因就是
Kotlin 属性的实现方式。

和 Java 不一样,Kotlin 没有提供单独定义域的方式。你必须使用 val 或者 var
来声明变量。这样有一个好处,就是省去了像 Java 一样定义 getters 和
setters 方法。

但是这需要一定的成本。每一个public的 val
变量都会生成一个「支持域」和一个能被 Java 调用的 getter
方法。每一个public的 var 变量都会生成 getter 和 setter 方法。

// kt 文件:
// 默认就是public,无需额外添加public修饰符
val strValPublic: String = "strValPublic"
var strVarPublic: String = "strVarPublic"

// 以下是反编译结果:
public final class VarAndValKt {
   @NotNull
   private static final String strValPublic = "strValPublic";
   @NotNull
   private static String strVarPublic = "strVarPublic";

   @NotNull
   public static final String getStrValPublic() {
      return strValPublic;
   }

   @NotNull
   public static final String getStrVarPublic() {
      return strVarPublic;
   }

   public static final void setStrVarPublic(@NotNull String var0) {
      Intrinsics.checkParameterIsNotNull(var0, "<set-?>");
      strVarPublic = var0;
   }
}

拓展:Intrinsics.checkParameterIsNotNull 方法其实很简单,原理:

public static void checkParameterIsNotNull(Object value, String paramName) {
    if (value == null) {
        throwParameterIsNullException(paramName);
    }
}

其实所有空安全的秘密都在这个类里面了

庆幸的是,私有属性的 getters 和 setters 会生成域而不是生成方法。

// kt文件:
private val strValPrivate: String = "strValPrivate"
private var strVarPrivate: String = "strVarPrivate"

// 以下是反编译结果:
public final class VarAndValKt {
   private static final String strValPrivate = "strValPrivate";
   private static String strVarPrivate = "strVarPrivate";
}

所以如果你把项目中Java代码转成Kotlin,而且之前的 Java
代码中定义了大量的公开域(这在定义常量的时候很常见),你会惊奇的发现最终编译生成的方法数量大幅上升。

如果你的 Android 应用快接近方法数限制了,我建议你为不需要自定义 getter
方法的常量加上 @JvmField 注解。这样会阻止 getters
方法的生成,从而减少你的方法数。

// kt 文件:
@JvmField
val strValPublic: String = "strValPublic"
@JvmField
var strVarPublic: String = "strVarPublic"

// 以下是反编译结果:
// 注意看,get set方法消失,取而代之的是private修饰符变成了public
public final class VarAndValKt {
   @JvmField
   @NotNull
   public static final String strValPublic = "strValPublic";
   @JvmField
   @NotNull
   public static String strVarPublic = "strVarPublic";
}

12.1.5 配置Kotlin 标准库依赖

除了上面的 kotlin-gradle-plugin 依赖之外,我们还需要添加 Kotlin
标准库的依赖:

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib"
}

如果针对 JavaScript,使用
compile "org.jetbrains.kotlin:kotlin-stdlib-js" 替代之。

如果是针对 JDK 7 或 JDK 8,那么可以使用扩展版本的 Kotlin
标准库,其中包含为新版 JDK 增加的额外的扩展函数。使用以下依赖之一来取代
kotlin-stdlib

compile "org.jetbrains.kotlin:kotlin-stdlib-jre7"
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8"

如果项目中使用 Kotlin 反射,添加反射依赖:

compile "org.jetbrains.kotlin:kotlin-reflect"

如果项目中使用测试框架,我们添加相应的测试库依赖:

testCompile "org.jetbrains.kotlin:kotlin-test"
testCompile "org.jetbrains.kotlin:kotlin-test-junit"
4. 没有CE机制

Kotlin官网对CE的解释:

CE

翻译一下:
Kotlin
没有受检的异常。这其中有很多原因,但我们会提供一个简单的例子。
以下是 JDK 中 StringBuilder 类实现的一个示例接口
Appendable append(CharSequence csq) throws IOException;
这个签名是什么意思? 它是说,每次我追加一个字符串到一些东西(一个
StringBuilder、某种日志、一个控制台等)上时我就必须捕获那些
IOException。 为什么?因为它可能正在执行 IO 操作(Writer 也实现了
Appendable)…… 所以它导致这种代码随处可见的出现

我们看到Java的CE机制被诟病了很久,但是如果你经过理性的分析,就会发现,Java
的有些设计看起来“繁复多余”,实际上却是经过深思熟虑的决定。Java
的设计者知道有些地方可以省略,却故意把它做成多余的。我们不能盲目地以为简短就是好,多写几个字就是丑陋不优雅,其实不是那样的。

Kotlin有异常机制,但不要求你在函数的类型里面声明可能出现的异常类型,也不使用静态类型系统对异常的处理进行检查和验证。那当我每调用一个函数(不管是标准库函数,第三方库函数,还是队友写的函数,甚至我自己写的函数),我都会疑惑这个函数是否会抛出异常。由于函数类型上不需要标记它可能抛出的异常,为了确保一个函数不会抛出异常,你就需要检查这个函数的源代码,以及它调用的那些函数的源代码,甚至整个调用树!

在这种疑虑的情况下,你就不得不做最坏的打算,你就得把代码写成:

try
{
    foo()
} 
catch (e:Exception)
{
    printf(e)
}

因为不知道 foo 函数里面会有什么异常出现,所以你的 catch
语句里面也不知道该做什么。大部分人只能在里面放一条
log,记录异常的发生。这是一种非常糟糕的写法,不但繁复,而且可能掩盖运行时错误。

那么 Java 呢?因为 Java 有
CE,所以当你看到一个函数没有声明异常,就可以放心的省掉
try-catch。所以这个问题,自然而然就被避免了,你不需要在很多地方疑惑是否需要写
try-catch。Java 编译器的静态类型检查会告诉你,在什么地方必须写
try-catch,或者加上 throws 声明。

12.1.6 增量编译

Kotlin 支持 Gradle
中可选的增量编译。增量编译跟踪构建之间源文件的改动,因此只有受这些改动影响的文件才会被编译。从
Kotlin 1.1.1 起,默认启用增量编译。

结尾

在学习过程中,我发现,如果有着扎实的Java基础,这东西掌握起来是很快的,所以到底学不学Kotlin,其实是不用着急的。一个新的语言想要快速的普及,那么可能只有在运行效率上有所提升,才是最大的优势,而Kotlin并不具备这样的属性。

我们可以看下Java和Kotlin的编译速度对比。

12.1.7 编译器选项

要指定附加的编译选项,可以使用 Kotlin 编译任务compileKotlin的
kotlinOptions 属性。

配置单个任务示例:

compileKotlin {
    kotlinOptions {
        suppressWarnings = true
    }
}
编译速度对比

我不会试图比较一行代码的编译速度;相反,比较的是将代码从Java转换为Kotlin是否会影响其总体构建的时间。

在转换之前,App Lock的Java代码有5,491个方法和12,371行代码。
改写后,这些数字下降到4,987方法和8,564行Kotlin代码。
在重写期间没有发生大的架构更改,因此在重写之前和之后测试编译时间应该很好地了解Java和Kotlin之间的构建时间的差异。我写了一个shell来重复执行gradle。所有测试连续进行10次。

  • clean + 不用Gradle daemon Build
    这是两种语言中构建时间最差的情况:从冷启动运行一个clean的构建。
    对于这个测试,我禁用了Gradle daemon。
    这里是十个构建所花费的时间:

Paste_Image.png

对于没有Gradle daemon
并且clean构建,Java编译比Kotlin快17%,但是大部分人不会这么编译他们的代码。

  • clean +Gradle daemon Build

Paste_Image.png

可以看到,Kotlin第一次运行所花费的时间与上一个方案的时间相同,但后续运行的性能逐步提高。

对于clean + Gralde daemon 编译,Java编译比Kotlin快13%。

所以Kotlin编译在完整代码情况下比Java慢一点。
但是你通常只会对几个文件进行更改后编译,所以,我们来看看Kotlin在增量编译是否可以赶上Java。

  • 增量编译

没有更改文件时使用增量编译

更改没有其他文件依赖的UI文件的增量编译

修改的源文件的增量编译

所以虽然Java在clean构建比Kotlin 快10-15%,但这些情况很少。
对于大多数开发人员来说,更常见的情况是部分构建,随着Gradle
daemon运行和增量编译的开启,Kotlin编译速度快或略快于Java。

所以,还是那句话,一个新的语言想要快速的普及,在运行效率上有所提升,才是最大的优势,Kotlin肯定值得学习的,但并没有传的那么夸张。有精力就去学习,有自己的学习计划也可以放一放,延后再学。

我想只有用得多了,Kotlin的优势才会慢慢展现出来,这需要一个较为漫长的过渡期。

转载请注明
原文出处:http://www.jianshu.com/p/f364e3f9cc36
有错误请多多指正!

12.2 使用 Kotlin 编写构建和配置文件

一个基于 Kotlin 来写 Gradle 构建脚本及插件的方式可能会是什么样的?
它对团队的帮助如何——尤其是大型团队——加快工作速度并编写结构更好、更易于维护的构建脚本?

这些可能性非常诱人。

因为 Kotlin 是一种静态类型语言,在 IDEA 和 Eclipse
中都有深入的支持,所以可以从自动补全到重构,以及其间的一切都能为 Gradle
用户提供适当的 IDE 支持。 而且由于 Kotlin
具有丰富的功能,如一等函数和扩展方法,因此它可以保留和改进 Gradle
构建脚本的最佳部分——包括简明的声明式语法以及轻松制作 DSL 的能力。

Gradle 团队认真地考察了这些可能性,与 Kotlin 团队密切合作,为 Gradle
开发一种新的基于 Kotlin 的构建语言——我们称之为 Gradle Script Kotlin。

下面我们就来简要介绍一下使用 Kotlin 脚本来编写 Gradle 的配置文件。

我们就以上一章中的 chapter11_kotlin_springboot 工程为例。

首先我们在根目录下新建一个settings.gradle 配置文件:

rootProject.name = 'chapter11_kotlin_springboot'
rootProject.buildFileName = 'build.gradle.kts'

指定 gradle 构建文件名是 ‘build.gradle.kts’ 。
然后,我们新建 ‘build.gradle.kts’ , 完整的内容如下:

buildscript {
    val kotlinVersion = "1.1.3-2"
    val springBootVersion = "2.0.0.M2"
    extra["kotlinVersion"] = kotlinVersion

    repositories {
        mavenCentral()
        maven { setUrl("https://repo.spring.io/snapshot") }
        maven { setUrl("https://repo.spring.io/milestone") }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
        classpath("org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion")
    }
}

apply {
    plugin("kotlin")
    plugin("kotlin-spring")
    plugin("eclipse")
    plugin("org.springframework.boot")
    plugin("io.spring.dependency-management")
}

version = "0.0.1-SNAPSHOT"

configure<JavaPluginConvention> {
    setSourceCompatibility(1.8)
    setTargetCompatibility(1.8)
}


repositories {
    mavenCentral()
    maven { setUrl("https://repo.spring.io/snapshot") }
    maven { setUrl("https://repo.spring.io/milestone") }
}

val kotlinVersion = extra["kotlinVersion"] as String

dependencies {
    compile("org.springframework.boot:spring-boot-starter-actuator")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-freemarker")
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}")
    compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
    runtime("org.springframework.boot:spring-boot-devtools")
    runtime("mysql:mysql-connector-java")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

提示: 按照上述步骤,新建完文件build.gradle.kts后,IDEA 可能识别不了这些
DSL 函数,这个时候我们重启一下 IDEA 即可(这是一个 bug,后面会修复)。

这里面的 Gradle DSL 的相关函数与类都在 Gradle 软件包的 lib 目录下:
lib/gradle-script-kotlin-(版本号).jar 。我们简单用下面的表格说明:

函数(类) 对应的gradle-script-kotlin代码
buildscript open fun buildscript(@Suppress("unused_parameter") block: ScriptHandlerScope.() -> Unit) = Unit
repositories fun ScriptHandler.repositories(configuration: RepositoryHandler.() -> Unit) =repositories.configuration()
buildscript.dependencies DependencyHandlerScope(scriptHandler.dependencies)
configure inline fun <reified T : Any> Project.configure(noinline configuration: T.() -> Unit)
Project.dependencies DependencyHandlerScope(dependencies).configuration()

也就是说,其实这些配置函数背后都是由 Gradle 的 DSL 来实现的。

其实,这些配置语法看起跟 Groovy 的很像。例如:

Groovy :

    repositories {
        mavenCentral()
        maven { url "https://repo.spring.io/snapshot" }
        maven { url "https://repo.spring.io/milestone" }
    }

Kotlin:

repositories {
    mavenCentral()
    maven { setUrl("https://repo.spring.io/snapshot") }
    maven { setUrl("https://repo.spring.io/milestone") }
}

再例如:
Groovy:

sourceCompatibility = 1.8
targetCompatibility = 1.8

Kotlin:

configure<JavaPluginConvention> {
    setSourceCompatibility(1.8)
    setTargetCompatibility(1.8)
}

提示: 本节示例工程源码
https://github.com/EasyKotlin/chapter11_kotlin_springboot/tree/build.gradle.kts

本章小结

本章我们简要介绍了使用 Kotlin 集成 Gradle
开发过程中的一些常用的配置方法。Gradle
是一个非常好用的构建工具,当我们的 Kotlin 工程的配置文件也是 Kotlin
代码的时候,我们的工作又更加单纯了许多,只需要专注 Kotlin 即可。

在下一章中,我们将学习使用 Kotlin 和 Anko 来进行Android开发的相关内容。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图