Cocoapods 1.3.0 版本之后官方已經(jīng)支持用 pod 集成單元測(cè)試譬嚣,詳情看官方指南搬男,此篇文章只針對(duì)當(dāng)時(shí) 1.0.0 版本下的情況,僅做保存記錄坡垫。
上周接到了個(gè)需求梭灿,老大要我們把項(xiàng)目代碼里某個(gè)庫覆蓋上單元測(cè)試。而那個(gè)庫沒有Demo冰悠,平時(shí)都是集成在工程里開發(fā)的堡妒。為啥沒有Demo,因?yàn)槟莻€(gè)庫依賴很重溉卓,說是個(gè)庫皮迟,實(shí)際只是把代碼用cocoapods拆分罷了……平時(shí)開發(fā)的時(shí)候,大家都是把庫集成在主工程里運(yùn)行桑寨。我想伏尼,單測(cè)寫在主工程的target里,這樣會(huì)顯得很雜尉尾,給人感覺是給整個(gè)工程做單元測(cè)試爆阶。那能不能弄一個(gè)Pod,專門把單測(cè)代碼寫在里面呢,既能git管理辨图,又可以分類管理班套。殊不知,這個(gè)過程徒役,坑如此多孽尽。
讓Pod的target類型變?yōu)閄CTest
像往常一樣我在命令行里窖壕,敲下一個(gè)熟悉的命令
pod lib create UnitTestPod
經(jīng)過簡(jiǎn)單的四個(gè)問題后忧勿,一個(gè)Pod生成了。
并在Demo里的Podfile熟練的寫下
target 'UnitTestPodDemo' do
pod 'UnitTestPod' , :path => 'UnitTestPod/UnitTestPod.podspec'
end
經(jīng)過一番pod install后瞻讽,打開了xcworkspace鸳吸。
?速勇?晌砾?這個(gè)Target的icon,有點(diǎn)不對(duì)勁啊…原來Cocoapods默認(rèn)生成的Pod是作為一個(gè)Target集成進(jìn)Pods這個(gè)XcodeProject里的烦磁,而且Target的默認(rèn)類型是Static Libaray养匈,也就是一個(gè)靜態(tài)庫。咋辦呢都伪?我要的是XCTest的Target類型呕乎。好吧,那看看Cocoapods的源碼吧陨晶。怎么找呢猬仁,有點(diǎn)Ruby基礎(chǔ)的人知道,Podfile里面寫東西實(shí)際上都是用Ruby語法實(shí)現(xiàn)的DSL(Domain Specific Language 領(lǐng)域特定語言)先誉。也就是相當(dāng)于實(shí)現(xiàn)了一套語法規(guī)則湿刽,比如target do,比如pod褐耳,比如:path=>這些诈闺,在Cocoapods都有對(duì)應(yīng)的語法實(shí)現(xiàn)。在執(zhí)行pod install的過程中铃芦,podfile中的信息被解析雅镊,然后把pod的信息進(jìn)行處理,并生成target并生成集成后的project文件杨帽。那對(duì)target的類型的寫入漓穿,也肯定也在生成Pods.xcodeproj的過程中。查看cocoapods的installer.rb注盈,發(fā)現(xiàn)里面有個(gè)intall方法如下:
def install!
prepare
resolve_dependencies
download_dependencies
verify_no_duplicate_framework_and_library_names
verify_no_static_framework_transitive_dependencies
verify_framework_usage
generate_pods_project
if installation_options.integrate_targets?
integrate_user_project
else
UI.section 'Skipping User Project Integration'
end
perform_post_install_actions
end
看起來這個(gè)過程可能是install的過程晃危,里面的generate_pods_project
方法,應(yīng)該就是生成pod_project的方法了。這個(gè)方法實(shí)現(xiàn)如下:
def generate_pods_project(generator = create_generator)
UI.section 'Generating Pods project' do
generator.generate!
@pods_project = generator.project
run_podfile_post_install_hooks
generator.write
generator.share_development_pod_schemes
write_lockfiles
end
end
再查看其類pods_project_generator.rb的generate方法(相當(dāng)于初始化方法)
def generate!
prepare
install_file_references
install_libraries
set_target_dependencies
end
這里面大概就是將依賴的文件和library加入工程僚饭,設(shè)置目標(biāo)依賴震叮。再看看install_libraries
方法
def install_libraries
UI.message '- Installing targets' do
pod_targets.sort_by(&:name).each do |pod_target|
target_installer = PodTargetInstaller.new(sandbox, pod_target)
target_installer.install!
end
aggregate_targets.sort_by(&:name).each do |target|
target_installer = AggregateTargetInstaller.new(sandbox, target)
target_installer.install!
end
add_system_framework_dependencies
end
end
在看看之類這個(gè)target_Installer實(shí)例,就是PodTargetInstaller類生成的鳍鸵∥辏看來這個(gè)類,應(yīng)該是生成target的類偿乖。再看看這個(gè)pod_target_installer.rb击罪,里面搜索一下type,居然沒有贪薪?媳禁??難道找錯(cuò)了画切?我又仔細(xì)看了一下竣稽,發(fā)現(xiàn)開始處有怎么一行代碼:
class PodTargetInstaller < TargetInstaller
看到這行代碼,我感覺和OC里的AClass : BClass
霍弹,這大概是繼承吧毫别。又找到了target_installer.rb里面的TargetInstaller類,終于發(fā)現(xiàn)了一行蛛絲馬跡典格。
def add_target
product_type = target.product_type
也就是說岛宦,target的類型,在cocoapods里钝计,是target的product_type屬性恋博。剩下的就是要把target的product_type,設(shè)置成cocoapods里的XCTest類型了私恬。那么接下來债沮,就有兩個(gè)問題要解決
- Cocoapods里的XCTest類型,是怎么表示的
- 在哪里插入這個(gè)改變類型的操作
也就是說涮瞻,product_type屬性其實(shí)是個(gè)String類型鲤拿,而XCTest的類型,就是'com.apple.product-type.bundle.unit-test'
那么第一個(gè)問題就解決了署咽。
剩下就是在哪里改變它了近顷。還記得剛才的generate_pods_project
方法么
def generate_pods_project(generator = create_generator)
UI.section 'Generating Pods project' do
generator.generate!
@pods_project = generator.project
run_podfile_post_install_hooks
generator.write
generator.share_development_pod_schemes
write_lockfiles
end
end
里面有個(gè)run_podfile_post_install_hooks
方法生音,難道,官方提供了podfile里的hook方法窒升?查看Cocoapods官網(wǎng)缀遍,發(fā)現(xiàn)官方還真提供了hooks!
有三種類型饱须,plugin是加上插件域醇,pre_install是提供hooks在下載好pod但還沒被install的時(shí)候,而post_install是東西都生成好了蓉媳,還沒被寫入磁盤的時(shí)候譬挚。看來在生成好后改變就可以了督怜。
根據(jù)官網(wǎng)的例子和前面的探索殴瘦,可以在podfile里加上hook方法如下
post_install do |installer|
installer.pods_project.targets.each do |target|
if target.name == "UnitTestPod"
target.product_type = 'com.apple.product-type.bundle.unit-test'
end
puts "`#{target.name}` change type to `#{target.product_type}`"
end
end
end
就是對(duì)installer的pod_project這個(gè)project對(duì)象的targerts數(shù)組每一個(gè)執(zhí)行一個(gè)循環(huán)狠角,當(dāng)找到我們需要的target時(shí)号杠,改變target的類型。puts是ruby里的log丰歌,相當(dāng)于NSLog姨蟋,printf。
好了立帖,重新pod install眼溶,可以發(fā)現(xiàn),target已經(jīng)變成了test的type了晓勇,而Xcode里的test欄也有了庫里的Test了堂飞。但這個(gè)時(shí)候,我們是無法引入并識(shí)別其它第三方庫的方法的绑咱,也找不到其它第三方庫的符號(hào)表绰筛。
讓Pod識(shí)別到別的靜態(tài)庫
這個(gè)時(shí)候,我們其它普通靜態(tài)庫的方法描融,在podspec里寫上s.dependency 'CDZPicker'
某個(gè)庫铝噩,再次pod install。這次窿克,ide識(shí)別了骏庸,再次運(yùn)行test,發(fā)現(xiàn)編譯報(bào)錯(cuò)了年叮,找不到符號(hào)表具被。如下圖:
同時(shí)可以發(fā)現(xiàn),實(shí)際上運(yùn)行test的時(shí)候只损,把依賴的庫編譯了一遍一姿。也就是說,現(xiàn)在這個(gè)test的target沒辦法找到編譯的產(chǎn)物(.o)。而在Cocoapods里生成的主工程的target里啸蜜,為啥就能找到第三方庫的編譯后產(chǎn)物呢坑雅?先講講正常引用靜態(tài)庫的因素,一個(gè)是告訴Xcode去哪里找衬横,也就是Target里Building Setting里的Library SearchPaths裹粤,里面指定了找編譯產(chǎn)物的路徑,另一個(gè)是告訴Xcode蜂林,要鏈接的靜態(tài)庫的名字遥诉,也就是Building Setting里的Other Linker Flags里用-l"靜態(tài)庫編譯產(chǎn)物名(去掉前面lib)"標(biāo)識(shí)。查看其BuildingSetting噪叙,發(fā)現(xiàn)其兩個(gè)決定的因素矮锈,都被Cocoapods配置好了,也就是target xxx do里做的睁蕾。
而默認(rèn)導(dǎo)入的Pod的target苞笨,這兩個(gè)參數(shù)都是"",也就是空的子眶。這個(gè)時(shí)候瀑凝,我們看看把這兩個(gè)設(shè)置設(shè)成默認(rèn)值會(huì)怎么樣呢?在post_install的hook方法里加入下面的代碼
target.build_configurations.each do |config|
config.build_settings['OTHER_LDFLAGS'] = '$(inherited)'
config.build_settings['LIBRARY_SEARCH_PATHS'] = '$(inherited)'
end
怎么知道這些設(shè)置對(duì)應(yīng)的鍵值是這些呢臭杰?Cocoapods實(shí)際上是通過生成XCConfig文件還配置這些的粤咪,在項(xiàng)目里搜索后綴名是xcconifg的文件,發(fā)現(xiàn)了Cocoapods寫的那些部分渴杆。
完整的XCConfig編寫可以參考Github上這個(gè)倉庫寥枝。而為啥設(shè)成默認(rèn)就可以了呢?在Building Setting里點(diǎn)擊Level模式查看磁奖,發(fā)現(xiàn)對(duì)于每一項(xiàng)設(shè)置囊拜,都有5個(gè)地方可以設(shè)置,層級(jí)從左到右点寥,如果在左邊的層級(jí)設(shè)置了艾疟,就取最左邊的設(shè)置覆蓋右邊的,而綠色的框代表每行的設(shè)置正在應(yīng)用的來源是來自哪里敢辩,也就是綠色框的代表的是最終的設(shè)置蔽莱。
而加上'(inherited)'同廉,那么依次類推仪糖。上面五欄里分別是Resolve柑司,Target,ConfigFile锅劝,Project攒驰,Default。第一層Resolve我不太清楚故爵,可能是編譯最終修復(fù)之類的玻粪,第二層就是target里修改的,第三層就是讀取對(duì)應(yīng)的xcconfig后綴文件里的配置诬垂,劲室,第四層是Project里修改的,最后是系統(tǒng)默認(rèn)的结窘。而這也是為什么一個(gè)Project可以對(duì)應(yīng)多個(gè)Target的原因很洋,target只是覆蓋了設(shè)置而已。而Cocoapods是把xcconfig寫好隧枫,并讓上層設(shè)置為'$(inherited)'喉磁,從而達(dá)到把配置寫進(jìn)Xcode。而在hook方法里悠垛,把target的config更改线定,相當(dāng)于把上層設(shè)置好,取下層XCConfig里的值确买。而我們看到Cocoapods默認(rèn)幫我們生成的Pod的XCConfig里,里面已經(jīng)寫好了正確的Library Search Path纱皆。
剩下就是Other Link Flag了湾趾,這個(gè)也很好解決,在podspec里寫上
s.pod_target_xcconfig = {'OTHER_LDFLAGS' => '$(inherited) -l"CDZPicker"'}
就好了派草。重新pod install一下搀缠,寫上單元測(cè)試代碼,點(diǎn)擊test近迁,OK艺普,一切順利。
等等鉴竭,點(diǎn)擊主工程的Run歧譬,好像跑不起來了……
曲線救國解決Test庫編譯問題
看看報(bào)錯(cuò),找不到庫的編譯產(chǎn)物搏存。思考了一下瑰步,應(yīng)該是因?yàn)門est的target比較特殊,并不會(huì)編譯自己并產(chǎn)生編譯產(chǎn)物璧眠。而記得剛才主工程的Other Linker Flag嗎缩焦,里面因?yàn)镃ocoapods認(rèn)為這個(gè)庫是個(gè)靜態(tài)庫读虏,是有編譯產(chǎn)物的且要鏈接的,所以有"-l'UnitTestPod'"袁滥。去掉之后盖桥,果然就可以編譯成功了。
但是這樣不夠優(yōu)雅题翻,每次Pod install之后葱轩,難道都要這樣刪掉嗎?
這時(shí)我想起Podfile里一個(gè)參數(shù)configurations藐握,一般我們會(huì)指定例如:configurations => ['Debug']
這樣來說明這個(gè)庫在Debug下才被編譯并鏈接進(jìn)主工程靴拱。既然這樣Release模式下就不會(huì)被鏈接,我們就可以利用這個(gè)特性猾普,新建一個(gè)沒用的Configuration,讓Podfile指定就可以了袜炕。在Project的Info里新建一個(gè)Test的Configuration,并在Podfile里的Pod指定:configurations => ['Test']
初家。
重新Pod install偎窘,可以看到Debug的Other Linker Flag里已經(jīng)沒有"-l'UnitTestPod'"來指定鏈接Pod了。編譯自然也就成功了溜在。
最后
最后的結(jié)果是陌知,因?yàn)槲覀児こ汤镉泻芏囝A(yù)編譯的庫,而預(yù)編譯的庫通過我們公司的一套方案來設(shè)置掖肋,可以根據(jù)每個(gè)人的設(shè)置仆葡,可以選擇源碼或預(yù)編譯,而生成的編譯產(chǎn)物預(yù)編譯的有一個(gè)前綴志笼。因?yàn)闆]辦法確定每個(gè)人某個(gè)庫的依賴的庫是否是預(yù)編譯的沿盅,所以靜態(tài)庫的名字是不確定的,可能是有前綴可能沒有纫溃,和每個(gè)人開發(fā)環(huán)境有關(guān)腰涧。在podspec里也沒辦法通過“-l‘靜態(tài)庫編譯產(chǎn)物名’”來鏈接到正確的編譯產(chǎn)物,也就沒辦法用這種方式進(jìn)行下去管理單元測(cè)試紊浩。沒想到最后填坑的結(jié)果窖铡,卻和公司另一套別的方案沖突了。雖然有些難過坊谁,但是在研究這個(gè)問題的幾天里费彼,我一個(gè)完全沒看過Ruby也不了解鏈接,Cocoapods也是按著例子寫的小白呜袁,開始到了解一些Ruby敌买,Cocoapods做了什么,Bulding Setting阶界,XCConfig虹钮,靜態(tài)庫怎么被鏈接的知識(shí)聋庵,還是感覺很開心的。
所有源碼和Demo
如果您覺得有幫助,不妨給個(gè)star鼓勵(lì)一下,歡迎關(guān)注&交流
有任何問題歡迎評(píng)論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz