2014年12月30日 星期二

Effective Objective-C 讀書會 第2篇

範圍

主題06:瞭解特性
主題07:從內部存取實例變數時,主要採用直接存取的方式
主題14:瞭解什麼是類別物件
主題18:優先採用不可變物件
主題27:使用類別延續類目隱藏實作細節

整理

主題06

一、Property 是什麼?

特性 (property),嗯…我完全不喜歡書中的解釋,也不喜歡「特性」這個中譯名。很不明確,所以筆者要引 OMG UML-Superstructure-formal 的定義:

7.3.45 Property (from Kernel, AssociationClasses, Interfaces)
A property is a structural feature.
A property related to a classifier by ownedAttribute represents an attribute, and it may also represent an association end. It relates an instance of the class to a value or collection of values of the type of the attribute. 
這裡說明了: property 是一種結構特徵。也就是說,它不是一種特例於某個語言的功能。若把類別一分為二:不是資料(或稱「attribue」、「成員變數」),就是行為(或稱「method」或「成員函式」)。沒有別的東西了。那 property 是什麼呢?

簡單講,property 是 attribute 的 subset。但是它不同於一般的 attribute,是因為它還具備有表示
association end 的職責。換句話說,property 是具備與其他類別建立關係的 attribute。這表示它有公開的性質。

在 Java 來說,宣告一個 private attribute 後,建立相對應的 getter/setter methods 就是讓該 attribute 變成一個 property 的例子。而 Objective-C 則是例用 @property 這個關鍵字讓 compiler 知道:這個 attribute 應該有對應的 getter/setter 實作。

所以對於 client end 來說,可以在得到某 instance 後,利用該 instance 提供的 getter/setter methods 對該 instance 所對應的類別中有定義的 attribute 進行存取。

在之前,我們還得在實作程式碼(.m檔)中以 @synthesize 將 attribute 與 property name 進行配對,這樣 compiler 才會在編譯階段把 getter/setter 加入至實作程式碼中。以現今的時間點,相信 synthesize 快被大家遺忘了…。

二、存取 Property 語法:用點號

「點號語法 (dot syntax)」是另一項專屬 property 的語法,背後只是代為呼叫該 property 對應的 getter/setter methods。可以不自己寫 getter/setter methods 及可以使用點號語法,說實在,真的是一個很省時間、很簡潔程式碼的作法。 Java 為何不引入這種語法糖呢…?

三、 Property attributes

好,問題來了:

  1. 如果我們只需要唯讀的 property 怎麼辦?也就是說,只想提供 getter method,但是不提供 setter method,做得到嗎?要怎麼告知 compiler 這項需求?
  2. 我們可以對 setter method 做進一步的控制嗎?根據我們對 heap 內物件保存方式的認知,我們可以在 setter method 內對傳入參數做進一步加工,例如 retain 它,那使用自動生成 setter method 時,該怎麼告知 compiler 這項需求?
  3. 我們可以撰寫自訂名稱的 getter/setter methods 嗎?

以上答案的解答,只在於:宣告 property 時,給予合適的設定即可。這裡用「設定」代稱「Property attribute」是由於不想和 object attribute 混淆。

書中分四類,我分三類:

  1. 和執行緒安全有關:nonatomic 及 atomic。一般用 nonatomic。為什麼?效能問題,而且使用良好的 GCD 操作取代對 NSThread 的控制,就大半可以不用擔心執行緒問題。
  2. 和 getter / setter methods 的生成有關:readwrite (預設值)、readonly(只生成 getter method)、getter= (getter method 的命名)、setter= (setter method 的命名)。
  3. 和記憶體管理「語義」有關:
    1. assign 和 weak :兩者作用類似,但 assign 用在數量型別、weak 用在物件。明顯和 stack /heap 的設計原理有關。
    2. strong:和過去 MRD 時代的 retain 為相同語義。
    3. copy:和 strong 不同的是,它不 retain 物件,而是複製出一份自己用的複本。
    4. unsafe_unretained:在下一回的讀書會中,會有更進一步的討論。

四、非脆弱的ABI (Application Binary Interface)

這裡強調非脆弱的 ABI 讓 Objective-C 能夠在「如果類別定義發生改變,儲存在類別物件的偏移量會被更新」,因此能解決執行期由新類別定義導致的不兼容問題。而克服直接讀取實例變數可能造成偏移量偏差的方法之一,就是使用 property 提供的 getter / setter methods。

主題07

這個主題延伸了主題06對於  Property 的討論。作者強烈地建議:
在內部直接存取實例變數,但是用 Property來設定實例變數。
為什麼呢要直接存取實例變數?

  1. 會比較快,因為不必透過 method dispatch 機制。
  2. 不會觸發 KVO 機制。作者是這麼說的,但是和我的實作不合。直接 assign 給實例變數還是會觸發 KVO 機制。更具體地說,使用 addObserver:forKeyPath:options:context 進行監控的實例變數,還是會觸發 observeValueForKeyPath:ofObject:change:context 方法。

為什麼呢要用 Property來設定實例變數?

  1. 可以遵循由 property attribute 所規範的記憶體管理語義。
  2. 偵錯容易,因為我們可以在 getter / setter methods 裡設定中斷點。
不過這個建議不是哪裡都可以用的。因為:
  1. 由於子類別可以 override 父類別的 getter / setter methods,所以若在 init method 中使用 getter / setter 則可能在子類別建立時發生意想不到的錯誤。
  2. 也有相反的情形:我們必須在 init method 中使用 setter method,因為該實例變數是繼承來的,不透過 setter method 根本存取不到。
  3. 若希望運用 lazy initialization 機制:除非真的用到,不然不產生實例,那麼就該使用 getter method 來進行存取。
聽起來這個建議的例子挺多的…,我覺得可以當成是重構時的思考項,在程式碼初生的階段,可能不用想這麼多。


主題14

大概物件導向語言都有個重要但是卻不常被提及的物件,那就是「類別物件」。我喜歡用 ActiveRecord 的設計語義來詮釋類別與物件:類別是專有名稱,是唯一的;物件是普通名稱,是可數的。

在有 namespace 或 package 結構的程式語言中,類別全名指的是包含該 namespace 或 package 的名稱。例如:在 Java 中,String 類別的全名為「java.lang.String」。而對於 Objective-C 來說則沒有 namespace / package 的部分。就像在 Java 世界不會有第二個 java.lang.String 類別,在 Objective-C 來說,也只有一個 NSString 類別。(正常的情況下)

當程式試圖利用一個類別、生成該類別的實體(物件)時,會先截入該類別至記憶體、生成類別物件,此物件為 singleton object,然後才是以該類別物件做為模子生成對應該類別的物件。

Objective-C  的每個物件的「第一個成員」,被規範為一個用來定義物件自己所屬類別為何的變數。這個變數為「is a (是)」指標。可以觀察一下最基礎的 Class 類別的定義:objc-class.h

在 Objective-C 的 Foundation 框架利用 Class 類別定義中的 「is-a 」指標,在執行期能進行物件型別的檢查,使得 NSObject 物件都能使用所謂的「內省 (introspection)」機制。說穿了,就是在執行期進一步確認某物件的確實類別、實作了哪些方法的判斷機制。

所謂的內省機制,至少包括了兩項方法:isMemberOfClass 及 isKindOfClass。

  1. isMemberOfClass:判斷某物件是否為特定類別的實體。
  2. isKindOfClass:判斷某物件是否為特定類別或其子類別的實體。

這兩個方尤其常用在從 collection 物件中取得單一 element 時。為什麼?來看看 NSArray 及 NSMutableArray 中的方法定義:

  1. - (id)objectAtIndex:(NSUInteger)index;  //NSArray.h
  2. - (void)addObject:(id)anObject; //NSMutableArray

