阿里三面試題

1.dSYM你是如何分析的?

2.多線程有哪幾種?你更傾向于哪一種?

3.單例弊端?

4.如何把異步線程轉換成同步任務進行單元測試?

5.介紹下App啟動的完成過程?

6.比如App啟動過慢,你可能想到的因素有哪些?

7.0x8badf00d表示是什么?

8.怎么防止反編譯?

9.說說你遇到到的技術難點?

10.說說你了解的第三方原理或底層知識?


1.dSYM你是如何分析的?

什么是 dSYM 文件

Xcode編譯項目后,我們會看到一個同名的 dSYM 文件,dSYM 是保存 16 進制函數地址映射信息的中轉文件,我們調試的 symbols 都會包含在這個文件中,并且每次編譯項目的時候都會生成一個新的 dSYM 文件,位于 /Users/<用戶名>/Library/Developer/Xcode/Archives 目錄下,對于每一個發布版本我們都很有必要保存對應的 Archives 文件 ( AUTOMATICALLY SAVE THE DSYM FILES 這篇文章介紹了通過腳本每次編譯后都自動保存 dSYM 文件)。

dSYM 文件有什么作用

當我們軟件 release 模式打包或上線后,不會像我們在 Xcode 中那樣直觀的看到用崩潰的錯誤,這個時候我們就需要分析 crash report 文件了,iOS 設備中會有日志文件保存我們每個應用出錯的函數內存地址,通過 Xcode 的 Organizer 可以將 iOS 設備中的 DeviceLog 導出成 crash 文件,這個時候我們就可以通過出錯的函數地址去查詢 dSYM 文件中程序對應的函數名和文件名。大前提是我們需要有軟件版本對應的 dSYM 文件,這也是為什么我們很有必要保存每個發布版本的 Archives 文件了。

如何將文件一一對應

每一個 xx.app 和 xx.app.dSYM 文件都有對應的 UUID,crash 文件也有自己的 UUID,只要這三個文件的 UUID 一致,我們就可以通過他們解析出正確的錯誤函數信息了。

1.查看 xx.app 文件的 UUID,terminal 中輸入命令 :

dwarfdump --uuid xx.app/xx (xx代表你的項目名)

2.查看 xx.app.dSYM 文件的 UUID ,在 terminal 中輸入命令:

dwarfdump --uuid xx.app.dSYM

3.crash 文件內第一行 Incident Identifier 就是該 crash 文件的 UUID。

dSYM工具

于是我抽了幾個小時的時間將這些命令封裝到一個應用中,也為以后解決bug提供了便利

使用步驟:

1.將打包發布軟件時的xcarchive文件拖入軟件窗口內的任意位置(支持多個文件同時拖入,注意:文件名不要包含空格)

2.選中任意一個版本的xcarchive文件,右邊會列出該xcarchive文件支持的CPU類型,選中錯誤對應的CPU類型。

3.對比錯誤給出的UUID和工具界面中給出的UUID是否一致。

4.將錯誤地址輸入工具的文本框中,點擊分析。

2.多線程有哪幾種?你更傾向于哪一種?

第一種:pthread

.特點:

1)一套通用的多線程API

2)適用于Unix\Linux\Windows等系統

3)跨平臺\可移植

4)使用難度大

b.使用語言:c語言

c.使用頻率:幾乎不用

d.線程生命周期:由程序員進行管理

第二種:NSThread

a.特點:

1)使用更加面向對象

2)簡單易用,可直接操作線程對象

b.使用語言:OC語言

c.使用頻率:偶爾使用

d.線程生命周期:由程序員進行管理

第三種:GCD

a.特點:

1)旨在替代NSThread等線程技術

2)充分利用設備的多核(自動)

b.使用語言:C語言

c.使用頻率:經常使用

d.線程生命周期:自動管理

第四種:NSOperation

a.特點:

1)基于GCD(底層是GCD)

2)比GCD多了一些更簡單實用的功能

3)使用更加面向對象

b.使用語言:OC語言

c.使用頻率:經常使用

d.線程生命周期:自動管理

多線程的原理

同一時間,CPU只能處理1條線程,只有1條線程在工作(執行)

多線程并發(同時)執行,其實是CPU快速地在多條線程之間調度(切換)

如果CPU調度線程的時間足夠快,就造成了多線程并發執行的假象

思考:如果線程非常非常多,會發生什么情況?

CPU會在N多線程之間調度,CPU會累死,消耗大量的CPU資源

每條線程被調度執行的頻次會降低(線程的執行效率降低)

多線程的優點

能適當提高程序的執行效率

能適當提高資源利用率(CPU、內存利用率)

多線程的缺點

開啟線程需要占用一定的內存空間(默認情況下,主線程占用1M,子線程占用512KB),如果開啟大量的線程,會占用大量的內存空間,降低程序的性能

線程越多,CPU在調度線程上的開銷就越大

程序設計更加復雜:比如線程之間的通信、多線程的數據共享

你更傾向于哪一種?

傾向于GCD:

GCD 技術是一個輕量的,底層實現隱藏的神奇技術,我們能夠通過GCD和block輕松實現多線程編程,有時候,GCD相比其他系統提供的多線程方法更加有效,當然,有時候GCD不是最佳選擇,另一個多線程編程的技術 NSOprationQueue 讓我們能夠將后臺線程以隊列方式依序執行,并提供更多操作的入口,這和 GCD 的實現有些類似。

這種類似不是一個巧合,在早期,MacOX 與 iOS 的程序都普遍采用Operation Queue來進行編寫后臺線程代碼,而之后出現的GCD技術大體是依照前者的原則來實現的,而隨著GCD的普及,在iOS 4 與 MacOS X 10.6以后,Operation Queue的底層實現都是用GCD來實現的。

那這兩者直接有什么區別呢?

1.?? ?GCD是底層的C語言構成的API,而NSOperationQueue及相關對象是Objc的對象。在GCD中,在隊列中執行的是由block構成的任務,這是一個輕量級的數據結構;而Operation作為一個對象,為我們提供了更多的選擇;

