over 6 years ago

For-In Loops

前面像是 array 或是 dictionary 如果取 index 或是 key value 就不再敘述。
若不需要用值只想要跑迴圈的話,可以用底線來省略值。

for _ in 15 {
}

While

跟一般 while 一樣,條件是 true 就會進入 body,一直到條件為 false 才會跳出。

var k = 0
while k > 10 {
    k += 1
}

也有類似其他語言的 do while 用法,此用法第一次跑迴圈時不會經過條件判斷。

let k = false
repeat {
    print("loop")
} while k

Switch

  • Swift 的 switch 判斷不局限於整數,可以用許多不同的型別。
  • Swift 的 switch 若遇到符合的 case,會直接結束,不會再跑下一個 case, 所以不需要用 break 跳開,但如果需要的話還是可以在 switch 內使用 break。
  • Swift 的 case 內不可為空的,否則會有 error。
    let s = "a"
    switch s {
    case "a":
        print ("a")
    case "A":
        print ("b")
    default:
        break
    }
    
    可以在 switch 的判斷內做數量判斷。
    let count = 100
    switch count {
    case 0:
        break
    case 1..<=50:
        break
    case 51..<=100:
        break
    default:
        break
    }
    
    可以透過 switch 對 tuple 做判斷,用底線可以代表忽略 tuple 內的這個位置。
    let t = (1,2,3,4)
    switch t {
    case (0,0,0,0):
        break
    case (,2,,_):
        break
    default:
        break
    }
    
    如果有需要的話,可以將判斷的值宣告成新的變數或常數。
    let k = 123
    switch k {
    case 1..<100:
        break
    case let x
        x
    }
    
    Switch 的判斷內可以加上 where 做額外的判斷
    let k = 123
    switch k {
    case 1..<100:
        break
    case let x where x > 120:
        x
    default:
        break
    }
    
    可以把多種 case 條件混合在一個 case 內。
    let k = 123
    switch k {
    case 1,2,3,4,5,6:
        break
    default:
        break
    }
    

Fallthrough

Fallthrough 只能用在 switch 內,用途是可以跳到最後一個 case。