由上述兩個方法得知,自 collections 物件中取出的 element 或要存入的 element 並沒有限定為特定類別的實體。和 Java 5 以後版本比較起來,少了「泛型」語法。在 Java 中,會這樣產生一個「只能由 String 實體為 element 的 List 物件」:
List<string> aList = new ArrayList<string>();
而對於 NSArray 來說,它可以裝載的 element 並不限任何型態,可以說很自由,但是更容易造成混亂(至少提供了造成混亂的空間)。所以在從 NSArray 中取出 element 或設值/插入值時,會比較頻繁地使用 isMemberOfClass 或 isKindOfClass 這兩個內省機制中的方法。


主題18

這個主題的標題是「優先採用不可變物件」。老實說,這應該是個錯誤比大於正確比的標題。因為看到這個標題時,我預設會看到例如 NSString 與 NSMutableString 之類的字眼,但是並沒有。在看過通篇文章後,我覺得標題應該區分為兩個:

  1. 留意物件的可變性是否合理。
  2. 使用 collection 物件時,要留意裡面 element 的可變性。
以書中例子所言,若有個功能近似 DTO (Data Transfer Object) 的物件,此物件的 data source 是來自某 Web Service,那麼由於不會有「回傳改變後的值」的需求,所以該物件的可變性是不合理的。

這種情形下,作者建議使用 property attribute 對該物件加以控制。也就是說,把該物件納入 property 的一員,並使用 readonly 的 property attribute 對其加以約束即可。

此外,假若 property 為 collection 類別時,要留意:我們可以讓 collection 保有的 element 是固定的,但是那不表示 element 的內含值是不可變的。即便如此,也不表示我們必須針對每個 element 的存取以內省機制加以檢查。作者說:
…不要透過內省機制檢視回轉給你的任何物件而判斷它是否可變…你應該不惜代價避免那樣做(不應該使用這樣的內省技術)…

說到底, collection 中的 element 是否可變,端視加入的 element 本身的可變性。而是否加入具有可變性的 element 的判斷,應該由程式的使用方明確表達其意圖,而非由 API 的提供方預設立場地加以限制。更何況,還有其他的技術可以繞過這樣內省技術的檢查,終究是無法完整防禦的。

這個主題的範例最過癮的地方在於示範了類目延續類目 (class-continuation category)如何覆寫 @interface 區段的 property 宣告設定。

類目延續類目…這個名字有夠爛,我接下來只稱呼它:寫在實作檔中的「暱名 category」。

在 @interface 宣告區動段,我們可以宣告某 property 為 readonly,然後在實作檔中的暱名 category 區段再一次宣它它為 readwrite,這樣的話 Runtime Library 還是會替該 property 建立 setter method,只是除了類別本身實作檔外,其他外部類別的物件是無法讀寫該 property 的。

是嗎?不是…其實透過 KVO 機制,Runtime Library 還是可以幫你找到那個實際上有實作的 setter ,然後使用它。所以…只能說使用暱名 category 的方法很不錯,但是也不是完美的。

主題27

其實在上一個主題已經提及了這個主題的主角:類目延續類目 (class-continuation category)。好吧,再說一次:我接下來只稱呼它:寫在實作檔中的「暱名 category」。

簡單講,我們可以把不想寫在公開介面 (.h 檔)上的任何資訊寫在這個暱名 category 中。

這點對於一般程式開發來說,其實不算是非常必要的觀念,但是對於 API 開發者來說,盡可能透露出最少的訊息給外界使用者是極為重要的。因為曝露在公開介面的資訊是不可以隨便修改及移除的,任意地修改將為 API 既有使用者不必要的困擾。

所以不論是有不需要公開的:

  • protocol
  • IBOutlet
  • IBAction
  • method
  • attribute
  • property
都可以寫在暱名 category 中。事實上,以往在 Xcode 中開啟 Assistant Editor 時,會開啟對應 IB 上選取中 ViewController 的 .h 檔,而在 Xcode 5 版後就預設改成開啟 .m 檔,而非 .h,就是這個考量。

使用暱名 category 的另一個好處是可以比較好搭配 Objective-C++程式碼。這部分我完全不在乎,所以不在此討論。時間好少,有更重要的書要念、有更切實際的文章要寫。


2014年12月23日 星期二

《我跟賈伯斯學到的40堂超強工作術》讀後整理

書名:我跟賈伯斯學到的40堂超強工作術
作者:山元賢治 (2004年認識了賈伯斯,成為Apple日本分公司社長)
原書名:「これからの世界」で働く君たちへ

對於書名的,筆者覺得有點什麼了點。原文書名若直譯為中文,大概是「給要在以後的世界工作的你們」,基本上沒有賈伯斯的名字,也沒有強調什麼超強工作術。作者的確當過 Apple 日本分公司的社長,當然也認識賈伯斯,也從他身上學到了不少;不過,作者的經歷也包括 IBM、Oracle 、EMC,內容中他也從這些各別不同公司的文化或人物裡學到了不少事物。所以這個書名,還是覺得有點太耍花招了。覺得原書名就很有誠意了。

這本書強調了不少重複於其他書中會有的概念,不過若加上作者的實際人生作為印證,則多多少少強化了訊息的可靠度及重要性。作者目前還開設了「山元塾」,用以作育能在未來以全世界為基本平台當目標的人才,而這本書,事實上就是出元塾的課程內容加以整理後的作品,雖然並沒有銀彈,但是反複地看類似的書,筆者還是覺得需要的。畢竟出了社會後,自己必須成為自己的老師,而這位老師也需要時時提醒自己的。提醒的方式,就是多多去經歷、閱讀、反思及行動。

整理如下:

一、健康的身體

這裡的「健康」是指充沛體力及易於達成目標的生活作息。試想,要成為以全世界為平台的人,會遇到什麼事?首先,會遇到很多人,隨之而來的是很多的溝通場合;得到達很多地方,要有能適應時差的能力;當然也會有不少會議、不少需要反思進行的決策、很多要睜開五感去學習的人事物。如果有沒相當的體力,是完成不了上述工作的。

書中提到:「體力就是一切的基石。沒有體力,我們甚至無法樂觀地看待人生。」,如果讀者也有一定的工作經驗及年紀的話,看到這句話應該也多有感觸。

在書中以目前 Apple CEO 提姆.庫克為例:「他每天早上都會去健身房運動,一大早就開始跑步、騎腳踏中及舉啞鈴…這樣的生活和工作方式,讓人覺得神清氣爽。」而且這樣的早晨型態並不是 Apple CEO 的特權、個人嗜好,而是作者認識的多數一流商務人士的共同生活模式。

此外,作者也斷言:「沒有領導者是夜貓子」。他提倡的是「早一小時起床,多累積一點實力」。這個論點也好多次被各類工作術相關的書籍所提及過了。原因很簡單:運用早晨人腦集中力最好、思路最敏捷、雜念最少的時刻、有效率地完成對自己而已最重要的事,然後日積月累,就得以收獲。這樣正面能量的正面循環,正是一種使成功變得比較容易的祕訣。

二、好奇心

當我們擁有健康的身體、充沛的精神後,才能回到我們原始的狀態:一種接近童稚的情懷。我指的是:對任何事情具備十足的好奇心、能即刻以行動滿足我們的好奇心,而且毫無保留地接受結果、收接結果。

在本書中提及「領導者一定具備超級強烈的好奇心」,而我認為:好奇心是人的天賦之一,但是若我們沒有健康的身體、充沛的精神,而是被生活中許許多多不懂割捨的煩人小事所累,那麼好奇心自然會關閉。就像靈魂不再自由,活在自己建造好的堅固地生活牢籠中。