2.?? ?在NSOperationQueue中,我們可以隨時取消已經設定要準備執行的任務(當然,已經開始的任務就無法阻止了),而GCD沒法停止已經加入queue的block(其實是有的,但需要許多復雜的代碼);

3.?? ?NSOperation能夠方便地設置依賴關系,我們可以讓一個Operation依賴于另一個Operation,這樣的話盡管兩個Operation處于同一個并行隊列中,但前者會直到后者執行完畢后再執行;

4.?? ?我們能將KVO應用在NSOperation中,可以監聽一個Operation是否完成或取消,這樣子能比GCD更加有效地掌控我們執行的后臺任務;

5.?? ?在NSOperation中,我們能夠設置NSOperation的priority優先級,能夠使同一個并行隊列中的任務區分先后地執行,而在GCD中,我們只能區分不同任務隊列的優先級,如果要區分block任務的優先級,也需要大量的復雜代碼;

6.?? ?我們能夠對NSOperation進行繼承,在這之上添加成員變量與成員方法,提高整個代碼的復用度,這比簡單地將block任務排入執行隊列更有自由度,能夠在其之上添加更多自定制的功能。

總的來說,Operation queue 提供了更多你在編寫多線程程序時需要的功能,并隱藏了許多線程調度,線程取消與線程優先級的復雜代碼,為我們提供簡單的API入口。從編程原則來說,一般我們需要盡可能的使用高等級、封裝完美的API,在必須時才使用底層API。但是我認為當我們的需求能夠以更簡單的底層代碼完成的時候,簡潔的GCD或許是個更好的選擇,而Operation queue 為我們提供能更多的選擇。

傾向于:NSOperation

NSOperation相對于GCD:

1,NSOperation擁有更多的函數可用,具體查看api。NSOperationQueue 是在GCD基礎上實現的,只不過是GCD更高一層的抽象。

2,在NSOperationQueue中,可以建立各個NSOperation之間的依賴關系。

3,NSOperationQueue支持KVO。可以監測operation是否正在執行(isExecuted)、是否結束(isFinished),是否取消(isCanceld)

4,GCD 只支持FIFO 的隊列,而NSOperationQueue可以調整隊列的執行順序(通過調整權重)。NSOperationQueue可以方便的管理并發、NSOperation之間的優先級。

使用NSOperation的情況:各個操作之間有依賴關系、操作需要取消暫停、并發管理、控制操作之間優先級,限制同時能執行的線程數量.讓線程在某時刻停止/繼續等。

使用GCD的情況:一般的需求很簡單的多線程操作,用GCD都可以了,簡單高效。

從編程原則來說,一般我們需要盡可能的使用高等級、封裝完美的API,在必須時才使用底層API。

當需求簡單,簡潔的GCD或許是個更好的選擇,而Operation queue 為我們提供能更多的選擇。

3.單例弊端?

優點:

1:一個類只被實例化一次,提供了對唯一實例的受控訪問。

2:節省系統資源

3:允許可變數目的實例。

缺點:

1:一個類只有一個對象,可能造成責任過重,在一定程度上違背了“單一職責原則”。

2:由于單利模式中沒有抽象層,因此單例類的擴展有很大的困難。

3:濫用單例將帶來一些負面問題,如為了節省資源將數據庫連接池對象設計為的單例類,可能會導致共享連接池對象的程序過多而出現連接池溢出;如果實例化的對象長時間不被利用,系統會認為是垃圾而被回收,這將導致對象狀態的丟失。

4.如何把異步線程轉換成同步任務進行單元測試?

在項目中的應用:強制把異步任務轉換為同步任務來方便進行單元測試

下面是 Parse 的一段代碼:

@interface PFEventuallyQueueTestHelper : NSObject {

dispatch_semaphore_t events[PFEventuallyQueueEventCount];

}

- (void)clear;

- (void)notify:(PFEventuallyQueueTestHelperEvent)event;

- (BOOL)waitFor:(PFEventuallyQueueTestHelperEvent)event;

注釋是這樣寫的:

PFEventuallyQueueTestHelper gets notifications of various events happening in the command cache, // so that tests can be synchronized. See CommandTests.m for examples of how to use this.

強制把異步任務轉換為同步任務來方便進行單元測試。這個用途信號量是最合適的用途。但注意并不推薦應用到除此之外的其它場景!

這種異步轉同步便于單元測試的用法類似于下面的寫法:

#define WAIT_FOREVER [self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:DBL_MAX];

#define NOTIFY [self notify:XCTAsyncTestCaseStatusSucceeded];

- (void)testInstallationMutated {

NSDictionary *dict = [self jsonWithFileName:@"TestInstallationSave"];

AVInstallation *installation = [AVInstallation currentInstallation];

[installation objectFromDictionary:dict];

[installation setObject:@(YES) forKey:@"enableNoDisturb"];

[installation saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {

XCTAssertNil(error);

NOTIFY;

}];

WAIT;

}

信號量屬性底層工具,他雖然非常強大,但在多數需要使用它的場合,最好從設計角度重新考慮,看是否可以不用,應該優先考慮使用諸如操作隊列這樣的高級工具。通常可以通過增加一個分派隊列配合 dispatch_suspend ,或者通過其它方式分解操作來避免使用信號量。信號量并非不好,只是它本身是鎖,能不使用就不用。盡量用 cocoa 框架中的高級抽象,信號量非常接近底層。所以除了上面的例子是最佳應用場景外,不推薦應用到除此之外的其它場景!

《關于dispatch_semaphore的使用》 中有這樣的描述:

關于信號量,一般可以用停車來比喻。

停車場剩余4個車位,那么即使同時來了四輛車也能停的下。如果此時來了五輛車,那么就有一輛需要等待。

信號量的值就相當于剩余車位的數目,dispatch_semaphore_wait函數就相當于來了一輛車,

dispatch_semaphore_signal,就相當于走了一輛車。停車位的剩余數目在初始化的時候就已經指明了(dispatch_semaphore_create(long value))

調用一次dispatch_semaphore_signal,剩余的車位就增加一個;調用一次dispatch_semaphore_wait剩余車位就減少一個;

