Farewell to Logdown

曾經Logdown是我覺得對developer來說是最棒的solution,不論是快速地用markdown編寫,或是直覺地上傳圖片,,還可以加上自己的subdomain,這些對我來說都是蠻足夠的。
如今,覺得功能已經不敷使用,也沒有在維護的感覺,而且總有一天會達到free limitation,所以也趁這個機會申請了自己的網域然後自己架一個部落格,所以,以後文章就不會在此發佈了。

以後部落格網址為: http://blog.michaelchen.io/

How to debug Xcode with Xcode 6?

Recently I wrote a xcode plugin and used a trick that is debugging Xcode with Xcode itself. Howere it's not working since Xcode's version update to 6. After googling for while, the main reason is that Xcode's new feature called View Debugging is enable on default. Therefore it may cause the crash and dump the logs like this:

2015-02-10 17:04:34.867 Xcode[13357:9179350] [MT] DVTAssertions: UNCAUGHT EXCEPTION (NSInternalInconsistencyException): Extension Xcode.IDEKit.CmdHandler.DebugSessionStressTest class 'DBGDebugMenuController' not found for required key 'handlerClass'
UserInfo: {
    DVTExtensionClassNameErrorKey = DBGDebugMenuController;
    DVTExtensionIdentifierErrorKey = "Xcode.IDEKit.CmdHandler.DebugSessionStressTest";
    DVTPlugInExecutablePathErrorKey = "/Applications/Xcode.app/Contents/PlugIns/DebuggerUI.ideplugin";
    DVTPlugInIdentifierErrorKey = "com.apple.dt.dbg.DebuggerUI";
}
Hints: None

The solution is go to your debug options: Edit Scheme > Run > Options and uncheck Enable user interface debugging

@Synchronized on Swift

以前在Objective-C上面處理lock/unlock時候,可以用@synchronized去解決像是:

@synchronized {
    // do something
}

可是到了Swift的時候怎麼辦呢?其實很簡單,因為其實@synchronized是幫你轉成這樣:

objc_sync_enter(self)
    // do something
objc_sync_exit(self);

因此在Swift上面可以封裝一層closure,加上Trailing Closures的特性使用起來更帥氣:

func synchronizd(lock: AnyObject, closure:()->()) {
        objc_sync_enter(lock)
        closure()
        objc_sync_exit(lock);
}

synchronizd(self) {
    // do something
}

雖然上面很帥,但是根據stackoverflow上面說有bug,當然比較簡單的解法也有直接用GCD queue來解決:

let lockQueue = dispatch_queue_create("com.test.LockQueue", nil)
dispatch_sync(lockQueue) {
    // code
}

reference:
http://stackoverflow.com/questions/24045895/what-is-the-swift-equivalent-to-objective-cs-synchronized
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html

Markdown 資源分享

現在已經幾乎脫離了Office時代,似乎沒有什麼機會去打開word來寫東西,目前主要的編輯器還是Sublime Text。但如果只是寫一些文件或是筆記的時候還是會希望多一點樣式,一方面是賞心悅目,另一方面當然也是方便閱讀。雖然說現在文字編輯器百百種,網頁上面的WYSIWYG也是琳琅滿目,身為一個geek是不屑再用滑鼠去點擊編輯樣式的,所以我們來介紹一下VIM的使用...誤)。

為了解決這個問題,最好的解決辦法就是使用Markdown。現在網路上的markdown支援格式也有很多種,也因為當初是為了寫github README才開始熟悉語法,因此最熟悉的還是github flavored的markdown。不同的平台或是編輯器也支援度不大相同,但是基本的樣式大家都是差不多的。

此外markdown還可以輸出成多種格式,像是最基本的html或是pdf都可以,因此很多人用來做簡報。而且檔案其實就是純文字檔,不論是要分享、複製貼上都很便利,連版本的diff都可以很輕鬆做,如此多的優點是不是很吸引人呢?現在網路上也是非常盛行的,但是承剛剛所說的因為各平台支援度都不太想同,前陣子也開始有人搞統一spec,以後大家都可以按照規格去寫也比較理想。