光是還原到擁有好奇心還是不足夠的。必須要有的是敏銳的觀察及理解,並且能提出直擊核心的問題。這樣的問題所承載的好奇心更具強烈的意圖,也才能是推進一切的動力來源。

擁有強烈的好奇心後,還得知道:好奇心的答案往往可以成為更多好奇心的起源。正應如此,這樣的循環自然追求的是更好的問題及解答,永無終止的一天。

書上說:「對新產品的滿意度,只維持到上市的下一秒。」

此外,好奇心能增加工作效率。作者在第15篇的標題提到:「做一件事,至少要問七個為什麼」。「工作上最重要的技能,就是『提問力』」。能夠問對問題,才能得到好的解答。而這個提問的源頭,和強烈的好奇心息息相關。

三、熱情

若說好奇心是天賦,熱情則比較接近是後天人格特質造就而成的。作者說:「世界上最強的武器,就是『熱情』」。

與熱情直接相關的,我認為是價值觀、是自我的人生故事,也是一種感動的傳遞。通常熱情不會憑空而來,這點和好奇心不同,熱情植基於對某特定價值的肯定與深掘,而那背後應該存在著一個讓自己感動的故事。由於想傳遞那樣的感動以予別人,所以我們會披星戴月去窮究、去創造及滿足我們的好奇心。所以熱情說來是一種過程的展現,而非一時的激情,這點總是容易被人誤會。

如果有誰認為自己不知道自己的熱情在於何處?所要為何?那大致源於缺欠自我的深掘。就像書中賈伯斯曾說過的那句話一樣:「You should all know already」。所謂「缺欠自我的深掘」說起來好像是自己努力不夠一樣,這點筆者也覺得:的確有些人比較幸運,在人生很早的階段就能知道自己的未來將要是什麼,也知道要到達彼岸該怎麼規劃、也擁有一定的資源及支援。不過,即使沒有那樣的幸運,在尋找自己熱情時也可能遭遇到一些危險及苦痛,但是總比就這樣放棄的好。筆者深深地這麼覺得。

四、時間管理

人生公平的事情不多,大概只有兩件:第一,每個人一天都只有24小時;第二,每個人都會死。

既然每天要投入在工作的時間超過與家人、朋友、寵物相處時間的總合,那麼好的時間管理,就是好的工作管理,甚至就是好的人生管理。

管理二字,我們拆開來看:「管」就是「管控」,筆者認為就是挑選自己要去做的事,而不是一味地窮忙。我們的時間都一樣,誰做的事能更能聚焦在美好的事物上,誰就過得更快樂。這裡可以引入「80/20」法則,或是一般的時間管理矩陣來進行分析。「理」則是「整理」,整理包括「組合」、「排列」。簡單的一般說法是:把性質相同的工作在合適的時間及場合組合在一起完成;此外,任務的執行順序需要依重要性做妥善的優先性管理,重要的先做。

整理其實還包括另一個前提,就是:當事情一團亂時,是沒辦法整理的。必須要先 devide and conquer,也就是得先分化大問題成為合適的小問題。這一點是書上沒有提的,恐怕是被認為是早該知道的預先原則了吧。

「重要的」先做,這句話本身並不精確。有時我們要求的重要性是完整,有時則是速度。這個又再一次牽涉到價值觀或現實性問題。這時我們要保持對原有信念的追求,但也同時不斷地修正與煅化我們追求信念的方式及態度。

此外,人際關係的管理也屬於這個範疇:別浪費自己寶貴的時間在不重要的人身上。除非家人及朋友,不同類型的「導師」也是值得投資時間的對象。如何尋找這樣的「導師」,則有賴自己 open mind 及觀察、主動積極地與其建立關係。而且很幸運的,真正值得跟隨的「導師」通常也樂於分享自己的經驗、或是不保留不矯情地提出直接的建言。

五、站在用戶立場

據說賈伯斯在公司內很常講的一句話是:「我想做出連我母親都會使用的產品」。

所謂站在用戶的立場,我想不是無限上綱的「以客為尊」。而是以自己確信的方式、方向,努力將人類的生活變得更美好。這沒有一定的法則,若存在的話,今天每家公司應該都可以像蘋果一樣成功才對。所以,真正該如何「以客為尊」,也是必須探掘的。賈伯斯已經去另一個世界創造產品了,我們只能從他留下的正面元素(他也不是完人)去嚐試找出自己的信念、方向及步驟。

有個簡潔的重點思維可以作為我們去深掘的指南:製造產品或提供服務的公司及其員工,也必須能夠被自家產品/服務所「感動」才行。這點聽起來很簡單,但是其實因為組織上政治的隔閡或功能性上的分工,很容易弱化團體目標,使願景在一般員工心裡成為不可自求的目標。那樣的目標,會變成公司的目標,而與個人的目標產生差距、甚或毫無關係。這樣生產出來的產品、提供的服務,自然也無法感動用戶、無法產生驚豔的體用者體驗。

六、捨棄昨天的成功

個人或企業或多或少都會累積些成功的經驗,並以此建立行為模式或實體組織。在這個時候,其實當下的我們,並非正處於該模式或組織的初生起點,而是往往已經站在峰點上了。就像雲宵飛車靜止不動、準備在下一秒向下俯衝的位置一樣。換言之,不論之前經歷過什麼成功,在模式或組織被建立的同時,其生命週期就在往歸零的方向不斷邁進。於是模式成為習慣,這讓個人排斥新事物的衝擊、成為即將被汰換的人或是會驅逐良幣的劣幣;於是組織開始僵化、甚至泛政治化,成為不會跳舞的大象。

作者在第24篇的第一句話就是:「工作上唯一不變法則,就是『持續改變』」。

所以,我們應該週期性地思索過去、現在與未來自己的位置、定位及前進的方向及方法。不同範圍大小的週期適合不同主題的思考及檢討,注意觀察自己及他人的心態及反應,千萬別讓昨日的成功造成明天的失敗。

反過來說,樂觀看待失敗、從失敗中汲取寶貴的經驗則是趨近成功的登門階。不論是成功或失敗的經驗,都是一時的,也都是資料庫的寶貴的資源,絕對不該浪費。

七、減法思考

人總是直覺性地使用加法來解決問題,但是其實進入感性時代的現在,減法思考才是把事情做對、做好、讓用戶感到滿意甚或驚豔的訣竅。減法的目的是讓我們能更加的專注於最主要的客戶,試圖使用最少量的操作步驟、功能的組合來讓產品更好用、服務更便利。所以作者提及:「懂得『選擇和專注』,是讓蘋果贏得IT業龍頭的重要關鍵。」

這個道理說不定是多數組數最難得到的 DNA,是必須進一步研究檢討的。


延伸閱讀

《少了一部分,為什麼更值錢?》






2014年12月22日 星期一

Effective Objective-C 讀書會 第1篇

範圍

主題01:熟悉 Objective-C 的根源
主題11:理解 objc_msgSend 的角色
主題29:理解參考計數

整理

主題01

打開 Objective-C 的身份證一看,它出生自1983年,是受Smalltalk語言啟發並擴充標準 ANSI C語言而成的物件導向程式語言。它是商標權是 Apple 的,Apple 也是它的主要開發者,而 Apple 將它作為開發 OS X 與 iOS 相關應用程式的主要程式語言。

Apple 是 Objective-C 的實作者,不過設計者另有其人。查查維基百科後,發現設計者有兩位,生平事績不多的樣子,而且和 Apple 的淵源不太明顯。不曉得那兩位設計者現在會不會覺得受寵若驚,或是因此名利雙收?這些猜想都未經查證了。