當剩余車位為0時,再來車(即調用dispatch_semaphore_wait)就只能等待。有可能同時有幾輛車等待一個停車位。有些車主

沒有耐心,給自己設定了一段等待時間,這段時間內等不到停車位就走了,如果等到了就開進去停車。而有些車主就像把車停在這,

所以就一直等下去。

《GCD dispatch_semaphore 信號量 協調線程同步》也有類似的比喻:

以一個停車場是運作為例。為了簡單起見,假設停車場只有三個車位,一開始三個車位都是空的。這時如果同時來了五輛車,看門人允許其中三輛不受阻礙的進入,然后放下車攔,剩下的車則必須在入口等待,此后來的車也都不得不在入口處等待。這時,有一輛車離開停車場,看門人得知后,打開車攔,放入一輛,如果又離開兩輛,則又可以放入兩輛,如此往復。

在這個停車場系統中,車位是公共資源,每輛車好比一個線程,看門人起的就是信號量的作用。 更進一步,信號量的特性如下:信號量是一個非負整數(車位數),所有通過它的線程(車輛)都會將該整數減一(通過它當然是為了使用資源),當該整數值為零時,所有試圖通過它的線程都將處于等待狀態。在信號量上我們定義兩種操作: Wait(等待) 和 Release(釋放)。 當一個線程調用Wait(等待)操作時,它要么通過然后將信號量減一,要么一直等下去,直到信號量大于一或超時。Release(釋放)實際上是在信號量上執行加操作,對應于車輛離開停車場,該操作之所以叫做“釋放”是因為加操作實際上是釋放了由信號量守護的資源。

這個比喻里可以用一個表格來表示:

5.介紹下App啟動的完成過程?

1. App啟動過程

??? ?解析Info.plist

??? ?加載相關信息,例如如閃屏

??? ?沙箱建立、權限檢查

??? ?Mach-O加載

??? ?如果是胖二進制文件,尋找合適當前CPU類別的部分

??? ?加載所有依賴的Mach-O文件(遞歸調用Mach-O加載的方法)

??? ?定位內部、外部指針引用,例如字符串、函數等

??? ?執行聲明為__attribute__((constructor))的C函數

??? ?加載類擴展(Category)中的方法

??? ?C++靜態對象加載、調用ObjC的 +load 函數

??? ?程序執行

·?? ?1.main函數

·?? ?2.執行UIApplicationMain函數

·?? ?  1.創建UIApplication對象

·?? ?  2.創建UIApplicationDelegate對象并復制

·?? ?  3.讀取配置文件info.plist,設置程序啟動的一些屬性,(關于info.plist的內容可網上搜索下)

·?? ?  4.創建應用程序的Main Runloop循環

·?? ?3.UIApplicationDelegate對象開始處理監聽到的事件

·?? ?  1.程序啟動成功之后,首先調用application:didFinishLaunchingWithOptions:方法,

·?? ?  如果info.plist文件中配置了啟動storyboard文件名,則加載storyboard文件。

·?? ?  如果沒有配置,則根據代碼來創建UIWindow--->UIWindow的rootViewController-->顯示

6.比如App啟動過慢,你可能想到的因素有哪些?

1. 影響啟動性能的因素

App啟動過程中每一個步驟都會影響啟動性能,但是有些部分所消耗的時間少之又少,另外有些部分根本無法避免,考慮到投入產出比,我們只列出我們可以優化的部分:

main()函數之前耗時的影響因素

??? ?動態庫加載越多,啟動越慢。

??? ?ObjC類越多,啟動越慢

??? ?C的constructor函數越多,啟動越慢

??? ?C++靜態對象越多,啟動越慢

??? ?ObjC的+load越多,啟動越慢

實驗證明,在ObjC類的數目一樣多的情況下,需要加載的動態庫越多,App啟動就越慢。同樣的,在動態庫一樣多的情況下,ObjC的類越多,App的啟動也越慢。需要加載的動態庫從1個上升到10個的時候,用戶幾乎感知不到任何分別,但從10個上升到100個的時候就會變得十分明顯。同理,100個類和1000個類,可能也很難查察覺得出,但1000個類和10000個類的分別就開始明顯起來。

同樣的,盡量不要寫__attribute__((constructor))的C函數,也盡量不要用到C++的靜態對象;至于ObjC的+load方法,似乎大家已經習慣不用它了。任何情況下,能用dispatch_once()來完成的,就盡量不要用到以上的方法。

main()函數之后耗時的影響因素

??? ?執行main()函數的耗時

??? ?執行applicationWillFinishLaunching的耗時

??? ?rootViewController及其childViewController的加載、view及其subviews的加載

applicationWillFinishLaunching的耗時

如果有這樣這樣的代碼:

答案是:

1.?? ?-[MQQTabBarController viewDidLoad]

2.?? ?-[MQQTab1ViewController viewDidLoad]

3.?? ?-[AppDelegate application:didFinishLaunchingWithOptions:]

4.?? ?-[MQQTab2ViewController viewDidLoad] (點擊了第二個tab之后加載)

5.?? ?-[MQQTab3ViewController viewDidLoad] (點擊了第三個tab之后加載)

一般而言,大部分情況下我們都會把界面的初始化過程放在viewDidLoad,但是這個過程會影響消耗啟動的時間。特別是在類似TabBarController這種會嵌套childViewController的ViewController的情況,它也會把部分children也初始化,因此各種viewDidLoad會遞歸的進行。

最簡單的解決的方法,是把viewController延后加載,但實際上這屬于一種掩耳盜鈴,確實,applicationWillFinishLaunching的耗時是降下來了,但用戶體驗上并沒有感覺變快。

更好一點的解決方法有點類似facebook,主視圖會第一時間加載,但里面的數據和界面都會延后加載,這樣用戶就會階段性的獲得視覺上的變化,從而在視覺體驗上感覺App啟動得很快。

【第二部分】優化的目標

由于每個App的情況有所不同,需要加載的數據量也有所不同,事實上我們無法使用一種統一的標準來衡量不同的App。蘋果。

??? ?應該在400ms內完成main()函數之前的加載

??? ?整體過程耗時不能超過20秒,否則系統會kill掉進程,App啟動失敗