let k = 123
switch k {
case 1,2,3,4,5,6:
        break
case 123:
        fallthrough
default:
        print(123")
    break
}

命名

幫 while, for-in 等等的 loop 方法來命名,可以在另外的判斷或是 loop 內來操作特定的 loop。
以下的例子是雙重迴圈,對外層迴圈來命名,並且在內層迴圈來停止最外層迴圈,讓整個迴圈停止。

testLoop: for _ in 15 {
    for index in 15 {
        if index == 1 {
            break testLoop
        }
    }
}

命名方法就是參照上述的例子,後方可用 while 或 switch 來取代,也可以用 continue 等關鍵字來操作 loop。

Guard

  • Guard 一定是判斷條件為 true。
  • Guard 一定要有 else,且 else 內一定要有 return 或是 break 等等跳開的方法。
    let num = 5
    guard num == 5 else {
        return
    }
    
    可以用 guard 判斷 Optionals 是否為 nil, 若 optionals 有值且後續想要直接使用的話,可以像之前提到與 if 一樣的方法來打開, 可以快速的同時判斷 optionals 是否為 nil 並取值。
    let num: Sting? = "123"
    guard let value = num else {
        return
    }
    value
    
 
over 6 years ago

Array 型別語法

完整的 array 型別語法如下,Element 代表 array 內儲存值的型別。

Array<Element>

以下是比較簡單的寫法,也比較建議使用。

[Element]

Arrray 初始化

若沒有初始值的時候,宣告 array 時要先定義儲存值的型別,也可以直接帶入值自行判斷。

var array = [Int]()
var array = [1,2,3]

若已宣告的 array 變成空 array,一樣會是初始化時定義的型別。

var array = [Int]()
array = []

相加既有的 array 來宣告新的 array。

let array1 = [1,2,3]
let array2 = [1,2,3]
let array3 = array1 + array2

Array 的操作

計算數量

let array = [1,2,3]
array.count

是否為空 array

let array = [1,2,3]
array.isEmpty

加上新的值

var array = [1,2,3]
array.append(4)

透過 range 取得值

let array = [1,2,3,4,5]
let array1 = array[03]

Array 與 loop

透過 loop 取值

let array = [1,2,3]
for value in array {
    value
}

透過 loop 取得 index 與 value

let array = [1,2,3]
for (index, value) in array.enumerated() {
    index
    value
}

Set 型別語法

Set 型別語法如下,Element 代表 set 內儲存值的型別,與 array 不同,沒有簡短的寫法。

Set<Element>

Set 初始化

若沒有初始值的時候,宣告 set 時要先定義儲存值的型別,
也可以直接帶入值自行判斷,但需要宣告是 set 類型,避免跟 array 搞混,
若變成空 set 時,一樣會是初始化時的型別。

var set = Set<Int>()
var set: Set = ["1","2","3"]

Set 的操作跟 array 完全相同

Set 排序

Set 是一個沒有經過排序的 list,但可以透過 function 讓 set 排序。

let set: Set = [3,2,5,1]
for value in set.sorted() {
    value
}

Dictionary 型別語法

完整的 dictionary 型別語法如下,需定義 key 與 value 的型別。

Dictionary<key, value>

以下是比較簡單的寫法,也比較建議使用。

[key: value]

Dictionary 初始化

若沒有初始值的時候,宣告 dictionary 時要先定義儲存值的型別,
也可以直接帶入值自行判斷。

var d = [String: String]()
var d = ["first": "value"]

若已宣告的 dictionary 變成空 dictionary,一樣是初始化時的型別。

var d = ["first": "value"]
d = [:]

Dictionary 的操作

計算數量

let d = ["first": "value"]
array.count

是否為空 dictionary

let d = ["first": "value"]
d.isEmpty

更新 dictionary 的值,若更新的 key 不存在,會直接新增。

var d = [:]
d.updateValue("value", forKey: "second")

移除 value 的方法就是設為 nil,或是 remove value。

var d = ["first": "value", "second": "value"]
d["first"] = nil
d.removeValue(forKey: "second")

Dictionary 與 loop

透過 loop 取得 value 與 key。

var d = ["first": "value", "second": "value"]
for (key, value) in d {
    key
    value
}

透過 loop 取得 key。

var d = ["first": "value", "second": "value"]
for key in d.keys {
    key
}

透過 loop 取得 value。

var d = ["first": "value", "second": "value"]
for value in d.values {
    value
}

所有的 collection type 都是 value type

 
over 6 years ago

Basic Operators

nil 判斷

第一個運算子放置 Optionals 並加上雙問號,可以判斷 Optionals 是否為 nil,
如果不為 nil 會帶入第一個運算子,是 nil 就會帶入第二個運算子。

let s = "abc"
var s2: String?
s2 = s2 ?? s

範圍運算元

應用在 for in loop 內,下述的 sample 表示 index 為 1 到 5。

for index in 15 {
    index
}

也可以把 range 指定給一個變數或常數傳遞

let range = 010

String and Characters

初始化與判斷空字串

有兩個方法可以初始化 String,初始值都是空字串。

var s = ""
var s = String()

判斷空字串的方法

s.isEmpty

String 為可變動字串

與 Objc 將字串分為 NSString 與 NSMutableString 兩種不同,
Swift 的 String 都是可變動的。

String 支援加號運算

let s = "abc" + "edf"

String 是 Value types

下述的 sample 把 s1 的值給 s2 時,會進行一次 copy,所以改動 s2 時,s1 並不會改動。

var s1 = "abc"
var s2 = s1
s2 = "def"

Characters

可以將 String 的每個字拆開成 Characters。

let s = "abc"
for character in s.characters {
    character
}

Character 的值只能是單一字串。

let c: Character = "1"

可以再將 Character 組合回 String。

let c: Character = "1"
let s = String(c)

String 與 Character 運算

String 可以相加,Character 不能相加,因爲只能是單一字串。
String 可加上 Character。

var s = "1"
let c: Character = "2"
s.append(c)

String 插入變數或常數

let num = 1
let s = “\(num) !!!

計算 String 長度

必須透過 Character 來計算長度。

let s = "12345"
let count = s.characters.count

String index

String 有一特殊型別叫做 String.Index,可以透過它得到每個字元在字串中的位置。
startIndex 是取得字串第一個位置 index。

let s = "123"
let s2 = s[s.startIndex]

endIndex 是取得字串最後一個位置 index,
但下述的 sample 會有 error,因為最後一個位置 index 等於字串長度,會超出字元 array。

let s = "123"
let s2 = s[s.endIndex]

若要取得字串最後一個字元,必須取得最後一個 index 的前一個位置的字元。

let s = "123"
let s2 = s[s.index(before:s.endIndex)]
 
over 6 years ago

檔案位置 {system root}/etc/bashrc

編輯 bashrc 檔案(如果無法編輯可用 sudo su),將以下 copy paste 覆蓋內容,
完成後關掉終端機重新打開。

# System-wide .bashrc file for interactive bash(1) shells.
if [ -z "$PS1" ]; then
   return
fi

#PS1='\h:\W \u\$ '
PS1='\[\e[0;33m\]\u \[\e[0;31m\]@ \[\e[0;32m\]\h \[\e[0;31m\]: \[\e[0;35m\]\w \[\e[0;37m\]\$ '
#PS1="\$(date +%H:%M) \[\033[1;32m\]\u@\[\033[0m\h\[\033[0m\]\[\033[1;36m\]:\w\[\033[0m\]$"

# Make bash check its window size after a process completes
shopt -s checkwinsize
# Tell the terminal about the working directory at each prompt.
if [ "$TERM_PROGRAM" == "Apple_Terminal" ] && [ -z "$INSIDE_EMACS" ]; then
    update_terminal_cwd() {
        # Identify the directory using a "file:" scheme URL,
        # including the host name to disambiguate local vs.
        # remote connections. Percent-escape spaces.
     local SEARCH=' '
     local REPLACE='%20'
     local PWD_URL="file://$HOSTNAME${PWD//$SEARCH/$REPLACE}"
     printf '\e]7;%s\a' "$PWD_URL"
    }
    PROMPT_COMMAND="update_terminal_cwd; $PROMPT_COMMAND"
fi
                                           
#terminal color
export PS1
export CLICOLOR='true'
export LSCOLORS='exfxcxdxbxegedabagacad' 
 
over 6 years ago

Swift 綜括了許多目前知名程式語言的特性,我對 Swift 的感想就是

對 iOS developer 而言,只使用 Objc 也能完成所有的事情,
那為何還要學 Swift 呢? 因為潮嗎?這可能是其中一個原因,
但最主要的原因,是可以透過學習不同程式語言的特型來精進自己對於程式設計的觀念。

相信每個想要學 Swift 的人都會從 Apple 提供的 The Swift Programming Language 開始看起,
它是一本非常齊全的字典,但內容的範例或是某些介紹寫的不是太好,
常常會跳開原本在講解的東西或是交錯其他章節,
因此我針對每個章節列出我覺得重要或是特別的部分,希望能有幫助學習 Swift。

版本 :Swift 3.1

The Basic

常數 ( let ) 與變數 ( var )

原有的 Objc 並沒有這個概念,常數宣告後不能修改,變數可以。

型別宣告

後面會講到因為 Swift 有型別判斷,所以通常不需要宣告型別,Swift 也能辨識,
但是也是因為後面會講到的型別安全性,
最好還是宣告每個變數或常數的型別,因為能增加 code 的易讀性或 debug。

var message: String

型別安全與型別判斷

若定義變數或常數沒有宣告型別時,Swift 會根據值來判斷屬於哪個型別,
就像以下的 sample,雖然這個變數沒有宣告 String,但仍然會被判斷為 String 型別,
且若你指定了 String 以外的的值給此變數,會出現 error 警告。

var message = "Hello World"

又如以下的 sample,會自動被判斷為 Double

let num = 3 + 0.14159

數字轉換

因為 Swift 的型別安全,因此以下的 sample 會有 error,因為兩個不同型別無法正常相加處理。

let num1 = 0.5
let num2 = 1
let num3 = num1 + num2

需對 Int 轉換成 Double。

let num3 = num1 + Double(num2)

Bool

Objc 的 BOOL 是可以用不同的值來宣告並自行轉換成 true or false,
比如可以給數字 99 或是一個非空字串,都會是 YES,給 nil 會是 NO 的狀況,
但同樣因為型別安全,所以 Swift 的 Bool 一定只能給予 true or false,

Tuples

將多個值組合成一個值,且每個值可以是不同類別。

let myTuples: (Int, String) = (1, "1")

透過 Tuples 來取值

myTuples.0
myTuples.1

也可以對 Tuples 內的值來定義名稱並取值

let myTuples = (num: 1, name: "1")
myTuples.num
myTuples.name

也可以各自拆分成變數或常數

let (num, name): (Int, String) = (1, "1")
num
name

Optionals

了解 Optionals 用法前,必須要先了解 nil 在 Swift 的定義,Objc 也有 nil,但兩者代表的意義不同。

首先是 Objc 的 nil,Objc 的 nil 只能用在物件上, C Structure 如 NSInteger 無法指定 nil,
因為 Objc 的 nil 是一個 id 類別的指標變數,指向的位置值為 0,
以下的 sample 可以解釋成 test 這個指標指向 nil 指標,因此在取值的時候,會是 0。

NSObject *test = nil;

Swift 的 nil 是一種特殊型別的值,這種值只能指定給 Optionals,用來表示變數或常數沒有值的情形,
可以想像將變數或常數放在一個盒子內,你知道這個盒子裡面放的是什麼,但不知道裡面有沒有東西,
nil 就像是每個盒子外面有沒有掛鑰匙的意思,
有鑰匙代表裡面有東西可以打開,
沒鑰匙代表裡面沒有東西所以打不開。
基本的 Optionals 宣告如下,在沒有初始值時,所有 Optionals 的值都是 nil,也可以隨時將它設為 nil。

var test: String?
test = "abc"
test = nil

Optionals 可以是 function 的回傳值,
例如 Int(),這個 function 原本是回傳一個 Int 型別的值,
但如果傳入無法轉換成 Int 的參數時,會回傳 Optionals 值為 nil。

Int("abc")

真正的值是放在 Optionals 這個盒子內,若要取得值就必須打開它,
如之前所述,每個盒子都會有一個有沒有鑰匙的狀態,沒有鑰匙就無法打開它,
沒有鑰匙硬打開盒子就像要打開 nil 的 Optionals 一樣,因此就會發生 error。

let num: Int? = 123
num!

你也可以透過 if 來打開 Optionals,如果 Optionals 是 nil 的話就不會進入判斷。

let num: Int? = 123
if let index = num {
    index
}

有些情況下已經很確定箱子裡一定有東西,可以在宣告 Optionals 的時候直接讓它是開啟狀態,
但它仍然是 Optionals,所以還是可以對他設定 nil。

var num: Int! = 123
num
num = nil
 
about 7 years ago

之前曾經分享過如果操作 Core Audio 來設計一個屬於自己的 Audio Player,
基本上這個 Audio Player 已經能播放絕大多數的音檔格式,
但有某些情況下你可能需要支援像 FLAC 這樣的音檔格式,
而 Core Audio 本身是不支援 FLAC 的,(你甚至沒有辦法在 iTunes 上同步 FLAC)。

絕大多數的人都會使用一套 Origami Engine 來支援播放 FLAC,
Origami Engine Github
由於在安裝方面,官方的說明甚少且不清楚,因此下面來敘述一下如何將 Origami Engine 帶入你的專案。

  1. 因為目前 Origami Engine 還未支援 ARC,我先假設你的專案有支援 ARC,所以你要先在你的專案內加上一個 static library。

  2. 將 static library 的 ARC 關閉。

  3. Origami 需要 FLAC 的 codec,因此你需要 submodule FLAC 的 iOS codec。
    git@github.com:jhurt/FLACiOS.git

  4. 手動將 Origami 與 FLACiOS 的檔案加入到 static library 底下。

  5. 你會發現某些檔案會找不到 header 檔,因此請在 static library 的 user header search paths 底下加入 $(SRCROOT)/FLACiOS/libFLAC/include。

  6. 再來就會發現在 static library 內的某些檔案會抓不到 NS 類型的 class,因此你可以加入一個 .pch 並且 import foundation,記得到 static library 的 prefix header 內加入這個檔案。

  7. 最後到你的 app target 內的 Target Dependenices 與 Link Binary With Library 加入此 static library。

 
over 7 years ago

WWDC(Worldwide Developers Conference)
若不清楚這是什麼或是想了解歷史的可參考

這次到舊金山參加了 WWDC 2015,是我第一次到現場參與 WWDC。

門票 Get!!


2014 年開始 WWDC 門票是採用登記隨機抽獎制,只要你的 Apple ID 有在某個 developer program 內,
就可以預先登記參加,Apple 會隨機抽選,在 2014 年之前都是先搶先贏制。

WWDC 會場(Moscone West)


在正式開始前一天要先去會場領 badge + 外套(圖中有人身穿 15 的那件),穿上它走在舊金山的街頭感覺非常特別,
因為在這幾天會時常看到身穿跟自己一樣外套的人,有一種身為 Apple Developer 的榮譽感。

Keynote


在現場你會感受到身旁的人對於台上新發表東西的興奮感,縱使這個東西你從來沒用過或是跟你正在進行的專案無關,
你也會跟隨著現場的氣氛感到莫名的興奮。

Session


以往的 Session 都必須要到隔天才有放出 Video,今年有些 Session 開始有直播,
所以你甚至可以到大廳舒舒服服的躺著看 Session。

Lab


這應該是現場參與 WWDC 最有價值的其中一項,也就是你可以直接與 Apple 的工程師溝通問問題,
這次出發前我準備了兩個問題,都是與 IAP 相關,而在現場聽完發表的 spotlight API 後也去問了相關的問題。

Party

會場有辦一個蠻特別的聚會,是將所有中國大陸開發者聚集起來,不過大部分的人都不是工程師,
主要都是新創公司的 co-founder。


after party 算是現場參與 WWDC 最有價值的另一項,許多公司會在當天活動結束後,
自己辦 party 來邀請工程師參加,有些是單純聊天聚會,有些是比較技術性的討論,
可以下載 Parties for WWDC ,這個 app 有每天的 after party 列表。

 
over 7 years ago

前言

上次使用 Audio Queue 來實作 streaming player,這次說明另外一個實作 Audio Unit。

Audio Unit

官方文件

在開始前要先解釋幾個 Audio Unit 的物件。

1. Audio Graph : 類似一個 Audio Unit 的容器。
2. Audio Node : 用來表示 Audio Graph 內的最小單位。
3. Audio Unit : 用來表述 Audio Node 的物件。

由於 Audio Unit 能對音訊做許多不同的處理,以下舉幾個例子來實作

  1. 普通播放
  2. 混音播放
  3. 同時輸入輸出

Get started!

Play Streaming Music

先確認最後要完成的實作流程。

1. Prepare Audio Graph

建立 Audio Graph 並且開啟。

AUGraph audioGraph;
status = NewAUGraph(&audioGraph);
status = AUGraphOpen(audioGraph);

將 Audio Node 加進去,並且要帶此 node 的 AudioComponentDescription,
再將 node 的資訊放到用來表示 audio node 的 audio unit 上。

AUNode node;
AudioUnit audioUnit;

- (AudioComponentDescription)unitDescription
{
    AudioComponentDescription outputUnitDescription;
    bzero(&outputUnitDescription, sizeof(AudioComponentDescription));
    outputUnitDescription.componentType = kAudioUnitType_Output;
    outputUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
    outputUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
    outputUnitDescription.componentFlags = 0;
    outputUnitDescription.componentFlagsMask = 0;
    return outputUnitDescription;
}

AudioComponentDescription unitDescription = [self unitDescription];
status = AUGraphAddNode(audioGraph, &unitDescription, &node);
status = AUGraphNodeInfo(audioGraph, node, &unitDescription, &audioUnit);

接著設定這個 audio unit 中 input 與 output 的格式,這邊都是設定 linerPCM,
在 remoteIO 的 audio unit 內,element 0 的 output 會接上輸出硬體(喇叭),
element 1 的 input 會接上輸入硬體(麥克風),
因為目前的輸入源是 audio data,不需要透過硬體來源,因此我們只需設定 element 0。

AudioStreamBasicDescription LinearPCMStreamDescription()
{
    AudioStreamBasicDescription destFormat;
    bzero(&destFormat, sizeof(AudioStreamBasicDescription));
    destFormat.mSampleRate = 44100.0;
    destFormat.mFormatID = kAudioFormatLinearPCM;
    destFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
    
    destFormat.mFramesPerPacket = 1;
    destFormat.mBytesPerPacket = 4;
    destFormat.mBytesPerFrame = 4;
    destFormat.mChannelsPerFrame = 2;
    destFormat.mBitsPerChannel = 16;
    destFormat.mReserved = 0;
    return destFormat;
}

AudioStreamBasicDescription destFormat = LinearPCMStreamDescription();

status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &destFormat, sizeof(destFormat));
status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &destFormat, sizeof(destFormat));

