Kotlin 如何用于 iOS 開發(fā) (Kotlin Native)
從這篇文章開始我們將一起研究下 Kotlin 是如何應(yīng)用于 iOS 開發(fā)的。在此之前我想讓大家重新認識一下 Kotlin 這門語言。很多人一直都認為它不就是門 JVM 語言和 Java、Scala 一樣都是跑在 JVM 虛擬機上。其實 Kotlin 并不僅僅是一門 JVM 語言,它的野心是真的大,JVM 語言已經(jīng)無法滿足它的雄心壯志了。它是一門多平臺的靜態(tài)編譯型語言,它可以用于 JVM 上 (只不過在 JVM 層面比較出名而已,導(dǎo)致很多人都認為它是門 JVM 語言),實則它可以編譯成 JavaScipt 運行在瀏覽器中也可以編譯成 IOS 的可運行文件跑在 LLVM 上。
1. Kotlin Native 的基本介紹
用官方的話來說 Kotlin Native 是一種將 Kotlin 代碼編譯為本機二進制文件的技術(shù),可以在沒有虛擬機的情況下運行。它是基于 LLVM 的后端,用于 Kotlin 編譯器和 Kotlin 標準庫的本機實現(xiàn)。
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 開發(fā)第一個 iOS 程序 (HelloWorld)
2.1 需要準備的開發(fā)工具
- 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 創(chuàng)建一個 Kotlin/Native 項目
- 選擇左側(cè)的 Kotlin/Native, 并選擇右側(cè)的 Sing View App with a Kotlin/Native Framework
- 填寫項目名和包名,選擇語言 Swift (這里先以 Swift 為例)
最后 finish 即可創(chuàng)建完畢 Kotlin/Native 項目,創(chuàng)建完畢后項目結(jié)構(gòu)如下:
2.3 運行 Kotlin/Native 項目
如果你比較幸運跑起來的話,效果應(yīng)該是在模擬器裝一個 APP 并且起了一個空白頁,終端上輸出了 "Hello from Kotlin!" 的 Log,類似這樣:
注意:但是你是真機測試,而且 Run 頂部默認只有一個 IOS Device 選項的話,然后你又點了 Run 說明而且會報如下錯誤
這個問題是因為默認 IOS Device 選項是表示用真機調(diào)試,然后這邊就需要一個 IOS 開發(fā)者賬號。設(shè)置開發(fā)者賬號的話,建議使用 Xcode 去打開該項目然后給該項目配置一個開發(fā)者賬號。
設(shè)置完畢 Xcode 后,AppCode 會自動檢測到刷新。
3. Kotlin/Native 開發(fā) IOS 運行原理分析
看到上面 IOS HelloWorld 項目運行起來,大家有沒有思考一個問題,Kotlin 的代碼的代碼是怎么在 IOS 設(shè)備上跑起來呢?
實際上,在這背后使用了一些腳本和工具在默默支撐著整個項目的運行,如前所述,Kotlin / Native 平臺有自己的編譯器,但每次想要構(gòu)建項目時手動運行它明顯不是高效的。 所以 Kotlin 團隊了選擇 Gradle。Kotlin / Native 使用 Gradle 構(gòu)建工具在 Xcode 中自動完成 Kotlin / Native 的整個構(gòu)建過程。在這里使用 Gradle 意味著開發(fā)人員可以利用其內(nèi)部增量構(gòu)建架構(gòu),只需構(gòu)建和下載所需內(nèi)容,從而節(jié)省開發(fā)人員的寶貴時間。
如果,你還對上述有點疑問不妨一起來研究下 Kotlin/Native 項目中的構(gòu)建參數(shù)腳本:
- 打開構(gòu)建腳本是需要在 Xcode 中打開的,具體可以參考如下圖:
通過以上項目可以分析到在 Xcode 中編譯一個 Kotlin/Native 項目,實際上在執(zhí)行一段 shell 腳本,并在 shell 腳本執(zhí)行中 gradlew 命令來對 Kotlin/Native 編譯,該腳本調(diào)用 gradlew 工具,該工具是 Gradle Build System 的一部分,并傳遞構(gòu)建環(huán)境和調(diào)試選項。
然后調(diào)用一個 konan gradle 插件實現(xiàn)項目編譯并輸出 xxx.kexe 文件,最后并把它復(fù)制到 iOS 項目構(gòu)建目錄 ("$TARGET_BUILD_DIR/$EXECUTABLE_PATH"
)。
最后來看下 Supporting Files 中的 build.gradle 構(gòu)建文件,里面就引入了 konan 插件 (Kotlin/Native 編譯插件), 有空的話建議可以深入研究下 konan 插件,這里其實也是比較淺顯分析了下整個編譯過程,如果深入研究 konan 插件源碼的話,更能透過現(xiàn)象看到 Kotlin/Native 本質(zhì),這點才是最重要的。
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 項目結(jié)構(gòu)分析
4.1 Kotlin/Native + Swift 項目結(jié)構(gòu)分析
我們知道 main 函數(shù)是很多應(yīng)用程序的入口,ios 也不例外,在 AppDelegate.swift 中有 @UIApplicationMain 的注解,這里就是 APP 啟動的入口。
@UIApplicationMain //main函數(shù)注解入口,所以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())//注意: 這里就是調(diào)用了Kotlin中的一個helloFromKotlin方法,并把返回值用Log打印出來,所以你會看到App啟動的時候是有一段Log被打印出來
return true
}
...
}
KotlinNativeFramework 類:
class KotlinNativeFramework {
fun helloFromKotlin() = "Hello from Kotlin!" //返回一個Hello from Kotlin!字符串
}
但是呢,有追求的程序員絕對不能允許跑出來的是一個空白頁面,空白頁面那還怎么裝逼呢?哈哈。在 ViewController.swift 中的 viewDidLoad 函數(shù)中加入一個文本 (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 項目結(jié)構(gòu)分析
在 IOS 同事幫助下,進一步了解 IOS APP 啟動基本知識,這將有助于我們接下來改造我們項目結(jié)構(gòu),使得它更加簡單,完全可以刪除額外的 Swift 代碼,包括 APP 啟動代理那塊都交由 Kotlin 來完成。
- 先創(chuàng)建一個 Kotlin/Native + OC 的項目,這里就不重復(fù)創(chuàng)建過程,直接把 OC 目錄結(jié)構(gòu)給出:
可以看到 OC 與 Swift 項目結(jié)構(gòu)差不多哈,可以看到其中有幾個重要的文件,main.m、AppDelegate.m、ViewController.m
main.m APP 啟動入口,相當于 main 函數(shù),先從 main 函數(shù)入手,然后一步步弄清整個啟動流程。
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));//這里也調(diào)用了AppDelegate類
}
}
然后轉(zhuǎn)到 AppDelegate.m, 可以看到在 didFinishLaunchingWithOptions 函數(shù)中調(diào)用了 KNFKotlinNativeFramework 中的 helloFromKotlin 函數(shù)。
- (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]);//注意這里調(diào)用helloFromKotlin,并輸出日志
return YES;
}
4.3 Kotlin/Native + Kotlin 項目結(jié)構(gòu)分析
到這里很多人就會問了,看你上面說了那么并沒有看到你 Kotlin 在做什么事,全是 Swift 和 OC 在做 APP 啟動?,F(xiàn)在就是告訴你 Kotlin 如何去替代它們做 APP 啟動的事了。
先新創(chuàng)建一個項目,這次創(chuàng)建的不再是 Sing View App with a Kotlin/Native Framework, 而是一個 Application 項目。
生成后的目錄文件全是 Kotlin 文件,具體如下:
生成的 main.kt 替代 main.m, 并設(shè)置對應(yīng)啟動的 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))//注意: 在這里設(shè)置對應(yīng)啟動的AppDelegate
}
}
}
生成 AppDelegate 替代原來的 AppDelegate.m,并且在內(nèi)部設(shè)置好啟動的 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 {//監(jiān)聽APP啟動完成,打印Log
NSLog("this is launch from kotlin appDelegate")
return true
}
companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta//注意:一定得有個companion object否則在main函數(shù)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 開發(fā)一個 iOS 地圖例子
5.1 IOS 項目 ViewController 與組件綁定過程分析
看到上面的運行 Demo,大家有沒有在思考一個問題 IOS 項目中的 ViewController 是怎么和 UI 組件綁定在一起的呢?我個人認為這個很重要,換句話說這就是 IOS 開發(fā)最基本的套路,如果這個都不弄明白的話,下面 Demo 開發(fā)就是云里霧里了,掌握了這個基本套路的話,作為一個 Android 開發(fā)者,你基本上就可以在 IOS 項目開發(fā)中任意折騰了。
在 kotlin 目錄下新建一個 KNMapViewController 類,并且它去繼承 UIViewController 以及實現(xiàn) MKMapViewDelegateProtocol 接口,并重寫 viewDidLoad () 函數(shù)。并且在 viewDidLoad 函數(shù)實現(xiàn) map 地圖基本配置。
//導(dǎo)入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創(chuàng)建一個在運行時可查找的類。
class KNMapViewController: UIViewController, MKMapViewDelegateProtocol {
@ObjCOutlet //注意: @ObjCOutlet注解很重要,主要是將mMapView屬性設(shè)置為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 中開發(fā)項目,實際上直接在 AppCode 中雙擊 Main.storyboard 就會自動使用 Xcode 打開當前整個項目,并打開這個項目):
給當前空的視圖綁定對應(yīng) ViewController, 這里是 KNMapViewController:
- 4、在當前空的視圖中添加一個 map view 組件并且設(shè)置組件的約束條件。
右擊組件 MKMapView 可以看到黑色對話框,里面 Referencing Outlets 還空的,說明當前 ViewController 沒有和 MKMapView 組件綁定:
配置 outlet, 這里說下 AppCode 很坑爹地方,需要手動去 source code 中手動配置 outlet,選中 main.storyboard 右擊 open as 然后選擇打開 source code:
在 view 和 viewController 結(jié)尾標簽之間配置 connection:
配置的 code 如下:
<connections>
<outlet property="mMapView" destination="dest id" id="generate id"/>
</connections>
<!--property屬性值就是KNMapViewController中的mMapView變量名;destination屬性值是一個map view標簽中id(可以在subviews標簽內(nèi)的mapView標簽中找到id), id屬性則是自動生成的,可以按照格式自己之指定一個,只要不出現(xiàn)重復(fù)的id即可-->
配置結(jié)果如下:
檢驗是否綁定成功,回到 main.stroyboard 視圖,右擊組件查看黑色框是否出現(xiàn)如下綁定關(guān)系,出現(xiàn)了則說明配置成功。
接著上述配置步驟,就可以回到 AppCode 中運行項目了:
6. 小結(jié)
到這里有關(guān) Kotlin 應(yīng)用于 iOS 的開發(fā)就結(jié)束了,通過這篇文章相信你對 Kotlin 這門語言有了全新的認識,它不僅僅再是一門 JVM 語言了,也不僅僅局限于 Android 應(yīng)用開發(fā)。你會發(fā)現(xiàn) Kotlin 的所謂的跨平臺僅僅是通過語言編譯器層面,通過同一個編譯器前端編譯出支持 LLVM、JVM 等不同后端,以此達到一門語言可以編寫多個平臺的應(yīng)用。下篇我們將一起來看下如何使用 Kotlin 開發(fā) Gradle 腳本,我們都知道 Gradle 使用的是 Groovy 語言,但是 Gradle 官方在 Gradle5.0 版本之后,支持 Kotlin 成為繼 Groovy 語言開發(fā) Gradle 另一門編程語言。