iOS 啟動(dòng)階段耗時(shí)進(jìn)行分析

前言

啟動(dòng)優(yōu)化一役后匙睹,超預(yù)期將所負(fù)責(zé)的 App 雙端啟動(dòng)的耗時(shí)都降低了65%以上,iOS 在iPhone7上速度達(dá)到了400毫秒以內(nèi)瑟由。就像產(chǎn)品們用后說的絮重,快到不習(xí)慣。由于 App 日活用戶過億歹苦,算一下每天為用戶省下的時(shí)間青伤,還是蠻有成就感的。

啟動(dòng)階段性能多維度分析

要優(yōu)化殴瘦,先要做到的是對啟動(dòng)階段各個(gè)性能緯度做分析狠角,包括主線程耗時(shí)、CPU蚪腋、內(nèi)存擎厢、I/O、網(wǎng)絡(luò)辣吃。這樣才能夠更加全面的掌握啟動(dòng)階段的開銷,找出不合理的方法調(diào)用芬探。啟動(dòng)越快神得,更多的方法調(diào)用就應(yīng)該做成按需執(zhí)行,將啟動(dòng)壓力分?jǐn)偼捣拢涣粝履切﹩?dòng)后方法都會(huì)依賴的方法和庫的初始化哩簿,比如網(wǎng)絡(luò)庫、Crash 庫等酝静。而剩下那些需要預(yù)加載的功能可以放到啟動(dòng)階段后再執(zhí)行节榜。

啟動(dòng)有哪幾種類型,啟動(dòng)有哪些階段呢别智?

啟動(dòng)類型分為:

  • Cold:App 重啟后啟動(dòng)宗苍,不在內(nèi)存里也沒有進(jìn)程存在。
  • Warm:App 最近結(jié)束后再啟動(dòng)薄榛,有部分在內(nèi)存但沒有進(jìn)程存在讳窟。
  • Resume:App 沒結(jié)束,只是暫停敞恋,全在內(nèi)存中丽啡,進(jìn)程也存在。

分析階段一般都是針對 Cold 類型進(jìn)行分析硬猫,目的就是要讓測試環(huán)境穩(wěn)定补箍。為了穩(wěn)定測試環(huán)境有時(shí)還需要找些穩(wěn)定的機(jī)型改执,對于 iOS 來說iPhone7性能中等,穩(wěn)定性也不錯(cuò)就很適合坑雅,Android 的 Vivo 系列也相對穩(wěn)定辈挂,華為和小米系列數(shù)據(jù)波動(dòng)就比較大。除了機(jī)型外控制測試機(jī)溫度也很重要霞丧,一旦溫度過高系統(tǒng)還會(huì)降頻執(zhí)行影響測試數(shù)據(jù)呢岗。有時(shí)候還會(huì)置飛行模式采用 Mock 網(wǎng)絡(luò)請求的方式來減少不穩(wěn)定的網(wǎng)絡(luò)影響測試數(shù)據(jù)。最好時(shí)重啟后退 iCloud 賬號(hào)蛹尝,放置一段時(shí)間再測后豫,更加準(zhǔn)確些。

了解啟動(dòng)的階段目的就是聚焦范圍突那,從用戶體驗(yàn)上來確定哪個(gè)階段要快挫酿,以便能夠讓用戶可視和響應(yīng)用戶操作的時(shí)間更快。

簡單來說 iOS 啟動(dòng)分為加載 Mach-O 和運(yùn)行時(shí)初始化過程愕难,加載 Mach-O 會(huì)先判斷加載的文件是不是 Mach-O早龟,通過文件第一個(gè)字節(jié),也叫魔數(shù)來判斷猫缭,當(dāng)是下面四種時(shí)可以判定是 Mach-O 文件:

  • 0xfeedface 對應(yīng)的 loader.h 里的宏是 MH_MAGIC
  • 0xfeedfact 宏是 MH_MAGIC_64
  • NXSwapInt(MH_MAGIC) 宏 MH_GIGAM
  • NXSwapInt(MH_MAGIC_64) 宏 MH_GIGAM_64

Mach-O 分為主要分為 中間對象文件(MH_OBJECT)葱弟、可執(zhí)行二進(jìn)制(MH_EXECUTE)、VM 共享庫文件(MH_FVMLIB)猜丹、Crash 產(chǎn)生的 Core 文件(MH_CORE)芝加、preload(MH_PRELOAD)、動(dòng)態(tài)共享庫(MH_DYLIB)射窒、動(dòng)態(tài)鏈接器(MH_DYLINKER)藏杖、靜態(tài)鏈接文件(MH_DYLIB_STUB)、符號(hào)文件和調(diào)試信息(MH_DSYM)這幾種脉顿。確定是 Mach-O 后蝌麸,內(nèi)核會(huì) fork 一個(gè)進(jìn)程,execve 開始加載艾疟。檢查 Mach-O Header来吩。隨后加載 dyld 和程序到 Load Command 地址空間。通過 dyld_stub_binder 開始執(zhí)行 dyld蔽莱,dyld 會(huì)進(jìn)行 rebase误褪、binding、lazy binding碾褂、導(dǎo)出符號(hào)兽间,也可以通過 DYLD_INSERT_LIBRARIES 進(jìn)行 hook。dyld_stub_binder 給偏移量到 dyld 解釋特殊字節(jié)碼 Segment 中正塌,也就是真實(shí)地址嘀略,把真實(shí)地址寫入到 la_symbol_ptr 里恤溶,跳轉(zhuǎn)時(shí)通過 stub 的 jump 指令跳轉(zhuǎn)到真實(shí)地址。 dyld 加載所有依賴庫帜羊,將動(dòng)態(tài)庫導(dǎo)出的 trie 結(jié)構(gòu)符號(hào)執(zhí)行符號(hào)綁定咒程,也就是 non lazybinding,綁定解析其他模塊功能和數(shù)據(jù)引用過程讼育,就是導(dǎo)入符號(hào)帐姻。

Trie 也叫數(shù)字樹或前綴樹,是一種搜索樹奶段。查找復(fù)雜度 O(m)饥瓷,m 是字符串的長度。和散列表相比,散列最差復(fù)雜度是 O(N),一般都是 O(1)舀奶,用 O(m)時(shí)間評(píng)估 hash。散列缺點(diǎn)是會(huì)分配一大塊內(nèi)存棺克,內(nèi)容越多所占內(nèi)存越大。Trie 不僅查找快线定,插入和刪除都很快娜谊,適合存儲(chǔ)預(yù)測性文本或自動(dòng)完成詞典。為了進(jìn)一步優(yōu)化所占空間斤讥,可以將 Trie 這種樹形的確定性有限自動(dòng)機(jī)壓縮成確定性非循環(huán)有限狀態(tài)自動(dòng)體(DAFSA)纱皆,其空間小,做法是會(huì)壓縮相同分支周偎。對于更大內(nèi)容,還可以做更進(jìn)一步的優(yōu)化撑帖,比如使用字母縮減的實(shí)現(xiàn)技術(shù)蓉坎,把原來的字符串重新解釋為較長的字符串;使用單鏈?zhǔn)搅斜砗伲?jié)點(diǎn)設(shè)計(jì)為由符號(hào)蛉艾、子節(jié)點(diǎn)、下一個(gè)節(jié)點(diǎn)來表示衷敌;將字母表數(shù)組存儲(chǔ)為代表 ASCII 字母表的256位的位圖勿侯。

盡管 Trie 對于性能會(huì)做很多優(yōu)化,但是符號(hào)過多依然會(huì)增加性能消耗缴罗,對于動(dòng)態(tài)庫導(dǎo)出的符號(hào)不宜太多助琐,盡量保持公共符號(hào)少,私有符號(hào)集豐富面氓。這樣維護(hù)起來也方便兵钮,版本兼容性也好蛆橡,還能優(yōu)化動(dòng)態(tài)加載程序到進(jìn)程的時(shí)間。

然后執(zhí)行 attribute 的 constructor 函數(shù)掘譬。舉個(gè)例子:

#include <stdio.h>

__attribute__((constructor))
static void prepare() {
    printf("%s\n", "prepare");
}

__attribute__((destructor))
static void end() {
    printf("%s\n", "end");
}

void showHeader() { 
    printf("%s\n", "header");
}

運(yùn)行結(jié)果:

ming@mingdeMacBook-Pro macho_demo % ./main "hi"
prepare
hi
end

運(yùn)行時(shí)初始化過程 分為:

  • 加載類擴(kuò)展
  • 加載 C++靜態(tài)對象
  • 調(diào)用+load 函數(shù)
  • 執(zhí)行 main 函數(shù)
  • Application 初始化泰演,到 applicationDidFinishLaunchingWithOptions 執(zhí)行完
  • 初始化幀渲染,到 viewDidAppear 執(zhí)行完葱轩,用戶可見可操作睦焕。

過程概括起來如下圖:

也就是說對啟動(dòng)階段的分析以 viewDidAppear 為截止。這次優(yōu)化之前已經(jīng)對 Application 初始化之前做過優(yōu)化靴拱,效果并不明顯垃喊,沒有本質(zhì)的提高,所以這次主要針對 Application 初始化到 viewDidAppear 這個(gè)階段各個(gè)性能多緯度進(jìn)行分析缭嫡。多維度具體包含內(nèi)容如下圖:

工具的選擇其實(shí)目前看來是很多的缔御,Apple 提供的 System Trace 會(huì)提供全面系統(tǒng)的行為,可以顯示底層系統(tǒng)線程和內(nèi)存調(diào)度情況妇蛀,分析鎖耕突、線程、內(nèi)存评架、系統(tǒng)調(diào)用等問題眷茁。總的來說纵诞,通過 System Trace 你能清楚知道每時(shí)每刻 App 對系統(tǒng)資源使用情況上祈。

