章节索引 :

Kotlin 如何用于 iOS 开发 (Kotlin Native)

从这篇文章开始我们将一起研究下 Kotlin 是如何应用于 iOS 开发的。在此之前我想让大家重新认识一下 Kotlin 这门语言。很多人一直都认为它不就是门 JVM 语言和 Java、Scala 一样都是跑在 JVM 虚拟机上。其实 Kotlin 并不仅仅是一门 JVM 语言,它的野心是真的大,JVM 语言已经无法满足它的雄心壮志了。它是一门多平台的静态编译型语言,它可以用于 JVM 上 (只不过在 JVM 层面比较出名而已,导致很多人都认为它是门 JVM 语言),实则它可以编译成 JavaScipt 运行在浏览器中也可以编译成 IOS 的可运行文件跑在 LLVM 上。

1. Kotlin Native 的基本介绍

用官方的话来说 Kotlin Native 是一种将 Kotlin 代码编译为本机二进制文件的技术,可以在没有虚拟机的情况下运行。它是基于 LLVM 的后端,用于 Kotlin 编译器和 Kotlin 标准库的本机实现
Kotlin/Native 目前支持以下平台:

  • — iOS (arm32, arm64, emulator x86_64);
  • — MacOS (x86_64);
  • — Android (arm32, arm64);
  • — Windows (mingw x86_64);
  • — Linux (x86_64, arm32, MIPS, MIPS little endian);
  • — WebAssembly (wasm32)。

为了更好说明 Kotlin/Native 能力,下面给出张官方的 Kotlin/Native 能力图:
图片描述

2. Kotlin 开发第一个 iOS 程序 (HelloWorld)

2.1 需要准备的开发工具

  • AppCode 2018.1(建议下载最新版本,这里不是最新版本不过也能用);
  • Kotlin/Native Plugin 181.5087.34(注意:插件和 AppCode IDE 的版本匹配问题,建议把 IDE 安装好,然后 IDE 搜索下载会默认给最佳匹配的插件版本的);
  • Xcode 9.2(注意:这里 Xcode 版本需要 AppCode 版本匹配,否则会有问题的,不过不匹配的话 IDE 会有提示的,建议如果 AppCode 2018.1 (Xcode 9.2), AppCode 2018.3 (Xcode 10.0));

2.2 创建一个 Kotlin/Native 项目

- 选择左侧的 Kotlin/Native, 并选择右侧的 Sing View App with a Kotlin/Native Framework

图片描述

- 填写项目名和包名,选择语言 Swift (这里先以 Swift 为例)

图片描述

最后 finish 即可创建完毕 Kotlin/Native 项目,创建完毕后项目结构如下:

图片描述

2.3 运行 Kotlin/Native 项目

如果你比较幸运跑起来的话,效果应该是在模拟器装一个 APP 并且起了一个空白页,终端上输出了 "Hello from Kotlin!" 的 Log,类似这样:

图片描述

注意:但是你是真机测试,而且 Run 顶部默认只有一个 IOS Device 选项的话,然后你又点了 Run 说明而且会报如下错误

图片描述

这个问题是因为默认 IOS Device 选项是表示用真机调试,然后这边就需要一个 IOS 开发者账号。设置开发者账号的话,建议使用 Xcode 去打开该项目然后给该项目配置一个开发者账号。

图片描述

设置完毕 Xcode 后,AppCode 会自动检测到刷新。

3. Kotlin/Native 开发 IOS 运行原理分析

看到上面 IOS HelloWorld 项目运行起来,大家有没有思考一个问题,Kotlin 的代码的代码是怎么在 IOS 设备上跑起来呢?

实际上,在这背后使用了一些脚本和工具在默默支撑着整个项目的运行,如前所述,Kotlin / Native 平台有自己的编译器,但每次想要构建项目时手动运行它明显不是高效的。 所以 Kotlin 团队了选择 Gradle。Kotlin / Native 使用 Gradle 构建工具在 Xcode 中自动完成 Kotlin / Native 的整个构建过程。在这里使用 Gradle 意味着开发人员可以利用其内部增量构建架构,只需构建和下载所需内容,从而节省开发人员的宝贵时间。

