本文加上讀者對Go語言和Swift語言都有必定了解, 可是對兩者混合使用不瞭解的同窗.ios
本教程是基於一個真實上架的iOS應用作的簡單的總結。git
咱們先看看運行效果:github
Go語言是Google公司於2010年開源的一個面向網絡服務和多併發環境的編程語言,特色是簡單。 可是由於簡單,也就只能實現90%的性能,這是Go語言的最大優勢,由於 少便是多 的道理不是每一個人都能領悟的。編程
Swift是Apple公司於2014年發佈的用來替代ObjectiveC的語言,主要面向iOS和OS X上的界面程序開發。 固然用swift來開發服務器也是你們關注的一個領域,做者看好在不遠的未來Swift將逐步替代C++和Rust語言。swift
Go語言和Swift語言原本是風馬牛不相及的兩個語言,爲什麼非必定要整到一塊兒呢? 緣由很簡單,由於做者是一個Go粉,同時也算是半個Swift粉;想試水iOS開發,可是實在是受不了ObjectiveC的裹腳布語法。xcode
補充下:本人雖然不喜歡ObjectiveC的語法,可是以爲ObjectiveC的runtime仍是很強悍的。 理論上,基於ObjectiveC的runtime,能夠用任何流行的編程語言來開發iOS應用,RubyMotion就是一個例子。服務器
其實,如今流行的絕大部分語言都有一個交集,就是c語言兼容的二進制接口。 因此說,C++流行並非C++多厲害,而是它選擇幾本無縫兼容了C語言的規範。網絡
可是,徹底兼容C語言的規範也有缺點,就是語言自己沒法自由地發展,由於不少地方會受到C語言編程模型的限制。 C++和ObjectiveC是兩個比較有表明的例子。架構
因此說,Swift一出世就兼容C語言的二進制接口規範,同時抱緊了ObjectiveC的runtime大腿,而去本身確實有很大優秀的特性。併發
可是,咱們這裏暫時不關心Swift和ObjectiveC的混合編程,咱們只關注做爲ObjectiveC子集的C語言如何與Swift混合編程。
Swift調用C函數的方法有多種:經過ObjectiveC橋接調用和直接調用。其實二者的原理是同樣的,我我的跟喜歡選擇最直接也最暴力的直接調用C函數的方式。
好比有一個C函數:
#include <stdio.h> void getInput(int *output) { scanf("%i", output); }
生成一個橋接的頭文件xxx-Bridging-Header.h
,裏面包含c函數規格說明:
void getInput(int *output);
swift就能夠直接使用了:
import Foundation var output: CInt = 0 getInput(&output) println(output)
若是不用橋接文件,能夠在swift中聲明一個Swift函數,對應C函數:
@_silgen_name("getInput") func getInput_swift(query:UnsafePointer<CInt>)
爲了明確區分C函數和swift函數,咱們將getInput
從新聲明爲getInput_swift
,使用方法和前面同樣:
import Foundation var output: CInt = 0 getInput_swift(&output) println(output)
Swift語言自己是自帶ARC的,用戶不多直接關注內存問題。可是C函數若是返回內存到Swift空間, Swift的ARC是無效的,須要手工釋放C內存。
假設咱們本身用C語言實現了一個字符串克隆的函數:
char* MyStrDup(char* s) { return strdup(s); }
在swift中能夠這樣使用:
@_silgen_name("MyStrDup") func MyStrDup_swift(query:UnsafePointer<CChar>) -> UnsafeMutablePointer<CChar> let p = MyStrDup_swift("hello swift-c!") let s = String.fromCString(p)! p.dealloc(1)
使用String.fromCString(p)!
從C字符串構建一個swift字符串,而後手工調用p.dealloc(1)
釋放c字符串內存空間。
函數調用和內存管理是跨語言編程中最重要的兩個基礎問題,目前已久初步能夠工做了。
Go語言提供了一個cgo的工具,用於Go語言和C語言交互。這是Go語言使用C語言的一個例子:
package main //#include <stdio.h> import "C" func main() { C.puts(C.CString("abc")) }
既然要交互,天然會涉及到C語言回調Go語言函數的情形。爲此,cgo提供了一個export
註釋命令, 用於生成Go語言函數對應的C語言函數:
//export MyStrDup func MyStrDup(s *C.char) *C.char { return C.strdup(s) }
MyStrDup
指定的名字必須和Go函數名字一致,函數的參數最後是C語言支持的類型。
如今,咱們就獲得了用Go語言實現的MyStrDup
函數,使用方法和前面的C語言實現的MyStrDup
是同樣的。
和引用C語言函數庫遇到的問題同樣,咱們如何在工程中引用這些C代碼或Go代碼實現的函數呢?
答案仍是來自C語言:將代碼構建爲C靜態庫或者C動態庫,而後將靜態庫或動態庫導入Swift工程。
可是,對於iOS來講,構建C靜態庫或者C動態庫的過程要麻煩(使用xcode也只是隱藏了構建的具體步驟)。
由於,iOS涉及到多種CPU架構:模擬器的x8六、4s的32位arm、5s之後的64位arm,64位arm中還有不一樣當版本...
這是C靜態庫或者C動態庫構建始終都要面對的問題。
Go1.6以後增長了構建C靜態庫的支持,交叉編譯也很是簡單,只須要設置好GOARCH
和GOOS
就行。
由於,iOS的GOOS
只有Darwin
一種類型,咱們只須要設置GOARCH
就能夠了。
要構建C靜態庫,咱們須要將上面的MyStrDup
實現放到一個main
包中:
package main //#include <string.h> import "C" func main() { // } //export MyStrDup func MyStrDup(s *C.char) *C.char { return C.strdup(s) }
main
包中的main
函數不會被執行,可是init
函數依然有效。
使用下面的命令就能夠構建當前系統的c靜態庫:
go build -buildmode=c-archive
要交叉編譯iOS可用的c靜態庫,咱們須要先設置GOARCH
,同時打開cgo特性(交叉編譯時,cgo默認是關閉的)。
下面是構建針對模擬器的x86/amd64類型的C靜態庫:
export CGO_ENABLED=1 export GOARCH=amd64 go build -buildmode=c-archive -o libmystrdup_amd64.a
咱們使用-o
參數指定了輸出的靜態庫文件名。構建命令同時還會生成一個頭文件(可能叫libmystrdup_386.h
), 咱們沒有用到這個頭文件,直接刪除掉就能夠。
下面是構建針對模擬器的x86/386類型的C靜態庫:
export CGO_ENABLED=1 export GOARCH=386 go build -buildmode=c-archive -o libmystrdup_386.a
在構建x86/386類型的C靜態庫時可能會有一些link錯誤,咱們暫時先用如下方法迴避。
建立一個patch_386.go
文件:
// Copyright 2016 <chaishushan{AT}gmail.com>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // 針對iOS模擬器link時缺乏的函數 // 屬於臨時解決方案 package main /* #include <stdio.h> #include <string.h> #include <stdlib.h> #include <time.h> size_t fwrite$UNIX2003(const void* a, size_t b, size_t c, FILE* d) { return fwrite(a, b, c, d); } char* strerror$UNIX2003(int errnum) { return strerror(errnum); } time_t mktime$UNIX2003(struct tm * a) { return mktime(a); } double strtod$UNIX2003(const char * a, char ** b) { return strtod(a, b); } int setenv$UNIX2003(const char* envname, const char* envval, int overwrite) { return setenv(envname, envval, overwrite); } int unsetenv$UNIX2003(const char* name) { return unsetenv(name); } */ import "C"
固然,仍是會有一些警告出現,暫時忽略它們。
而後,將C靜態庫加入到ios的xcode工程文件就能夠了。
x86構建是比較簡單的,由於咱們能夠默認使用本地的構建命令。 可是,若是要構建arm的靜態庫,則須要先配置好構建環境。
我從Go代碼中扣出了一個clangwrap.sh
腳本(好像是在$GOROOT/misci/ios
目錄):
#!/bin/sh # This uses the latest available iOS SDK, which is recommended. # To select a specific SDK, run 'xcodebuild -showsdks' # to see the available SDKs and replace iphoneos with one of them. SDK=iphoneos SDK_PATH=`xcrun --sdk $SDK --show-sdk-path` export IPHONEOS_DEPLOYMENT_TARGET=7.0 # cmd/cgo doesn't support llvm-gcc-4.2, so we have to use clang. CLANG=`xcrun --sdk $SDK --find clang` if [ "$GOARCH" == "arm" ]; then CLANGARCH="armv7" elif [ "$GOARCH" == "arm64" ]; then CLANGARCH="arm64" else echo "unknown GOARCH=$GOARCH" >&2 exit 1 fi exec $CLANG -arch $CLANGARCH -isysroot $SDK_PATH "$@"
裏面比較重要的是IPHONEOS_DEPLOYMENT_TARGET
環境變量,這裏意思是目標最低支持ios7.0系統。
構建arm64環境的靜態庫:
export CGO_ENABLED=1 export GOARCH=arm64 export CC=$PWD/clangwrap.sh export CXX=$PWD/clangwrap.sh go build -buildmode=c-archive -o libmystrdup_arm64.a
構建armv7環境的靜態庫:
export CGO_ENABLED=1 export GOARCH=arm export GOARM=7 export CC=$PWD/clangwrap.sh export CXX=$PWD/clangwrap.sh go build -buildmode=c-archive -o libmystrdup_armv7.a
而後咱們用lipo
命令將以上這些不一樣的靜態庫打包到一個靜態庫中:
lipo libmystrdup_386.a libmystrdup_adm64.a libmystrdup_arm64.a libmystrdup_armv7.a -create -output libmystrdup.a
這樣的話,只要引入一個靜態庫就能夠支持不一樣cpu類型的目標了。
毛主席教導咱們:要在戰爭中學習戰爭。
野雞醫院 這個app是做者第一個iOS應用,這篇教程也是在iOS開發過程逐步學習總結的結果。
完整的例子:
全部的代碼都可以避免費獲取(BSD協議): https://github.com/chai2010/ptyy