Objective-C 的長相挺特別,有很多的「方括號」,方法名稱的長度被人誤稱為「冗長」。冗長的說法大概是對比於成名早於它的前輩程式碼語言,因為那些前輩很愛用縮寫,那些前輩的程式員簡直就當自己是魔法使,程式語言就是他們的咒語一樣難解、難記但是能讓電腦很酷地工作。比較起來,Objective-C冗長的方法名稱,造就了較佳的程式碼可讀性。假若大家同意:自己每天讀程式碼的時間都比寫程式碼的時間長,大概就會同意 Objective-C 的特性之一是「可讀性高」而不是「冗長」。

Objective-C 是怎麼把物件導向的特性加入至 C 語言中的?答案是:將 Smalltalk 的訊息傳遞機制導入 C 語言。這個答案成為主題01的重點。

messaging 架構和 function calling 架構差在哪?很簡單講就是:前者是在 runtime 階段決定執行內容,而後者則是在  compiler 階段就先決定好。由於一般高階程式語言都是:compile 階段
 -> linking 階段 -> runtime 階段的執行順序,所以可知:messaging 架構花在最後的 runtime 階段的執行成本高,而 function calling 架構則在 compile 階段就決定好,這樣每次執行的執行效能應該會比較好。

從可程式化的靈活性來看,由於 messaging 架構是到 runtime 階段才決定實際執行的內容,所以比這一點的話, messaging 架構的靈活度又高於 function calling 架構。也就是說,messaging 架構動態支援比較好。

Objective-C 的 messaging 架構主要是由 Objective-C 的 Runtime Library 實作。身為一般的開發者其實是用不太到 Runtime Library的,除非你正在為 Objective-C 實作 debugger 或跨語言的橋接。這是官方說法。

主題01的第二個重點,則是提到了和 C 語法在記憶體設計相關的機制。簡單講,程式語言在執行期會在主記憶體內規劃出三個區段用來運作語言本身。三個區段分別為 global、stack 及 heap。程序導向語言中的 global 變數或是物件導向語言中的類別屬性會放在 global,而生命週期有明確規範的資料結構會存在 stack,而物件會存放於 heap。

為什麼物件存放在 heap?因為它的生命週期無法用 scope 規範:物件可以在整個  App 裡被各種其他的物件參考,只有在它本身成為孤島或某一孤島的一員時,生命週期才會準備被結束 (還不是立即),所以不符合能放在 stack 的原則。這個其實想想 stack 的工作原理就能明白。


圖片來源:http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap


主題11

這個主題延伸了主題01中的 messaging 機制。
很簡單地講,就是當有一個式子如下:

id value = [someObject messageName: parameter];

而 compiler 會將其轉換成:

id value = object_msgSend(someObject, @selector(messageName:), parameter);

在 runtime 階段,Runtime Library 負責在 someObject 中找尋 messageName:,若找不到就往父物件去找,都找不到時,會引發 message forwarding 機制。找尋 messageName: 的過程會記錄在 someObject 對應的類別有含有的一份 fast map中做為快取,所以 messaging 其實也不會比 function calling 慢多少。

有關於 object_msgSend 相關函式還有很多相關的方法,要進一步鑽研,就得去看 Runtime Library了。


主題29

這個主題延伸自主題01中提到的:物件存放在 heap 區的觀念。

由於物件的生命週期是由還有多少其他的物件擁有/需要他來決定的,而不是由可結構化的程式區塊/scope決定,所以物件被放在 heap 區,而非 stack。而這個主題要回答的問題是:什麼時候在 heap 中的物件該是生?何時該被銷毀?

很簡單的說法是:當有人需要物件,它就因應而生;當再也沒有別的物件需要它時,它就等著被系統回收。(嚴格講起來要提一下 run loop,不過…先這樣理解吧)

Objective-C 用於管理 heap 中物件的方式,叫「參考計數」。想像每個物件身上掛著這個計數器,它一出生時,大抵上計數器上的值為「1」:因為產生該物件的物件將擁有該物件。

所謂的產生,大概說來是指使用 alloc 及 init 相關訊息來新創一個物件的行為。
然後當有別的物件也需要「他」時,可以向「他」傳遞 retain 訊息;反之,可以傳遞 release 訊息來表示不再需要他。

一旦「他」這個物件身上掛的計數器為0,他就會被系統標示成非使用中,然後等著被消滅。

這個機制有個簡單的原則:誰產生/擁有了某物件,就要負責釋放他。這個講來簡單,但是還是挺需要練習和思索的。

此外,還有以下注意事項:

  1. 一旦某物件的計數歸零,就會被標示為非使用中,但是其參考變數仍指向它,很可能造成不可預期又難以尋找的臭蟲。所以若確定 release 後計數應該歸零,最後在下一行緊接著把參考變數指向 nil 值。
  2. 設定  property 特性時,使用 retain / strong 的話,在 setter 執行時,其內容相等於:保留傳入參數、釋放原屬性,然後把傳入參數指派給原屬性。這個可延伸至主題06。
  3. 這個主題中,延伸了 autorelease 這個機制的討論。要很簡單的說,就是把 release 的時機延後到出了 autorelease pool 的範圍再說。這個可延伸至主題34。
  4. 這個主題中,還提到了物件的保留循環 (retain cycle)。有個更傳神的說法是「孤島效應 (island of isolation)」。簡單地說,就是有一組物件互相擁有/需要,但是他們和應用程式中的其他物件都沒有掛勾到,而自成了一個小圈圈。應用程式用不到它們,而它們的計數值也都不為零 (因為小圈圈內的物件互相需要),所以也不會被系統偵測為應該消毀的一群佔著記憶體不工作的物件小團體。這個可延伸至主題33討論。

延伸閱讀

  1. 維基百科 Objective-C
  2. Objective-C Runtime Reference

對於 Swift 語言一點想法

對於 Apple 今年發佈的 Swift 語言,在自己經過學習及試作的經驗之後,我發現自己目前還是不太了解:為什麼  Apple 需要發展這一套語言?

Apple 和生產產品、提供服務的企業都有著傾聽消費者心聲的共同理念。有為數不少的企業也只是口頭上講講,實際執行上總有無數種無奈、而不去切實地去執行該理念的業務理由;但是這樣的理念似乎是 Apple 的 DNA  之一。

但是,上述並不代表 Apple 會去迎合消費者。因為消費者的需求也不總是對的,而 Apple 在做的,是幫他們想到什麼是自己真正的需求。這種減法藝術,在 Apple 的產品上有很明顯的著力。

回到 Swift 語言的開發議題。既然 Apple 並不會刻意去迎合消費者,那我個人覺得:Apple 企圖吸納更多 Script 語言族群開發者的意圖的此類說法,似乎並不合理。事實上,對於習於 Objective-C 的 Mac OS/iOS 原生開發者而言,轉換到 Script 語言也不是一件成本低的事。

扣除 Web App 的開發途徑,原先能在 Mac OS / iOS 上進行開發能使用 C, C++ 及 Objective-C,再加上一些框架的話,也有使用 Ruby 或 JavaScript 的選擇。也就是說,如果 Script 族群的開發者想切入 Mac App 的市場,也早就有相關的方法,而 Apple 提供官方語言的作法,也不一定會被領情。

此外,早期開發 Mac 相關應用程式的族群和開發其他平台相關應用程式的族群,在個性及偏好上就不太是重複太多的一群,對於產出的風格、作法都有不一樣的表現。以目前 App Store 上的發展程度來看,Apple 對於其他原先外在的開發者族群,實在也沒有一定得納入其下的必要;反之,接下來對於 App 品質的審評反而會變得更困難才是。