加上觀察 audio unit 狀態改變的 listener

status = AudioUnitAddPropertyListener(audioUnit, kAudioOutputUnitProperty_IsRunning, MyAudioUnitPropertyListenerProc, (__bridge void *)(self));

跟著接上 input 的 renderCallback,跟剛剛設定格式一樣,我們是針對 element 0 的 callback。

static OSStatus RenderCallback(void *userData, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData)

AURenderCallbackStruct callbackStruct;
callbackStruct.inputProcRefCon = (__bridge void *)(self);
callbackStruct.inputProc = RenderCallback;

status = AUGraphSetNodeInputCallback(audioGraph, node, 0, &callbackStruct);

最後初始化

status = AUGraphInitialize(audioGraph);

2. Prepare Audio Data

  • Parse
與 Audio Queue 相同,可以參考 iOS : Audio Streaming ( Audio Queue )
  • Store

在上次的 Audio Queue 中,我們使用 NSData 來儲存 packet data 與 packet description data,
這次我們改用另一種方法來儲存,兩種方法都可以達成目的。

首先我們先建立一個 struct,用來表示每個 packet 資料以及讀取到的 packet index。

typedef struct {
    AudioStreamPacketDescription packetDescription;
    void *data;
} AudioPacketInfo;

AudioPacketInfo *packets;
size_t packetReadIndex;