如果,你还对上述有点疑问不妨一起来研究下 Kotlin/Native 项目中的构建参数脚本

  • 打开构建脚本是需要在 Xcode 中打开的,具体可以参考如下图:

图片描述

通过以上项目可以分析到在 Xcode 中编译一个 Kotlin/Native 项目,实际上在执行一段 shell 脚本,并在 shell 脚本执行中 gradlew 命令来对 Kotlin/Native 编译,该脚本调用 gradlew 工具,该工具是 Gradle Build System 的一部分,并传递构建环境和调试选项。

然后调用一个 konan gradle 插件实现项目编译并输出 xxx.kexe 文件,最后并把它复制到 iOS 项目构建目录 ("$TARGET_BUILD_DIR/$EXECUTABLE_PATH")。

最后来看下 Supporting Files 中的 build.gradle 构建文件,里面就引入了 konan 插件 (Kotlin/Native 编译插件), 有空的话建议可以深入研究下 konan 插件,这里其实也是比较浅显分析了下整个编译过程,如果深入研究 konan 插件源码的话,更能透过现象看到 Kotlin/Native 本质,这点才是最重要的。

buildscript {
    ext.kotlin_version = '1.2.0'
    repositories {
        mavenCentral()
        maven {
            url "https://dl.bintray.com/jetbrains/kotlin-native-dependencies"
        }
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:0.7"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin'

repositories {
    mavenCentral()
}

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

apply plugin: 'konan'

konan.targets = [
    'ios_arm64', 'ios_x64'
]
        
konanArtifacts {
    program('KotlinNativeOC')
}

4. Kotlin/Native 项目结构分析

4.1 Kotlin/Native + Swift 项目结构分析

我们知道 main 函数是很多应用程序的入口,ios 也不例外,在 AppDelegate.swift 中有 @UIApplicationMain 的注解,这里就是 APP 启动的入口。

@UIApplicationMain //main函数注解入口,所以AppDelegate类相当于启动入口类
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?//默认加了UIWindow



    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // KNFKotlinNativeFramework class is located in the framework that is generated during build.
    // If it is not resolved, try building for the device (not simulator) and reopening the project
    NSLog("%@", KNFKotlinNativeFramework().helloFromKotlin())//注意: 这里就是调用了Kotlin中的一个helloFromKotlin方法,并把返回值用Log打印出来,所以你会看到App启动的时候是有一段Log被打印出来
                  
    return true
    }
    ...
}

KotlinNativeFramework 类:

class KotlinNativeFramework {
    fun helloFromKotlin() = "Hello from Kotlin!" //返回一个Hello from Kotlin!字符串
}

但是呢,有追求的程序员绝对不能允许跑出来的是一个空白页面,空白页面那还怎么装逼呢?哈哈。在 ViewController.swift 中的 viewDidLoad 函数中加入一个文本 (UILabel)。

class ViewController: UIViewController {
    override func viewDidLoad() {
    super.viewDidLoad()
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 300, height: 21))
        label.center = CGPoint(x: 160, y: 285)
        label.textAlignment = .center
        label.font = label.font.withSize(15)
        label.text = "Hello IOS, I'm from Kotlin/Native"
        view.addSubview(label)
    }
    override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
    }
}

最后重新 run 一遍,效果如下:

图片描述

4.2 Kotlin/Native + Objective C 项目结构分析

在 IOS 同事帮助下,进一步了解 IOS APP 启动基本知识,这将有助于我们接下来改造我们项目结构,使得它更加简单,完全可以删除额外的 Swift 代码,包括 APP 启动代理那块都交由 Kotlin 来完成。

- 先创建一个 Kotlin/Native + OC 的项目,这里就不重复创建过程,直接把 OC 目录结构给出:

图片描述

可以看到 OC 与 Swift 项目结构差不多哈,可以看到其中有几个重要的文件,main.m、AppDelegate.m、ViewController.mmain.m APP 启动入口,相当于 main 函数,先从 main 函数入手,然后一步步弄清整个启动流程。