400ms內完成main()函數前的加載的建議值是怎樣定出來的呢?其實我也沒有太深究過這個問題,但是,當用戶點擊了一個App的圖標時,iOS做動畫到閃屏圖出現的時長正好是這個數字,我想也許跟這個有關。

針對不同規模的App,我們的目標應該有所取舍。例如,對于像手機QQ這種集整個SNG的代碼大成擼出來的App,對動態庫的使用在所難免,但對于WiFi管家,由于在用戶連接WiFi的時候需要非常快速的響應,所以快速啟動就非常重要。

那么,如何定制優化的目標呢?首先,要確定啟動性能的界限,例如,在各種App性能的指標中,哪一此屬于啟動性能的范疇,哪一些則于App的流暢度性能?我認為應該首先把啟動過程分為四個部分:

1.?? ?main()函數之前

2.?? ?main()函數之后至applicationWillFinishLaunching完成

3.?? ?App完成所有本地數據的加載并將相應的信息展示給用戶

4.?? ?App完成所有聯網數據的加載并將相應的信息展示給用戶

1+2一起決定了我們需要用戶等待多久才能出現一個主視圖,同時也是技術上可以精確測量的時長,1+2+3決定了用戶視覺上的等待出現有用信息所需要的時長,1+2+3+4決定了我們需要多少時間才能讓我們需要展示給用戶的所有信息全部出現。

淘寶的iOS客戶端無疑是各部分都做得非常優秀的典型。它所承載的業務完全不比微信和手機QQ少,但幾乎瞬間完成了啟動,并利用緩存機制使得用戶馬上看到“貌似完整”的界面,然后立即又刷新了剛剛聯網更新回來的信息。也就是說,無論是技術上還是視覺上,它都非常的“快”。

1. 移除不需要用到的動態庫

因為WiFi管家是個小項目,用到的動態庫不多,自動化處理的優勢不大,我這里也就簡單的把依賴的動態移除出項目,再根據編譯錯誤一個一個加回來。如果有靠譜的方法,歡迎大家補充一下。

2. 移除不需要用到的類

項目做久了總有一些吊詭的類像幽靈一樣驅之不去,由于【不要相信產品經理】的思想作怪,需求變更后,有些類可能用不上了,但卻因為擔心需求再變回來就沒有移除掉,后來就徹底忘記要移除了。

為了解決這個歷史問題,在這個過程中我試過多種方法來掃描沒有用到的類,其中有一種是編譯后對ObjC類的指針引用進行反向掃描,可惜實際上收獲不是很明顯,而且還要寫很多例外代碼來處理一些特殊情況。后來發現一個叫做fui(Find Unused Imports)的開源項目能很好的分析出不再使用的類,準確率非常高,唯一的問題是它處理不了動態庫和靜態庫里提供的類,也處理不了C++的類模板。

使用方法是在Terminal中cd到項目所在的目錄,然后執行fui find,然后等上那么幾分鐘(是的你沒有看錯,真的需要好幾分鐘甚至需要更長的時間),就可以得到一個列表了。由于這個工具還不是100%靠譜,可根據這個列表,在Xcode中手動檢查并刪除不再用到的類。

實際上,日常對代碼工程的維護非常重要,如果制定好一套半廢棄代碼的維護方法,小問題就不會積累成大問題。有時候對于一些暫時不再使用的代碼,我也很糾結于要不要svn rm,因為從代碼歷史中找刪除掉的文件還是不太方便。不知道大家有沒有相關的經驗可以分享,也請不吝賜教。

3. 合并功能類似的類和擴展(Category)

由于Category的實現原理,和ObjC的動態綁定有很強的關系,所以實際上類的擴展是比較占用啟動時間的。盡量合并一些擴展,會對啟動有一定的優化作用。不過個人認為也不能因為它占用啟動時間而去逃避使用擴展,畢竟程序員的時間比CPU的時間值錢,這里只是強調要合并一些在工程、架構上沒有太大意義的擴展。

4. 壓縮資源圖片

壓縮圖片為什么能加快啟動速度呢?因為啟動的時候大大小小的圖片加載個十來二十個是很正常的,圖片小了,IO操作量就小了,啟動當然就會快了。

事實上,Xcode在編譯App的時候,已經自動把需要打包到App里的資源圖片壓縮過一遍了。然而Xcode的壓縮會相對比較保守。另一方面,我們正常的設計師由于需要符合其正常的審美需要生成的正常的PNG圖片,因此圖片大小是比較大的,然而如果以程序員的直男審美而采用過激的壓縮會直接激怒設計師。

解決各種矛盾的方法就是要找出一種相當靠譜的壓縮方法,而且最好是基本無損的,而且壓縮率還要特別高,至少要比Xcode自動壓縮的效果要更好才有意義。經過各種試驗,最后發現唯一可靠的壓縮算法是TinyPNG,其它各種方法,要么沒效果,要么產生色差或模糊。但是非常可惜的是TinyPNG并不是完全免費的,而且需要通過網絡請求來壓縮圖片(應該是為了保護其牛逼的壓縮算法)。

為了解決這個問題,我寫了一個類來執行這個請求,請參見閱讀原文里的SSTinyPNGRequest和SSPNGCompressor。因為這個項目只有我一個人在用所以代碼寫得有點隨意,有問題可以私聊也可以在評論里問,有改進的方法也非常歡迎指正。另外說明一下,使用這個類需要你自行到?https://tinypng.com/developers?申請APIKey,每一個用戶每月有500張圖片壓縮是免費的,而每個郵箱可以注冊一個用戶,你懂的。

5. 優化applicationWillFinishLaunching

隨著項目做的時間長了,applicationWillFinishLaunching里要處理的代碼會越積越多,WiFi管家的iOS版本有一段時間沒有控制好,里面的邏輯亂得有點丟人。因為可能涉及到一些項目的安全性問題,這里不能分享所有的優化細節及發現的思路。僅列出在applicationWillFinishLaunching中主要需要處理的業務及相關問題的改進方案。

