Bo2SS

Bo2SS

1 基礎篇(中)

今天要分享的是:基礎篇(調試測試和發布階段)。

image

06 | 透過注入動態庫實現極速編譯調試?#

雖然我們可以透過將部分代碼先編譯成二進制集成到工程裡,來避免每次都全量編譯來加快編譯速度,但每次編譯還是需要重啟 App,再走一遍調試流程。

那麼原生代碼怎樣實現動態極速調試呢?我們先看看有哪些工具實現了動態調試

1)Swift Playground,任何代碼修改都能實時反饋出來;

2)Flutter Hot Reload,Flutter 會在點擊 VSCode 調試欄的 reload 按鈕後,查看自上次編譯後改動過的代碼,重新編譯涉及到的代碼庫,還包括主庫及其相關聯庫。這些重新編譯過的庫都會轉換成內核文件發到 Dart VM 裡,Dart VM 重新加載新的內核文件,載入後使 Flutter framework 觸發所有 Widgets 和 Render Objects 進行重建、重佈局、重繪。

那麼,Cocoa 框架想要達到極速調試應該要怎麼做呢?

Injection for Xcode#

Injection 工具可以動態地將 Swift 或 Objective-C 的代碼在已運行的程序中執行,以加快調試速度,同時保證程序不用重啟。

工作原理:Injection 會監聽源代碼文件的變化,如果文件被改動了,Injection Server 就會執行 rebuildClass 重新進行編譯、打包成動態庫,也就是 .dylib 文件,然後使用 writeSting 方法通過 Socket 通知運行的 App。原理示意圖如下:

Injection 的工作原理示意圖 ——《極客時間》

07 | OCLint 、Clang 和 Infer ,靜態分析工具比較#

首先介紹三個常用的複雜度指標,靜態分析工具可以借助它們來分析代碼是否需要優化和重構。

  1. 圈複雜度,指的是遍歷一個模塊時的複雜度,這個複雜度是由分支語句比如 if、for,還有運算符比如 &&、|| 等等共同確定的。一般來說,圈複雜度在 11 以上時就非常高了,這時需要考慮重構,不然就會因為測試用例的數量過高而難以維護。
  2. NPath 度,指一個方法所有可能執行的路徑數量。一般高於 200 就需要考慮降低複雜度了。
  3. NCSS 度,指不包含註釋的源代碼行數。NCSS 度過大表示方法或類做的事情太多,影響代碼的維護性和可讀性,應該拆分或重構。一般方法行數不過百,類的行數不過千。

再提前申明使用靜態分析工具的兩大缺陷:

  1. 需要耗費更長的時間。相比於編譯過程,靜態分析本身就包含了編譯最耗時的 IO 和語法分析階段,當發現深層次程序錯誤時,還會對當前分析的方法、參數、變量去和整個工程相關代碼一起做分析。
  2. 只能檢查出那些專門設計好的、可查找的錯誤。對於特定類型的錯誤分析,還需要開發者靠自己的能力寫一些插件並添加進去。

下面正式比較 3 款常用靜態分析工具:

  • OCLint 是基於 Clang Tooling 開發的靜態分析工具。
  • Clang 靜態分析器基於 C++ 開發的開源工具,是 Clang 項目的一部分,構建在 Clang 和 LLVM 之上。
  • Infer 是基於 OCaml 語言編寫的 Facebook 開源靜態分析工具。
靜態分析工具優缺點
靜態分析工具優點缺點
OCLint檢查規則多、定制性強可定制度過高,易用性較差
Clang 靜態分析器與 Xcode 的集成度高,支持命令行檢查規則少,檢查粒度較粗
Infer效率高,支持增量分析,也可小範圍分析可定制性中等

綜合來看,Infer 在準確性、性能效率、規則、擴展性、易用性整體度上的把握是做得最好的,值得一試。

參考資料

08 | 如何利用 Clang 為 App 提質?#

除了之前提到的 Clang 靜態分析工具,基於 Clang 還可以開發出保障 App 質量的系統平台,比如CodeChecker,具備了代碼增量分析、代碼可視化、代碼質量報告生成等能力,或者在線網頁代碼導航工具,比如 Mozilla 開發的 DXR,方便在便攜設備上去操作、分析問題。