System Trace 能查看線程的狀態(tài),可以了解高優(yōu)線程使用相對于 CPU 數(shù)量是否合理浙芙,可以看到線程在執(zhí)行登刺、掛起、上下文切換嗡呼、被打斷還是被搶占的情況纸俭。虛擬內(nèi)存使用產(chǎn)生的耗時(shí)也能看到,比如分配物理內(nèi)存南窗,內(nèi)存解壓縮揍很,無緩存時(shí)進(jìn)行緩存的耗時(shí)等。甚至是發(fā)熱情況也能看到万伤。

System Trace 還提供手動(dòng)打點(diǎn)進(jìn)行信息顯式窒悔,在你的代碼中 導(dǎo)入 sys/kdebug_signpost.h 后,配對 kdebug_signpost_start 和 kdebug_signpost_end 就可以了敌买。這兩個(gè)方法有五個(gè)參數(shù)简珠,第一個(gè)是 id,最后一個(gè)是顏色虹钮,中間都是預(yù)留字段北救。

Xcode11開始 XCTest 還提供了測量性能的 Api荐操。蘋果在2019年 WWDC 啟動(dòng)優(yōu)化專題 Optimizing App Launch - WWDC 2019 - Videos - Apple Developer 上也介紹了 Instruments 里的最新模板 App launch 如何分析啟動(dòng)性能。但是要想達(dá)到對啟動(dòng)數(shù)據(jù)進(jìn)行留存取均值珍策、Diff托启、過濾、關(guān)聯(lián)分析等自動(dòng)化操作攘宙,App launch 目前還沒法做到屯耸。

主線程耗時(shí)

多個(gè)維度性能緯度分析中最重要,最終用戶體感到的是主線程耗時(shí)分析蹭劈。對主線程方法耗時(shí)可以直接使用Messier - 簡單易用的Objective-C方法跟蹤工具 - everettjf - 首先很有趣 生成 trace json 進(jìn)行分析疗绣,或者參看這個(gè)代碼GCDFetchFeed/SMCallTraceCore.c at master · ming1016/GCDFetchFeed · GitHub,自己手動(dòng) hook objc_msgSend 生成一份Objective-C 方法耗時(shí)數(shù)據(jù)進(jìn)行分析铺韧。還有種插樁方式多矮,可以解析 IR(加快編譯速度),然后在每個(gè)方法前后插入耗時(shí)統(tǒng)計(jì)函數(shù)哈打。文章后面我會(huì)著重介紹如何開發(fā)工具進(jìn)一步分析這份數(shù)據(jù)塔逃,以達(dá)到監(jiān)控啟動(dòng)階段方法耗時(shí)的目的。

hook 所有的方法調(diào)用料仗,對詳細(xì)分析時(shí)很有用湾盗,不過對于整個(gè)啟動(dòng)時(shí)間影響很大,要想獲取啟動(dòng)每個(gè)階段更準(zhǔn)確的時(shí)間消耗還需要依賴手動(dòng)埋點(diǎn)立轧。為了更好的分析啟動(dòng)耗時(shí)問題格粪,手動(dòng)埋點(diǎn)也會(huì)埋的越來越多,也會(huì)影響啟動(dòng)時(shí)間精確度氛改,特別是當(dāng)團(tuán)隊(duì)很多帐萎,模塊很多時(shí),問題會(huì)突出胜卤。但疆导,每個(gè)團(tuán)隊(duì)在排查啟動(dòng)耗時(shí)往往只會(huì)關(guān)注自己或相關(guān)某幾個(gè)模塊的分析,基于此瑰艘,可以把不同模塊埋點(diǎn)分組是鬼,靈活組合肤舞,這樣就可以照顧到多種需求了紫新。

CPU

為什么分析啟動(dòng)慢除了分析主線程方法耗時(shí)外,還要分析其它緯度的性能呢李剖?

我們先看看啟動(dòng)慢的表現(xiàn)芒率,啟動(dòng)慢意味著界面響應(yīng)慢、網(wǎng)絡(luò)慢(數(shù)據(jù)量大篙顺、請求數(shù)多)偶芍、CPU 超負(fù)荷降頻(并行任務(wù)多充择、運(yùn)算多),可以看出影響啟動(dòng)的因素很多匪蟀,還需要全面考慮椎麦。

對于 CPU 來說,WWDC 的 What’s New in Energy Debugging - WWDC 2018 - Videos - Apple Developer 里介紹了用 Energy Log 來查 CPU 耗電材彪,當(dāng)前臺(tái)三分鐘或后臺(tái)一分鐘 CPU 線程連續(xù)占用80%以上就判定為耗電观挎,同時(shí)記錄耗電線程堆棧供分析。還有一個(gè) MetrickKit 專門用來收集電源和性能統(tǒng)計(jì)數(shù)據(jù)段化,每24小時(shí)就會(huì)對收集的數(shù)據(jù)進(jìn)行匯總上報(bào)嘁捷,Mattt 在 NShipster 網(wǎng)站上也發(fā)了篇文章MetricKit - NSHipster專門進(jìn)行了介紹。那么 CPU 的詳細(xì)使用情況如何獲取呢显熏?也就是說哪個(gè)方法用了多少 CPU雄嚣。

有好幾種獲取詳細(xì) CPU 使用情況的方法。線程是計(jì)算機(jī)資源調(diào)度和分配的基本單位喘蟆。CPU 使用情況會(huì)提現(xiàn)到線程這樣的基本單位上缓升。task_theads 的 act_list 數(shù)組包含所有線程,使用 thread_info 的接口可以返回線程的基本信息履肃,這些信息定義在 thread_basic_info_t 結(jié)構(gòu)體中仔沿。這個(gè)結(jié)構(gòu)體內(nèi)的信息包含了線程運(yùn)行時(shí)間、運(yùn)行狀態(tài)以及調(diào)度優(yōu)先級(jí)尺棋,其中也包含了 CPU 使用信息 cpu_usage封锉。獲取方式參看 objective c - Get detailed iOS CPU usage with different states - Stack Overflow。GT GitHub - Tencent/GT: GT (Great Tit) is a portable debugging tool for bug hunting and performance tuning on smartphones anytime and anywhere just as listening music with Walkman. GT can act as the Integrated Debug Environment by directly running on smartphones. 里也有獲取 CPU 的代碼膘螟。

整體 CPU 占用率可以通過 host_statistics 函數(shù)可以取到 host_cpu_load_info成福,其中 cpu_ticks 數(shù)組是 CPU 運(yùn)行的時(shí)鐘脈沖數(shù)量。通過 cpu_ticks 數(shù)組里的狀態(tài)荆残,可以分別獲取 CPU_STATE_USER奴艾、CPU_STATE_NICE、CPU_STATE_SYSTEM 這三個(gè)表示使用中的狀態(tài)内斯,除以整體 CPU 就可以取到 CPU 的占比蕴潦。通過 NSProcessInfo 的 activeProcessorCount 還可以得到 CPU 的核數(shù)。線上數(shù)據(jù)分析時(shí)會(huì)發(fā)現(xiàn)相同機(jī)型和系統(tǒng)的手機(jī)俘闯,性能表現(xiàn)卻截然不同潭苞,這是由于手機(jī)過熱或者電池?fù)p耗過大后系統(tǒng)降低了 CPU 頻率所致。所以如果取得 CPU 頻率后也可以針對那些降頻的手機(jī)來進(jìn)行針對性的優(yōu)化真朗,以保證流暢體驗(yàn)此疹。獲取方式可以參考 GitHub - zenny-chen/CPU-Dasher-for-iOS: CPU Dasher for iOS source code. It only supports ARMv7 and ARMv7s architectures.

內(nèi)存

要想獲取 App 真實(shí)的內(nèi)存使用情況可以參看 WebKit 的源碼,webkit/MemoryFootprintCocoa.cpp at 52bc6f0a96a062cb0eb76e9a81497183dc87c268 · WebKit/webkit · GitHub 。JetSam會(huì)判斷 App 使用內(nèi)存情況蝗碎,超出閾值就會(huì)殺死 App湖笨,JetSam 獲取閾值的代碼在 darwin-xnu/kern_memorystatus.c at 0a798f6738bc1db01281fc08ae024145e84df927 · apple/darwin-xnu · GitHub。整個(gè)設(shè)備物理內(nèi)存大小可以通過 NSProcessInfo 的 physicalMemory 來獲取蹦骑。

網(wǎng)絡(luò)

對于網(wǎng)絡(luò)監(jiān)控可以使用 Fishhook 這樣的工具 Hook 網(wǎng)絡(luò)底層庫 CFNetwork慈省。網(wǎng)絡(luò)的情況比較復(fù)雜,所以需要定些和時(shí)間相關(guān)的關(guān)鍵的指標(biāo)眠菇,指標(biāo)如下:

  • DNS 時(shí)間
  • SSL 時(shí)間
  • 首包時(shí)間
  • 響應(yīng)時(shí)間

有了這些指標(biāo)才能夠有助于更好的分析網(wǎng)絡(luò)問題辫呻。啟動(dòng)階段的網(wǎng)絡(luò)請求是非常多的,所以 HTTP 的性能是非常要注意的琼锋。以下是 WWDC 網(wǎng)絡(luò)相關(guān)的 Session:

I/O