packetCount = 2048;
packets = (AudioPacketInfo *)calloc(packetCount, sizeof(AudioPacketInfo));

跟之前一樣,parse 出的資料主要有 data length、packet count、packet data 與
packet description。
針對每個 packet 初始化一個 AudioPacketInfo,並放入 packet data 與 packet description。

- (void)storePacketData:(const void * )inBytes dataLength:(UInt32)inLength packetDescriptions:(AudioStreamPacketDescription* )inPacketDescriptions packetsCount:(UInt32)inPacketsCount
{
    
    @synchronized (self) {
        
        for (size_t index = 0; index < inPacketsCount; index ++) {
            
            AudioStreamPacketDescription emptyDescription;
                        
            AudioStreamPacketDescription *currentDescription = inPacketDescriptions ? &(inPacketDescriptions[index]) : &emptyDescription;
            
            AudioPacketInfo *nextInfo = &packets[packetWriteIndex];
            nextInfo->data = malloc(currentDescription->mDataByteSize);
            memcpy(nextInfo->data, inBytes + currentDescription->mStartOffset, currentDescription->mDataByteSize);
            memcpy(&nextInfo->packetDescription, currentDescription, sizeof(AudioStreamPacketDescription));
        }
    }
}

3. Audio Converter

與 Audio Queue 不同,Audio Unit 需要自行將資料轉換成 LinerPCM 資料。

首先先自行建立一個 class,裡面包含 audio converter、audio buffer list、buffer size、
轉換前格式與目標格式。