#import <UIKit/UIKit.h>
#import "AppDelegate.h"


int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));//这里也调用了AppDelegate类
    }

}

然后转到 AppDelegate.m, 可以看到在 didFinishLaunchingWithOptions 函数中调用了 KNFKotlinNativeFramework 中的 helloFromKotlin 函数。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // KNFKotlinNativeFramework class is located in the framework that is generated during build.
    // If it is not resolved, try building for the device (not simulator) and reopening the project
    NSLog(@"%@", [[[KNFKotlinNativeFramework alloc] init] helloFromKotlin]);//注意这里调用helloFromKotlin,并输出日志
                  
    return YES;
}

4.3 Kotlin/Native + Kotlin 项目结构分析

到这里很多人就会问了,看你上面说了那么并没有看到你 Kotlin 在做什么事,全是 Swift 和 OC 在做 APP 启动。现在就是告诉你 Kotlin 如何去替代它们做 APP 启动的事了。

先新创建一个项目,这次创建的不再是 Sing View App with a Kotlin/Native Framework, 而是一个 Application 项目。
图片描述

生成后的目录文件全是 Kotlin 文件,具体如下:

图片描述

生成的 main.kt 替代 main.m, 并设置对应启动的 AppDelegate:

import kotlinx.cinterop.autoreleasepool
import kotlinx.cinterop.cstr
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.toCValues
import platform.Foundation.NSStringFromClass
import platform.UIKit.UIApplicationMain

fun main(args: Array<String>) {
    memScoped {
        val argc = args.size + 1
        val argv = (arrayOf("konan") + args).map { it.cstr.getPointer(memScope) }.toCValues()

        autoreleasepool {
            UIApplicationMain(argc, argv, null, NSStringFromClass(AppDelegate))//注意: 在这里设置对应启动的AppDelegate
        }
    }
}

生成 AppDelegate 替代原来的 AppDelegate.m,并且在内部设置好启动的 Window。

import kotlinx.cinterop.initBy
import platform.Foundation.NSLog
import platform.UIKit.*

class AppDelegate : UIResponder(), UIApplicationDelegateProtocol {
    override fun init() = initBy(AppDelegate())
    private var _window: UIWindow? = null
    override fun window() = _window
    override fun setWindow(window: UIWindow?) { _window = window }
    override fun application(application: UIApplication, didFinishLaunchingWithOptions: Map<Any?, *>?): Boolean {//监听APP启动完成,打印Log
        NSLog("this is launch from kotlin appDelegate")
        return true
}
    companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta//注意:一定得有个companion object否则在main函数NSStringFromClass(AppDelegate)会报错
}

再生成一个 ViewController, 这个 ViewController 很类似 Android 中的 Activity。

import kotlinx.cinterop.*
import platform.Foundation.*
import platform.UIKit.*

@ExportObjCClass
class ViewController : UIViewController {

    constructor(aDecoder: NSCoder) : super(aDecoder)
    override fun initWithCoder(aDecoder: NSCoder) = initBy(ViewController(aDecoder))

    @ObjCOutlet
    lateinit var label: UILabel

    @ObjCOutlet
    lateinit var textField: UITextField

    @ObjCOutlet
    lateinit var button: UIButton

    @ObjCAction
    fun buttonPressed() {
        label.text = "Konan says: 'Hello, ${textField.text}!'"
    }
}

运行出来的效果如下:
图片描述

5 Kotlin/Native 开发一个 iOS 地图例子

5.1 IOS 项目 ViewController 与组件绑定过程分析

看到上面的运行 Demo,大家有没有在思考一个问题 IOS 项目中的 ViewController 是怎么和 UI 组件绑定在一起的呢?我个人认为这个很重要,换句话说这就是 IOS 开发最基本的套路,如果这个都不弄明白的话,下面 Demo 开发就是云里雾里了,掌握了这个基本套路的话,作为一个 Android 开发者,你基本上就可以在 IOS 项目开发中任意折腾了。