對于 I/O 可以使用 Frida ? A world-class dynamic instrumentation framework | Inject JavaScript to explore native apps on Windows, macOS, GNU/Linux, iOS, Android, and QNX 這種動(dòng)態(tài)二進(jìn)制插樁技術(shù)放闺,在程序運(yùn)行時(shí)去插入自定義代碼獲取 I/O 的耗時(shí)和處理的數(shù)據(jù)大小等數(shù)據(jù)。Frida 還能夠在其它平臺(tái)使用缕坎。

關(guān)于多維度分析更多的資料可以看看歷屆 WWDC 的介紹怖侦。下面我列下16年來 WWDC 關(guān)于啟動(dòng)優(yōu)化的 Session,每場都很精彩谜叹。

延后任務(wù)管理

經(jīng)過前面所說的對主線程耗時(shí)方法和各個(gè)緯度性能分析后匾寝,對于那些分析出來沒必要在啟動(dòng)階段執(zhí)行的方法,可以做成按需或延后執(zhí)行荷腊。 任務(wù)延后的處理不能粗獷的一口氣在啟動(dòng)完成后在主線程一起執(zhí)行艳悔,那樣用戶僅僅只是看到了頁面,依然沒法響應(yīng)操作女仰。那該怎么做呢猜年?套路一般是這樣,創(chuàng)建四個(gè)隊(duì)列疾忍,分別是:

  • 異步串行隊(duì)列
  • 異步并行隊(duì)列
  • 閑時(shí)主線程串行隊(duì)列
  • 閑時(shí)異步串行隊(duì)列

有依賴關(guān)系的任務(wù)可以放到異步串行隊(duì)列中執(zhí)行乔外。異步并行隊(duì)列可以分組執(zhí)行,比如使用 dispatch_group一罩,然后對每組任務(wù)數(shù)量進(jìn)行限制杨幼,避免 CPU、線程和內(nèi)存瞬時(shí)激增影響主線程用戶操作聂渊,定義有限數(shù)量的串行隊(duì)列差购,每個(gè)串行隊(duì)列做特定的事情,這樣也能夠避免性能消耗短時(shí)間突然暴漲引起無法響應(yīng)用戶操作汉嗽。使用 dispatch_semaphore_t 在信號(hào)量阻塞主隊(duì)列時(shí)容易出現(xiàn)優(yōu)先級(jí)反轉(zhuǎn)欲逃,需要減少使用,確保QoS傳播诊胞∨玻可以用dispatch group 替代,性能一樣撵孤,功能不差迈着。異步編程可以直接 GCD 接口來寫,也可以使用阿里的協(xié)程框架 coobjc coobjc邪码。

作為一個(gè)開發(fā)者裕菠,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這有個(gè)iOS交流群:642363427闭专,不管你是小白還是大牛歡迎入駐 奴潘,分享BAT,阿里面試題、面試經(jīng)驗(yàn)影钉,討論技術(shù)画髓,iOS開發(fā)者一起交流學(xué)習(xí)成長!

閑時(shí)隊(duì)列實(shí)現(xiàn)方式是監(jiān)聽主線程 runloop 狀態(tài)平委,在 kCFRunLoopBeforeWaiting 時(shí)開始執(zhí)行閑時(shí)隊(duì)列里的任務(wù)奈虾,在 kCFRunLoopAfterWaiting 時(shí)停止。

優(yōu)化后如何保持廉赔?

攻易守難肉微,就像剛到新團(tuán)隊(duì)時(shí)將包大小減少了48兆,但是一年多一直能夠守住除了決心還需要有手段蜡塌。對于啟動(dòng)優(yōu)化來說碉纳,將各個(gè)性能緯度通過監(jiān)控的方式盯住是必要的,但是發(fā)現(xiàn)問題后快速馏艾、便捷的定位到問題還是需要找些突破口劳曹。我的思路是將啟動(dòng)階段方法耗時(shí)多的按照時(shí)間線一條一條排出來,每條包括方法名琅摩、方法層級(jí)厚者、所屬類、所屬模塊迫吐、維護(hù)人库菲。考慮到便捷性志膀,最好還能方便的查看方法代碼內(nèi)容熙宇。

接下來我通過開發(fā)一個(gè)工具,跟你詳細(xì)說說怎么實(shí)現(xiàn)這樣的效果溉浙。設(shè)計(jì)最終希望展示內(nèi)容如下:

解析 json

如前面所說在輸出一份 Chrome trace 規(guī)范的方法耗時(shí) json 后烫止,先要解析這份數(shù)據(jù)。這份 json 數(shù)據(jù)類似下面的樣子:

{"name":"[SMVeilweaa]upVeilState:","cat":"catname","ph":"B","pid":2381,"tid":0,"ts":21},
{"name":"[SMVeilweaa]tatLaunchState:","cat":"catname","ph":"B","pid":2381,"tid":0,"ts":4557},
{"name":"[SMVeilweaa]tatTimeStamp:state:","cat":"catname","ph":"B","pid":2381,"tid":0,"ts":4686},
{"name":"[SMVeilweaa]tatTimeStamp:state:","cat":"catname","ph":"E","pid":2381,"tid":0,"ts":4727},
{"name":"[SMVeilweaa]tatLaunchState:","cat":"catname","ph":"E","pid":2381,"tid":0,"ts":5732},
{"name":"[SMVeilweaa]upVeilState:","cat":"catname","ph":"E","pid":2381,"tid":0,"ts":5815},
…

通過 Chrome 的 Trace-Viewer 可以生成一個(gè)火焰圖戳稽。其中 name 字段包含了類馆蠕、方法和參數(shù)的信息期升,cat 字段可以加入其它性能數(shù)據(jù),ph 為 B 表示方法開始互躬,為 E 表示方法結(jié)束播赁,ts 字段表示。

json 分詞

讀取 json 文件

// 根據(jù)文件路徑返回文件內(nèi)容
public static func fileContent(path: String) -> String {
    do {
        return try String(contentsOfFile: path, encoding: String.Encoding.utf8)
    } catch {
        return “”
    }
}

let bundlePath = Bundle.main.path(forResource: “startTrace”, ofType: “json”)
let jsonPath = bundlePath ?? “”
let jsonContent = FileHandle.fileContent(path: jsonPath)

jsonContent 就是 json 內(nèi)容字符串吼渡。寫一個(gè)字符切割函數(shù)將字符串按照自定義符號(hào)集來切割容为。

public func allTkFast(operaters:String) -> [Token] {
    var nText = text.replacingOccurrences(of: “ “, with: “ starmingspace “)
    nText = nText.replacingOccurrences(of: “\n”, with: “ starmingnewline “)
    let scanner = Scanner(string: nText)
    var tks = [Token]()
    var set = CharacterSet()
    set.insert(charactersIn: operaters)
    set.formUnion(CharacterSet.whitespacesAndNewlines)

    while !scanner.isAtEnd {
        for operater in operaters {
            let opStr = operater.description
            if (scanner.scanString(opStr) != nil) {
                tks.append(.id(opStr))
            }
        }
        var result:NSString?
        result = nil
        if (scanner.scanUpToCharacters(from: set) != nil) {
            let resultString = result! as String
            if resultString == “starmingnewline” {
                tks.append(.newLine)
            } else if resultString == “starmingspace” {
                tks.append(.space)
            } else {
                tks.append(.id(result! as String))
            }
        }
    }
    tks.append(.eof)
    return tks
}

將切割的字符保存為 Token 結(jié)構(gòu)體的一個(gè)個(gè) token。Token 結(jié)構(gòu)體定義如下:

public enum Token {
    case eof
    case newLine
    case space
    case comments(String)      // 注釋
    case constant(Constant)    // float寺酪、int
    case id(String)            // string
    case string(String)        // 代碼中引號(hào)內(nèi)字符串
}

public enum Constant {
    case string(String)
    case integer(Int)
    case float(Float)
    case boolean(Bool)
}

代碼中的 eof 表示 token 是文件結(jié)束坎背,newLine 是換行 token。Constant 是枚舉關(guān)聯(lián)值寄雀,通過枚舉關(guān)聯(lián)值可以使枚舉能夠具有更多層級(jí)得滤。后面還需要將枚舉值進(jìn)行判等比較,所以還需要擴(kuò)展枚舉的 Equatable 協(xié)議實(shí)現(xiàn):

extension Token: Equatable {
    public static func == (lhs: Token, rhs: Token) -> Bool {
        switch (lhs, rhs) {
        case (.eof, .eof):
            return true
        case (.newLine, .newLine):
            return true
        case (.space, .space):
            return true
        case let (.constant(left), .constant(right)):
            return left == right
        case let (.comments(left), .comments(right)):
            return left == right
        case let (.id(left), .id(right)):
            return left == right
        case let (.string(left), .string(right)):
            return left == right
        default:
            return false
        }
    }
}

通用的 token 結(jié)構(gòu)解析完成盒犹。接下來就是設(shè)計(jì)一個(gè) json 特有的 token 結(jié)構(gòu)耿戚。對于 json 來說換行和空格可以過濾掉,寫個(gè)函數(shù)過濾換行和空格的 token:

public func allTkFastWithoutNewLineAndWhitespace(operaters:String) -> [Token] {
    let allToken = allTkFast(operaters: operaters)
    let flAllToken = allToken.filter {
        $0 != .newLine
    }
    let fwAllToken = flAllToken.filter {
        $0 != .space
    }
    return fwAllToken
}

json 的操作符有:

{}[]”:,

所以 operaters 參數(shù)可以是這些操作符阿趁。完整的 Lexer 類代碼在 MethodTraceAnalyze/Lexer.swift膜蛔。使用 Lexer 類的 allTkFastWithoutNewLineAndWhitespace 方法可以取得 token 集合。

JSONToken

為了轉(zhuǎn)成 json 的 token脖阵,我先設(shè)計(jì)一個(gè) json token 的結(jié)構(gòu) JSONToken皂股。