@interface SKAudioConverter : NSObject
{
    AudioStreamBasicDescription audioStreamDescription;
    AudioStreamBasicDescription destFormat;
    AudioConverterRef converter;
    AudioBufferList *renderBufferList;
    UInt32 renderBufferSize;
}
- (instancetype)initWithSourceFormat:(AudioStreamBasicDescription *)sourceFormat;

AudioConverterNew 帶入轉換前格式與目標格式以及 audio converter ref 來建立 converter。

- (instancetype)initWithSourceFormat:(AudioStreamBasicDescription *)sourceFormat
{
    self = [super init];
    if (self) {
        audioStreamDescription = *sourceFormat;
        destFormat = LinearPCMStreamDescription();
        AudioConverterNew(&audioStreamDescription, &destFormat, &converter);
    }
    return self;
}

建立一個 AudioBufferList,以八秒來計算最大 packet 數量。

UInt32 packetSize = 44100 * 1 * 8;
renderBufferList = (AudioBufferList *)calloc(1, sizeof(UInt32) + sizeof(AudioBuffer));
renderBufferList->mNumberBuffers = 1;
renderBufferList->mBuffers[0].mNumberChannels = 2;
renderBufferList->mBuffers[0].mDataByteSize = packetSize;
renderBufferList->mBuffers[0].mData = calloc(1, packetSize);

4. Render Callback

啟動 audioGraph 與 audioUnit。

AUGraphStart(audioGraph);
AudioOutputUnitStart(audioUnit);

Audio Unit 的 callback 的資料中有,inNumberFrames(packet size)、ioData (sample data)與
inBusNumber(element)。

typedef OSStatus
(*AURenderCallback)(    void *                           inRefCon,
                        AudioUnitRenderActionFlags *  ioActionFlags,
                        const AudioTimeStamp *         inTimeStamp,
                        UInt32                           inBusNumber,
                        UInt32                           inNumberFrames,
                        AudioBufferList * __nullable   ioData);

接下來我們要把資料格式轉換成 LinerPCM 格式,使用 AudioConverterFillComplexBuffer,
參數包含 audio converter、AudioConverter callback、custom data、packet size 以及
audio buffer list。

- (OSStatus)requestNumberOfFrames:(UInt32)inNumberOfFrames ioData:(AudioBufferList  *)inIoData busNumber:(UInt32)inBusNumber buffer:(SKAudioBuffer *)inBuffer
{
    UInt32 packetSize = inNumberOfFrames;
    NSArray *args = @[self, inBuffer];
    OSStatus status = noErr;
    
    @synchronized(inBuffer) {
        status = AudioConverterFillComplexBuffer(converter, AudioConverterFiller, (__bridge void *)(args), &packetSize, renderBufferList, NULL);
    }
}

在 converter callback ,把之前儲存的 audio data 與 audio description 塞到 renderBufferlist 內。

void *data = currentPacketInfo.data;
UInt32 length = (UInt32)currentPacketInfo.packetDescription.mDataByteSize;
ioData->mBuffers[0].mData = data;
ioData->mBuffers[0].mDataByteSize = length;

static AudioStreamPacketDescription aspdesc;
*outDataPacketDescription = &aspdesc;
aspdesc.mDataByteSize = length;
aspdesc.mStartOffset = 0;
aspdesc.mVariableFramesInPacket = 1;

回到剛剛 Audio Unit 的 callback 中,再將 renderBufferlist 已轉換好的資料塞到 ioData 內。

- (OSStatus)requestNumberOfFrames:(UInt32)inNumberOfFrames ioData:(AudioBufferList  *)inIoData busNumber:(UInt32)inBusNumber buffer:(SKAudioBuffer *)inBuffer
{
  ...... 
  
    if (noErr == status && packetSize) {
        inIoData->mNumberBuffers = 1;
        inIoData->mBuffers[0].mNumberChannels = 2;
        inIoData->mBuffers[0].mDataByteSize = renderBufferList->mBuffers[0].mDataByteSize;
        inIoData->mBuffers[0].mData = renderBufferList->mBuffers[0].mData;
        status = noErr;
    }
    return status;
}

Mix Audio

一樣先確認最後要完成的實作流程。

跟剛剛實作 Playing Streaming Music 一樣,要先建立 audio graph 與 output node。

NewAUGraph(&audioGraph);
AUGraphOpen(audioGraph);

AudioComponentDescription outputUnitDescription = [self outputUnitDescription];
AUGraphAddNode(audioGraph, &outputUnitDescription, &outputNode);
AUGraphNodeInfo(audioGraph, outputNode, &outputUnitDescription, &outputAudioUnit);
UInt32 maxFPS = 4096;
AudioUnitSetProperty(outputAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0,&maxFPS, sizeof(maxFPS));

按照實作流程需要多一個屬於 mix unit 的 audio node。

- (AudioComponentDescription)mixUnitDescription
{
    AudioComponentDescription mixerUnitDescription;
    bzero(&mixerUnitDescription, sizeof(AudioComponentDescription));
    mixerUnitDescription.componentType = kAudioUnitType_Mixer;
    mixerUnitDescription.componentSubType = kAudioUnitSubType_MultiChannelMixer;
    mixerUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
    mixerUnitDescription.componentFlags = 0;
    mixerUnitDescription.componentFlagsMask = 0;
    return mixerUnitDescription;
}

AudioComponentDescription mixUnitDescription = [self mixUnitDescription];
AUGraphAddNode(audioGraph, &mixUnitDescription, &mixNode);
AUGraphNodeInfo(audioGraph, mixNode, &mixUnitDescription, &mixAudioUnit);
AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0,&maxFPS, sizeof(maxFPS));

再來把 mix node 的 output 與 remote I/O node 的 input 連接起來。

AUGraphConnectNodeInput(audioGraph, mixNode, 0, outputNode, 0)

設定 remote I/O node input 與 output 的 LinerPCM 格式。

AudioStreamBasicDescription destFormat = LinearPCMStreamDescription();

AudioUnitSetProperty(outputAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(outputAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &destFormat, sizeof(destFormat));

設定 mix node 的 input format 格式,兩個 bus 都需要設定。

AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 1, &destFormat, sizeof(destFormat));

之後一樣設定 render callback,但要注意區分不同 bus 給予不同 callback。