那麼,倒底為什麼需要發展 Swift 呢?隨便猜猜好了…例如…未來 Safari 也能執行 Swift 嗎?(提供引擎,甚至能和 JavaScript 相轉換?)也就是說,反過來把 Web 也給統一了?

身為 Objective-C 的主要實作者的 Apple 倒底是怎麼佈這一步棋的哩…,就繼續看下去吧。

2014年6月17日 星期二

2014年6月13日 星期五

iOS7 視圖移轉(畫面遷移)



整理成以下圖示:




再補充一個片段,在 iOS7 的 UIViewController.h 中有以下宣告:

@interface UIViewController(CustomTransitioning)

@property (nonatomic,assign) id <UIViewControllerTransitioningDelegate> transitioningDelegate NS_AVAILABLE_IOS(7_0);


@end

至於想知道更詳畫說明的人,大家一起出來喝杯咖啡邊聊吧。哈哈…

2014年6月5日 星期四

iOS 8 自訂鍵盤試作

前兩天 iOS8 的登場,筆者覺得亮點挺多、延伸性很廣。雖然習慣看硬不看軟的國內媒體似乎還是不夠滿意的樣子。

iOS8 提供了Extension 機制,而其中筆者第一個想試作的,就是:自訂鍵盤。

二話不說,趕快來介紹一下。

I. Introduction


1. 未來可以下載實作自訂鍵盤的 App,安裝後就能在「設定」-> 「鍵盤」-> 「新增鍵盤」中看到 "購買的鍵盤" 的項目。只要用以往一樣的方式新增鍵盤,就可以跨 App 地使用各種自訂鍵盤了哦。

2. 開發方面,要先安裝好 Xcode 6 以上版本(含beta),然後使用 iOS8 模擬器 (有測試機直接安裝 iOS8 更好)來運行 App。

II. Implementation


1. 新增 Project
2. 新增 Target,並選定「iOS」-> 「Application extension」-> 「Custom Keyboard」。接下來輸入 Project Name、選定開發語言 (也可以選 Swift)後就完成了哦。
3. Target 新增完後,於 Project navigator 中會產生新 Target 對應的 Group,預設會有 Info.plist 及程式檔 (使用 Objective-C 的話,會有一對 .h/.m 檔)
4. 預設範例裡只有一個鍵:可切換鍵盤,該鍵的配置方式是使用程式碼,而非 IB。我的試作使用 xib,選擇在 initWithNibName:bundle 中自行建立並 add subview。
5. 向左刪除一個字元:[self.textDocumentProxy deleteBackward];
6. 切換鍵盤:[self advanceToNextInputMode];
7. 這裡的 self 指的是 UIInputViewController 子類別的實體。UIInputViewController 是:

NS_CLASS_AVAILABLE_IOS(8_0) @interface UIInputViewController : UIViewController

8. 要在 textField/textView 等元件輸出字串時,使用:[self.textDocumentProxy insertText:@"對應鍵盤邏輯的文字"];
9. 其餘按鍵的部分就和一般的 App 開發無異。

III. Deployment


1. 先選擇 App 原來的 Target 進行程式安裝,再選擇 Extension Target 進行安裝。
2. 於 「設定」-> 「鍵盤」-> 「新增鍵盤」 中加入自訂鍵盤
3. 接下來就可以開任意一個 App (備忘錄、Safari...),切換鍵盤到自訂鍵盤後進行輸入。

IV. Issue


1. 就筆者有試用到的:在 Spotlight 或使用提醒事項新增列表時,無法進行鍵入。
2. 依官網說明,設定好 App 本身的 Target 及 Extension 的 Target 中 Info.plist 的 bundle display name 後,就可以在正常顯示自行的鍵盤名稱。
如下:

 (1) Keyboard group name in Purchased Keyboards list in Settings
 (2) Keyboard name in Settings
 (3) Keyboard name in globe key menu

不過我設定後,第(3)的顯示不正常:當設備的語言環境為中文時,長壓「地球」按鈕時,出現的是「繁體中文」;若設備的語言環境為英文時,則顯示「English」。
希望在後續版本會改掉。

V. Reference


https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/index.html#//apple_ref/doc/uid/TP40014214-CH20-SW1

https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/Keyboard.html

2014年4月14日 星期一

重構教程

重構教程


程式語言:Objective-C
IDE: Xcode 5

有關重構範例:
[來源] IOS7QRCodeDemo
[功能說明] 使用 iOS7 AVFoundation framework 編寫 QRCode 讀取功能。
[特色]不需依賴其他第三方框架

重構理由:
1. 可讀性/可維護性
2. 延展性 (封裝性/模組化能力/置換與轉換能力)
3. 安全性
4. 最佳化能力

重構步驟:

步驟一:將原始碼專案目錄建立 SCM Repository。這裡使用的是 Git。
理由:以利還原及追蹤。這是重要的第一步。

> cd ${QR Codes} 目錄
> git init .

步驟二:審視並調整群組結構
理由:更好的可讀性,加速閱讀及查找速度。

說明:
1. 和主流程相關的部份,我收納在 Main 群組,並以建議閱讀的順序排列。
2. 把 View 和 Model 分開收放。
3. 群組之間的順序也是以建議閱讀的順序排列。
4. 群組的分類方式,可依循的規範參考有以下數種選擇,我們可以擇一或混合使用:
  (1) 依照團隊慣例 (coding guideline)
  (2) 參考泛用性高的框架 (ex: Ruby on Rails)
  (3) 參考建置工具的標準建議 (ex: Maven)


步驟三:審視資訊隱藏
理由:沒必要放在 .h 檔中變成 public 資訊的程式內容,我把它們移至 .m 檔中。
(紅色部分表示「刪除」,綠色部分表示「加入」,下圖表示:將部份程式碼從 ViewController.h 移至 ViewController.m )


技巧:只要註解掉任一行宣告,IDE沒有報錯的話,就代表其沒有任何外部程式碼需要參考它,當然也就能放心進行隱藏。重點是:別做沒必要的曝光。此項技巧是我們使用 IDE 的重要理由之一。


步驟四:重構 _previewLayer.frame = _previewView.bounds

理由:這行程式碼的用意是讓 _previewLayer 的尺寸相等於 _previewView,我打算讓它能被更快看懂。例如,我希望重構結果是這樣:

[self makeSameSize:_previewView resizeView:_previewLayer];

說明:
1. 由於原式是無法進行方法抽取的,所以要分幾個程式進行重構。第一招是:加入區域變數!(紅色部份是原式,我改寫成綠色部份。然後編譯、測試。)


2.  然後進行方法的抽出。


3. 然後再把區域變數 inline 回去。


4. 可以看出來:42~43行的兩個區域變數已經不需要了,就拿掉它吧。



步驟五:協調單一函式內敘句的質量密度


理由:
1. 39, 40 行都是訊息傳送/方法呼叫,但是 42 ~ 53 行的程式碼是述句,在物理結構上就不一致。
2. 由上圖上紅色圓角矩形所示,viewDidLoad 內總共做了5件事,其中3件事是以述句的組合構成。若是把5件事都以同樣的密度陳述,用更直覺得方式表達會更好。

說明:
1. 首先,先把第3、第4件事,用抽出方法進行重構,結果如下:


先刻意留下第 61 行的第5件事,只把第 55 行及第 57 行的第3、第4件事抽出來後,停在這兒說明一件事。請看看第45行的註解,該註解在 registerNotificationForCameraOnOff 方法抽出後,顯示不太重要了!於是,我們可以將它進行移除。