什麼是 Clang?#

我們先看看iOS 開發的完整編譯流程圖

image

其中,左側黑塊 Clang 是 C、C++、Objective-C 的編譯前端, Swift 有自己的編譯前端 SIL optimizer。

Clang 是基於 C++ 開發的,其源碼質量非常高,有很多值得學習的地方,比如說目錄清晰、功能解耦做得很好、分類清晰方便組合和復用、代碼風格統一而且規範、註釋量大便於閱讀等。並且,它不光工程代碼量巨大,而且工具也非常多,但相互間的關係複雜,好在 Clang 提供了一個易用性很高的黑盒 Driver,封裝了前端命令和工具鏈命令,大大提升了其易用性。

Clang 做了哪些事?#

1)對代碼進行詞法分析,切分成 Token。

Token 類型分為 4 類:

  • 關鍵字:語法中的關鍵字,比如 if、else、while、for 等;
  • 標識符:變量名;
  • 字面量:值、數字、字符串;
  • 特殊符號:加減乘除等符號。

2)再進行語法分析,將 Token 組合成語義生成節點,從而構成抽象語法樹(AST)。

節點主要分成 Type 類型、Decl 聲明、Stmt 陳述這三種,通過擴展這三類節點,就能夠表現出無限的代碼形態。

Clang 提供了什麼能力?#

Clang 為一些需要分析代碼語法、語義信息的工具提供了基礎設施: LibClang、Clang Plugin 和 LibTooling。

LibClang#

LibClang 可以訪問 Clang 上層高級抽象的能力,比如獲取所有 Token、遍歷語法樹、代碼補全等。由於 API 很穩定,Clang 版本更新對其影響不大。但是,LibClang 並不能完全訪問到 Clang AST 信息。

Clang Plugins#

Clang Plugins 是在運行時由編譯器加載的動態庫,可以讓你在 AST 上做些操作,並集成到編譯中,成為編譯的一部分。

LibTooling#

LibTooling 是一個 C++ 接口,通過 LibTooling 能夠編寫獨立運行的語法檢查和代碼重構工具。

與 LibClang 相比,它的接口沒有那麼穩定,需關注 AST 的 API 升級,也無法開箱即用;與 Clang Plugins 相比,它無法影響編譯過程。但是,它能夠完全控制 Clang AST ,並可以獨立運行。

相關資料:

Tutorial for building tools using LibTooling and LibASTMatchers(使用 LibTooling 構建一個語言轉換工具)

09 | 無侵入的埋點方案如何實現?#

在 iOS 開發中,埋點可以解決兩大類問題:1)了解用戶使用 App 的行為,2)降低線上問題的分析難度。

常見的埋點方式主要包括三種:

1)代碼埋點:手寫代碼來埋點。特點是埋點精確,方便調試,但開發和維護的工作量較大。

2)可視化埋點:將埋點增加和修改的工作可視化。特點是提升埋點體驗。

3)無埋點:更確切地說是 “全埋點”,埋點代碼不會出現在業務代碼中。特點是容易管理和維護,但只能針對通用的埋點需求。

可視化埋點和無埋點,都屬於無侵入的埋點方案,那麼該如何實現呢?

運行時方法替換#

在 iOS 開發中最常見的三種埋點,就是對頁面進入次數、頁面停留時間、點擊事件的埋點。

對於這三種常見情況,我們都可以通過運行時方法替換技術來插入埋點代碼,以實現無侵入的埋點方法:

1)先寫一個運行時方法替換的類 SMHook,加上替換的方法 hookClass:fromSelector

2)根據埋點類型確定要替換的方法和標識符,在 +load () 方法裡使用 SMHook 進行方法替換。

下面為無侵入埋點 - 運行時方法替換信息對照表,僅供參考:

無侵入埋點-運行時方法替換信息
埋點類型替換方法標識符
頁面進入次數、頁面停留時間UIViewController 的生命週期NSStringFromClass([self class])
UITableView(特例)setDelegateNSStringFromClass([self class])
點擊事件點擊事件的方法NSStringFromSelector(action) + NSStringFromClass([target class])
手勢事件initWithTarget:action:NSStringFromSelector(action) + NSStringFromClass([target class])