這里大部分都是一些苦逼活,但有一點特別值得分享的是,有一些優化,是無法在數據上體現的,但是視覺上卻能給用戶較大的提升。例如在【各種業務請求配置更新】的部分,經過分析優化后,啟動過程并發的http請求數量從66條壓縮到了23條,如此一來為啟動成功后新聞資訊及其圖片的加載留出了更多的帶寬,從而保證了在第一時間完成新聞資訊的加載。實際測試表明,光做KPI的事情是不夠的,人還是需要有點理想,經過優化,在視覺體驗上進步非常明顯。

另外,過程中請教過SNG的大牛們,聽說他們因為需要在applicationWillFinishLaunching里處理的業務更多,所以還做了管理器管理這些任務,不過因為WiFi管家是個小項目,有點殺雞用牛刀的感覺,因此沒有深入研究。

6. 優化rootViewController加載

考慮到我作為一只程序猴,工資還行,為了給公司節約成本,在優化之前,當然需要先測試一下哪些ViewController的加載耗時比較大,然后再深入到具體業務中看哪些部分存在較大的優化空間。同時,先做優化效果明顯的部分也有利于增強自己的信心。

在開始講述問題之前,我們先來看一下wife管家的UI層次結構:

一個看似簡單的界面由于承載了很多業務需求,代碼量其實已經非常驚人。這里我不具體講述這些驚人的業務量了,抽象而言可WiFi管家的UI架構總體而言基于TabBarController的框架,三個tab分別是“連接”、“發現”及“我的”。App啟動的時候,根據加載原理,會加載TabBarController、第一個Tab(“連接”)的ViewController及其所有childViewController。

UI構架請看如下示意圖,其中藍色的部分需要在App啟動的時候立即加載:

一個看似簡單的界面由于承載了很多業務需求,代碼量其實已經非常驚人。這里我不具體講述這些驚人的業務量了,抽象而言可WiFi管家的UI架構總體而言基于TabBarController的框架,三個tab分別是“連接”、“發現”及“我的”。App啟動的時候,根據加載原理,會加載TabBarController、第一個Tab(“連接”)的ViewController及其所有childViewController。

對所有啟動相關的模塊打錨點計算耗時后,發現tabBarController和connectingViewController分別占用了applicationWillFinishLaunching耗時的31%和24%。加載耗費了大量時間,這跟它所需要承載的邏輯任務似乎并不對稱。于是檢查相關代碼進行深入分析,發現了幾個問題比較嚴重:

1.?? ?有些程序員可能架構意識不是太強,直接在tabBarController的啟動過程中插入了各種奇怪的業務,例如檢查WiFi連接狀態變化、配置拉取,而這些業務顯然應該在另外的某些地方統一處理,而不應該在一個ViewController上。

2.?? ?由于一些歷史原因,連接頁的視圖控制器connectingViewController包含了三個childViewController:WiFiViewController、3GViewController、errorViewController,分別在WiFi狀態、3G狀態和出錯狀態下展示界面(三選一,其中一個展示的時候其它兩個視圖會隱藏)。

3.?? ?大部分view都是直接加載完的。有些界面的加載非常復雜,比如再進入App時會展示一個檢查WiFi可用性和安全性的動畫,由于需要疊加較多圖片,這部分視圖的加載耗時較多。

由于隨著幾次改版之后,連接頁的UI架構已經變得很不合理,歷史包袱還是比較重的,而且耦合比較嚴重,幾乎無法改動,因此決定重構。至于tabBarController,檢查代碼后決定簡單的把不相關的業務做一些遷移,優化childViewController的加載過程,不作重構。

由于本篇主要講啟動性能優化,重構涉及的軟件工程和設計模式方面的東西就不詳細論述了,對啟動優化的過程,主要是使用了更合理的分層結構,使得啟動得以在更短的時間內完成。

至此,WiFi管家的啟動性能基本優化完畢。

7. 挖掘最后一點性能優化

由于WiFi管家是一個具有WiFi連接能力的App,因此有可能在后臺過程中完成冷啟動過程(實際上是在用戶進入系統的WiFi設置時,iOS會啟動WiFi管家,以便請求WiFi密碼)。在這種情況下,整個rootViewController都是不需要加載的。

【第四部分】總結

??? ?利用DYLD_PRINT_STATISTICS分析main()函數之前的耗時

??? ?重新梳理架構,減少動態庫、ObjC類的數目,減少Category的數目

??? ?定期掃描不再使用的動態庫、類、函數,例如每兩個迭代一次

??? ?用dispatchonce()代替所有的__attribute__((constructor))函數、C++靜態對象初始化、ObjC的+load

??? ?在設計師可接受的范圍內壓縮圖片的大小,會有意外收獲

??? ?利用錨點分析applicationWillFinishLaunching的耗時

??? ?將不需要馬上在applicationWillFinishLaunching執行的代碼延后執行

??? ?rootViewController的加載,適當將某一級的childViewController或subviews延后加載

??? ?如果你的App可能會被后臺拉起并冷啟動,可考慮不加載rootViewController

??? ?不應放過的一些小細節

??? ?異步操作并不影響指標,但有可能影響交互體驗,例如大量網絡請求導致數據擁堵

??? ?有時候一些交互上的優化比技術手段效果更明顯,視覺上的快決不是冰冷的數據可以解釋的,好好和你們的設計師談談動畫

7.0x8badf00d表示是什么?

0x8badf00d: 讀做 “ate bad food”! (把數字換成字母,是不是很像 :p)該編碼表示應用是因為發生watchdog超時而被iOS終止的。??通常是應用花費太多時間而無法啟動、終止或響應用系統事件。

0xbad22222: 該編碼表示 VoIP 應用因為過于頻繁重啟而被終止。

0xdead10cc: 讀做 “dead lock”!該代碼表明應用因為在后臺運行時占用系統資源,如通訊錄數據庫不釋放而被終止 。

0xdeadfa11: 讀做 “dead fall”! 該代碼表示應用是被用戶強制退出的。根據蘋果文檔, 強制退出發生在用戶長按開關按鈕直到出現 “滑動來關機”, 然后長按 Home按鈕。強制退出將產生 包含0xdeadfa11 異常編碼的崩潰日志, 因為大多數是強制退出是因為應用阻塞了界面。