這個動作將代出重構後程式碼的幾個象徵:
 (1) 每個 method 的行數均不多,而且每一行的執行目的,都有一個單一、難再切割的目的。
 (2) 幾乎可以拿掉大部份的註解:因為方法名稱已經足夠傳達程式碼的意圖了。

最後的結果如下:

這裡要注意部分是:為程式碼的閱讀者的習慣提出設想。

一般來說,我們對於文字的閱讀方向是:由上而下,由左至右。為了能讓程式碼的閱讀者有要順暢的閱讀體驗,把 viewDidLoad 方法提至最上方。原因很簡單:閱碼者應該會想知道 viewDidLoad 做了哪幾件事?然後視其需要,再決定要不要 drill down 讀下去。

然後,方法的呼叫順序與方法的擺放順序盡可能一致,有利於查找。

到了這個步驟完成,會發現重構過程式碼應該易讀、看起來乾淨,沒有不需要的註解(通常註解會用不同顏色區分,也會干擾閱讀感覺)。當然,美感方面有個人的差異,這點只能說重構者要 do my best。


步驟六:思考、追究並改善下列程式碼

理由:在 setupCaptureSession 方法中,有下列程式碼,

if (_captureSession)
        return;

這是什麼意思呢?更直覺的寫法應該是:

if (_captureSession!=nil)
        return;

那為什麼 setupCaptureSession 方法中的 _captureSession 何時會等於 nil 呢?
單看程式碼是找不出原因的。但是,若去思考 setupCaptureSession 的被呼叫位置 : viewDidLoad  方法內,大約就可以找到原因:是在防止出現 memory warning 發生時可能造成 _captureSession 不為 nil 而又重複執行一次 setupCaptureSession 的情況發生。

使用方法抽出,以 isMemoryWarningOccuredAndCaptureSessionCreatedAlready 命名之:

- (BOOL)isMemoryWarningOccuredAndCaptureSessionCreatedAlready
{
    return (_captureSession!=nil);
}

- (void)setupCaptureSession
{
    if ([self isMemoryWarningOccuredAndCaptureSessionCreatedAlready])
    {
        return;
    }

   //以下忽略
}

上面的程式碼中,雖然方法名字很長,不過重構者必須以假設:起碼,日後回來看程式碼的自己要能理解程式的真實動作及其意圖才行,而且長名在編譯後就會消失,所以用意圖明確的名稱絕對值得。此外,即使 if 或其他條件句中的述句只有一句,也最好完整加上大括號。在為數者眾、以及我自己團隊的 coding guideline 中,我都會加上此一要求。


步驟七:持續重構 setupCaptureSession method


可以看到第65行是很長的註解,67~72行是在偵測裝置是否擁有相機。若沒有就離開此 method。一樣,用方法抽出進行重構,抽出 isNoVideoCamera 方法:



步驟8:收攏整理 isMemoryWarningOccuredAndCaptureSessionCreatedAlready 和 isNoVideoCamera

理由:可以看出這兩個方法都是 setupCaptureSession 執行的前置條件,所以可以再進一步收攏,並整理好順序如下。

- (void)setupCaptureSession
{
    if ([self isMemoryWarningOccuredAndCaptureSessionCreatedAlready] ||
        [self isNoVideoCamera])
    {
        return;
    }
    
    _captureSession = [[AVCaptureSession alloc] init];
    
    //略
    
}

- (BOOL)isMemoryWarningOccuredAndCaptureSessionCreatedAlready
{
    return (_captureSession!=nil);
}

- (BOOL)isNoVideoCamera
{
    _videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    
    if (_videoDevice == nil) {
        NSLog(@"No video camera on this device!");
        return YES;
    }
    return NO;
}


步驟9:


理由:
觀察或實驗一下第64~65行,會發現:
1. 主要是為了建立、設定 _previewLayer。
2. 建立 _previewLayer 時需要傳入 _captureSession 。

其中,「需要傳入 _captureSession 」成為這兩行寫在 setupCaptureSession 方法中的唯一理由。但是 _previewLayer 本身的建立與設定 capture session 並不是絕對需要關聯在一起的同一件事。

方法:
1. 把 64,65行進行方法抽出,並且使其呼叫順序符合需求。(在 setupCaptureSession 方法後再進行呼叫)



步驟10:質疑 running property 的必要性。

- (void)stopRunning {
    if (!_running) return;
    [_captureSession stopRunning];
    _running = NO;
}

- (void)startRunning
{
    if (_running)
        return;
    [_captureSession startRunning];
    _metadataOutput.metadataObjectTypes = _metadataOutput.availableMetadataObjectTypes;
    _running = YES;
}

理由:
在上面程式碼中, 對 running 這個 property 存在的必要性,感到質疑,因為 captureSession 有一個 instance method : isRunning 應該是相同功能的方法。在單線程運行的 App 裡,像 running 這種 flag 變數的審查不太難。

說明:
1. 註解 running property 並檢視其影響範圍。
2. 以 [_captureSession isRunning] 取代 _running 並移除多餘的程式碼
3. 測試

結果如下:

#pragma mark camera methods

- (void)stopRunning {
    if (![_captureSession isRunning])
    {
        return;
    }
    [_captureSession stopRunning];
}

- (void)startRunning
{
    if ([_captureSession isRunning])
    {
        return;
    }
    [_captureSession startRunning];
    _metadataOutput.metadataObjectTypes = _metadataOutput.availableMetadataObjectTypes;
}

嗯,應該可以再進一步重構。結果如下:

#pragma mark - camera methods

- (void)captureSessionSwitchON:(BOOL)yes
{
    if (yes && ![_captureSession isRunning]) {
        [_captureSession startRunning];
    } else if ((!yes && [_captureSession isRunning])){
        [_captureSession stopRunning];
    }
}

當然,原本 stopRunning 及 startRunning 方法的引用也要跟著修改。


步驟11:質疑雙層 enumerateObjectsUsingBlock 的必要

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    NSMutableSet *foundBarcodes = [[NSMutableSet alloc] init];
    
    [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop)
     {
         
         [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop) {
             NSLog(@"Metadata: %@", obj);
             if ([obj isKindOfClass:[AVMetadataMachineReadableCodeObject class]])
             {
                 AVMetadataMachineReadableCodeObject *code = (AVMetadataMachineReadableCodeObject*)[_previewLayer transformedMetadataObjectForMetadataObject:obj];
                 Barcode *barcode = [self processMetadataObject:code];
                 [foundBarcodes addObject:barcode];
             }
         }];
         
         dispatch_sync(dispatch_get_main_queue(), ^{
             ...(略)
             }];
             
             
        });
         
     }];

說明:

1. 原程式中在 captureOutput:didOutputMetadataObjects:fromConnection 中以雙層巢狀的 enumerateObjectsUsingBlock 呼叫寫成。可以嚐試把內層結構提出與外層結構處於同一層次,並觀察結果是否有異。經過實驗後,我決定把這個巢狀結構移除。

2. 然後將原本內層enumerateObjectsUsingBlock中的程式碼,以方法抽出進行重構。

- (void)findAndBuildBarcodes:(NSMutableSet *)foundBarcodes obj:(AVMetadataObject *)obj
{
    if ([obj isKindOfClass:[AVMetadataMachineReadableCodeObject class]])
    {
        AVMetadataMachineReadableCodeObject *code = (AVMetadataMachineReadableCodeObject*)[_previewLayer transformedMetadataObjectForMetadataObject:obj];
        Barcode *barcode = [self processMetadataObject:code];
        [foundBarcodes addObject:barcode];
    }
}

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    NSMutableSet *foundBarcodes = [[NSMutableSet alloc] init];
    
    [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop)
     {
         [self findAndBuildBarcodes:foundBarcodes obj:obj];
         
         ...(略)    
             
        });
         
     }];
    
         ...(略) 
}