事件唯一標識#

一個視圖下相同 UIButton 的不同實例,僅僅通過 “action 選擇器名”+“視圖類名” 的組合還不能夠區分開。這時,我們就需要有一個唯一標識來區分不同的事件。

每個子視圖在父視圖中都會有自己的索引,可以結合它生成事件唯一標識。特例:UITableViewCell 可以通過 indexPath 來確定每個 Cell 的唯一性;UIAlertController 可以通過內容來確定它的唯一標識...

但是,事件唯一標識的準確性難以保障(如視圖層級在運行時被更改,因需求迭代頁面更新頻繁),所以通過運行時方法替換進行無侵入埋點方案,一般只是用於一些功能和視圖穩定的地方

思考:是否可以使用 Clang AST 的接口,在構建時遍歷 AST,從而將所需要的埋點代碼加進去。

10 | 包大小:從資源和代碼層面瘦身#

蘋果對 iOS App 大小有嚴格限制:下載大小(200 MB)超限會阻礙用戶在蜂窩網絡下載 App ,影響新用戶轉化;可執行文件 text 段大小(iOS 7-:80MB,iOS 7 - 8:60MB,iOS 9+:500MB)超限將導致 App 審核被拒,影響 App 上架;另外,App 包體積過大,還會影響用戶升級率。

所以,控制包大小至關重要。接下來,介紹一些常見的包大小瘦身方法。

官方 App Thinning#

App Thinning 是由蘋果公司推出的一項改善 App 下載進程的新技術,主要解決用戶下載 App 耗費過高流量,以及佔用 iOS 設備過大存儲空間的問題。

兩個瘦身點

  1. 圖片資源尺寸。根據 iOS 設備屏幕尺寸來匹配不同尺寸的資源,比如,iPhone 6 只會下載 2x 分辨率的圖片資源,iPhone 6plus 則只會下載 3x 分辨率的圖片資源。
  2. 芯片指令集架構文件。用戶下載時就只會下載一個適合自己設備的芯片指令集架構文件,因為 App 也會有 32 位、64 位不同芯片架構的優化版本。

三種瘦身方式

  1. App Slicing :在你向 iTunes Connect 上傳 App 後,對 App 做切割,創建不同的變體,以適用不同的設備。
  2. On-Demand Resources :主要是為遊戲多關卡場景服務,根據用戶的關卡進度下載隨後幾個關卡的資源,並且已經過關的資源也會被刪掉,這樣可以減少初裝 App 的包大小。
  3. Bitcode :針對特定設備進行包大小優化,優化不明顯。

如何使用呢

這裡的大部分工作都是由 Xcode 和 App Store 幫你完成的~

你只需要通過 Xcode 添加 xcassets 目錄(File > New File > Asset Catalog),然後將 2x 分辨率和 3x 分辨率的圖片添加進來。

芯片指令集架構文件按照默認設置, App Store 會根據設備創建不同的變體,每個變體裡只有當前設備需要的指令集文件。

無用圖片資源#

推薦開源工具: LSUnusedResources ,可以通過直接添加規則來處理,相當於幫你做了以下 4 步:

  1. 通過 find 命令獲取項目中的所有圖片資源文件;
  2. 通過 find 命令和正則匹配找出源碼中使用到的資源名;
  3. 上述兩者取差集,得到的就是無用資源了;
  4. 最後,可以通過系統類 NSFileManger 來刪除無用資源。

圖片資源壓縮#

推薦的壓縮方案:轉成 WebP 格式(Google 開源項目;壓縮率高,支持有損和無損兩種壓縮模式;支持 Alpha 透明和 24-bit 顏色數,不會像 PNG8 那樣因為色彩不夠而出現毛邊)。

相關的壓縮工具cwebp(Google,命令行)、iSparta(騰訊,GUI)。

需權衡的地方:顯示 WebP 圖片需要使用 libwebp 進行解析(libwebp Demo),其在 CPU 消耗和解碼時間上會比 PNG 高兩倍,這又是空間和時間上的取捨了。

作者建議

1)圖片大小超過 100KB,考慮使用 WebP ;