其實,當初寫部落格也是一直覺得要調整文章樣式是件很麻煩的事情,後來也是因為logdown支援markdown才龍心大悅的一頭栽入,後來很多blog平台也有extension支援,不過還是覺得如果是built-in的平台最開心,當然可能會覺得比較geek一點XD

最後整理一些自己寫markdown的資源,也順便記錄一下,請笑納:

Github Cheat Sheet

一定要參考的cheat sheet

https://github.com/tiimgreen/github-cheat-sheet


Dillinger

這是我最愛用來寫readme的線上編輯器,因為他就是github flavored,這樣放上去絕對沒問題。而且他最近改版變得好看了(悅)

http://dillinger.io/


StackEdit

chrome上面最愛的markdown editor,風格是我的菜

https://stackedit.io/editor


MOU

Mac上面最帥氣的markdown editor,之前已經停止更新一陣子,現在作者在籌募平台上面想要重新出發,不過也有人直接clone一份在github上面open source,真是太威了

http://25.io/mou/


Marxico

最強悍的Evernote第三方markdown editor,功能完善介面也非常好看,只可惜多台電腦sync有些問題,但是還是非常強大

http://marxi.co/


Sublime Text

很方便拉~也有syntax highlight~單純推一下神器

http://www.sublimetext.com/

NSURL with unicode in Swift

雖然在Swift字串中支援unicode,不過要轉換成NSURL還是要注意一下!因為在NSURLinit(string:)URLString必須是符合RFC 2396,否則會回傳nil。

The URL string with which to initialize the NSURL object. This URL string must conform to URL format as described in RFC 2396, and must not be nil. This method parses URLString according to RFCs 1738 and 1808.

此時剛好我們又用 String Interpolation去填補字串,竟然直接crash而不是給你nil。下面就是失敗的例子:

var str = "中文"
var URL = NSURL(string: "http://example.com/q=\(str)")  // nil

而必須要要加上stringByAddingPercentEncodingWithAllowedCharacters()讓string正確的encode過:

var str = "中文".stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!
var URL = NSURL(string: "http://example.com/q=\(str)")

雖然Swift的字串已經是非常好操作,不過這是Range<String.Index>之後踩到的雷。

iOS 在背景初始化 mapview 會導致crash

平常開發上面我們知道不能在背景使用openGL,否則app會直接crash給你看。像是這篇Technical Q&A QA1766提到要特別注意當app切換到背景時,要確保沒有再處理相關openGL的訊息。

而這次比較特別是在收到memory warnning後,viewController又重新去init一個mapview,因為mapview會使用到openGL而導致crash,最後你會收到類似這樣的crash log:

0   libGPUSupportMercury.dylib     0x334e98a6 gpus_ReturnNotPermittedKillClient + 10
1   libGPUSupportMercury.dylib     0x334ea360 gpusSubmitDataBuffers + 108
2   libGPUSupportMercury.dylib     0x334ea1e0 gldCreateContext + 204
3   GLEngine                         0x2f15e536 gliCreateContextWithShared + 598
4   OpenGLES                         0x2f23aab0 -[EAGLContext initWithAPI:properties:] + 404
5   OpenGLES                         0x2f23a8fa -[EAGLContext initWithAPI:sharegroup:] + 110
6   VectorKit                        0x377d003e ggl::OESContext::OESContext(ggl::GLDevice*, std::__1::shared_ptr<ggl::OESSharegroup>) + 486
7   VectorKit                        0x377c9160 ggl::GLDevice::createRenderer() + 88
8   VectorKit                        0x376e8ce4 -[MDDisplayLayer _createGLLayer] + 164
9   VectorKit                        0x376e8a82 -[MDDisplayLayer init] + 66
10  VectorKit                        0x3741b3ca -[VKMapView initWithGlobe:shouldRasterize:inBackground:] + 482
11  MapKit                           0x2df578dc -[MKBasicMapView initWithFrame:andGlobe:shouldRasterize:] + 356
12  MapKit                           0x2df89b24 -[MKMapView _commonInitFromIB:gestureRecognizerHostView:showsAttribution:] + 968
13  MapKit                           0x2df75c76 -[MKMapView initWithFrame:] + 126