重構12:質疑 captureOutput:didOutputMetadataObjects 方法內,NSMutableSet *foundBarcodes 的合理範圍。

原程式:

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    NSMutableSet *foundBarcodes = [[NSMutableSet alloc] init];
    
    [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop)
     {
              ...(略) 
    }];
    
         ...(略) 
}

決定縮小其範圍,並以 findAndBuildBarcodes 取代 findAndBuildBarcodes:obj。至少,看起來更明白 foundBarcodes 這個 set 只與和它真正有關係的程式碼在一起。(和「…略2」所忽略的程式碼無關)

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    
    [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop)
     {
         NSMutableSet *foundBarcodes = [self findAndBuildBarcodes:obj];
         
         dispatch_sync(dispatch_get_main_queue(), ^{
                 ...(略)
        });
         
     }];
    
    ...(略2)
}


重構13:抽出 removeAllPreviewLayers 及 drawNewPreviewLayers

原程式:

dispatch_sync(dispatch_get_main_queue(), ^{
             // Remove all old layers
             NSArray *allSublayers = [_previewView.layer.sublayers copy];
             [allSublayers enumerateObjectsUsingBlock:^(CALayer *layer, NSUInteger idx, BOOL *stop) {
                 if (layer != _previewLayer) {
                     [layer removeFromSuperlayer];
                 }
             }];
             
             // Add new layers
             [foundBarcodes enumerateObjectsUsingBlock:^(Barcode *barcode, BOOL *stop) {
                 CAShapeLayer *boundingBoxLayer = [CAShapeLayer new];
                 boundingBoxLayer.path = barcode.boundingBoxPath.CGPath;
                 boundingBoxLayer.lineWidth = 2.0f;
                 boundingBoxLayer.strokeColor = [UIColor greenColor].CGColor;
                 boundingBoxLayer.fillColor = [UIColor colorWithRed:0.0f green:1.0f blue:0.0f alpha:0.5f].CGColor;
                 [_previewView.layer addSublayer:boundingBoxLayer];
                 
                 CAShapeLayer *cornersPathLayer = [CAShapeLayer new];
                 cornersPathLayer.path = barcode.cornersPath.CGPath;
                 cornersPathLayer.lineWidth = 2.0f;
                 cornersPathLayer.strokeColor = [UIColor blueColor].CGColor;
                 cornersPathLayer.fillColor = [UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:0.5f].CGColor;
                 [_previewView.layer addSublayer:cornersPathLayer];

             }];

重構後:

現在可以一眼就看出:在 dispatch_sync 中,主要是在 main queue 中完成兩件工作。

dispatch_sync(dispatch_get_main_queue(), ^{
             [self removeAllPreviewLayers];
             [self drawNewPreviewLayers:foundBarcodes];

});

- (void)removeAllPreviewLayers
{
    NSArray *allSublayers = [_previewView.layer.sublayers copy];
    [allSublayers enumerateObjectsUsingBlock:^(CALayer *layer, NSUInteger idx, BOOL *stop) {
        if (layer != _previewLayer) {
            [layer removeFromSuperlayer];
        }
    }];
}

- (void)drawNewPreviewLayers:(NSMutableSet *)foundBarcodes
{
    [foundBarcodes enumerateObjectsUsingBlock:^(Barcode *barcode, BOOL *stop) {
        CAShapeLayer *boundingBoxLayer = [CAShapeLayer new];
        boundingBoxLayer.path = barcode.boundingBoxPath.CGPath;
        boundingBoxLayer.lineWidth = 2.0f;
        boundingBoxLayer.strokeColor = [UIColor greenColor].CGColor;
        boundingBoxLayer.fillColor = [UIColor colorWithRed:0.0f green:1.0f blue:0.0f alpha:0.5f].CGColor;
        [_previewView.layer addSublayer:boundingBoxLayer];
        
        CAShapeLayer *cornersPathLayer = [CAShapeLayer new];
        cornersPathLayer.path = barcode.cornersPath.CGPath;
        cornersPathLayer.lineWidth = 2.0f;
        cornersPathLayer.strokeColor = [UIColor blueColor].CGColor;
        cornersPathLayer.fillColor = [UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:0.5f].CGColor;
        [_previewView.layer addSublayer:cornersPathLayer];
    }];
}

其餘重構:大抵上依循前面步驟,對 creatPathToFirstCorner:code: 方法進行重構。

原程式:

- (Barcode *)processMetadataObject:(AVMetadataMachineReadableCodeObject*)code {
    
    // 1  Query the dictionary of Barcode objects to see if a Barcode with the same contents is already cached.
    Barcode *barcode = [self findOrCreateNewBarcode:code];
    
    // Create the path joining code's corners
    
    // 4 Instantiate cornersPath to store the path joining the four corners of the code.
    CGMutablePathRef cornersPath = CGPathCreateMutable();
    
    // 5 Convert the first corner coordinate to CGPoint instances using some CoreGraphics calls.
    CGPoint point;
    CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)code.corners[0], &point);
    
    // 6 Begin the path at the corner defined in Step 5.
    CGPathMoveToPoint(cornersPath, nil, point.x, point.y);
    
    // 7 Loop through the other three corners, creating the path as you go.
    for (int i = 1; i < code.corners.count; i++) {
        CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)code.corners[i], &point);
        CGPathAddLineToPoint(cornersPath, nil, point.x, point.y);
    }
    
    // 8 Close the path by joining the fourth point to the first point.
    CGPathCloseSubpath(cornersPath);
    
    // 9  Create a UIBezierPath object from cornersPath and store it in the Barcode object
    
    barcode.cornersPath = [UIBezierPath bezierPathWithCGPath:cornersPath];
    CGPathRelease(cornersPath);
    
    // Create the path for the code's bounding box
    
    // 10 Create the bounding box path using bezierPathWithRect:.
    barcode.boundingBoxPath = [UIBezierPath bezierPathWithRect:code.bounds];
    
    // 11  Finally, return the Barcode object.
    return barcode;

}

重構後:

- (Barcode *)processMetadataObject:(AVMetadataMachineReadableCodeObject*)code {
    
    Barcode *barcode = [self findOrCreateNewBarcode:code];
    
    CGMutablePathRef cornersPath = [self creatBarcodeCornerPathes:code];
    
    [self setupBarcodePreviewPath:cornersPath barcode:barcode code:code];
    
    return barcode;

}

- (Barcode *)findOrCreateNewBarcode:(AVMetadataMachineReadableCodeObject *)code {
    
    Barcode *barcode = _barcodes[code.stringValue];
    
    if (barcode == nil) {
        barcode = [Barcode new];
        _barcodes[code.stringValue] = barcode;
    }
    
    barcode.metadataObject = code;
    
    return barcode;
}

- (CGMutablePathRef)creatBarcodeCornerPathes:(AVMetadataMachineReadableCodeObject *)code {
    
    CGPoint point;
    CGMutablePathRef cornersPath = [self creatPathToFirstCorner:&point code:code];
    
    [self buildPathesWithAllCorners:point cornersPath:cornersPath code:code];
    
    CGPathCloseSubpath(cornersPath);
    
    return cornersPath;
}

- (CGMutablePathRef)creatPathToFirstCorner:(CGPoint *)point_p code:(AVMetadataMachineReadableCodeObject *)code {
    
    CGMutablePathRef cornersPath = CGPathCreateMutable();
    CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)code.corners[0], &(*point_p));
    CGPathMoveToPoint(cornersPath, nil, point_p->x, point_p->y);
    
    return cornersPath;
}