public struct JSONToken {
    public let type: JSONTokenType
    public let value: String
}

public enum JSONTokenType {
    case startDic   // {
    case endDic     // }
    case startArray // [
    case endArray   // ]
    case key        // key
    case value      // value
}

根據(jù) json 的本身設(shè)計(jì),主要分為 key 和 value命黔,另外還需要些符號(hào)類型呜呐,用來進(jìn)行進(jìn)一步的解析。解析過程的狀態(tài)設(shè)計(jì)為三種悍募,用 State 枚舉表示:

private enum State {
    case normal
    case keyStart
    case valueStart
}

在 normal 狀態(tài)下蘑辑,會(huì)記錄操作符類型的 json token,當(dāng)遇到{符號(hào)后坠宴,下一個(gè)是“符號(hào)就會(huì)更改狀態(tài)為 keyStart洋魂。另一種情況就是在遇到,符號(hào)后,下一個(gè)是”符號(hào)也會(huì)更改狀態(tài)為 keyStart喜鼓。

狀態(tài)更改成 valueStart 的條件是遇到:符號(hào)副砍,當(dāng)下一個(gè)是“時(shí)進(jìn)入 valueStart 狀態(tài),如果不是“符號(hào)庄岖,就需要做區(qū)分豁翎,是{或者[時(shí)直接跳過:符號(hào),然后記錄這兩個(gè)操作符隅忿。其它情況表示 value 不是字符而是數(shù)字心剥,直接記錄為 json token 就可以了邦尊。完整 json token 的解析代碼見 MethodTraceAnalyze/ParseJSONTokens.swift

JSONToken 集合目前還只是扁平態(tài)优烧,而 json 數(shù)據(jù)是有 key 和 value 的多級(jí)關(guān)系在的蝉揍,比如 value 可能是字符串或數(shù)字,也可能是另一組 key value 結(jié)構(gòu)或者 value 的數(shù)組集合匙隔。所以下面還需要定義一個(gè) JSONItem 結(jié)構(gòu)來容納多級(jí)關(guān)系。

JSONItem

JSONItem 的結(jié)構(gòu)體定義如下:

public struct JSONItem {
    public var type: JSONItemType
    public var value: String
    public var kvs: [JSONItemKv]
    public var array: [JSONItem]
}

// 類型
public enum JSONItemType {
    case keyValue
    case value
    case array
}

// key value 結(jié)構(gòu)體
public struct JSONItemKv {
    public var key: String
    public var value: JSONItem
}

JSONItem 的類型分三種熏版,key value纷责、value 和 array 的,定義在 JSONItemType 枚舉中撼短。分別對應(yīng)的三個(gè)存儲(chǔ)字段是 kvs再膳,里面是 JSONItemKv 類型的集合;value 為字符串曲横;array 是 JSONItem 的集合喂柒。

定義好了多層級(jí)的結(jié)構(gòu),就可以將 JSONToken 的集合進(jìn)行分析禾嫉,轉(zhuǎn)到 JSONItem 結(jié)構(gòu)上灾杰。思路是在解析過程中碰到閉合符號(hào)時(shí),將扁平的閉合區(qū)間內(nèi)的 JSONToken 放到集合里熙参,通過遞歸函數(shù) recursiveTk 遞歸出多層級(jí)結(jié)構(gòu)出來艳吠。所以需要設(shè)置四個(gè)狀態(tài):

enum rState {
    case normal
    case startDic
    case startArr
    case startKey
}

當(dāng)碰到{符號(hào)進(jìn)入 startDic 狀態(tài),遇到[符號(hào)進(jìn)入 startKey 狀態(tài)孽椰,遇到}和]符號(hào)時(shí)會(huì)結(jié)束這兩個(gè)狀態(tài)昭娩。在 startDic 或 startKey 狀態(tài)中時(shí)會(huì)收集過程中的 JSONToken 到 recursiveTkArr 集合里。這個(gè)分析完整代碼在這 MethodTraceAnalyze/ParseJSONItem.swift黍匾。

來一段簡單的 json 測試下:

{
    “key1”: “value1”,
    “key2”: 22,
    “key3”: {
        “subKey1”: “subValue1”,
        “subKey2”: 40,
        “subKey3”:[
            {
                “sub1Key1”: 10,
                “sub1Key2”:{
                    “sub3Key1”: “sub3Value1”,
                    “sub3Key2”: “sub3Value2”
                }
            },
            {
                “sub1Key1”: 11,
                “sub1Key2”: 15
            }
        ],
        “subKey4”: [
            “value1”,
            23,
            “value2”
        ],
        “subKey5”: 2
    }
}

使用 ParseJSONItem 來解析

let jsonOPath = Bundle.main.path(forResource: “test”, ofType: “json”)
let jOrgPath = jsonOPath ?? “”
let jsonOContent = FileHandle.fileContent(path: jOrgPath)

let item = ParseJSONItem(input: jsonOContent).parse()

得到的 item 數(shù)據(jù)如下圖所示

可以看到栏渺,item 的結(jié)構(gòu)和前面的 json 結(jié)構(gòu)是一致的。

json 單測

為了保證后面對 json 的解析修改和完善對上面列的測試 case 解析結(jié)果不會(huì)有影響锐涯,可以寫個(gè)簡單測試類來做磕诊。這個(gè)類只需要做到將實(shí)際結(jié)果和預(yù)期值做比較,相等即可通過纹腌,不等即可提示并中斷秀仲,方便定位問題。因此傳入?yún)?shù)只需要有運(yùn)行結(jié)果壶笼、預(yù)期結(jié)果神僵、描述就夠用了。我寫個(gè) Test 協(xié)議覆劈,通過擴(kuò)展默認(rèn)實(shí)現(xiàn)一個(gè)比較的方法保礼,以后需要單測的類遵循這個(gè)協(xié)議就可以使用和擴(kuò)展單測功能了沛励。Test 協(xié)議具體代碼如下:

protocol Test {
    static func cs(current:String, expect:String, des:String)
}

// compare string 對比兩個(gè)字符串值
extension Test {
    static func cs(current:String, expect: String, des: String) {
        if current == expect {
            print(“? \(des) ok,符合預(yù)期值:\(expect)”)
        } else {
            let msg = “? \(des) fail炮障,不符合預(yù)期值:\(expect)”
            print(msg)
            assertionFailure(msg)
        }
    }
}

寫個(gè) TestJSON 遵循 Test 協(xié)議進(jìn)行單測目派。測試各個(gè)解析后的值,比如測試 item第一級(jí) key value 配對數(shù)量可以這樣寫:

let arr = item.array[0].kvs
cs(current: “\(arr.count)”, expect: “3”, des: “all dic count”)

打印的結(jié)果就是:

? all dic count ok胁赢,符合預(yù)期值:3

完整單測代碼在這里:MethodTraceAnalyze/TestJSON.swift

解析 Launch Trace 的 json

前面說的 JSONItem 是通用的多層級(jí) json 結(jié)構(gòu)體企蹭。對于啟動(dòng)的 json,實(shí)際要表現(xiàn)的方法調(diào)用鏈和 json 的層級(jí)并不是對應(yīng)的智末。方法調(diào)用鏈?zhǔn)峭ㄟ^ ph 字段表示谅摄,B 表示方法開始,E 表示方法結(jié)束系馆,中間會(huì)有其它方法調(diào)用的閉合送漠,這些方法在調(diào)用鏈里可以被稱為調(diào)用方法的子方法。

為了能夠表現(xiàn)出這樣的調(diào)用鏈關(guān)系由蘑,我設(shè)計(jì)了下面的 LaunchItem 結(jié)構(gòu):

結(jié)構(gòu)體代碼如下:

public struct LaunchItem {
    public let name: String  // 調(diào)用方法
    public var ph: String    // B 代表開始闽寡、E 代表結(jié)束、BE 代表合并后的 Item尼酿、其它代表描述
    public var ts: String    // 時(shí)間戳爷狈,開始時(shí)間
    public var cost: Int     // 耗時(shí) ms
    public var times: Int    // 執(zhí)行次數(shù)
    public var subItem: [LaunchItem]   // 子 item
    public var parentItem:[LaunchItem] // 父 item
}

通過 ParseJSONTokens 類來獲取 JSONToken 的集合。

tks = ParseJSONTokens(input: input).parse()

找出 name裳擎、ph惯悠、ts 字段數(shù)據(jù)轉(zhuǎn)到 LaunchItem 結(jié)構(gòu)體中酬姆。這部分代碼實(shí)現(xiàn)在這里 MethodTraceAnalyze/ParseLaunchJSON.swift

遍歷 LaunchItem 集合,完善 LaunchItem 的信息留量,先完善 LaunchItem 的 cost 和 subItem 的信息幢泼。在方法調(diào)用鏈同一級(jí)時(shí)依據(jù) ph 字段將相同方法 B 和 E 之間的 LaunchItem 都放到一個(gè)數(shù)組里横辆,通過棧頂和棧底的 ts 字段值相減就能夠得到 cost 的值策吠,也就是方法的耗時(shí),代碼如下:

let b = itemArr[0]
let e = itemArr[itemArr.count - 1]
let cost = Int(e.ts)! - Int(b.ts)!

當(dāng)這個(gè)數(shù)組數(shù)量大于2指孤,代表方法里還會(huì)調(diào)用其它的方法启涯,通過遞歸將調(diào)用鏈中的子方法都取出來,并放到 subItem 里恃轩。

pItem.subItem.append(recusiveMethodTree(parentItem: rPItem, items: newItemArr))

代碼見MethodTraceAnalyze/LaunchJSON.swift里的 launchJSON 函數(shù)结洼。

