攜程火車(chē)票iOS項(xiàng)目開(kāi)發(fā)體驗(yàn)優(yōu)化實(shí)踐
?作者 | 東海 ,攜程移動(dòng)開(kāi)發(fā)專(zhuān)家,專(zhuān)注于移動(dòng)端框架、移動(dòng)端性能。
元帥,攜程資深軟件工程師,致力于平臺(tái)基建開(kāi)發(fā)。
一、背景
現(xiàn)在各大公司的APP都采用的是組件化架構(gòu),組件化架構(gòu)帶來(lái)了高內(nèi)聚、低耦合、平臺(tái)化等諸多有點(diǎn),使工程結(jié)構(gòu)更加清晰,工程管理更加輕松。iOS工程大多采用CocoaPod進(jìn)行組件化管理,一些大型的項(xiàng)目需要打包平臺(tái)來(lái)執(zhí)行組件打bundle包和APP打測(cè)試包的任務(wù),在開(kāi)發(fā)方面會(huì)采用二進(jìn)制與源碼切換的方式來(lái)提高編譯速度。
組件化雖然對(duì)APP項(xiàng)目的工程管理帶來(lái)了巨大的好處,但是對(duì)開(kāi)發(fā)人員來(lái)講,存在著一些繁瑣的問(wèn)題:
在開(kāi)發(fā)中,如果需要調(diào)試未解開(kāi)源碼的組件,就需要重新執(zhí)行命令解開(kāi)相應(yīng)組件的源碼才能進(jìn)行調(diào)試。
每切換一次組件的源碼,都需要在終端輸入一串加了各種參數(shù)的命令來(lái)執(zhí)行pod install,手動(dòng)輸入慢,而且容易出錯(cuò)。
組件化使得組件顆粒度變得越來(lái)越細(xì),每個(gè)人所管理的組件數(shù)量就會(huì)多,每次組件更新都需要在打包平臺(tái)上進(jìn)行打包,等組件bundle包打完,再打測(cè)試包進(jìn)行驗(yàn)證。
這些雖然能讓工作正常進(jìn)行,但是繁瑣重復(fù)的操作卻影響了開(kāi)發(fā)人員的開(kāi)發(fā)效率。
二、現(xiàn)狀
攜程火車(chē)票APP一直以來(lái)采用的也是組件化管理,在去年改用CocoaPod進(jìn)行組件化管理,隨著業(yè)務(wù)的迭代和基礎(chǔ)建設(shè)不斷的完善,pod組件也越來(lái)越精細(xì)化,目前pod組件數(shù)量已超60+。
pod組件數(shù)量越龐大,開(kāi)發(fā)人員維護(hù)的成本也會(huì)越高,不僅要管理維護(hù)每個(gè)pod組件的更新,還要處理pod組件打bundle包問(wèn)題、測(cè)試包打包時(shí)間長(zhǎng)的問(wèn)題。上述繁瑣、重復(fù)、耗時(shí)的操作困擾著我們的iOS開(kāi)發(fā)人員。如果能盡可能的對(duì)這些開(kāi)發(fā)體驗(yàn)問(wèn)題進(jìn)行優(yōu)化,那么必然會(huì)帶來(lái)開(kāi)發(fā)人員效率的提升。
三、優(yōu)化方案
為了讓開(kāi)發(fā)人員調(diào)試代碼更加方便,打包測(cè)試體驗(yàn)更好,開(kāi)發(fā)過(guò)程更加專(zhuān)注,我們做了許多方面的操作優(yōu)化和技術(shù)實(shí)踐,主要有:
3.1 通過(guò)技術(shù)手段,實(shí)現(xiàn)二進(jìn)制調(diào)試
在開(kāi)發(fā)過(guò)程,難免會(huì)遇到自己想調(diào)試的組件沒(méi)有解開(kāi)源碼,程序在運(yùn)行中崩潰但是崩潰在了未解開(kāi)源碼的組件上,自己看到的只是一堆不明所以的匯編代碼(圖1),無(wú)法像源碼調(diào)試那樣看到足夠豐富的調(diào)試信息。
圖1
3.1.1 二進(jìn)制文件分析
如何才能不解開(kāi)源碼也能調(diào)試二進(jìn)制、崩潰在了二進(jìn)制組件上也能定位到具體哪一行成了我們新的問(wèn)題。我們找了各種資料,找到了美團(tuán)有款zsource的CocoaPod插件可以進(jìn)行二進(jìn)制調(diào)試,雖未開(kāi)源,但大致邏輯文章里已經(jīng)羅列的很清晰,大致原理:
以libXXXX.a二進(jìn)制文件為例,用 MachOView 來(lái)查看二進(jìn)制文件,以獲取到更友好的二進(jìn)制信息。我們可以看到 “debug_str” Section 這些信息都存在了二進(jìn)制的中。debug_str在編譯的時(shí)候內(nèi)部會(huì)記錄源碼地址:
圖2
使用命令在終端輸入:
dwarfdump ./libXXXX.a | grep 'XXXX'
注意到了 AT_name 這個(gè)字段名,去DWARF 1.1.0 Reference文檔中查閱,我們可以得知:
- 一個(gè)DW_AT_name屬性,其值是一個(gè)以空字符結(jié)尾的字符串,其中包含從其派生編譯單元的主源文件的完整或相對(duì)路徑名。
- 一個(gè)DW_AT_comp_dir屬性,其值是一個(gè)以空值結(jié)尾的字符串,其中包含編譯命令的當(dāng)前工作目錄,該編譯命令以某種形式將Forelax視為主機(jī)系統(tǒng),從而生成此編譯單元。
XXXX.swift源文件存在這個(gè)地址下: /Users/marshal/Desktop/XXXX/XXXX/XXXX.swift
這個(gè)地址就是編譯時(shí)源碼所在地址,Debug調(diào)試的時(shí)候,編譯器會(huì)先從這里拿對(duì)應(yīng)映射地址去加載源碼文件。如果存在對(duì)應(yīng)地址存在源碼文件時(shí),就能進(jìn)入源碼調(diào)試。
3.1.2 腳本開(kāi)發(fā)
了解基礎(chǔ)原理后,那接下來(lái)的事情就是解決各種問(wèn)題障礙:
1)要獲取到靜態(tài)庫(kù)的源碼。
2)獲取靜態(tài)庫(kù)中存儲(chǔ)的編譯靜態(tài)庫(kù)時(shí)源碼文件所在的路徑。
3)在本地創(chuàng)建上面????獲取的路徑,讓靜態(tài)庫(kù)的源碼和該路徑關(guān)聯(lián)起來(lái)。
- 問(wèn)題1:我們當(dāng)時(shí)制作二進(jìn)制包時(shí)為了方便切換源碼調(diào)試,在pod install的時(shí)候源碼+.a會(huì)同時(shí)下載到本地。
- 問(wèn)題2:在美團(tuán)的文章中可以了解到,使用dwarfdump 命令可以獲取靜態(tài)庫(kù)中存儲(chǔ)的編譯靜態(tài)庫(kù)時(shí)源碼文件所在的路徑。
- 問(wèn)題3:這個(gè)問(wèn)題,我想大多數(shù)人第一個(gè)的想法是把靜態(tài)庫(kù)的源碼copy到本地創(chuàng)建的靜態(tài)庫(kù)編譯目錄里面,但是我們采用更加輕巧的方式:通過(guò)軟連接命令ln將兩個(gè)目標(biāo)關(guān)聯(lián)起來(lái)。
最終我們通過(guò)開(kāi)發(fā)腳本解決了上面的問(wèn)題,通過(guò)Hook post_integrate 將腳本穿插到pod install的過(guò)程中,使整個(gè)過(guò)程順暢自然。
主要腳本代碼如下:
#鏈接,.a文件位置, 源碼目錄,工程名
def link(lib_file,target_path,basename)
#查詢(xún)?cè)创a所在位置
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
#通過(guò)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}"
#當(dāng)前項(xiàng)目的pod目錄
pod_path = "#{path_root}/#{project}/Pods"
installer.pods_project.targets.each do |target|
bunlde_name = target.name
#這里可以根據(jù)環(huán)境變量選擇性開(kāi)啟源碼調(diào)試
enableDebug = ENV["#{bunlde_name}_DEBUGLIB"]
enableDebug = true
if enableDebug
DebugLibCode.new().link(lib_file,target_path,basename)
end
end
end
整個(gè)流程如下圖3:
圖3
3.1.3 方案調(diào)優(yōu)
通過(guò)上面的腳本雖然實(shí)現(xiàn)了二進(jìn)制靜態(tài)庫(kù)的調(diào)試,但是在推廣和使用的時(shí)候又遇到了新的問(wèn)題:
1)每個(gè)開(kāi)發(fā)人員第一次執(zhí)行二進(jìn)制調(diào)試腳本的時(shí)候都會(huì)報(bào)錯(cuò),因?yàn)闄?quán)限問(wèn)題,需要開(kāi)發(fā)人員手動(dòng)在 Users下面創(chuàng)建一個(gè)cbuilder的用戶目錄。
2)每次pod install的時(shí)間變長(zhǎng)了很多,經(jīng)過(guò)多次測(cè)量,在M1芯片的電腦上,從未接入二進(jìn)制調(diào)試執(zhí)行pod install到接入后增加超過(guò)了60%;在Inter芯片的電腦上,增加超過(guò)了 70%,如圖4:
圖4
對(duì)于問(wèn)題1,開(kāi)發(fā)人員手動(dòng)cbuilder的用戶目錄是個(gè)不合理的操作,我們把這個(gè)操作集成到 ZTPodTool內(nèi)(ZTPodTool是我們開(kāi)發(fā)的一個(gè)podfile管理工具,下面會(huì)詳細(xì)介紹),讓ZTPodTool來(lái)創(chuàng)建cbuilder用戶目錄,開(kāi)發(fā)人員就能無(wú)感知的開(kāi)發(fā)。但是嘗試了各種創(chuàng)建目錄的api發(fā)現(xiàn)都不能創(chuàng)建這個(gè)目錄,這個(gè)問(wèn)題困擾了我們好久。
查找了大量資料,發(fā)現(xiàn)AppleScript是一個(gè)與macOS結(jié)合非常緊密的腳本語(yǔ)言,它顯著的特點(diǎn)就是可以控制其他macOS上的應(yīng)用程序,通過(guò)使用它可以完成一些繁瑣重復(fù)的工作。代碼如下:
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];
對(duì)于問(wèn)題2,我們分析發(fā)現(xiàn),通過(guò)dwarfdump命令來(lái)解析二進(jìn)制文件獲取源碼路徑,會(huì)先加載整個(gè)二進(jìn)制到內(nèi)存中,再通過(guò)grep、head、cut等命令解析出取源碼路徑目錄,這個(gè)過(guò)程非常耗時(shí)。工程里面的pod組件庫(kù)越多,這個(gè)過(guò)程耗時(shí)就越大。
這些耗時(shí)的命令就為了獲取個(gè)路徑,如果能通過(guò)其他途徑獲取路徑就可以把這些時(shí)間節(jié)省下來(lái),可以省下一大筆時(shí)間開(kāi)支。于是我們想到,既然是打包機(jī)上的路徑,那就讓打包機(jī)打包時(shí)把包相關(guān)信息用json保存在產(chǎn)物目錄下,在install的時(shí)候,通過(guò)讀取產(chǎn)物里面的json文件就可以獲取打包源碼路徑。
優(yōu)化腳本后,經(jīng)過(guò)測(cè)量,和之前pod install的時(shí)間相差無(wú)幾(圖5)。就這樣,我們的開(kāi)發(fā)人員可以無(wú)差別的調(diào)試各個(gè)組件的代碼了。
圖5
3.2 另辟蹊徑,解決M1電腦iOS模擬器剪切板問(wèn)題
用M1系列電腦在iOS模擬器上開(kāi)發(fā)的人員基本上都會(huì)遇到一個(gè)非常棘手的問(wèn)題,那就是模擬器的剪切板無(wú)法和電腦的剪切板互通,開(kāi)發(fā)人員也無(wú)法給剪切板賦值,一賦值就報(bào)錯(cuò):
[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 }}
在蘋(píng)果的官方論壇上反饋這個(gè)問(wèn)題,得到的回復(fù)卻是這樣的:
//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?
如果剪切板不能用,在模擬器中輸入地址或者長(zhǎng)文本,對(duì)iOS、RN和H5的開(kāi)發(fā)者都是非常耗時(shí)、非常痛苦的事情。為此我們想了一個(gè)輕巧的辦法,繞過(guò)了這個(gè)系統(tǒng)bug,完美解決了這個(gè)問(wèn)題,主要流程如下:
圖6
我們給自己的APP自定了快捷鍵 Ctrl + V,用于觸發(fā)用戶進(jìn)行粘貼操作 :
- (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:@""]
];
}
本地服務(wù)我們是開(kāi)發(fā)了一個(gè)mac客戶端,主要功能是:在本地起一個(gè)Http服務(wù),專(zhuān)門(mén)處理獲取當(dāng)前電腦剪切板內(nèi)容的請(qǐng)求。它顯示在系統(tǒng)狀態(tài)欄上,方便控制服務(wù)的開(kāi)啟、停止和退出,支持修改端口號(hào)(圖7)。點(diǎn)擊這里即可下載使用。
圖7
獲取當(dāng)前輸入框的代碼如下:
@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(@"讀取出錯(cuò),請(qǐng)檢查服務(wù)是否打開(kāi)");
} 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
}
加入這項(xiàng)優(yōu)化后,開(kāi)發(fā)人員只用使用Ctrl + V可以把mac電腦的剪切板內(nèi)容粘貼到iOS模擬器的輸入框中了,和正常的復(fù)制粘貼的功能體驗(yàn)完全一樣。
3.3 開(kāi)發(fā)可視化工具,集成各種功能
組件的日漸繁多,使得podfile文件操作變的復(fù)雜,打包組件bundle包變的頻繁,打測(cè)試包時(shí)間變的冗長(zhǎng)。為了簡(jiǎn)化終端輸入命令、打組件包和APP測(cè)試包的繁瑣操作,我們開(kāi)發(fā)了一款可視化工具ZTPodTool,圖8。這個(gè)工具不僅能直接展示出組件間的依賴(lài)層級(jí)關(guān)系,而且可以直接在工具上提交打組件包請(qǐng)求,不用再到瀏覽器的打包平臺(tái)進(jìn)行頻繁切換頁(yè)面的點(diǎn)擊操作。
圖8
在ZTPodTool上,不僅可以便捷地操作每個(gè)組件的源碼與二進(jìn)制切換、打組件包,而且支持打測(cè)試包(圖9)。
圖9
開(kāi)發(fā)人員點(diǎn)擊install按鈕,ZTPodTool就會(huì)根據(jù)用戶的源碼設(shè)置拼裝好命令,然后自動(dòng)打開(kāi)顯示日志更友好的終端,讓終端來(lái)執(zhí)行該命令。雖然通過(guò)NSTask和NSPipe也可以執(zhí)行pod install命令,但是獲取到的StandardOutput日志無(wú)法高亮,看起來(lái)十分痛苦。要是能直接在終端執(zhí)行,那樣對(duì)開(kāi)發(fā)者就更友好了,查閱蘋(píng)果文檔后,發(fā)現(xiàn)官方?jīng)]有提供“終端”的SDK供開(kāi)發(fā)者使用,在當(dāng)時(shí)如何通過(guò)其他途徑喚起終端執(zhí)行命令成一件必須解決的事情。
最終還是靠上文提到AppleScript來(lái)解決了這個(gè)問(wèn)題,下面是兩種調(diào)用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];
我們加入的更加人性化的功能還有:
- 收到測(cè)試包打包完成消息后,開(kāi)發(fā)人員通常發(fā)送安裝包二位給測(cè)試人員進(jìn)行驗(yàn)證。在ZTPodTool上,我們支持了打包后自動(dòng)發(fā)送包的二維碼給所選的測(cè)試人員,無(wú)需開(kāi)發(fā)人員再通知。
- 列表中組件越來(lái)越多,開(kāi)發(fā)人員尋找選擇自己維護(hù)的組件也需要更多的時(shí)間。為此我們支持了關(guān)注列表功能,開(kāi)發(fā)人員只看到自己關(guān)注的組件。
- 在ZTPodTool上輸入版本號(hào),就可以更新各個(gè)pod組件的版本。
- install完成后自動(dòng)打開(kāi)工程
3.4 優(yōu)化打包流程,更快打出測(cè)試包
二進(jìn)制打包最大的痛點(diǎn)就在于打ipa前先打獨(dú)立組件二進(jìn)制,多個(gè)組件依賴(lài),需要串行打依賴(lài)bundle,整體的打包流程上耗時(shí)比較大。在測(cè)試階段,如果測(cè)試包能快速的打出來(lái),這無(wú)疑能顯著的提升bug的驗(yàn)收效率。我們每次提交代碼后打包都是這樣的流程(圖10):
圖10
上面的流程無(wú)論那種方式打包都要等到組件包打完之后才能打測(cè)試包,出測(cè)試包的時(shí)間取決于打組件包的數(shù)量與組件間的依賴(lài)關(guān)系。有沒(méi)有辦法縮短這一流程呢?我們?cè)诒镜亻_(kāi)發(fā)的時(shí)候編譯很快,到了打測(cè)試包的時(shí)候卻要先打組件包才能打測(cè)試包,如果打包機(jī)也可以自定義部分源碼編譯,那么就不用等待組件先編譯完成了。這樣就直接省去了打組件包的時(shí)間,可以更快速的打包。
讓打包機(jī)支持部分源碼打包,首先得配置好podfile文件,但是開(kāi)發(fā)者不可能提交podfile的修改,那樣的話會(huì)造成git沖突。于是我們另辟蹊徑,把需要變?yōu)樵创a依賴(lài)的組件名作為打包網(wǎng)絡(luò)請(qǐng)求的部分參數(shù),打包平臺(tái)在打包的時(shí)候?qū)⑦@部分參數(shù)寫(xiě)入到環(huán)境變量里面,然后修改打包腳本,讓其在開(kāi)始執(zhí)行pod install前去讀取這些參數(shù),如果有需要源碼編譯的組件,就按照參數(shù)去修改podfile,這樣一通操作下來(lái)就讓打包機(jī)完美支持了。
為了更完善這個(gè)功能,我們?cè)陂_(kāi)發(fā)人員點(diǎn)擊打包后,可以選擇是否同時(shí)打組件包,再結(jié)合上面提到打包后自動(dòng)通知測(cè)試人員的功能,現(xiàn)在的流程是這樣的(圖11):
圖11
從上面簡(jiǎn)化的流程可以看出,我們將原有的串行任務(wù)改為了可并行執(zhí)行的任務(wù)。經(jīng)過(guò)多次實(shí)驗(yàn)對(duì)比,排除打包排隊(duì)情況的干擾,所有組件bundle平均打包時(shí)間為203秒,全bundle打測(cè)試包時(shí)間為367秒,部分源碼打包時(shí)間為384秒,所以理想環(huán)境情況打包效率提升32.6%。
- 原來(lái)總打包時(shí)長(zhǎng)為:203 + 367 = 570
- 打包效率提升為:(570-384)/570 = 0.326315
考慮到實(shí)際情況,打完組件bundle包,開(kāi)發(fā)或者測(cè)試人員收到通知后才會(huì)在打包平臺(tái)進(jìn)行打測(cè)試包操作,還要勾選一些配置等信息。如果要打多個(gè)組件bundle包,組件之間還有依賴(lài)關(guān)系的話,那么就需要更多時(shí)間才能打出測(cè)試包,而源碼打包基本不受組件依賴(lài)的影響。所以這項(xiàng)優(yōu)化使得出包效率會(huì)遠(yuǎn)遠(yuǎn)超過(guò)32.6%。
四、總結(jié)
無(wú)論是架構(gòu)演進(jìn)、流程優(yōu)化還是制作工具,工程師們總是希望用技術(shù)手段去減少重復(fù)工作,提高人效。篇幅原因,做這些優(yōu)化的過(guò)程中遇到的很多問(wèn)題及解決方案都沒(méi)羅列出來(lái)。目前還有些已知的問(wèn)題還沒(méi)解決,這些已知問(wèn)題是我們持續(xù)優(yōu)化的動(dòng)力,也相信我們能為開(kāi)發(fā)者帶來(lái)更優(yōu)秀的開(kāi)發(fā)體驗(yàn)。?