- (void)buildPathesWithAllCorners:(CGPoint)point cornersPath:(CGMutablePathRef)cornersPath code:(AVMetadataMachineReadableCodeObject *)code {
    for (int i = 1; i < code.corners.count; i++) {
        CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)code.corners[i], &point);
        CGPathAddLineToPoint(cornersPath, nil, point.x, point.y);
    }
}

- (void)setupBarcodePreviewPath:(CGMutablePathRef)cornersPath
                        barcode:(Barcode *)barcode
                           code:(AVMetadataMachineReadableCodeObject *)code {
    
    barcode.cornersPath = [UIBezierPath bezierPathWithCGPath:cornersPath];
    CGPathRelease(cornersPath);
    
    barcode.boundingBoxPath = [UIBezierPath bezierPathWithRect:code.bounds];
}


結語:

本範例主要在表達一些重構的觀念及技巧,其實在一個步驟內就還包含了數個小步驟,並不像本文中講的那麼簡潔。假若讀者有一起跟著操作,一定知道我在說什麼。

所有的程式碼均集中在 ViewController.m 中,所以這樣的重構結果,算是很基礎的。不過,這卻是重要的基礎。假設,未來有任何功能性需求的擴充,或是希望將這份程式碼延伸成樣版、模組,或是做為其他應用的基礎,相信妥善重構後的結果,一定能讓上述種種變更需求都顯得更優雅、更可行。至少,對於撰寫者而言,應該能保持較佳的工作心情及效率。

最後注意到的一點是:這個範例,正好是很不容易進行 TDD 的範例。至少目前我還沒找到可以把 UIImage 以 AVMetadataMachineReableCodeObject 進行解析的方式。不過儘管如此,在每一個步驟之間,重構者都必須得要不斷進行測試,並確定功能無損後才進行 commit,這個紀律是非常重要的。

2014年3月28日 星期五

Tweaks Framework

Tweaks::Facebook

Tweaks 是由 Facebook 三天前(2014.3.25)所發佈的一套用於協助 prototyping 的框架。

在進行 App 的視覺設計時,最準確的方式就是:將 App 佈署到行動裝置上,並且實際在各種場合下進行各種操作。

什麼叫「各種場合」?

例如:
一套支援計步健身的 App,其色彩設計上的規劃必須考慮使用者在大太陽之下能不能看得清楚?
以銀髮族為角色的 App,其字體大小的最小尺寸、以及不會破壞排版的最大尺寸為多少?
App 內的動畫播放速度要多快不會太擔誤使用者的時間?但是又能看得清楚、得到預想的效果?

什麼叫「各種操作」?

例如:
對於單手持握裝置的使用者而言,App 內提供的按鈕位置是否容易觸發?是否操作旅行的路徑最短?

上述需求,可能會引起的因應動作是:開發者得不斷在程式碼/設定檔中調整各項參數,然後再一次進行至行動裝置的佈署及執行。顯然,有點花時間,對吧?而且,決定視覺設計的人,也可能不是程式開發者本身,這樣一來,要確定哪一個參數合適的過程中的溝通成本就不低。

Tweaks 可以讓上述情境 smooth 一些。

我從 FBTweakExample 中的程式碼來進行解釋。

一、FBTweakValue

_rootViewController.view.backgroundColor = [UIColor colorWithRed:0.9
                                                        green:0.9
                                                         blue:0.9
                                                        alpha:1.0];

上面的程式碼設定了 root 這個視圖控制器所管理的視圖的背景色,以RGB & Alpha 值進行設定。
改寫成下方 statement :

_rootViewController.view.backgroundColor

[UIColor colorWithRed:FBTweakValue(@"Window", @"Color", @"Red", 0.9, 0.0, 1.0)       
                green:FBTweakValue(@"Window", @"Color", @"Green", 0.9, 0.0, 1.0)
                 blue:FBTweakValue(@"Window", @"Color", @"Blue", 0.9, 0.0, 1.0alpha:1.0];

這裡使用了 FBTweakValue 。是什麼意思呢?我們直接來看輸出:



第一個畫面是由 Tweaks 這個框架所提供的 (FBTweakViewController)。請注意看到第一個畫面中的「Window」row 和第二畫面中的「COLOR」session,請對應於上面程式碼中 FBTweakValue 的第一、第二個參數。而第二畫面中的 Red 就是對應第三個參數。我想大家也能猜出:第四個參數就是「預設值 0.9」。第五、第六個參數是最小值和最大值。

也就是說:只要我們將程式裡的某個值 (value) 代換成 FBTweakValue 這個 macro 的話,在 FBTweakViewController 啟動後就會把前三項參數產生為第一畫面列表中的一個列 (UITableViewCell)、第二畫面中的分段(Section) 及 分段 中的列。

以此例來說,提供了浮點數型別的三個數字作為第4~6個參數,FBTweakViewController 會自動生成 step 元件(兩個按鈕,一個減一個加)。

我們可以透過點按 step 元件來改變顏色的RGB值。

要注意的一點是:設定完之後,要把 App 先從背景移除後再開啟,該值才會生效。什麼?不能直接生效嗎?可以,不過得換個 macro 來用:FBTweakBind。


二、FBTweakBind

_label.text = @“Tweaks”;

上方的程式碼中,把標籤 _label 的文字設定為 Content。我們以下方的程式進行改寫:

  FBTweakBind(_label, text, @"Content", @"Text", @"String", @"Tweaks");

第一個參數是 UI 元件,第二個參數則是該元件的屬性第三 ~ 五個參數…直接看圖吧…,第六個參數則為預設值。


可以看出來,第三~五個參數是怎麼被產生出設定用的 UI 元件的。
這裡因為用的是 FBTweakBind 這個 macro ,所以值被變更的同時,回到 App 去看的話,就能馬上看到效果的變化。


三、FBTweakInline

上面提及的兩個 macro 有個共同之處:都是和某個 UI 元件的特定屬性相關。透過 FBTweakInline 可以用觀察者模式對某個設定項的值進行偵測,並於該值改變時調整 App 內任何的設定參數(不綁特定 UI 元件)。直接看實例吧:

_flipTweak = FBTweakInline(@"Window", @"Effects", @"Upside Down", NO);
  [_flipTweak addObserver:self];

- (void)tweakDidChange:(FBTweak *)tweak
{
  if (tweak == _flipTweak) {
    _window.layer.sublayerTransform = CATransform3DMakeScale(1.0, [_flipTweak.currentValue boolValue] ? -1.0 : 1.0, 1.0);
  }
}

  

畫面上的  EFFECTS -> Upside Down 預設值為 NO,並由程式碼的 host class 作為觀察者。只要 Upside Down 的值發者變化,則 call-back method : tweakDidChange 會被呼叫,然後就可以在該 method 內指定 App 要因應做何改變了(此範例是把螢幕轉180度,變成上下顛倒)。


以上就是 FBTweakExample 中和 FBTweaks 框架有關的示範了。


結語:

1. 可以用於 App 的設計階段。假設 coder 運用地夠純熟,大概不會太影響開發速度;UI designer 可以自行透過介面做各項參數的調整而不需要一再和 coder 反複針對單一參數進行溝通。
2. FBTweakExample 的寫法是為了容易說明,不過在程式碼中嵌入 FBTweak macro 時也會對程式碼 intention 的表達有影響:閱讀流暢度會受影響。要有更好的封裝:同時也得防止封裝影響偵錯。
3. 用於 Production … 嗯…持保留態度。
4. 可以看出這套框架應該能減少 UI design coder 之間的溝通成本,這個應該很合適以不斷修正體驗設計的開發團隊。對於 UI design program develop 流程分開且 one-step 執行 (non-iteration) 的團隊則不適用。