8.怎么防止反編譯?

1.本地數據加密

iOS應用防反編譯加密技術之一:對NSUserDefaults,sqlite存儲文件數據加密,保護帳號和關鍵信息

2.URL編碼加密

iOS應用防反編譯加密技術之二:對程序中出現的URL進行編碼加密,防止URL被靜態分析

3.網絡傳輸數據加密

iOS應用防反編譯加密技術之三:對客戶端傳輸數據提供加密方案,有效防止通過網絡接口的攔截獲取數據

4.方法體,方法名高級混淆

iOS應用防反編譯加密技術之四:對應用程序的方法名和方法體進行混淆,保證源碼被逆向后無法解析代碼

5.程序結構混排加密

iOS應用防反編譯加密技術之五:對應用程序邏輯結構進行打亂混排,保證源碼可讀性降到最低

6.借助第三方APP加固,例如:網易云易盾

9.說說你遇到到的技術難點?

你遇到的問題難度,一定程度決定了你的水平。如實反映就好。只是一道承上啟下的問題!

以下問題是群友提供的:

問題1.地理空間距離計算優化

不管是“離我最近”還是“智能排序”,都涉及到計算用戶位置與各個團購單子或者商家的距離(注:在智能排序中距離作為一個重要的參數參與排序打分)。以篩選商家為例,北京地區有5~6w個POI(本文將商家稱之為POI),當用戶進入商家頁,請求北京全城+所有品類+離我最近/智能排序時,我們篩選服務需要計算一遍用戶位置與北京全城所有POI的距離。

這種大量計算距離的場景十分消耗資源,從測試來看目前5w個點僅計算一遍距離就需要7ms,而到100w的時候就需要140多ms,隨著數據的快速增長,篩選服務的性能將會非常堪憂。

如何優化距離的計算,進而提高計算速度、降低cpu使用率已經迫在眉睫。移動后臺團購組在此方向上進行了些許探索,下文將分為4部分展開:1)地理空間距離計算原理;2)Lucene使用的距離計算公式;3)優化方案;4)實際應用。

2 地理空間距離計算原理

地理空間距離計算方法較多,目前我們使用的可以分為兩類:1)球面模型,這種模型將地球看成一個標準球體,球面上兩點之間的最短距離即大圓弧長,這種方法使用較廣,在我們服務端被廣泛使用;2)橢球模型,該模型最貼近真實地球,精度也最高,但計算較為復雜,目前客戶端有在使用,但實際上我們的應用對精度的要求沒有那么高。

下面著重介紹我們最常用的基于球面模型的地理空間距離計算公式,推導也只需要高中數學知識即可。

該模型將地球看成圓球,假設地球上有A(ja,wa),B(jb,wb)兩點(注:ja和jb分別是A和B的經度,wa和wb分別是A和B的緯度),A和B兩點的球面距離就是AB的弧長,AB弧長=R*角AOB(注:角AOB是A跟B的夾角,O是地球的球心,R是地球半徑,約為6367000米)。如何求出角AOB呢?可以先求AOB的最大邊AB的長度,再根據余弦定律可以求夾角。

如何求出AB長度呢?

1)根據經緯度,以及地球半徑R,將A、B兩點的經緯度坐標轉換成球體三維坐標;

2)根據A、B兩點的三維坐標求AB長度;

3)根據余弦定理求出角AOB;

4)AB弧長=R*角AOB.

#3 Lucene使用的地理空間距離算法

團購app后臺使用lucene來篩選團購單子和商家,lucene使用了spatial4j工具包來計算地理空間距離,而spatial4j提供了多種基于球面模型的地理空間距離的公式,其中一種就是上面我們推導的公式,稱之為球面余弦公式。還有一種最常用的是Haversine公式,該公式是spatial4j計算距離的默認公式,本質上是球面余弦函數的一個變換,之前球面余弦公式中有cos(jb-ja)項,當系統的浮點運算精度不高時,在求算較近的兩點間的距離時會有較大誤差,Haversine方法進行了某種變換消除了cos(jb-ja)項,因此不存在短距離求算時對系統計算精度的過多顧慮的問題。

1)Haversine公式代碼

public static double distHaversineRAD(double lat1, double lon1, double lat2, double lon2) {

double hsinX = Math.sin((lon1 - lon2) * 0.5);

double hsinY = Math.sin((lat1 - lat2) * 0.5);

double h = hsinY * hsinY +

(Math.cos(lat1) * Math.cos(lat2) * hsinX * hsinX);

return 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h)) * 6367000;

}

2)Haversine公式性能

目前北京地區在線服務有5w個POI,計算一遍距離需要7ms。現在數據增長特別快,未來北京地區POI數目增大到100w時,我們篩選服務僅計算距離這一項就需要消耗144多ms,性能十分堪憂。(注:本文測試環境的處理器為2.9GHz Intel Core i7,內存為8GB 1600 MHz DDR3,操作系統為OS X10.8.3,實驗在單線程環境下運行)

POI數目

耗時(ms)

5w

7

10w

14

100w

144

#4 優化方案

通過抓棧我們發現消耗cpu較多的線程很多都在執行距離公式中的三角函數例如atan2等。因此我們的優化方向很直接---消減甚至消除三角函數。基于此方向我們嘗試了多種方法,下文將一一介紹。

##4.1 坐標轉換方法

1)基本思路

之前POI保存的是經緯度(lon,lat),我們的計算場景是計算用戶位置與所有篩選出來的poi的距離,這里會涉及到大量三角函數計算。一種優化思路是POI數據不保存經緯度而保存球面模型下的三維坐標(x,y,z),映射方法如下:

x = Math.cos(lat)Math.cos(lon);

y = Math.cos(lat)Math.sin(lon);

z = Math.sin(lat);

那么當我們求夾角AOB時,只需要做一次點乘操作。比如求(lon1,lat1)和 (lon2,lat2)的夾角,只需要計算x1x2 + y1y2 + z1*z2, 這樣避免了大量三角函數的計算。

在得到夾角之后,還需要執行arccos函數,將其轉換成角度,AB弧長=角AOB*R(R是地球半徑)。