在 kotlin 目录下新建一个 KNMapViewController 类,并且它去继承 UIViewController 以及实现 MKMapViewDelegateProtocol 接口,并重写 viewDidLoad () 函数。并且在 viewDidLoad 函数实现 map 地图基本配置。

//导入Kotlin以与Objective-C和一些Cocoa Touch框架互操作。
import kotlinx.cinterop.*
import platform.CoreLocation.CLLocationCoordinate2DMake
import platform.Foundation.*
import platform.MapKit.MKCoordinateRegionMake
import platform.MapKit.MKCoordinateSpanMake
import platform.MapKit.MKMapView
import platform.MapKit.MKMapViewDelegateProtocol
import platform.UIKit.*

@ExportObjCClass//注意: @ExportObjCClass注解有助于Kotlin创建一个在运行时可查找的类。
class KNMapViewController: UIViewController, MKMapViewDelegateProtocol {
    @ObjCOutlet //注意: @ObjCOutlet注解很重要,主要是将mMapView属性设置为outlet。这允许您将Main.storyboard中的MKMapview链接到此属性。
    lateinit var mMapView: MKMapView
    constructor(aDecoder: NSCoder) : super(aDecoder)
    override fun initWithCoder(aDecoder: NSCoder) = initBy(KNMapViewController(aDecoder))
    override fun viewDidLoad() {
        super.viewDidLoad()
        val center = CLLocationCoordinate2DMake(32.07, 118.78)
        val span = MKCoordinateSpanMake(0.7, 0.7)
        val region = MKCoordinateRegionMake(center, span)

        with(mMapView) {
            delegate = this@KNMapViewController
            setRegion(region, true)
        }
    }
}

用 Xcode 打开项目中的 Main.storyboard, 删除原来自动生成一些视图组件 (如果你处于 AppCode 中开发项目,实际上直接在 AppCode 中双击 Main.storyboard 就会自动使用 Xcode 打开当前整个项目,并打开这个项目):

图片描述

给当前空的视图绑定对应 ViewController, 这里是 KNMapViewController:

图片描述

  • 4、在当前空的视图中添加一个 map view 组件并且设置组件的约束条件。
    图片描述
    右击组件 MKMapView 可以看到黑色对话框,里面 Referencing Outlets 还空的,说明当前 ViewController 没有和 MKMapView 组件绑定:
    图片描述

配置 outlet, 这里说下 AppCode 很坑爹地方,需要手动去 source code 中手动配置 outlet,选中 main.storyboard 右击 open as 然后选择打开 source code:

图片描述

在 view 和 viewController 结尾标签之间配置 connection:

图片描述
配置的 code 如下:

<connections>
   <outlet property="mMapView" destination="dest id" id="generate id"/>
</connections>
<!--property属性值就是KNMapViewController中的mMapView变量名;destination属性值是一个map view标签中id(可以在subviews标签内的mapView标签中找到id), id属性则是自动生成的,可以按照格式自己之指定一个,只要不出现重复的id即可-->

配置结果如下:

图片描述

检验是否绑定成功,回到 main.stroyboard 视图,右击组件查看黑色框是否出现如下绑定关系,出现了则说明配置成功。

图片描述

接着上述配置步骤,就可以回到 AppCode 中运行项目了:

图片描述

6. 小结

到这里有关 Kotlin 应用于 iOS 的开发就结束了,通过这篇文章相信你对 Kotlin 这门语言有了全新的认识,它不仅仅再是一门 JVM 语言了,也不仅仅局限于 Android 应用开发。你会发现 Kotlin 的所谓的跨平台仅仅是通过语言编译器层面,通过同一个编译器前端编译出支持 LLVM、JVM 等不同后端,以此达到一门语言可以编写多个平台的应用。下篇我们将一起来看下如何使用 Kotlin 开发 Gradle 脚本,我们都知道 Gradle 使用的是 Groovy 语言,但是 Gradle 官方在 Gradle5.0 版本之后,支持 Kotlin 成为继 Groovy 语言开发 Gradle 另一门编程语言。