展示啟動(dòng)方法鏈

前面通過 launchJSON 函數(shù)取到了方法調(diào)用鏈的根部 LaunchItem。使用 recusiveItemTree 函數(shù)遞歸這個(gè)根 LaunchItem 叉跛,可以輸出方法調(diào)用關(guān)系圖松忍。很多工程在啟動(dòng)階段會(huì)執(zhí)行大量方法,很多方法耗時(shí)很少筷厘,可以過濾那些小于10毫秒的方法鸣峭,讓分析更加聚焦宏所。

展示效果如上圖所示,完整代碼在 MethodTraceAnalyze/LaunchJSON.swift 里的 tree 函數(shù)里摊溶。圖中的階段切換爬骤,比如 T1到 T2的切換可以在 recusiveItemTree 函數(shù)中設(shè)置,對應(yīng)的處理代碼是:

// 獲取 T1 到 T5 階段信息莫换,其中 updateLauncherState 函數(shù)名需要替換成自己階段切換的函數(shù)名霞玄,最多5個(gè)階段
if methodName == “updateLauncherState:” {
    currentT += 1
    if currentT > 5 {
        currentT = 5
    }
}

耗時(shí)的高低也做了顏色的區(qū)分。外部耗時(shí)指的是子方法以外系統(tǒng)或沒源碼的三方方法的耗時(shí)拉岁,規(guī)則是父方法調(diào)用的耗時(shí)減去其子方法總耗時(shí)坷剧。代碼如下:

// 獲取外部耗時(shí)
var sysCost = 0
if aItem.subItem.count > 0 {
    for aSubItem in aItem.subItem {
        sysCost += aSubItem.cost
    }
}
sysCost = (aItem.cost - sysCost) / 1000

bundle、owner膛薛、業(yè)務(wù)線這三項(xiàng)需要根據(jù)自己工程情況來听隐,如果工程使用的是 excel 做的記錄可以導(dǎo)出為 csv 格式文件补鼻,參考 LaunchJSON 類里的 loadSimpleKeyValueDicWithCsv 函數(shù)進(jìn)行 csv 數(shù)據(jù)讀取哄啄。如果數(shù)據(jù)是在服務(wù)端,輸出為 json 的話就更好辦了风范,使用前面寫的 ParseJSONItem 類就能夠進(jìn)行數(shù)據(jù)解析了咨跌,可以參考 LaunchJSON 類里的 parseBundleOwner 函數(shù)。展示示例里我先置為默認(rèn)的暫無了硼婿。

目前為止通過過濾耗時(shí)少的方法調(diào)用锌半,可以更容易發(fā)現(xiàn)問題方法。但是寇漫,有些方法單次執(zhí)行耗時(shí)不多刊殉,但是會(huì)執(zhí)行很多次,累加耗時(shí)會(huì)大州胳,這樣的情況也需要體現(xiàn)在展示頁面里记焊。另外外部耗時(shí)高時(shí)或者碰到自己不了解的方法時(shí),是需要到工程源碼里去搜索對應(yīng)的方法源碼進(jìn)行分析的栓撞,有的方法名很通用時(shí)還需要花大量時(shí)間去過濾無用信息遍膜。

因此接下來還需要做兩件事情,首先累加方法調(diào)用次數(shù)和耗時(shí)瓤湘,體現(xiàn)在展示頁面中瓢颅,另一個(gè)是從工程中獲取方法源碼能夠在展示頁面中進(jìn)行點(diǎn)擊顯示。

對于方法調(diào)用次數(shù)和總耗時(shí)的統(tǒng)計(jì)我寫在了 LaunchJSON 類的 allMethodAndSubMethods 函數(shù)里弛说,思路就是遍歷所有的 LaunchItem挽懦,碰到相同的 item name 就對次數(shù)和耗時(shí)進(jìn)行累加。代碼如下:

let allItems = LaunchJSON.leaf(fileName: fileName, isGetAllItem: true)

var mergeDic = [String:LaunchItem]()
for item in allItems {
    let mergeKey = item.name // 方法名為標(biāo)識(shí)
    if mergeDic[mergeKey] != nil {
        var newItem = mergeDic[mergeKey]
        newItem?.cost += item.cost // 累加耗時(shí)
        newItem?.times += 1 // 累加次數(shù)
        mergeDic[mergeKey] = newItem
    } else {
        mergeDic[mergeKey] = item
    }
}

展示時(shí)判斷次數(shù)大于1時(shí)木人,耗時(shí)大于0時(shí)展示出來巾兆。

var mergeStr = “”
if preMergeItemDic.keys.contains(“\(bundleName+className+methodName)”) {
    //
    let mItem = preMergeItemDic[“\(bundleName+className+methodName)”]
    if mItem?.times ?? 0 > 1 && (mItem?.cost ?? 0) / 1000 > 0 {
        mergeStr = “(總次數(shù)\(mItem?.times ?? 0)猎物、總耗時(shí)\((mItem?.cost ?? 0) / 1000))”
    }
}

展示的效果如下:

展示方法源碼

在頁面上展示源碼需要先解析 .xcworkspace 文件,通過 .xcworkspace文件取到工程里所有的 .xcodeproj 文件角塑。分析 .xcodeproj 文件取到所有 .m 和.mm 源碼文件路徑蔫磨,解析源碼,取到方法的源碼內(nèi)容進(jìn)行展示圃伶。

解析 .xcworkspace

打開.xcworkspace堤如,可以看到這個(gè)包內(nèi)主要文件是 contents.xcworkspacedata。內(nèi)容是一個(gè) xml:

<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "group:GCDFetchFeed.xcodeproj">
   </FileRef>
   <FileRef
      location = "group:Pods/Pods.xcodeproj">
   </FileRef>
</Workspace>

所以下面需要對 xml 進(jìn)行分析窒朋。xml 的操作符有 <>=\”/?![]搀罢,通過這些操作符能夠取到通用的 token 集合 tokens。

tokens = Lexer(input: input, type: .plain).allTkFast(operaters: “<>=\”/?![]”)

根據(jù) xml 的規(guī)則侥猩,將解析狀態(tài)分為 normal榔至、startTag、cdata 三種欺劳。定義的枚舉為:

private enum State {
    case normal
    case startTag
    case cdata
}