以後還是要多注意一下背景狀態下的memory warning,而且發現使用模擬器是重置不出來的,如果要在實機測試memory warning 可以使用private method:

[[UIApplication sharedApplication] performSelector:@selector(_performMemoryWarning)];

reference:

http://stackoverflow.com/questions/12713781/occasionally-ios-6-mkmapview-crashes-in-initwithframe

MySQL 嚴格模式

最近把舊的php code原封不動地移植到新的機器上時,發現原來的api竟然動不了,似乎是種魔咒,一切如同莫非定律般的發生。環境幾乎都來原來的主機相同,後來發現原本mysql的設定值不一樣。
原來之前在設計table scheme都沒有考慮到預設值,之前有資料庫會去幫你處理問題,不過升級到mysql 5.6.6以上的版本sql_mode會有預設值。

sql_mode = "STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION"

因此只要修改設定檔my.cnfsql_mode中的STRICT_TRANS_TABLES拿掉就可以,不過還是建議好好refactor會比較實在。

Audio Queue 初心者體驗

簡述

Audio Queue 是在Core Audio層級比較高階的API,可以用來錄音及播放音樂,也可以用codec去解析壓縮過的音檔,根據官方文檔知道支援以下格式:

  • Linear PCM.
  • 任何Apple平台支援的音檔格式
  • 任何使用者自己提供的解碼器(codec)

基本架構

  • buffer用來讓audio data暫存
  • buffer queue用來處理buffer的佇列
  • audio queue callback 一些callback讓你去設定及使用

不論是錄音或是播放都是一樣的架構,只是差別在inputoutput的不同而已。

運作方式

Audio Queue從名字上面就可以了解他是一個audio的佇列,一開始先透過AudioQueueAllocateBuffer產生多個AudioQueueBufferRef,這些buffer是用來儲存要播放的音源資料,透過AudioQueue去管理,當AudioQueueDispose的時候也一併被釋放,要播放或是錄音時會透過AudioQueueEnqueueBuffer將buffer讀進queue之中,當buffer使用完之後會重新填滿再enqueue進去,一直循環到播放結束為止。

不免俗放一下Apple 解釋播放音樂的流程圖:

實作流程

整個播放步驟如下:

  1. 定義自己的player結構,用來儲存音源格式、路徑、狀態等等。
  2. 實作AudioQueue播放時候的callback
  3. 計算一個buffer所需的最佳的小
  4. 讀取檔案取得音源資訊
  5. 建立一個AudioQueue準備播放
  6. 配置buffer並排進queue中,設定相關參數後就可以呼叫AudioStart開始播放。
  7. 使用完之後記得AudioDispose釋放記憶體。

其實整個過程是非常清楚,但是因為Apple文件非常的片段,所以在沒有足夠相關背景知識下很難去拼湊完整的程式碼,而且在實作之前建議先讀取Core Audio的Overview,雖然內容非常的冗長,不過因為裡面非常詳細的解釋整個Core Audio的機制,了解後再去撰寫程式會比較知道方向。

首先按照步驟來,我們先定自己的manage state:

// 從文件中可知道建議最佳Buffer數量為3
static const int kNumberOfBuffers = 3;