2)否則,可以使用 TinyPng (網頁工具)或者 ImageOptim (GUI 工具)進行圖片壓縮。

代碼瘦身#

App 的安裝包主要是由資源和可執行文件組成的。

那麼聊完了資源瘦身,接下來就是對可執行文件進行瘦身,也就是找到並刪除無用代碼的過程,思路類似找無用圖片資源的 4 步走。

1)首先,通過 LinkMap 找出方法和類的全集;

獲取 LinkMap :將工程文件 > Targets > Build Setting 裡的 Write Link Map File 設置為 Yes,然後指定 Path to Link Map File 的路徑,即可在每次編譯後得到 LinkMap 文件。

LinkMap 文件的組成如下圖所示:

LinkMap 文件主要組成 ——《極客時間》

  • Object File:代碼工程的所有文件;
  • Section:代碼段在生成的 Mach-O 文件裡的偏移位置和大小;
  • Symbols:每個方法、類、block,以及它們的大小。

2)然後,找到使用過的方法和類;3)取二者的差集得到無用代碼;4)最後,由人工確認無用代碼可刪除後,進行刪除即可。這些過程共有 3 種方式選擇:

A. 使用 MachOView 軟件查看 Mach-O 文件。

在 Section 信息中,__objc_selrefs 里是被調用過的方法,__objc_classrefs 里是被調用過的類,__objc_superrefs 里是調用過 super 的類。

但是,這些不包括運行時動態調用的方法(因為 Objective-C 的動態性),還需要二次確認

B. 直接使用 AppCode 軟件找到無用代碼。

前提:工程代碼量沒有達到百萬行。

通過 AppCode > Code > Inspect Code 分析完後,Unused code 里展示了所有的無用代碼。

但是,也有一些情況會被誤判為無用,也需要人工二次確認,如:

  • performSelector 方式調用的方法,如 [self performSelector:@selector (xxx)];
  • 在子類中使用的父類方法;
  • 運行時聲明的類,如 NSClassFromString 調用的類、[[self class] xxx] 這樣不指定類名被使用的類、使用 registerClass 的 UITableView 自定義的 Cell;
  • 通過點的方式使用的屬性;
  • JSONModel 里定義的未使用的協議...

C. 運行時檢查類是否真正被使用過。

通過方式 1 和 2 找到並刪除了無用代碼,但包裡可能還存在無用代碼,比如在執行靜態檢查時被用到的代碼,在線上可能連它們的入口都沒有了,更不用說被用到。

ObjC 的 runtime 源碼裡,有一個判斷類是否初始化過的函數 isInitialized,並且它返回的結果會保存到元類(class_rw_t 結構體 flags 信息的第 1<<29 位)。

具體編寫運行時無用類檢查工具時,我們通過這個是否初始化的信息:

  • 先在線下測試環節檢查所有類,查出沒有初始化的類;
  • 然後在上線後對沒有初始化的類,進行多版本觀察;
  • 確認真正沒有用到的類後,刪掉。

11 | 熱點問題答疑(一):基礎模塊問題答疑#

動態庫加載方式#

有兩種

  1. 在程序開始運行時通過 dyld 動態加載。通過 dyld 加載的動態庫,需要在編譯時進行鏈接,鏈接時會做標記,綁定的地址在加載後再決定。
  2. 顯式運行時鏈接,即在運行時通過動態鏈接器提供的 API dlopen 和 dlsym 來加載。這種方式,在編譯時是不需要參與鏈接的。不過,通過這種運行時加載遠程動態庫的 App,蘋果公司是不允許上線 App Store 的,所以只能用於線下調試環節。

App 啟動速度的相關問題#

學習匯編學到什麼程度合適

  • 如果工作不涉及逆向和安全領域,能夠看懂匯編代碼就非常不錯了;
  • 如果你想學匯編語言的話,多動手去編寫和調試代碼,同時結合使用 Xcode 工具。

參考資料:

Mike Ash 的 “Dissecting objc_msgSend on ARM64”,剖析 objc_msgSend 源碼,詳細講述了裡面的 ARM64 匯編代碼。

額外推薦:

王垠的博客

只有掌握了某個方面的知識,才能在碰到問題時想到用這個知識去解決問題,所以至少先了解這些知識吧~

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。