當(dāng)遇到<符號(hào)時(shí)唧取,更改解析狀態(tài)為 startTag。如果<符號(hào)后面跟的是![CDATA[表示是 cdata 標(biāo)簽划提,狀態(tài)需要改成 cdata枫弟。實(shí)現(xiàn)代碼如下:

// <tagname …> 和 <![CDATA[
if currentState == .normal && currentToken == .id(“<“) {
    // <![CDATA[
    if peekTk() == .id(“!”) && peekTkStep(step: 2) == .id(“[“) && peekTkStep(step: 3) == .id(“CDATA”) && peekTkStep(step: 4) == .id(“[“) {
        currentState = .cdata
        advanceTk() // jump <
        advanceTk() // jump !
        advanceTk() // jump [
        advanceTk() // jump CDATA
        advanceTk() // jump [
        return
    }

    // <tagname …>
    if currentTokens.count > 0 {
        addTagTokens(type: .value) // 結(jié)束一組
    }
    currentState = .startTag
    advanceTk()
    return
}

在 startTag 和 cdata 狀態(tài)時(shí)會(huì)將遇到的 token 裝到 currentTokens 里,在結(jié)束狀態(tài)時(shí)加入到 XMLTagTokens 這個(gè)結(jié)構(gòu)里記錄下來鹏往。XMLTagTokens 的定義如下:

public struct XMLTagTokens {
    public let type: XMLTagTokensType
    public let tokens: [Token]
}

currentTokens 會(huì)在狀態(tài)結(jié)束時(shí)記錄到 XMLTagTokens 的 tokens 里淡诗。startTag 會(huì)在>符號(hào)時(shí)結(jié)束。cdata 會(huì)在]]>時(shí)結(jié)束伊履。這部分實(shí)現(xiàn)代碼見 MethodTraceAnalyze/ParseStandXMLTagTokens.swift 韩容。

接下來對 XMLTagTokens 集合進(jìn)行進(jìn)一步分析,XML 的 tag 節(jié)點(diǎn)分為單標(biāo)簽比如 唐瀑、開標(biāo)簽比如

群凶、閉合標(biāo)簽比如

、標(biāo)簽值介褥、xml 標(biāo)識(shí)說明座掘,這五類。因此我定義了標(biāo)簽節(jié)點(diǎn)的類型枚舉 XMLTagNodeType:

public enum XMLTagNodeType {
    case xml
    case single // 單個(gè)標(biāo)簽
    case start  // 開標(biāo)簽 <p>
    case value  // 標(biāo)簽的值 <p>value</p>
    case end    // 閉合的標(biāo)簽 </p>
}

標(biāo)簽節(jié)點(diǎn)除了類型信息柔滔,還需要有屬性集合溢陪、標(biāo)簽名和標(biāo)簽值,結(jié)構(gòu)體定義為:

public struct XMLTagNode {
    public let type: XMLTagNodeType
    public let value: String // 標(biāo)簽值
    public let name: String  // 標(biāo)簽名
    public let attributes: [XMLTagAttribute] // 標(biāo)簽屬性
}

解析 XML 標(biāo)簽節(jié)點(diǎn)相比較于 HTML 來說會(huì)簡化些睛廊,HTML的規(guī)則更加的復(fù)雜形真,以前使用狀態(tài)機(jī)根據(jù) W3C 標(biāo)準(zhǔn)HTML Standard專門解析過,狀態(tài)機(jī)比較適合于復(fù)雜的場景,具體代碼在這里 HTN/HTMLTokenizer.swift 咆霜〉寺可以看到按照 W3C 的標(biāo)準(zhǔn),設(shè)計(jì)了一個(gè) HTNStateType 狀態(tài)枚舉蛾坯,狀態(tài)特別多光酣。對于 XML 來說狀態(tài)會(huì)少些:

enum pTagState {
    case start
    case questionMark
    case xml
    case tagName
    case attributeName
    case equal
    case attributeValue
    case startForwardSlash
    case endForwardSlash
    case startDoubleQuotationMarks
    case backSlash
    case endDoubleQuotationMarks
}

XML 標(biāo)簽節(jié)點(diǎn)的解析我沒有用狀態(tài)機(jī),將解析結(jié)果記錄到了 XMLTagNode 結(jié)構(gòu)體中脉课。標(biāo)簽節(jié)點(diǎn)解析過程代碼在這里 MethodTraceAnalyze/ParseStandXMLTags.swift 救军。標(biāo)簽節(jié)點(diǎn)解析完后還需要解決 XML 的層級(jí)問題,也就是標(biāo)簽包含標(biāo)簽的問題倘零。

先定義一個(gè)結(jié)構(gòu)體 XMLNode唱遭,用來記錄 XML 的節(jié)點(diǎn)樹:

public struct XMLNode {
    public let name: String
    public let attributes: [XMLTagAttribute]
    public var value: String
    public var subNodes: [XMLNode]
}

其中 subNodes 是 XMLNode 的子節(jié)點(diǎn)集合,解析出 XMLNode 的思路是根據(jù)前面輸出的 XMLTagNode 的類型來分析呈驶,當(dāng)遇到類型是 start 到遇到相同 name 的 end 之間不斷收集 XMLTagNode 到 currentTagNodeArr 數(shù)組里拷泽,end 時(shí)將這個(gè)數(shù)組添加到 tagNodeArrs 里,然后開始收集下一組 start 和 end袖瞻。關(guān)鍵代碼如下:

// 當(dāng)遇到.end 類型時(shí)將一組 XMLTagNode 加到 tagNodeArrs 里司致。然后重置。
if node.type == .end && node.name == currentTagName {
    currentState = .end
    currentTagNodeArr.append(node)
    // 添加到一級(jí)
    tagNodeArrs.append(currentTagNodeArr)
    // 重置
    currentTagNodeArr = [XMLTagNode]()
    currentTagName = “”
    continue
}

對于 xml 類型標(biāo)簽和 single 類型的會(huì)直接保存到 tagNodeArrs 里虏辫。接下來對 tagNodeArrs 這些由 XMLTagNode 組成的數(shù)組集進(jìn)行分析蚌吸。如果 tagNodeArr 的數(shù)組數(shù)量是1時(shí)锈拨,表示這一層級(jí)的 tag 是 xml 或者單標(biāo)簽的情況比如<?xml version=”1.0” encoding=”UTF-8”?> 或 這種砌庄。數(shù)量是2時(shí)表示開閉標(biāo)簽里沒有其他的標(biāo)簽,類似

這種奕枢。當(dāng) tagNodeArr 的數(shù)量大于2時(shí)娄昆,可能有兩種情況,一種是 tagNode 為 value 類型比如

section value

缝彬,其他情況就是標(biāo)簽里會(huì)嵌套標(biāo)簽萌焰,需要遞歸調(diào)用 recusiveParseTagNodes 函數(shù)進(jìn)行下一級(jí)的解析。這部分邏輯在 recusiveParseTagNodes 函數(shù)里谷浅,相關(guān)代碼如下:

for tagNodeArr in tagNodeArrs {
    if tagNodeArr.count == 1 {
        // 只有一個(gè)的情況扒俯,即 xml 和 single
        let aTagNode = tagNodeArr[0]
        pNode.subNodes.append(tagNodeToNode(tagNode: aTagNode))
    } else if tagNodeArr.count == 2 {
        // 2個(gè)的情況,就是比如 <p></p>
        let aTagNode = tagNodeArr[0] // 取 start 的信息
        pNode.subNodes.append(tagNodeToNode(tagNode: aTagNode))
    } else if tagNodeArr.count > 2 {
        // 大于2個(gè)的情況
        let startTagNode = tagNodeArr[0]
        var startNode = tagNodeToNode(tagNode: startTagNode)
        let secondTagNode = tagNodeArr[1]

        // 判斷是否是 value 這種情況比如 <p>paragraph</p>
        if secondTagNode.type == .value {
            // 有 value 的處理
            startNode.value = secondTagNode.value.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
            pNode.subNodes.append(startNode)
        } else {
            // 有子標(biāo)簽的情況
            // 遞歸得到結(jié)果
            var newTagNodeArr = tagNodeArr
            newTagNodeArr.remove(at: tagNodeArr.count - 1)
            newTagNodeArr.remove(at: 0)
            pNode.subNodes.append(recusiveParseTagNodes(parentNode: startNode, tagNodes: newTagNodeArr))
        } // end else
    } // end else if
} // end for

完成 xcworkspace 的 XML 解析一疯,獲取 XML 的節(jié)點(diǎn)樹如下所示:

寫個(gè)單測撼玄,保證后面增加功能和更新優(yōu)化解析后不會(huì)影響結(jié)果。單測代碼在這里 MethodTraceAnalyze/TestXML.swift墩邀。

解析 .xcodeproj

通過 XML 的解析可以獲取 FileRef 節(jié)點(diǎn)內(nèi)容掌猛, xcodeproj 的文件路徑就在 FileRef 節(jié)點(diǎn)的 location 屬性里。每個(gè) xcodeproj 文件里會(huì)有 project 工程的源碼文件眉睹。為了能夠獲取方法的源碼進(jìn)行展示荔茬,那么就先要取出所有 project 工程里包含的源文件的路徑废膘。

取 xcodeproj 文件路徑的方式如下:

if aFile.fileName == “contents.xcworkspacedata” {
    let root = ParseStandXML(input: aFile.content).parse()
    let workspace = root.subNodes[1]

    for fileRef in workspace.subNodes {
        var fileRefPath = fileRef.attributes[0].value
        fileRefPath.removeFirst(6)

        // 判斷是相對路徑還是絕對路徑
        let arr = fileRefPath.split(separator: “/“)
        var projectPath = “”
        if arr.count > 2 {
            projectPath = “\(fileRefPath)/project.pbxproj”
        } else {
            projectPath = “/\(pathStr)/\(fileRefPath)/project.pbxproj”
        }
        // 讀取 project 文件內(nèi)容分析
        allSourceFile += ParseXcodeprojSource(input: projectPath).parseAllFiles()

    } // end for fileRef in workspace.subNodes
} // end for

如上面代碼所示,ParseXcodeprojSource 是專門用來解析 xcodeproj 的慕蔚,parseAllFiles 方法根據(jù)解析的結(jié)果丐黄,取出所有 xcodeproj 包含的源碼文件。

xcodeproj 的文件內(nèi)容看起來大概是下面的樣子孔飒。

其實(shí)內(nèi)容還有很多孵稽,需要一個(gè)個(gè)解析出來。

分析后分詞的分割符號(hào)有 /*={};\”,() 這些十偶,根據(jù)這些分割符號(hào)設(shè)計(jì)分詞的 token 類型 XcodeprojTokensType菩鲜,XcodeprojTokensType 為枚舉包含下面十個(gè)類型:

public enum XcodeprojTokensType {
    case codeComment // 注釋
    case string
    case id
    case leftBrace // {
    case rightBrace // }
    case leftParenthesis // (
    case rightParenthesis // )
    case equal // =
    case semicolon // ;
    case comma // ,
}

codeComment、string惦积、id 這些類型會(huì)由多個(gè) token 組成接校,所以最好將 xcodeproj 的基礎(chǔ) token 設(shè)計(jì)為下面的樣子:

public struct XcodeprojTokens {
    public let type: XcodeprojTokensType
    public let tokens: [Token]
}

由 tokens 字段記錄多個(gè) token。實(shí)現(xiàn)分詞代碼在這 MethodTraceAnalyze/ParseXcodeprojTokens.swift

xcodeproj 文件雖然不是 json狮崩,但是大小括號(hào)的規(guī)則和 json 還比較類似蛛勉,大括號(hào)里的數(shù)據(jù)類似字典可以用 key、value 配對記錄睦柴,小括號(hào)數(shù)據(jù)類似數(shù)組诽凌,記錄 value 就可以。這樣可以設(shè)計(jì) xcodeproj 的節(jié)點(diǎn)類型為:

public enum XcodeprojNodeType {
    case normal
    case root // 根節(jié)點(diǎn)

    case dicStart // {
    case dicKey
    case dicValue
    case dicEnd   // }

    case arrStart // (
    case arrValue
    case arrEnd   // )
}

如上面定義 XcodeprojNodeType 枚舉坦敌,其大括號(hào)內(nèi)數(shù)據(jù)的 key 類型為 dicKey侣诵,value 類型為 dicValue。小括號(hào)的 value 類型為 arrValue狱窘。節(jié)點(diǎn)設(shè)計(jì)為:

public struct XcodeprojNode {
    public let type: XcodeprojNodeType
    public let value: String
    public let codeComment: String
    public var subNodes: [XcodeprojNode]
}

解析代碼都在這里 MethodTraceAnalyze/ParseXcodeprojNode.swift 杜顺。

xcodeproj 也有層級(jí),所以也需要構(gòu)建一個(gè)樹結(jié)構(gòu)蘸炸。結(jié)構(gòu)代碼如下:

public struct XcodeprojTreeNode {
    public var type: XcodeprojTreeNodeType
    public var value: String
    public var comment: String
    public var kvs: [XcodeprojTreeNodeKv]
    public var arr: [XcodeprojTreeNodeArrayValue]
}

public enum XcodeprojTreeNodeType {
    case value
    case keyValue
    case array
}

public struct XcodeprojTreeNodeKey {
    public var name: String
    public var comment: String
}

public struct XcodeprojTreeNodeArrayValue {
    public var name: String
    public var comment: String
}

public struct XcodeprojTreeNodeKv {
    public var key: XcodeprojTreeNodeKey
    public var value: XcodeprojTreeNode
}

考慮到 xcodeproj 里的注釋很多躬络,也都很有用,因此會(huì)多設(shè)計(jì)些結(jié)構(gòu)來保存值和注釋搭儒。思路是根據(jù) XcodeprojNode 的類型來判斷下一級(jí)是 key value 結(jié)構(gòu)還是 array 結(jié)構(gòu)穷当。如果 XcodeprojNode 的類型是 XcodeprojNode 的類型是 dicStart 表示下級(jí)是 key value 結(jié)構(gòu)。如果類型是 arrStart 就是 array 結(jié)構(gòu)淹禾。當(dāng)碰到類型是 dicEnd 同時(shí)和最初 dicStart 是同級(jí)時(shí)馁菜,遞歸下一級(jí)樹結(jié)構(gòu)。而 arrEnd 不用遞歸稀拐,xcodeproj 里的 array 只有值類型的數(shù)據(jù)火邓。生成節(jié)點(diǎn)樹結(jié)構(gòu)這部分代碼實(shí)現(xiàn)在這里 MethodTraceAnalyze/ParseXcodeprojTreeNode.swift

斷點(diǎn)看生成的結(jié)構(gòu)如下圖:

其中 section 內(nèi)容都在 objects 里

有了基本節(jié)點(diǎn)樹結(jié)構(gòu)以后就可以設(shè)計(jì) xcodeproj 里各個(gè) section 的結(jié)構(gòu)。主要有一下的 section:

  • PBXBuildFile:文件,最終會(huì)關(guān)聯(lián)到 PBXFileReference
  • PBXContainerItemProxy:部署的元素
  • PBXFileReference:各類文件铲咨,有源碼躲胳、資源、庫等文件
  • PBXFrameworksBuildPhase:用于 framework 的構(gòu)建
  • PBXGroup:文件夾纤勒,可嵌套坯苹,里面包含了文件與文件夾的關(guān)系
  • PBXNativeTarget:Target 的設(shè)置
  • PBXProject:Project 的設(shè)置,有編譯工程所需信息
  • PBXResourcesBuildPhase:編譯資源文件摇天,有 xib粹湃、storyboard、plist以及圖片等資源文件
  • PBXSourcesBuildPhase:編譯源文件(.m)
  • PBXTargetDependency: Taget 的依賴
  • PBXVariantGroup:.storyboard 文件
  • XCBuildConfiguration:Xcode 編譯配置泉坐,對應(yīng) Xcode 的 Build Setting 面板內(nèi)容
  • XCConfigurationList:構(gòu)建配置相關(guān)为鳄,包含項(xiàng)目文件和 target 文件

根據(jù) xcodeproj 的結(jié)構(gòu)規(guī)則設(shè)計(jì)結(jié)構(gòu)體:

// project.pbxproj 結(jié)構(gòu)
public struct Xcodeproj {
    var archiveVersion = “”
    var classes = [XcodeprojTreeNodeArrayValue]()
    var objectVersion = “” // 區(qū)分 xcodeproj 不同協(xié)議版本
    var rootObject = PBXValueWithComment(name: “”, value: “”)

    var pbxBuildFile = [String:PBXBuildFile]()
    var pbxContainerItemProxy = [String:PBXContainerItemProxy]()
    var pbxFileReference = [String:PBXFileReference]()
    var pbxFrameworksBuildPhase = [String:PBXFrameworksBuildPhase]()
    var pbxGroup = [String:PBXGroup]()
    var pbxNativeTarget = [String:PBXNativeTarget]()
    var pbxProject = [String:PBXProject]()
    var pbxResourcesBuildPhase = [String:PBXResourcesBuildPhase]()
    var pbxSourcesBuildPhase = [String:PBXSourcesBuildPhase]()
    var pbxTargetDependency = [String:PBXTargetDependency]()
    var pbxVariantGroup = [String:PBXVariantGroup]()
    var xcBuildConfiguration = [String:XCBuildConfiguration]()
    var xcConfigurationList = [String:XCConfigurationList]()

    init() {

    }
}

具體每個(gè)字段集合元素的結(jié)構(gòu)體比如 PBXBuildFile 和 PBXFileReference 對應(yīng)的結(jié)構(gòu)體和 xcodeproj 的 section 結(jié)構(gòu)對應(yīng)上。然后使用 ParseXcodeprojTreeNode 解析的節(jié)點(diǎn)樹結(jié)構(gòu)生成最終的 Xcodeproj section 的結(jié)構(gòu)體腕让。解析過程在這里 MethodTraceAnalyze/ParseXcodeprojSection.swift孤钦。

調(diào)試看到 Xcodeproj 的結(jié)構(gòu)如下:

對 xcodeproj 的解析也寫了單測來保證后期 MethodTraceAnalyze/TestXcodeproj.swift

得到 section 結(jié)構(gòu) Xcodeproj 后纯丸,就可以開始分析所有源文件的路徑了偏形。根據(jù)前面列出的 section 的說明,PBXGroup 包含了所有文件夾和文件的關(guān)系觉鼻,Xcodeproj 的 pbxGroup 字段的 key 是文件夾俊扭,值是文件集合,因此可以設(shè)計(jì)一個(gè)結(jié)構(gòu)體 XcodeprojSourceNode 用來存儲(chǔ)文件夾和文件關(guān)系坠陈。XcodeprojSourceNode 結(jié)構(gòu)如下:

public struct XcodeprojSourceNode {
    let fatherValue: String // 文件夾
    let value: String // 文件的值
    let name: String // 文件名
    let type: String
}

通過遍歷 pbxGroup 可以將文件夾和文件對應(yīng)上萨惑,文件名可以通過 pbxGroup 的 value 到 PBXFileReference 里去取。代碼如下:

var nodes = [XcodeprojSourceNode]()

// 第一次找出所有文件和文件夾
for (k,v) in proj.pbxGroup {
    guard v.children.count > 0 else {
        continue
    }

    for child in v.children {
        // 如果滿足條件表示是目錄
        if proj.pbxGroup.keys.contains(child.value) {
            continue
        }
        // 滿足條件是文件
        if proj.pbxFileReference.keys.contains(child.value) {
            guard let fileRefer = proj.pbxFileReference[child.value] else {
                continue
            }

            nodes.append(XcodeprojSourceNode(fatherValue: k, value: child.value, name: fileRefer.path, type: fileRefer.lastKnownFileType))
        }
    } // end for children

} // end for group

接下來需要取得完整的文件路徑畅姊。通過 recusiveFatherPaths 函數(shù)獲取文件夾路徑咒钟。這里需要注意的是需要處理 ../ 這種文件夾路徑符吹由,獲取完整路徑的實(shí)現(xiàn)代碼可以看這里 MethodTraceAnalyze/ParseXcodeprojSource.swift若未。

有了每個(gè)源文件的路徑,接下來就可以對這些源文件進(jìn)行解析了倾鲫。

解析 .m .mm 文件

對 Objective-C 解析可以參考 LLVM粗合,這里只需要找到每個(gè)方法對應(yīng)的源碼,所以自己也可以實(shí)現(xiàn)乌昔。分詞前先看看 LLVM 是怎么定義 token 的隙疚。定義文件在這里 https://opensource.apple.com/source/lldb/lldb-69/llvm/tools/clang/include/clang/Basic/TokenKinds.def 。根據(jù)這個(gè)定義我設(shè)計(jì)了 token 的結(jié)構(gòu)體磕道,主體部分如下:

// 切割符號(hào) [](){}.&=*+-<>~!/%^|?:;,#@
public enum OCTK {
    case unknown // 不是 token
    case eof // 文件結(jié)束
    case eod // 行結(jié)束
    case codeCompletion // Code completion marker
    case cxxDefaultargEnd // C++ default argument end marker
    case comment // 注釋
    case identifier // 比如 abcde123
    case numericConstant(OCTkNumericConstant) // 整型供屉、浮點(diǎn) 0x123,解釋計(jì)算時(shí)用,分析代碼時(shí)可不用
    case charConstant // ‘a(chǎn)’
    case stringLiteral // “foo”
    case wideStringLiteral // L”foo”
    case angleStringLiteral // <foo> 待處理需要考慮作為小于符號(hào)的問題

    // 標(biāo)準(zhǔn)定義部分
    // 標(biāo)點(diǎn)符號(hào)
    case punctuators(OCTkPunctuators)

    //  關(guān)鍵字
    case keyword(OCTKKeyword)

    // @關(guān)鍵字
    case atKeyword(OCTKAtKeyword)
}

完整的定義在這里 MethodTraceAnalyze/ParseOCTokensDefine.swift伶丐。分詞過程可以參看 LLVM 的實(shí)現(xiàn) clang: lib/Lex/Lexer.cpp Source File悼做。我在處理分詞時(shí)主要是按照分隔符一一對應(yīng)處理,針對代碼注釋和字符串進(jìn)行了特殊處理哗魂,一個(gè)注釋一個(gè) token肛走,一個(gè)完整字符串一個(gè) token。我分詞實(shí)現(xiàn)代碼 MethodTraceAnalyze/ParseOCTokens.swift录别。

作為一個(gè)開發(fā)者朽色,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這有個(gè)iOS交流群:642363427组题,不管你是小白還是大牛歡迎入駐 葫男,分享BAT,阿里面試題、面試經(jīng)驗(yàn)崔列,討論技術(shù)腾誉,iOS開發(fā)者一起交流學(xué)習(xí)成長!

由于只要取到類名和方法里的源碼峻呕,所以語法分析時(shí)利职,只需要對類定義和方法定義做解析就可以,語法樹中節(jié)點(diǎn)設(shè)計(jì):

// OC 語法樹節(jié)點(diǎn)
public struct OCNode {
    public var type: OCNodeType
    public var subNodes: [OCNode]
    public var identifier: String   // 標(biāo)識(shí)
    public var lineRange: (Int,Int) // 行范圍
    public var source: String       // 對應(yīng)代碼
}

// 節(jié)點(diǎn)類型
public enum OCNodeType {
    case `default`
    case root
    case `import`
    case `class`
    case method
}

其中 lineRange 記錄了方法所在文件的行范圍瘦癌,這樣就能夠從文件中取出代碼猪贪,并記錄在 source 字段中。

解析語法樹需要先定義好解析過程的不同狀態(tài):

private enum RState {
    case normal
    case eod                   // 換行
    case methodStart           // 方法開始
    case methodReturnEnd       // 方法返回類型結(jié)束
    case methodNameEnd         // 方法名結(jié)束
    case methodParamStart      // 方法參數(shù)開始
    case methodContentStart    // 方法內(nèi)容開始
    case methodParamTypeStart  // 方法參數(shù)類型開始
    case methodParamTypeEnd    // 方法參數(shù)類型結(jié)束
    case methodParamEnd        // 方法參數(shù)結(jié)束
    case methodParamNameEnd    // 方法參數(shù)名結(jié)束

    case at                    // @
    case atImplementation      // @implementation

    case normalBlock           // oc方法外部的 block {}讯私,用于 c 方法
}

完整解析出方法所屬類热押、方法行范圍的代碼在這里 MethodTraceAnalyze/ParseOCNodes.swift

解析 .m 和 .mm 文件,一個(gè)一個(gè)串行解的話斤寇,對于大工程桶癣,每次解的速度很難接受,所以采用并行方式去讀取解析多個(gè)文件娘锁,經(jīng)過測試牙寞,發(fā)現(xiàn)每組在60個(gè)以上時(shí)能夠最大利用我機(jī)器(2.5 GHz 雙核Intel Core i7)的 CPU,內(nèi)存占用只有60M莫秆,一萬多.m文件的工程大概2分半能解完间雀。分組并行的代碼實(shí)現(xiàn)如下:

let allPath = XcodeProjectParse.allSourceFileInWorkspace(path: workspacePath)
var allNodes = [OCNode]()
let groupCount = 60 // 一組容納個(gè)數(shù)
let groupTotal = allPath.count/groupCount + 1

var groups = [[String]]()
for I in 0..<groupTotal {
    var group = [String]()
    for j in I*groupCount..<(I+1)*groupCount {
        if j < allPath.count {
            group.append(allPath[j])
        }
    }
    if group.count > 0 {
        groups.append(group)
    }
}

for group in groups {
    let dispatchGroup = DispatchGroup()
    for node in group {
        dispatchGroup.enter()
        let queue = DispatchQueue.global()
        queue.async {
            let ocContent = FileHandle.fileContent(path: node)
            let node = ParseOCNodes(input: ocContent).parse()
            for aNode in node.subNodes {
                allNodes.append(aNode)
            }
            dispatchGroup.leave()
        } // end queue async
    } // end for
    dispatchGroup.wait()
} // end for

使用的是 dispatch group 的 wait,保證并行的一組完成再進(jìn)入下一組镊屎。

現(xiàn)在有了每個(gè)方法對應(yīng)的源碼惹挟,接下來就可以和前面 trace 的方法對應(yīng)上。頁面展示只需要寫段 js 就能夠控制點(diǎn)擊時(shí)展示對應(yīng)方法的源碼缝驳。

頁面展示

在進(jìn)行 HTML 頁面展示前连锯,需要將代碼里的換行和空格替換成 HTML 里的對應(yīng)的 和 归苍。

let allNodes = ParseOC.ocNodes(workspacePath: “/Users/ming/Downloads/GCDFetchFeed/GCDFetchFeed/GCDFetchFeed.xcworkspace”)

var sourceDic = [String:String]()
for aNode in allNodes {
    sourceDic[aNode.identifier] = aNode.source.replacingOccurrences(of: “\n”, with: “</br>”).replacingOccurrences(of: “ “, with: “&nbsp;”)
}

用 p 標(biāo)簽作為源碼展示的標(biāo)簽,方法執(zhí)行順序的編號(hào)加方法名作為 p 標(biāo)簽的 id运怖,然后用 display: none; 將 p 標(biāo)簽隱藏霜医。方法名用 a 標(biāo)簽,click 屬性執(zhí)行一段 js 代碼驳规,當(dāng) a 標(biāo)簽點(diǎn)擊時(shí)能夠顯示方法對應(yīng)的代碼肴敛。這段 js 代碼如下:


function sourceShowHidden(sourceIdName) {
    var sourceCode = document.getElementById(sourceIdName);
    sourceCode.style.display = “block”;
}

最終效果如下圖:

將動(dòng)態(tài)分析和靜態(tài)分析進(jìn)行了結(jié)合,后面可以通過不同版本進(jìn)行對比吗购,發(fā)現(xiàn)哪些方法的代碼實(shí)現(xiàn)改變了医男,能展示在頁面上。還可以進(jìn)一步靜態(tài)分析出哪些方法會(huì)調(diào)用到 I/O 函數(shù)捻勉、起新線程镀梭、新隊(duì)列等,然后展示到頁面上踱启,方便分析报账。

讀到最后,可以看到這個(gè)方法分析工具并沒有用任何一個(gè)輪子埠偿,其實(shí)有些是可以使用現(xiàn)有輪子的透罢,比如 json、xml冠蒋、xcodeproj羽圃、Objective-C 語法分析等,之所有沒有用是因?yàn)椴煌喿邮褂玫恼Z言和技術(shù)區(qū)別較大抖剿,當(dāng)格式更新時(shí)如果使用的單個(gè)輪子沒有更新會(huì)影響整個(gè)工具朽寞。開發(fā)這個(gè)工具主要工作是在解析上,所以使用自有解析技術(shù)也能夠讓所做的功能更聚焦斩郎,不做沒用的功能脑融,減少代碼維護(hù)量,所要解析格式更新后缩宜,也能夠自主去更新解析方式肘迎。更重要的一點(diǎn)是可以親手接觸下這些格式的語法設(shè)計(jì)。

結(jié)語

今天說了下啟動(dòng)優(yōu)化的技術(shù)手段脓恕,總的說膜宋,對啟動(dòng)進(jìn)行優(yōu)化的決心重要程度是遠(yuǎn)大于技術(shù)手段的,決定著是否能夠優(yōu)化的更多炼幔。技術(shù)手段有很多,我覺得手段的好壞區(qū)別只是在效率上史简,最差的情況全用手動(dòng)一個(gè)個(gè)去查耗時(shí)也是能夠解題的乃秀。

最近看了魯迅的一段話肛著,很有感觸,分享一下:

我們好像都是愛生病的人
苦的很
我的一生
好像是在不斷生病和罵人中就過去多半了
我三十歲不到跺讯,牙齒就掉光了
滿口義齒
我戒酒
吃魚肝油
以望延長我的生命
倒不盡是為了我的愛人
大半是為了我的敵人
我自己知道的枢贿,我并不大度
說到幸福
只得面向過去
或者面向除了墳?zāi)挂酝夂翢o任何希望的將來
每個(gè)戰(zhàn)士都是如此
我們活在這樣的地方
我們活在這樣的時(shí)代

查看原文
如果您覺得還不錯(cuò),麻煩在文末 “點(diǎn)個(gè)贊” 或者 評(píng)論 “Mark”刀脏,謝謝您的支持

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末局荚,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子愈污,更是在濱河造成了極大的恐慌耀态,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件暂雹,死亡現(xiàn)場離奇詭異首装,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)杭跪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門仙逻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涧尿,你說我怎么就攤上這事系奉。” “怎么了姑廉?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵喜最,是天一觀的道長。 經(jīng)常有香客問我庄蹋,道長瞬内,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任限书,我火速辦了婚禮虫蝶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘倦西。我一直安慰自己能真,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布扰柠。 她就那樣靜靜地躺著粉铐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卤档。 梳的紋絲不亂的頭發(fā)上蝙泼,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音劝枣,去河邊找鬼汤踏。 笑死织鲸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的溪胶。 我是一名探鬼主播搂擦,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼哗脖!你這毒婦竟也來了瀑踢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤才避,失蹤者是張志新(化名)和其女友劉穎橱夭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體工扎,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡徘钥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肢娘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呈础。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖橱健,靈堂內(nèi)的尸體忽然破棺而出而钞,到底是詐尸還是另有隱情,我是刑警寧澤拘荡,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布臼节,位于F島的核電站,受9級(jí)特大地震影響珊皿,放射性物質(zhì)發(fā)生泄漏网缝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一蟋定、第九天 我趴在偏房一處隱蔽的房頂上張望粉臊。 院中可真熱鬧,春花似錦驶兜、人聲如沸扼仲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽屠凶。三九已至,卻和暖如春肆资,著一層夾襖步出監(jiān)牢的瞬間矗愧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國打工迅耘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贱枣,地道東北人监署。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓颤专,卻偏偏與公主長得像纽哥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子栖秕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容