攜程火車票iOS項目開發(fā)體驗優(yōu)化實踐
?作者 | 東海 ,攜程移動開發(fā)專家,專注于移動端框架、移動端性能。
元帥,攜程資深軟件工程師,致力于平臺基建開發(fā)。
一、背景
現(xiàn)在各大公司的APP都采用的是組件化架構,組件化架構帶來了高內聚、低耦合、平臺化等諸多有點,使工程結構更加清晰,工程管理更加輕松。iOS工程大多采用CocoaPod進行組件化管理,一些大型的項目需要打包平臺來執(zhí)行組件打bundle包和APP打測試包的任務,在開發(fā)方面會采用二進制與源碼切換的方式來提高編譯速度。
組件化雖然對APP項目的工程管理帶來了巨大的好處,但是對開發(fā)人員來講,存在著一些繁瑣的問題:
在開發(fā)中,如果需要調試未解開源碼的組件,就需要重新執(zhí)行命令解開相應組件的源碼才能進行調試。
每切換一次組件的源碼,都需要在終端輸入一串加了各種參數(shù)的命令來執(zhí)行pod install,手動輸入慢,而且容易出錯。
組件化使得組件顆粒度變得越來越細,每個人所管理的組件數(shù)量就會多,每次組件更新都需要在打包平臺上進行打包,等組件bundle包打完,再打測試包進行驗證。
這些雖然能讓工作正常進行,但是繁瑣重復的操作卻影響了開發(fā)人員的開發(fā)效率。
二、現(xiàn)狀
攜程火車票APP一直以來采用的也是組件化管理,在去年改用CocoaPod進行組件化管理,隨著業(yè)務的迭代和基礎建設不斷的完善,pod組件也越來越精細化,目前pod組件數(shù)量已超60+。
pod組件數(shù)量越龐大,開發(fā)人員維護的成本也會越高,不僅要管理維護每個pod組件的更新,還要處理pod組件打bundle包問題、測試包打包時間長的問題。上述繁瑣、重復、耗時的操作困擾著我們的iOS開發(fā)人員。如果能盡可能的對這些開發(fā)體驗問題進行優(yōu)化,那么必然會帶來開發(fā)人員效率的提升。
三、優(yōu)化方案
為了讓開發(fā)人員調試代碼更加方便,打包測試體驗更好,開發(fā)過程更加專注,我們做了許多方面的操作優(yōu)化和技術實踐,主要有:
3.1 通過技術手段,實現(xiàn)二進制調試
在開發(fā)過程,難免會遇到自己想調試的組件沒有解開源碼,程序在運行中崩潰但是崩潰在了未解開源碼的組件上,自己看到的只是一堆不明所以的匯編代碼(圖1),無法像源碼調試那樣看到足夠豐富的調試信息。
圖1
3.1.1 二進制文件分析
如何才能不解開源碼也能調試二進制、崩潰在了二進制組件上也能定位到具體哪一行成了我們新的問題。我們找了各種資料,找到了美團有款zsource的CocoaPod插件可以進行二進制調試,雖未開源,但大致邏輯文章里已經(jīng)羅列的很清晰,大致原理:
以libXXXX.a二進制文件為例,用 MachOView 來查看二進制文件,以獲取到更友好的二進制信息。我們可以看到 “debug_str” Section 這些信息都存在了二進制的中。debug_str在編譯的時候內部會記錄源碼地址:
圖2
使用命令在終端輸入:
dwarfdump ./libXXXX.a | grep 'XXXX'
注意到了 AT_name 這個字段名,去DWARF 1.1.0 Reference文檔中查閱,我們可以得知:
- 一個DW_AT_name屬性,其值是一個以空字符結尾的字符串,其中包含從其派生編譯單元的主源文件的完整或相對路徑名。
- 一個DW_AT_comp_dir屬性,其值是一個以空值結尾的字符串,其中包含編譯命令的當前工作目錄,該編譯命令以某種形式將Forelax視為主機系統(tǒng),從而生成此編譯單元。
XXXX.swift源文件存在這個地址下: /Users/marshal/Desktop/XXXX/XXXX/XXXX.swift
這個地址就是編譯時源碼所在地址,Debug調試的時候,編譯器會先從這里拿對應映射地址去加載源碼文件。如果存在對應地址存在源碼文件時,就能進入源碼調試。
3.1.2 腳本開發(fā)
了解基礎原理后,那接下來的事情就是解決各種問題障礙:
1)要獲取到靜態(tài)庫的源碼。
2)獲取靜態(tài)庫中存儲的編譯靜態(tài)庫時源碼文件所在的路徑。
3)在本地創(chuàng)建上面????獲取的路徑,讓靜態(tài)庫的源碼和該路徑關聯(lián)起來。
- 問題1:我們當時制作二進制包時為了方便切換源碼調試,在pod install的時候源碼+.a會同時下載到本地。
- 問題2:在美團的文章中可以了解到,使用dwarfdump 命令可以獲取靜態(tài)庫中存儲的編譯靜態(tài)庫時源碼文件所在的路徑。
- 問題3:這個問題,我想大多數(shù)人第一個的想法是把靜態(tài)庫的源碼copy到本地創(chuàng)建的靜態(tài)庫編譯目錄里面,但是我們采用更加輕巧的方式:通過軟連接命令ln將兩個目標關聯(lián)起來。
最終我們通過開發(fā)腳本解決了上面的問題,通過Hook post_integrate 將腳本穿插到pod install的過程中,使整個過程順暢自然。
主要腳本代碼如下:
#鏈接,.a文件位置, 源碼目錄,工程名
def link(lib_file,target_path,basename)
#查詢源碼所在位置
dir = (`dwarfdump "#{lib_file}" | grep "AT_comp_dir" | head -1 | cut -d \\" -f2 `)
#創(chuàng)建目錄
FileUtils.mkdir_p(dir)
#鏈接
FileUtils.rm_rf(File.join(dir,basename))
`ln -s #{target_path} #{dir}`
end
#通過pod post_integrate集成腳本
post_integrate do |installer|
openStaticLibDebug(installer,"project")
end
def openStaticLibDebug(installer,project)
if !ENV["DEBUGLIB"]
return
end
#腳本目錄
path_root = "#{Pathname.new(File.dirname(__FILE__)).realpath}"
#當前項目的pod目錄
pod_path = "#{path_root}/#{project}/Pods"
installer.pods_project.targets.each do |target|
bunlde_name = target.name
#這里可以根據(jù)環(huán)境變量選擇性開啟源碼調試
enableDebug = ENV["#{bunlde_name}_DEBUGLIB"]
enableDebug = true
if enableDebug
DebugLibCode.new().link(lib_file,target_path,basename)
end
end
end
整個流程如下圖3:
圖3
3.1.3 方案調優(yōu)
通過上面的腳本雖然實現(xiàn)了二進制靜態(tài)庫的調試,但是在推廣和使用的時候又遇到了新的問題:
1)每個開發(fā)人員第一次執(zhí)行二進制調試腳本的時候都會報錯,因為權限問題,需要開發(fā)人員手動在 Users下面創(chuàng)建一個cbuilder的用戶目錄。
2)每次pod install的時間變長了很多,經(jīng)過多次測量,在M1芯片的電腦上,從未接入二進制調試執(zhí)行pod install到接入后增加超過了60%;在Inter芯片的電腦上,增加超過了 70%,如圖4:
圖4
對于問題1,開發(fā)人員手動cbuilder的用戶目錄是個不合理的操作,我們把這個操作集成到 ZTPodTool內(ZTPodTool是我們開發(fā)的一個podfile管理工具,下面會詳細介紹),讓ZTPodTool來創(chuàng)建cbuilder用戶目錄,開發(fā)人員就能無感知的開發(fā)。但是嘗試了各種創(chuàng)建目錄的api發(fā)現(xiàn)都不能創(chuàng)建這個目錄,這個問題困擾了我們好久。
查找了大量資料,發(fā)現(xiàn)AppleScript是一個與macOS結合非常緊密的腳本語言,它顯著的特點就是可以控制其他macOS上的應用程序,通過使用它可以完成一些繁瑣重復的工作。代碼如下:
NSString *script = @"do shell script \" /bin/mkdir -m 777 /Users/cbuilder\" with administrator privileges";
NSError *errorInfo = nil;
NSAppleScript *appleScript = [[NSAppleScript new] initWithSource:script];
NSAppleEventDescriptor * eventResult = [appleScript executeAndReturnError:&errorInfo];
對于問題2,我們分析發(fā)現(xiàn),通過dwarfdump命令來解析二進制文件獲取源碼路徑,會先加載整個二進制到內存中,再通過grep、head、cut等命令解析出取源碼路徑目錄,這個過程非常耗時。工程里面的pod組件庫越多,這個過程耗時就越大。
這些耗時的命令就為了獲取個路徑,如果能通過其他途徑獲取路徑就可以把這些時間節(jié)省下來,可以省下一大筆時間開支。于是我們想到,既然是打包機上的路徑,那就讓打包機打包時把包相關信息用json保存在產(chǎn)物目錄下,在install的時候,通過讀取產(chǎn)物里面的json文件就可以獲取打包源碼路徑。
優(yōu)化腳本后,經(jīng)過測量,和之前pod install的時間相差無幾(圖5)。就這樣,我們的開發(fā)人員可以無差別的調試各個組件的代碼了。
圖5
3.2 另辟蹊徑,解決M1電腦iOS模擬器剪切板問題
用M1系列電腦在iOS模擬器上開發(fā)的人員基本上都會遇到一個非常棘手的問題,那就是模擬器的剪切板無法和電腦的剪切板互通,開發(fā)人員也無法給剪切板賦值,一賦值就報錯:
[CoreServices] _LSSchemaConfigureForStore failed with error Error Domain=NSOSStatusErrorDomain
Code=-10817 "(null)" UserInfo={_LSFunction=_LSSchemaConfigureForStore, ExpectedSimulatorHash=
{length = 32, bytes =0x4014b70c 8322afc9 dfb06ed8 13148b48 ... b6adae0d b2637192 }, _LSLine=
405, WrongSimulatorHash={length = 32, bytes = 0x073253e6 9a9b67cc 089d6640 ca4fdb3e ...
46b00d8b bca98999 }}
在蘋果的官方論壇上反饋這個問題,得到的回復卻是這樣的:
//https://developer.apple.com/forums/thread/682395
So far I’ve been ignoring this thread because it started out with folks running Xcode and the simulator under Rosetta. This isn’t a supported configuration and I recommend that you switch to running these natively.
However, it’s now clear that multiple folks are hitting this while running Xcode and the simulator natively. Have any of you filed a bug about this? If so, please post your bug number?
如果剪切板不能用,在模擬器中輸入地址或者長文本,對iOS、RN和H5的開發(fā)者都是非常耗時、非常痛苦的事情。為此我們想了一個輕巧的辦法,繞過了這個系統(tǒng)bug,完美解決了這個問題,主要流程如下:
圖6
我們給自己的APP自定了快捷鍵 Ctrl + V,用于觸發(fā)用戶進行粘貼操作 :
- (NSArray<UIKeyCommand *> *)keyCommands {
NSArray *a = [super keyCommands];
if (a) {
NSMutableArray *commands = [NSMutableArray arrayWithArray:a];
[commands addObject:[UIKeyCommand keyCommandWithInput:@"v"
modifierFlags:UIKeyModifierControl
action:@selector(posteboardCommand:)
discoverabilityTitle:@""]];
return commands;
}
return @[
[UIKeyCommand keyCommandWithInput:@"v"
modifierFlags:UIKeyModifierControl
action:@selector(posteboardCommand:)
discoverabilityTitle:@""]
];
}
本地服務我們是開發(fā)了一個mac客戶端,主要功能是:在本地起一個Http服務,專門處理獲取當前電腦剪切板內容的請求。它顯示在系統(tǒng)狀態(tài)欄上,方便控制服務的開啟、停止和退出,支持修改端口號(圖7)。點擊這里即可下載使用。
圖7
獲取當前輸入框的代碼如下:
@interface UIResponder (FirstResponder)
+ (id)currentFirstResponder;
@end
static __weak id currentFirstResponder;
@implementation UIResponder (firstResponder)
+(id)currentFirstResponder {
currentFirstResponder = nil;
[[UIApplication sharedApplication] sendAction:@selector(findFirstResponder:) to:nil from:nil forEvent:nil];
return currentFirstResponder;
}
-(void)findFirstResponder:(id)sender {
currentFirstResponder = self;
}
@end
//觸發(fā)獲取剪切板的操作如下:
- (void)posteboardCommand:(UIKeyCommand *)command
{
#if TARGET_IPHONE_SIMULATOR
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url = [NSURL URLWithString:@"http://127.0.0.1:8123/getPasteboardString"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"GET";
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(@"讀取出錯,請檢查服務是否打開");
} else {
NSString *pasteString = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
UIResponder* aFirstResponder = [UIResponder currentFirstResponder];
if ([aFirstResponder isKindOfClass:[UITextField class]]) {
[(UITextField *)aFirstResponder setText:pasteString];
} else if ([aFirstResponder isKindOfClass:[UITextView class]]) {
[(UITextView *)aFirstResponder setText:pasteString];
} else {
}
}
});
}];
[task resume];
#endif
}
加入這項優(yōu)化后,開發(fā)人員只用使用Ctrl + V可以把mac電腦的剪切板內容粘貼到iOS模擬器的輸入框中了,和正常的復制粘貼的功能體驗完全一樣。
3.3 開發(fā)可視化工具,集成各種功能
組件的日漸繁多,使得podfile文件操作變的復雜,打包組件bundle包變的頻繁,打測試包時間變的冗長。為了簡化終端輸入命令、打組件包和APP測試包的繁瑣操作,我們開發(fā)了一款可視化工具ZTPodTool,圖8。這個工具不僅能直接展示出組件間的依賴層級關系,而且可以直接在工具上提交打組件包請求,不用再到瀏覽器的打包平臺進行頻繁切換頁面的點擊操作。
圖8
在ZTPodTool上,不僅可以便捷地操作每個組件的源碼與二進制切換、打組件包,而且支持打測試包(圖9)。
圖9
開發(fā)人員點擊install按鈕,ZTPodTool就會根據(jù)用戶的源碼設置拼裝好命令,然后自動打開顯示日志更友好的終端,讓終端來執(zhí)行該命令。雖然通過NSTask和NSPipe也可以執(zhí)行pod install命令,但是獲取到的StandardOutput日志無法高亮,看起來十分痛苦。要是能直接在終端執(zhí)行,那樣對開發(fā)者就更友好了,查閱蘋果文檔后,發(fā)現(xiàn)官方?jīng)]有提供“終端”的SDK供開發(fā)者使用,在當時如何通過其他途徑喚起終端執(zhí)行命令成一件必須解決的事情。
最終還是靠上文提到AppleScript來解決了這個問題,下面是兩種調用AppleScript的方式:
//方式一
NSTask* task = [[NSTask alloc] init];
task.launchPath = @"/usr/bin/osascript";
task.arguments = @"tell application \"Terminal\" to do script \" pod install --repo-update";
task.currentDirectoryPath = @"/Users/zhangsan/iosWorkSpace";
task.environment = @{
@"LANG":@"zh_CN.UTF-8",
@"PATH":@"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/git/bin:/usr/local/"
};
[task launch];
//方式二
NSError *err = nil;
NSAppleScript *appleScript = [[NSAppleScript new] initWithSource:@"tell application \"Terminal\" to do script \" pod install --repo-update"];
NSAppleEventDescriptor * eventResult = [appleScript executeAndReturnError:&err];
我們加入的更加人性化的功能還有:
- 收到測試包打包完成消息后,開發(fā)人員通常發(fā)送安裝包二位給測試人員進行驗證。在ZTPodTool上,我們支持了打包后自動發(fā)送包的二維碼給所選的測試人員,無需開發(fā)人員再通知。
- 列表中組件越來越多,開發(fā)人員尋找選擇自己維護的組件也需要更多的時間。為此我們支持了關注列表功能,開發(fā)人員只看到自己關注的組件。
- 在ZTPodTool上輸入版本號,就可以更新各個pod組件的版本。
- install完成后自動打開工程
3.4 優(yōu)化打包流程,更快打出測試包
二進制打包最大的痛點就在于打ipa前先打獨立組件二進制,多個組件依賴,需要串行打依賴bundle,整體的打包流程上耗時比較大。在測試階段,如果測試包能快速的打出來,這無疑能顯著的提升bug的驗收效率。我們每次提交代碼后打包都是這樣的流程(圖10):
圖10
上面的流程無論那種方式打包都要等到組件包打完之后才能打測試包,出測試包的時間取決于打組件包的數(shù)量與組件間的依賴關系。有沒有辦法縮短這一流程呢?我們在本地開發(fā)的時候編譯很快,到了打測試包的時候卻要先打組件包才能打測試包,如果打包機也可以自定義部分源碼編譯,那么就不用等待組件先編譯完成了。這樣就直接省去了打組件包的時間,可以更快速的打包。
讓打包機支持部分源碼打包,首先得配置好podfile文件,但是開發(fā)者不可能提交podfile的修改,那樣的話會造成git沖突。于是我們另辟蹊徑,把需要變?yōu)樵创a依賴的組件名作為打包網(wǎng)絡請求的部分參數(shù),打包平臺在打包的時候將這部分參數(shù)寫入到環(huán)境變量里面,然后修改打包腳本,讓其在開始執(zhí)行pod install前去讀取這些參數(shù),如果有需要源碼編譯的組件,就按照參數(shù)去修改podfile,這樣一通操作下來就讓打包機完美支持了。
為了更完善這個功能,我們在開發(fā)人員點擊打包后,可以選擇是否同時打組件包,再結合上面提到打包后自動通知測試人員的功能,現(xiàn)在的流程是這樣的(圖11):
圖11
從上面簡化的流程可以看出,我們將原有的串行任務改為了可并行執(zhí)行的任務。經(jīng)過多次實驗對比,排除打包排隊情況的干擾,所有組件bundle平均打包時間為203秒,全bundle打測試包時間為367秒,部分源碼打包時間為384秒,所以理想環(huán)境情況打包效率提升32.6%。
- 原來總打包時長為:203 + 367 = 570
- 打包效率提升為:(570-384)/570 = 0.326315
考慮到實際情況,打完組件bundle包,開發(fā)或者測試人員收到通知后才會在打包平臺進行打測試包操作,還要勾選一些配置等信息。如果要打多個組件bundle包,組件之間還有依賴關系的話,那么就需要更多時間才能打出測試包,而源碼打包基本不受組件依賴的影響。所以這項優(yōu)化使得出包效率會遠遠超過32.6%。
四、總結
無論是架構演進、流程優(yōu)化還是制作工具,工程師們總是希望用技術手段去減少重復工作,提高人效。篇幅原因,做這些優(yōu)化的過程中遇到的很多問題及解決方案都沒羅列出來。目前還有些已知的問題還沒解決,這些已知問題是我們持續(xù)優(yōu)化的動力,也相信我們能為開發(fā)者帶來更優(yōu)秀的開發(fā)體驗。?