此方法性能如下:

POI數目

耗時(ms)

5w

3

10w

8

100w

88

2)進一步優化

我們的業務場景是在一個城市范圍內進行距離計算,因此夾角AOB往往會比較小,這個時候sinAOB約等于AOB,因此我們可以將 Math.acos(cosAOB)R 改為Math.sqrt(1 - cosAOBcosAOB)*R,從而完全避免使用三角函數,優化后性能如下。

POI數目

耗時(ms)

5w

0.2

10w

0.5

100w

4

##4.2 簡化距離計算公式方法

1)基本思路

我們的業務場景僅僅是在一個城市范圍內進行距離計算,也就是說兩個點之間的距離一般不會超過200多千米。由于范圍小,可以認為經線和緯線是垂直的,如圖所示,要求A(116.8,39,78)和B(116.9,39.68)兩點的距離,我們可以先求出南北方向距離AM,然后求出東西方向距離BM,最后求矩形對角線距離,即sqrt(AMAM + BMBM)。

南北方向AM = R緯度差Math.PI/180.0;

東西方向BM = R經度差Cos<當地緯度數* Math.PI/180.0>

這種方式僅僅需要計算一次cos函數。

public static double distanceSimplify(double lat1, double lng1, double lat2, double lng2, double[] a) {

double dx = lng1 - lng2; // 經度差值

double dy = lat1 - lat2; // 緯度差值

double b = (lat1 + lat2) / 2.0; // 平均緯度

double Lx = toRadians(dx) * 6367000.0* Math.cos(toRadians(b)); // 東西距離

double Ly = 6367000.0 * toRadians(dy); // 南北距離

return Math.sqrt(Lx * Lx + Ly * Ly);? // 用平面的矩形對角距離公式計算總距離

}

}

我們對這個方法的有效性和性能進行驗證。

1.1)有效性驗證

我們首先檢驗這種簡化是否能滿足我們應用的精度,如果精度較差將不能用于實際生產環境。

我們的方法叫distanceSimplify,lucene的方法叫distHaversineRAD。下表是在不同尺度下兩個方法的相差情況。

測試點對

distanceSimplify(米)

distHaversineRAD(米)

差別(米)

(39.941,116.45)(39.94, 116.451)

140.0285167225230

140.02851671981400

0.0

(39.96, 116.45)(39.94, 116.40)

4804.421262839180

4804.421153907680

0.0

(39.96, 116.45)(39.94, 117.30)

72444.81551882200

72444.54071519510

0.3

(39.26, 115.25)(41.04, 117.30)

263525.6167839860

263508.55921886700

17.1

可以看到兩者在百米、千米尺度上幾乎沒有差別,在萬米尺度上也僅有分米的差別,此外由于我們的業務是在一個城市范圍內進行篩選排序,所以我們選擇了北京左下角和右上角兩點進行比較,兩點相距有260多千米,兩個方法差別17m。從精度上看該優化方法能滿足我們應用需求。

1.2)性能驗證

POI數目

耗時(ms)

5w

0.5

10w

1.1

100w

11

2)進一步優化

我們看到這里計算了一次cos這一三角函數,如果我們能消除此三角函數,那么將進一步提高計算效率。

如何消除cos三角函數呢?

采用多項式來擬合cos三角函數,這樣不就可以將三角函數轉換為加減乘除了嘛!

首先決定多項式的最高次數,次數為1和2顯然都無法很好擬合cos函數,那么我們選擇3先嘗試吧,注:最高次數不是越多越好,次數越高會產生過擬合問題。

使用org.apache.commons.math3這一數學工具包來進行擬合。中國的緯度范圍在10~60之間,即我們將此區間離散成Length份作為我們的訓練集。

public static double[] trainPolyFit(int degree, int Length){

PolynomialCurveFitter polynomialCurveFitter = PolynomialCurveFitter.create(degree);

double minLat = 10.0; //中國最低緯度

double maxLat = 60.0; //中國最高緯度

double interv = (maxLat - minLat) / (double)Length;

List weightedObservedPoints = new ArrayList();

for(int i = 0; i < Length; i++) {

WeightedObservedPoint weightedObservedPoint = new WeightedObservedPoint(1,? minLat + (double)i*interv, Math.cos(toRadians(x[i])));

weightedObservedPoints.add(weightedObservedPoint);

}

return polynomialCurveFitter.fit(weightedObservedPoints);

}

public static double distanceSimplifyMore(double lat1, double lng1, double lat2, double lng2, double[] a) {

//1) 計算三個參數

double dx = lng1 - lng2; // 經度差值

double dy = lat1 - lat2; // 緯度差值

double b = (lat1 + lat2) / 2.0; // 平均緯度

//2) 計算東西方向距離和南北方向距離(單位:米),東西距離采用三階多項式

double Lx = (a[3] * b*b*b? + a[2]* b*b? +a[1] * b + a[0] ) * toRadians(dx) * 6367000.0; // 東西距離

double Ly = 6367000.0 * toRadians(dy); // 南北距離

//3) 用平面的矩形對角距離公式計算總距離

return Math.sqrt(Lx * Lx + Ly * Ly);

}

我們對此優化方法進行有效性和性能驗證。

2.1)有效性驗證

我們的優化方法叫distanceSimplifyMore,lucene的方法叫distHaversineRAD,下表是在不同尺度下兩個方法的相差情況。

測試點對

distanceSimplifyMore(米)

distHaversineRAD(米)

差別(米)

(39.941,116.45)(39.94, 116.451)

140.0242769266660

140.02851671981400

0.0

(39.96, 116.45)(39.94, 116.40)

4804.113098854450

4804.421153907680

0.3

(39.96, 116.45)(39.94, 117.30)

72438.90919479560

72444.54071519510

5.6

(39.26, 115.25)(41.04, 117.30)

263516.676171262

263508.55921886700

8.1

可以看到在百米尺度上兩者幾乎未有差別,在千米尺度上僅有分米的區別,在更高尺度上如72千米僅有5.6m米別,在264千米也僅有8.1米區別,因此該優化方法的精度能滿足我們的應用需求。

