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