// 定義自己的strct
typedef struct {
    
   // 檔案格式的描述
 AudioStreamBasicDescription mDataFormat;
    
    // Audio Queue
 AudioQueueRef mQueue;
    
    // 用來暫存的buffer
 AudioQueueBufferRef mBuffers[kNumberOfBuffers];
    
    // 檔案來源
 AudioFileID mAudioFile;
    
    // buffer 所需大小
 UInt32 bufferByteSize;
    
    // 目前讀取到的packet數量
 SInt64 mCurrentPacket;
    
    // 總共的packet
 UInt32 mNumberPacketToRead;
    
    // 音檔packet的描述
 AudioStreamPacketDescription *mPacketDecription;
    
    // 用來判斷是否正在播放
 bool mIsRunning;

} MCAudioPlayerState;

這邊是用一個struct去儲存,是因為參考apple文件去實作,不然網路上有許多範例是直接變成Class的成員變數存取。第二步驟就是去實作AudioQueueNewOutputAudioQueueOutputCallback,也就是要處理output出來的buffer:

static void HandleOutputBuffer(void *inAqData, AudioQueueRef inQueue, AudioQueueBufferRef inBuffer)
{
    MCAudioPlayerState *aqData = inAqData;
    if (aqData->mIsRunning ==0) {
        return;
    }

   // 從檔案讀取出需要播放的packet數量以及使用多少byte
 UInt32 numberBytesReadFromFile;
    UInt32 numberPackets = aqData->mNumberPacketToRead;
    AudioFileReadPackets(aqData->mAudioFile, false, &numberBytesReadFromFile, aqData->mPacketDecription, aqData->mCurrentPacket, &numberPackets, inBuffer->mAudioData);

    if (numberPackets>0) {
       // 設定buffer所使用的byte大小
     inBuffer->mAudioDataByteSize = numberBytesReadFromFile;
        
        // 將buffer放進queue之中
     AudioQueueEnqueueBuffer(aqData->mQueue, inBuffer, (aqData->mPacketDecription ? numberPackets : 0), aqData->mPacketDecription);
        
        // 將讀取packet位置往前移
     aqData->mCurrentPacket += numberPackets;
    }
    else {
       // 如果沒有packet要讀取,代表已經讀完檔案
     AudioQueueStop(aqData->mQueue, false);
        aqData->mIsRunning = false;
    }
}

上面這個步驟是在處理從mAudioFile中取得還有多少packet要讀取,AudioFileReadPackets會把資料放進buffer中的mAudioData裡面,接著設定buffer大小就可以排進queue之中。如果沒有多餘的packet要讀取的話,就將queue關閉。 下一步是要來處理一下buffer大小:

void DeriveBufferSize(AudioStreamBasicDescription asbd, UInt32 maxPacketSize, Float64 seconds, UInt32 *outputBufferSize, UInt32 *outputNumberOfPacketToRead)
{
    static const int maxBufferSize = 0x50000;    //320k
 static const int minBufferSize = 0x4000;     //64k

   // 如果有取得到frame資訊,就去計算一段時間內(seconds)需要取得多少packet
 if (asbd.mFramesPerPacket!=0) {
        Float64 numberPacketForTime = asbd.mSampleRate / asbd.mFramesPerPacket * seconds;
        *outputBufferSize = numberPacketForTime * maxPacketSize;
    }
    else {
       // 如果沒有就取最大packet或是buffer的size
     *outputBufferSize = MAX(maxBufferSize, maxPacketSize);
    }

   // 限制buffer size在定義的range裡面
 if (*outputBufferSize > maxBufferSize && *outputBufferSize > maxPacketSize) {
        *outputBufferSize = maxBufferSize;
    }
    else {
        if (*outputBufferSize < minBufferSize) {
            *outputBufferSize = minBufferSize;
        }
    }

    *outputNumberOfPacketToRead = *outputBufferSize / maxPacketSize;
}