也可以設定某一個 bus 的音量。

AudioUnitSetParameter(mixAudioUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, 0.5, 0);

也可以設定一個 bus 在右聲道,另一個 bus 在左聲道。

AudioUnitSetParameter(mixAudioUnit, kMultiChannelMixerParam_Pan, kAudioUnitScope_Input, 0, 1, 1);
AudioUnitSetParameter(mixAudioUnit, kMultiChannelMixerParam_Pan, kAudioUnitScope_Input, 1, -1, 1);

Hardware Input And Ouput

實作流程

建立 Audio Graph、output node 與 mix node

NewAUGraph(&audioGraph);
AUGraphOpen(audioGraph)

AudioComponentDescription outputUnitDescription = [self outputUnitDescription];
AUGraphAddNode(audioGraph, &outputUnitDescription, &remoteIONode);
AUGraphNodeInfo(audioGraph, remoteIONode, &outputUnitDescription, &remoteIOAudioUnit);
UInt32 maxFPS = 4096;
AudioUnitSetProperty(remoteIOAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0,&maxFPS, sizeof(maxFPS));

AudioComponentDescription mixUnitDescription = [self mixUnitDescription];
AUGraphAddNode(audioGraph, &mixUnitDescription, &mixNode);
AUGraphNodeInfo(audioGraph, mixNode, &mixUnitDescription, &mixAudioUnit);
AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0,&maxFPS, sizeof(maxFPS));

- (AudioComponentDescription)outputUnitDescription
{
    AudioComponentDescription outputUnitDescription;
    bzero(&outputUnitDescription, sizeof(AudioComponentDescription));
    outputUnitDescription.componentType = kAudioUnitType_Output;
    outputUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
    outputUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
    outputUnitDescription.componentFlags = 0;
    outputUnitDescription.componentFlagsMask = 0;
    return outputUnitDescription;
}

- (AudioComponentDescription)mixUnitDescription
{
    AudioComponentDescription mixerUnitDescription;
    bzero(&mixerUnitDescription, sizeof(AudioComponentDescription));
    mixerUnitDescription.componentType = kAudioUnitType_Mixer;
    mixerUnitDescription.componentSubType = kAudioUnitSubType_MultiChannelMixer;
    mixerUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
    mixerUnitDescription.componentFlags = 0;
    mixerUnitDescription.componentFlagsMask = 0;
    return mixerUnitDescription;
}

按照流程,需要把 mix node 的 output 與 remote I/O 的 Input 接起來,
以及 remote I/O 的 output 與 mix node 的 input 接起來。

AUGraphConnectNodeInput(audioGraph, mixNode, 0, remoteIONode, 0);
AUGraphConnectNodeInput(audioGraph, remoteIONode, 1, mixNode, 1);

remote I/O 的 input 預設是關閉,因此要先把它打開,設定 flag = 1。

AudioUnitSetProperty(remoteIOAudioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, busOne, &oneFlag, sizeof(oneFlag));

再設定每個 node 的 input 與 output 格式。