2.2)性能驗證

POI數目

耗時(ms)

5w

0.1

10w

0.3

100w

4

#5 實際應用

坐標轉換方法和簡化距離公式方法性能都非常高,相比lucene使用的Haversine算法大大提高了計算效率,然而坐標轉換方法存在一些缺點:

a)坐標轉換后的數據不能被直接用于空間索引。lucene可以直接對經緯度進行geohash空間索引,而通過空間轉換變成三維數據后不能直接使用。我們的應用有附近范圍篩選功能(例如附近5km的團購單子),通過geohash空間索引可以提高范圍篩選的效率;

b)坐標轉換方法增大內存開銷。我們會將坐標寫入倒排索引中,之前坐標是2列(經度和緯度),現在變成3列(x,y,z),在使用中我們往往會將這數據放入到cache中,因此會增大內存開銷;

c)坐標轉換方法增大建索引開銷。此方法本質上是將計算從查詢階段放至到索引階段,因此提高了建索引的開銷。

基于上述原因我們在實際應用中采用簡化距離公式方法(通過三次多項式來擬合cos三角函數),此方法在團購篩選和商家篩選的距離排序、智能排序中已經開始使用,與之前相比,篩選團購時北京全城美食品類距離排序響應時間從40ms下降為20ms。

問題2.iOS應用架構,我的架構設計?

可從以下4個方面回答:

iOS應用架構談 view層的組織和調用方案 ?

iOS應用架構談 網絡層設計方案 ?

iOS應用架構談 動態部署方案 ?

iOS應用架構談 本地持久化方案?

什么樣app的架構叫好架構?

代碼整齊,分類明確,沒有common,沒有core

不用文檔,或很少文檔,就能讓業務方上手

思路和方法要統一,盡量不要多元

沒有橫向依賴,萬不得已不出現跨層訪問

對業務方該限制的地方有限制,該靈活的地方要給業務方創造靈活實現的條件

易測試,易拓展

保持一定量的超前性

接口少,接口參數少

高性能

第一類:精簡型應用架構

這類架構的文章分析主要還是圍繞MVC展開,以蘋果自帶UIViewController優劣為出發點,再結合主流的MVP,MVVM,MVCS等變種進行分析演變。這類的探討重點在于M,V,C三類角色的定義以及之間的數據事件流向的規范。很多小型應用所面臨的問題及其架構層面的解決方案都集中在這一類。

第二類:綜合型應用架構

對于用戶量級在千萬級或以上的應用來說,MVC這一層面的思考已無法應對業務瘋狂增長所帶來的負擔。這類應用往往需要專業資深的架構師出面進行深層次的思考設計,業內不少大廠如淘寶,天貓,攜程等都做過一些分享。不過到了這一層級的戰斗,不光考驗架構師的技術積累,更重要的是架構師對于業務的整體理解。我姑且把這類架構名之為:綜合型應用架構。綜合型應用架構一般不會提到MVC,更多是在探討“層”與“模塊”的劃分和耦合。后面我會就幾個經典樣本做下詳盡深入的分析。

第三類:深度優化的綜合型應用架構

綜合型應用架構是應對大規模業務增長的必經之路,一旦架構成型,后期業務膨脹會不停的打磨架構本身,產品本身對體驗質量的追求會要求架構師和技術團隊不停的優化架構細節。這種優化可以分為兩塊,第一是組件或模塊劃分的粒度越來越細,第二是組件模塊的深度優化,比如網絡層的深度優化,sqlite優化(多線程,FTS,安全等),數據加密,HotFix,Hybrid等,一些開源的第三方庫已不能滿足要求,需要團隊自己重造輪子。這一層面的架構設計涉及面廣,對架構師,團隊技術人員的技術深度和業務理解能力有較高要求,短短一篇技術文章往往只能走馬觀花的介紹個大概,每一次優化幾乎都可以作為一個專題來講解。

第四類:組織型應用架構

這類架構在第三類的基礎之上更進了一步,除了關注系統層面的架構設計之外,更對團隊或部門之間協作方式,各系統模塊的演進方式,產品發布流程等都做了規范。除去業務膨脹帶來的壓力,人員增長,各團隊協作依賴增強等都會對app的質量,迭代速度產生影響,這些問題也需要從架構層面去解決。這類結合技術架構和組織架構的分享還比較少。

以上四種類型的架構又可以看做一般App從簡至繁,公司規模隨之增長的演進過程,技術圈絕大部分的架構類分享文章都可以歸為上述四類。

對于什么是架構的學術定義,似乎大家并不太在意,更關心的是如何解決自身項目當下的問題。雖然在我看來第一類架構更像是在討論設計模式,但這里面確實又有非常多的知識可以深入挖掘,這里就把所有“解決應用整體設計問題”的討論都歸類于架構這一話題。

值得一提的是,架構師的視野和積累一般都受限于自己所經歷項目及業務的規模。如果有機會,工程師還是應該盡可能去BAT這類巨頭級公司歷練一下,知識深度和廣度的構建絕非紙上可得。

可從以下方面回答:(舉例說明,攜程開發提供)

核心功能SDK化

通訊、定位、Hybrid、數據庫、登錄、分享、基礎庫等

直接提供給其他BU獨立App使用

公用業務功能組件化

地圖、日歷、城市、圖片、通訊錄等13個公共組件

減少各BU重復開發工作量

性能數據指標采集:

網絡性能:網絡服務成功率、平均耗時、耗時分布

定位:獲取經緯度成功率、城市定位成功率

啟動時間、內存、流量等指標

多種緯度:系統、App版本、網絡狀況、位置等

網絡優化

使用TCP長連接實現網絡服務

根據網絡狀況2G/3G/4G/WIFI進行調優參數

根據連接/讀/寫不同階段使用重試機制

使用IP列表避免DNS解析失敗或者劫持

根據網絡延遲選擇服務端IP(使用Ping)

使用ProtocolBuffer+Gzip減少Payload

10.說說你了解的第三方原理或底層知識?

Runtime、Runloop、block

SD原理、YYCache、GCD源碼分析、JSPatch原理等!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容