透過DeriveBufferSize去初始化packet數量,這邊比較需要瞭解到packetsample rate以及frame之間的關係,否則不知道在計算什麼數值,目前只了解到buffer太小的話會播不出聲音,應該有一個最佳的範圍但還沒有理解這麼深。
到這步驟我們已經可以開始撰寫讀取檔案的部分,還有取得一些檔案基本資訊,假設我們在自定義的- (instancetype)initWithContentsOfFileURL:(NSURL *)inFileUR裡面開始初始化audioPlayer

- (instancetype)initWithContentsOfFileURL:(NSURL *)inFileURL
{
    self = [super init];
    if (self) {
        
        // 開啟檔案
     CFURLRef url = (__bridge CFURLRef)inFileURL;
        OSStatus status = AudioFileOpenURL(url, kAudioFileReadPermission, 0, &aqData.mAudioFile);
        if (status == noErr) {
            // 透過AudioFileGetProperty從檔案中獲得音檔資訊
            UInt32 dataFormat = sizeof(aqData.mDataFormat);
            AudioFileGetProperty(aqData.mAudioFile, kAudioFilePropertyDataFormat, &dataFormat, &aqData.mDataFormat);
            
            // 建立Audio Queue
            AudioQueueNewOutput(&aqData.mDataFormat, HandleOutputBuffer, &aqData, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &aqData.mQueue)

            // 取出最大的packet size計算buffer會需要
            UInt32 maxPacketSize;
            UInt32 propertySize = sizeof(maxPacketSize);
            AudioFileGetProperty(aqData.mAudioFile, kAudioFilePropertyPacketSizeUpperBound, &propertySize, &maxPacketSize)

            // 計算需要讀取的packet
            DeriveBufferSize(aqData.mDataFormat, maxPacketSize, 0.5, &aqData.bufferByteSize, &aqData.mNumberPacketToRead);

            // 如果是VBR的話會有packet description
            BOOL isFormatVBR = aqData.mDataFormat.mBytesPerPacket == 0 || aqData.mDataFormat.mFramesPerPacket == 0;
            if (isFormatVBR) {
                aqData.mPacketDecription = (AudioStreamPacketDescription *) malloc(aqData.mNumberPacketToRead * sizeof(AudioStreamPacketDescription));
            }
            else {  // CBR不需要設定
                aqData.mPacketDecription = NULL;
            }

            // 調整音量
            Float32 gain = 1.0;
            AudioQueueSetParameter(aqData.mQueue, kAudioQueueParam_Volume, gain);

            // 初始化設定
            aqData.mCurrentPacket = 0;
            aqData.mIsRunning = true;

            // 這邊就是開始初始化buffer並且丟入我們剛剛寫的callback之中取讀取資料
            for (int i = 0; i<kNumberOfBuffers; i++) {
                AudioQueueAllocateBuffer(aqData.mQueue, aqData.bufferByteSize, &aqData.mBuffers[i]);
                HandleOutputBuffer(&aqData, aqData.mQueue, aqData.mBuffers[i]);
            }

            // 萬事俱備後就可以開始播放
            checkStatus(AudioQueuePrime(aqData.mQueue, kNumberOfBuffers, NULL));
            checkStatus(AudioQueueStart(aqData.mQueue, NULL));
        }
    }
    return self;
}

上面其實包含了56的步驟,因為拆開來看不太好去理解,所以整個放在一起就一目了然了。整個過程中先透過AudioFileOpenURL去開啟檔案,接著再去取出音檔的完整資訊,下一步就是建立AudioQueue並指定處理的buffer的callback,也就是剛剛我們時做的HandleOutputBuffer,接著我們透過DeriveBufferSize計算每次取出來的packet數量,最後再判斷是否音檔為VBR格式。因為在VBR格式中的音檔bytes-per-packetframes-per-packet會是不同的,所以mBytesPerPacketmFramesPerPacket會是0,所以我們才會需要一堆AudioStreamPacketDescription去計算要讀取多少byte到記憶體中。以上基本設定做好之後就可以開始播放,不過這邊還稍微調整一下音量,最後再播放之前透過AudioQueuePrime去告訴AudioQueue我們有預先處理3個buffer,然後就可以順利的播放。