AudioUnitSetProperty(remoteIOAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(remoteIOAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &destFormat, sizeof(destFormat));
        
AudioUnitSetProperty(remoteIOAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 1, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(remoteIOAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &destFormat, sizeof(destFormat));
        
AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &destFormat, sizeof(destFormat));

後續就跟上面例子一樣接好 mix node 的 render callback,
就可以邊播放音樂邊讓自己的聲音透過輸入裝置輸出。

後續

Audio Unit 相對於 Audio Queue 較複雜,前置動作要先依照需求將 graph 與 node 設置完成,
再自行將音訊轉成 LPCM 格式塞到 ioData 內,但是能夠做較多變化,除了上述的例子外,
還有如去人聲(左聲道減右聲道)或 EQ 等處理。
詳細的實作可以參考我的 github AudioUnitSample

 
over 7 years ago

Core Audio 實作的方法上有兩種,分別是 Audio Queue 與 Audio Unit。

這兩種實作方式有什麼差別,該怎麼選擇呢?
Audio Queue 相較於 Audio Unit 操作上較簡易方便,但卻無法對聲音做特殊處理,
Audio Unit 在這一點可以針對聲音做混音或 EQ 等處理,
可以依照 Streaming Player 的需求來決定要使用哪一種實作。

Audio Queue

關於 Audio Queue 的介紹與架構可以先看 Apple 的官方說明
About Audio Queue

Get started !

1. Prepare Audio Queue

AudioQueueRef 用來當作表示 audio queue 物件本身
AudioStreamBasicDescription 用來敘述 audio queue 的播放格式

AudioStreamBasicDescription audioStreamDescription;
AudioQueueRef audioQueue;

AudioQueueNewOutput 是用來產生 Audio Queue output 的方法,
要帶的參數有剛剛建立的 AudioQueueRef 與 AudioStreamBasicDescription,
與 Audio Queue 要資料的 callback 與 runloop。

AudioQueueNewOutput(&audioStreamDescription, audioQueueOutputCallback, (__bridge void *)(self), CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &audioQueue);

加上一個隨時能知道 audio queue 狀態的 listener callback。

AudioQueueAddPropertyListener(audioQueue, kAudioQueueProperty_IsRunning, audioQueuePropertyListenerProc, (__bridge void *)(self));

若想要取得 audio queue 資訊,如音量。

AudioQueueGetParameter(audioQueue, kAudioQueueParam_Volume, &deviceVolume)

若想要對 audio queue 設定某些 property,如 codec。

AudioQueueSetProperty(audioQueue, kAudioQueueProperty_HardwareCodecPolicy, &val, sizeof(UInt32));

2. Prepare Audio Data

  • Parse - 將 data 轉成指定格式的 audio data

AudioFileStreamID 表示 stream parser。

AudioFileStreamID audioFileStreamID;

AudioFileStreamOpen 加上 file type 與 listener 跟 parse result 的 callback。

關於 file type 是設定 parser 需 parse 出來的格式,但是在文件內的敘述是 inFileTypeHint,
也就是說其實 parser 會自動判別所需要的格式,但若格式較特殊可以在這邊提示 parser 參照此 type。

AudioFileStreamOpen((__bridge void *)(self), audioFileStreamPropertyListenerProc, audioFileStreamPacketsProc, kAudioFileMP3Type, &audioFileStreamID);

把下載的 data parse 成 audio data

AudioFileStreamParseBytes(audioFileStreamID, (UInt32)[inData length], [inData bytes], 0);

listener callback 的資料會有 parser(inAudioFileStream)、檔案類型(inPropertyID),
可以把它存成 AudioStreamBasicDescription,用來確認之後 parsed data 是否相同格式或是某些計算。

typedef void (*AudioFileStream_PropertyListenerProc)(
                                            void *                           inClientData,
                                            AudioFileStreamID                inAudioFileStream,
                                            AudioFileStreamPropertyID        inPropertyID,
                                            AudioFileStreamPropertyFlags *    ioFlags);
                      
AudioStreamBasicDescription description;
UInt32 descriptionSize = sizeof(description);
AudioFileStreamGetProperty(inAudioFileStream, inPropertyID, &descriptionSize, &description);

parse 出的資料主要有 data length(inNumberBytes)、packet count(inNumberPackets)、
packet data(inInputData) 與 packet description(inPacketDescriptions)。

typedef void (*AudioFileStream_PacketsProc)(
                                            void *                           inClientData,
                                            UInt32                           inNumberBytes,
                                            UInt32                           inNumberPackets,
                                            const void *                  inInputData,
                                            AudioStreamPacketDescription *inPacketDescriptions);
  • Store - 將 parse 出的資料儲存下來

重要資訊有 packet data 與 packet description,我們使用 NSData 來儲存。

NSMutableData *audioData;
NSMutableData *packetDescData;

每次 parse 出來的 packet data append 到 NSData 內。

[audioData appendBytes:inBytes length:inLength];

再來是存 packet description data,以下是 AudioStreamPacketDescription 的 property,
mStartOffset 代表此 packet 的起始位置,mDataByteSize 則是代表此 packet 的大小。

struct  AudioStreamPacketDescription
{
    SInt64  mStartOffset;
    UInt32  mVariableFramesInPacket;
    UInt32  mDataByteSize;
};

parse 出來的 packet description 內的 mStartOffset 與 mDataByteSize 關係如下圖

parse 出來包含的是一個區塊的 packet data 與 packet description data,
所以每次 packet description 的 mStartOffset 都是從 0 開始,
由於使用 NSData 將 packet data 儲存成一段連續的資料,為了知道每個 packet data 在 NSData 內的實際位置,
每個 packet description 的 mStartOffset 都應該從上次 audio data 存到的位置開始計算。

for (NSUInteger packetIndex = 0; packetIndex < inPacketsCount ; packetIndex ++) {
  inPacketDescriptions[packetIndex].mStartOffset += audioData.length;
}
[packetDescData appendBytes:inPacketDescriptions length:sizeof(AudioStreamPacketDescription) * inPacketsCount];

3. Enqueue Data

啟動之後 audioQueue 就會開始 callback 要資料。

AudioQueueStart(audioQueue, NULL);

enqueue data 需要的資料主要有三個。

1.enqueue 的 packet 數量。

可以透過之前存的 AudioStreamBasicDescription 來計算出一秒有幾個 packet。

- (double)_packetsPerSecond
{
   AudioStreamBasicDescription audioStreamDescription = [delegate usedAudioStreamBasicDescription];
  return audioStreamDescription.mSampleRate / audioStreamDescription.mFramesPerPacket;
}

這樣就能夠決定一次 enqueue 的音檔秒數,假設一次最多只想給 8 秒的 packet 數量。

size_t packetSize = (NSUInteger)([self _packetsPerSecond] * 8.0);
size_t packetCount;
for (packetCount = 0; packetCount < packetSize; packetCount ++) {
}

packetCount 就是這次 enqueue 的 packet 數量。

2.存 audio data 的指標的 array 的指標。

簡單來說就是需要一個雙星號的 array 指標來存 audio data 的指標位置。

const void **data = calloc(packetSize, sizeof(void *));

使用之前存的 packetDescData 當中的 mStartOffset 來算出 audio data 實際資料的位置。

AudioStreamPacketDescription* packetDescriptions = (AudioStreamPacketDescription* )packetDescData.bytes;
for (index = 0; index < packetSize; index ++) {
  data[index] = audioData.bytes + packetDescriptions[readPacketIndex].mStartOffset;
}

data 就是這次 enqueue 的 data 指標。

3.audio data 的 packet description data。

我們所存的 packetDescription 為了要判別每個 packet data 在 NSData 內的位置,
因此有更動過 mStartOffset,現在要還原這個 packet description data,
也就是重新設定 mStartOffset,讓這個 packet description data 是敘述這段 packet data。

size_t offset = 0;
AudioStreamPacketDescription *descs = calloc(packetSize, sizeof(AudioStreamPacketDescription));
for (index = 0; index < packetSize; index ++) {
            memcpy(&(descs[index]), &packetDescriptions[readPacketIndex], sizeof(AudioStreamPacketDescription));
            descs[index].mStartOffset = offset;
            offset += descs[index].mDataByteSize;
}

desc 就是敘述這次 enqueue 的 audio data 資料。


資料準備好之後,就可以開始建立 audioQueueBuffer,須先計算出 buffer 大小。

UInt32 totalSize = 0;

for (index = 0 ; index < packetCount ; index++) {
  totalSize += inPacketDescriptions[index].mDataByteSize;
}
AudioQueueBufferRef buffer;
AudioQueueAllocateBuffer(audioQueue, totalSize, &buffer);
buffer->mAudioDataByteSize = totalSize;

buffer 開好之後就將資料塞進去,然後 enqueue。

for (index = 0 ; index < packetCount ; index++) {
  memcpy(buffer->mAudioData + desc[index].mStartOffset, data[index], desc[index].mDataByteSize);
}
AudioQueueEnqueueBuffer(audioQueue, buffer, packetCount, desc);

後續

Audio Queue 雖然是較底層的 API,但如果深入去了解其實發現操作並不算太難,
上面是從線上取得音檔一直到播出第一秒的聲音為止的步驟,
這是我已經封裝好的 SKAudioQueue,可以給有興趣參考或是想直接使用的人。

 
about 8 years ago

前言

ComponentKit 是 Facebook 團隊開發的 UI Framework
Introducing ComponentKit

ComponentKit gave us performance and reliability gains in News Feed UI Rendering and made the system easier to reason about
Facebook 強調 ComponentKit 改善並強化了 Facbook app 上動態牆的效能,
非常吸引人想去了解這個 framework 究竟有多神奇

開始

Getting Started
在這份文件中,Facebook 大略提到使用 ComponentKit 上的一些細節與概念。

首先,Facebook 使用 Objective-C++ 來實作 ComponentKit,
在文件內有提到為何要使用 Objective-C++,不管原因為何,
對於要使用這個 framework 的開發者而言,首先要做的就是專案內的檔案,也必須跟著是 .mm 檔才行。

再來,由於文件內提到,ComponentKit really shines when used with a UICollectionView.
既然如此,我們就試著用 CompontentKit 來實作 CollectionView。

我們仍然先建立起 UICollectionView 與 UICollectionViewLayout,
在這邊我們其實不用設定 collectionView 的 delegate、dataSource 與 registerCell,
因為當你把 collectionView 給 CKCollectionViewDataSource,
CKCollectionViewDataSource 會用自己定義把這些洗掉,因此設定也沒有什麼意義。

UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.itemSize = CGSizeMake(100.0, 300.0);
UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0.0, 64.0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame)) collectionViewLayout:layout];