最後不要忘記把使用過的資源釋放掉:

- (void)dealloc
{
    AudioQueueDispose(aqData.mQueue, true);
    AudioFileClose(aqData.mAudioFile);
    free(aqData.mPacketDecription);
}

小結

看過Core Audio的Overview內容,只能說非常的枯燥乏味,而且還需要補充不少相關背景知識,不然沒有了解音訊的處理原理是無法很能體會一些關鍵字。重點是看完之後還是無從下手,當然選了比較上層的API下手,給自己建立一點信心,但萬萬沒想到才使用第一個API就遇到不少挫折。首先要了解很多API是用C/C++去實作,所以如果沒有具備c強大的背景後盾(不過大家應該都是沒有這個困擾我想...),是有點難理解他們API的使用方式。再來就是非常不好debug,因為你會先迷惘在每個function回傳的OSStatus,常常是一個非常神秘的數字,還好有google大神可以幫忙解謎一下。後來還在github找到很好的工具,可以直接幫你去爬framework裡面的說明,算是非常好用的debug tool。

最後要面對的是如果出現一些memory問題,是無法用objective-c方式去除錯,因為根本不知道那個address是在指誰,出現這種莫名的錯誤會讓你撞牆好久....然後千萬注意不要typo,否則也是會陷入鬼打牆。這次經驗就是因為typo而用錯function,但萬萬沒想到他們傳入的params都一樣,在模擬器會出現不知所謂的EXC_BAD_ACCESS,竟然實機還可以播放(what the...),一度以為是Apple的bug,但後來重寫一遍後才發覺原來使用錯誤,而且傳入的參數初始方法不同才導致不知名的crash。

發現心得感想比技術內容還多,基本上覺得是如果把文件看的仔仔細細的話,雖然會感到迷惘跟無力但實作應該沒有問題,不過還是建議有一份可以參考的sample會比較好理解,不過自己去摸索到播放的路程會比較坎坷,接下來要練習比較low level的API,感覺Core Audio這趟旅程非常漫長。

iTunes Connect what the...

自從WWDC之後iTunes Connect大改版,雖然有上去看看新的面貌,不過一直沒有機會真正去操作。今天如然有人問我為什麼他上傳App screenshot的時候一直出現Your file could not be loaded. Try again.的錯誤訊息。結果試了半天還是不知道為什麼,最後才爬文看到說原來不能用中文檔名...不然你會看到這個錯誤提示


實在是不知道該說什麼....真的是WTF

iOS Code Protection

大學問

其實這部分是一門很大的學問,方法不止從你的code裡面下手,甚至可以針對complier做調整,諸多方法都是可以達到一定的安全。最好方法是在設計架構的時候就俱有安全意識,比如說機密資料不要明碼儲存,存放的地方也是一大關鍵,還有API連線選擇採用https協定等等方式去提高程式安全性。不過強中自有強中手,一旦被鎖定上了,還是有機會被攻破城門的。儘管如此,我們還是要做到基本保護,這邊就簡單介紹一下如果保護自己辛苦寫的程式碼。

簡單混淆

一般最常見的手法就是class-dump出來看一下有哪些classmethod,因此利用一些技巧可以保護程式,即使class-dump之後仍然無法輕易理解內容,以免程式碼架構外洩後使別人有攻擊的機會。最快速的方式之一就是將method混淆視聽,利用macro去改變method名稱。
現在下面是程式中一個class:

@interface SomeClass : NSObject
- (void)doSomething;
@end

@implementation SomeClass
- (void)doSomething
{
    NSLog(@"I'm doing something.");
}
@end

class-dump出來就可以很清楚明瞭這個class的結構:

//..(略)...
@interface SomeClass : NSObject
{
}

- (void)doSomething;
@end
//..()...

這時候我們加上macro去改變原本method的名稱:

#define SomeClass YouCannotSeeMe
#define doSomething Hahahah

@interface SomeClass : NSObject
- (void)doSomething;
@end

@implementation SomeClass
- (void)doSomething
{
    NSLog(@"I'm doing something.");
}
@end

之後再class-dump結果就變成看不太懂目的的結構:

@interface YouCannotSeeMe : NSObject
{
}
- (void)Hahahah;
@end

這樣一來就算有人dump出來的class都是一堆比較沒有用的東西,因為還是有很多其它方法去破解你的App。此外,這個做法有些缺點,當你使用到KVC的時候會變得很複雜也危險,而且還有method參數有很多的時候也無法使用,如果SomeClass存在多個class之中,可能還會出現意想不到的compile結果。

這時候是借助第三方工具的好時候,利用obfuscator這個關鍵字去搜尋,有找到一個obfuscator-llvm/obfuscator工具會在編譯的時候幫你做混淆,加強code的安全性,但是使用這種工具也是會遇到一些棘手的事情。除此之外,還有另一個比較實際的方法就是用C/C++去撰寫比較重要的部分,至少這麼做不容易被解析,安全性也相對提高。
下面有一個例子用c function來說

void logSomething(id self, SEL _cmd);
@interface SomeClass()
@end

@implementation SomeClass
- (void)doSomething
{
    logSomething(self, _cmd);
}

void logSomething(id self, SEL _cmd)
{
    NSLog(@"This is a c function");
}
@end

如此以來他從dump的資訊中只能取得- (void)doSomething這個method而已,程式碼相對比較安全。

Swift呢

目前的class_dump還是對swift無法使用,但是相信日後很快就可以了。但我突然有一個想法,既然swift可以支援unicode的字元,所以如果我用中文當作method名稱,一來可以不失程式碼的閱讀性,二來也可以達到加密的效果,當然假設破解的人對中文不了解。可以寫出像這個樣的程式碼:

func 加密數字(a: Int) {
    return md5(a)
}

感覺還可以很多發揮,也許是值得探討的一塊。

總結

最後再翻資料的時候看到一個很值得去探討的文章,是Rob Napier寫的Obfuscating Cocoa這篇文章,裡面在探討說到底要花多少精力在做安全保護,提出3個好玩的百分比分別是70%75%還有90%的防護。

基本上做到70%的保護已經足夠了,不會花費太多時間在上面,又可以做到大部份的防護,至於剩下的30%也不影響整個產品。其主要希望把時間跟精力放在真正需要的客戶上面,而不是為了那些30%的人。

接著是做到75%的保護,也是大部分人達到的程度,可能也是最蠢的(他說的..),作者的論點是說為了多麼一點防護,卻花了很多心思在上面而忽略真正產品開發的需求,也可能將原本簡單的解決方案變成更複雜的解決方案,因此這麼做不太經濟。

最後是90%的防護,就是有特別的人專門去做安全防護,完完全全針對安全問題去保護程式,因此這麼做效果也是相對比較好的,當中也提到Apple要避免jailbreak就是符合這個部分。

而作者最後的結論是沒有百分之百的防護,因為一山還有一山高,只要有心還是可以破解你的App。這邊也讓我想起之前看過一本書,裡面提到說「到底要做多少安全機制才是足夠的,基本上只要當他花費精力成本大於你產品的成本時候就足夠了。」簡單來說他必須要發這麼多時間跟精力去破解你的程式,還不如直接花錢去買就好了,還蠻認同這個觀點,當然如果他是惡意或是純興趣破解的話就另當別論了。

9/15更新:有一個不錯的工具提供完整的混淆,連KVO都可以正常使用。參考:https://github.com/Polidea/ios-class-guard

參考資料

http://www.raywenderlich.com/45645/ios-app-security-analysis-part-1
http://robnapier.net/obfuscating-cocoa
http://www.sicpers.info/2010/07/on-private-methods/