CKCollectionViewDataSource 是 ComponentKit 中專為 UICollectionView 設計的元件,
讓你不需要去使用 CKComponentDataSource 去操作更多細節,直接幫你封裝好一個專門的元件,
他扮演的角色跟我們常用的 UICollectionView 或 UITableView 中的 dataSource 類似,
一般的 dataSource 對象是 cell,而這個 dataSource 對象是 component。

在 init 時,帶入的參數比較特別的是 componentProvider,他是一個 protocol,
讓 CKComponentDataSource 可以知道是誰要來負責來實作給予 component 的 class。

CKCollectionViewDataSource *fuckingSource = [[CKCollectionViewDataSource alloc] initWithCollectionView:collectionView supplementaryViewDataSource:nil componentProvider:[self class] context:nil cellConfigurationFunction:nil];

我們可以試試看插入一個 section 跟一個 row,

CKArrayControllerSections sections;
CKArrayControllerInputItems items;
sections.insert(0);
items.insert({0,0}, @"1");
[fuckingSource enqueueChangeset:{sections, items} constrainedSize:{{0,0}, {1000, 1000}}];

在 class 中必須實作 CKComponentProvider 這個 delegate 的 method,
當 CKComponentDataSource 被加入了資料, CKComponentDataSource 會傳一個 model 物件,
必須依照這個 model 給予一個相對應的 CKComponent,而這個 model 就是當時 insert 時帶的 object,
假設我們要在這個 component 內顯示一個 UIView。

+ (CKComponent *)componentForModel:(id<NSObject>)model context:(id<NSObject>)context
{
    return [CKComponent newWithView:{[UIView class], {{@selector(setBackgroundColor:), [UIColor redColor]}}} size:{100.0, 300.0}];
}

那假設我們要一個紅色背景的 UIView 內有一個綠色背景 UILabel,
label y 軸為 50.0,label 的文字顯示我們的 model,要怎麼描述呢?

+ (CKComponent *)componentForModel:(id<NSObject>)model context:(id<NSObject>)context
{
    return [CKCompositeComponent newWithComponent:[CKStackLayoutComponent newWithView:{[UIView class], {{@selector(setBackgroundColor:), [UIColor redColor]}}} size:{100.0, 300.0} style:{} children:{{[CKInsetComponent newWithInsets:{.top = 50.0, .left = 0.0, .bottom = 0.0, .right = 0.0} component:[CKComponent newWithView:{[UILabel class],{{@selector(setBackgroundColor:), [UIColor greenColor]}, {@selector(setText:), [NSString stringWithFormat:@"%@",model]}}} size:{50.0, 50.0}]]}}]];
}

若不是使用 CollectionView,必須要改用 CKComponentDataSource,
基本上 CKCollectionViewDataSource 只是把 CKComponentDataSource 包起來讓你更容易操作,
因此要怎麼使用 CKComponentDataSource,可以去看看 CKCollectionViewDataSource.mm 如何實作。

寫到這邊,我決定要放棄了,因此直接來講結論。

結論

1. 思考如何設計 CompnentKit 的 UI,而不用實際去實作
呼應文件提到的 A simple analogy is to think of a component as a stencil: a fixed description that can be used to paint a view but that is not a view itself.
從上面的例子可以看到,我們不需實際上去 init、addSubView 或是 setFrame,只需要描述我們所需要的 UI

2. immutable models to immutable components
我覺得這個是 ComponentKit 最主要處理的問題,
以 Facebook 的動態牆來思考的話,在使用者進入動態牆的頁面時,不可能將全部的動態牆訊息一次載入,
必然是分段進行載入,因此儲存訊息的 model 必然一直在更新,這時候畫面必須不斷的跟著 model 更新,
這時候使用者又不斷的在 scroll,畫面應該會相當卡。
我們可以從 Dive Deeper 這裡看出,
modle 與 component 在 background thread 之間的關係,也大概明白為何會這樣設計。

3. 應用與實用
ComponentKit 文件內提到許多優勢與好處,若想做一個新的專案,可以從基底開始就使用 ComponentKit 來實作,
但若要導入至既有專案內,檔案、語法或結構都要調整,成本其實是相當高的。
文件內不斷提到 readable 是透過敘述式的語法,可以讓設計者從 UI code 就可以了解 data model,
但我認為對於已經很習慣將 model 跟 view 切開來實作的設計者,反而對於這樣的 code 會感到很困惑。

4. 是個好範本
CompnentKit 提供蠻多很好的設計理念,enqueue Compnent 的概念或是
immutable model 與 UI 非同步更新部分,有很多可以學習的地方。