iOS摸魚週報 第四十三期

語言: CN / TW / HK

本期概要

  • 話題:dyld4 開源了。
  • Tips:Fix iOS12 libswift_Concurrency.dylib crash bug
  • 面試模塊:Synchronized 源碼解讀
  • 優秀博客:Swift Protocol 進階
  • 學習資料:南大軟件分析課程,iOS 開發學習圖譜
  • 開發工具:貝爾實驗室開發的有向圖/無向圖自動佈局應用,支持 dot 腳本繪製結構圖,流程圖等。

本期話題

@zhangferry:Apple 最近開源了 dyld4 的代碼。通過閲讀它的 Readme 文檔,我們可以大致瞭解到 dyld4 相對 dyld3 做的改進有哪些。dyld3 出於對啟動速度的優化,增加了啟動閉包。應用首啟和發生變化時將一些啟動數據創建為閉包存到本地,下次啟動將不再重新解析數據,而是直接讀取閉包內容。這種方法的理想情況是應用程序和系統應很少發生變化,因為如果這兩者經常變化,即意味着閉包可能面臨失效。為了應對這類場景,dyld4 採用了 Prebuilt + JustInTime 的雙解析模式,Prebuild 對應的就是 dyld3 中的啟動閉包場景,JustInTime 大致對應 dyld2 中的實時解析,JustInTime 過程是可以利用 Prebuild 的緩存的,所以性能也還可控。應用首啟、包體或系統版本更新、普通啟動,dyld4 將根據緩存有效與否選擇合適的模式進行解析。

dyld3 在不使用啟動閉包的情況下會 fallback 到 dyld2,兩套代碼分別在兩邊,不利於行為的統一和維護,dyld4 做了邏輯統一(@鵝喵 補充)。所以 dyld4 的設計目標是更優的兼容性和邏輯統一。

還有一點,細心的開發者還在 dyld4 源碼裏發現了 realityOS 及 realityOS_Sim 相關的代碼註釋。很大可能蘋果的 VR/AR 設備已經準備差不多了,靜待今年的 WWDC 吧。

地址:apple-oss-distributions/dyld

開發Tips

整理編輯:Hello World

iOS12 libswift_Concurrency.dylib crash 問題修復

最近很多朋友都遇到了 iOS12 上 libswift_Concurrency 的 crash 問題,Xcode 13.2 release notes 中有提到是 Clang 編譯器 bug,13.2.1 release notes 説明已經修復,但實際測試並沒有。

crash 的具體原因是 Xcode 編譯器在低版本 iOS12 上沒有將 libswift_Concurrency.dylib 庫剔除,反而是將該庫嵌入到 ipa 的 Frameworks 路徑下,導致動態鏈接時 libswift_Concurrency 被鏈接引發 crash。

問題分析

通過報錯信息 Library not loaded: /usr/lib/swift/libswiftCore.dylib 分析是動態庫沒有加載,提示是 libswift_Concurrency.dylib 引用了該庫。iOS12 本不該鏈接這個庫,崩潰後通過 image list 查看加載的鏡像文件會找到 libswift_Concurrency 的路徑是 ipa/Frameworks 下的,查詢資料瞭解到是 Xcode13.2 及其以上版本在做 Swift Concurrency 向前兼容時出現的 bug

問題定位

在按照 Xcode 13.2 release notes 提供的方案,將 libswiftCore 設置為 weak 並指定 rpath 後,crash 信息變更,此時 error 原因是 ___chkstk_darwin 符號找不到;根據 error Referenced from 發現還是 libswift_Concurrency 引用的,通過:

bash $ nm -u xxxAppPath/Frameworks/libswift_Concurrency.dylib 查看所有未定義符號(類型為 U), 其中確實包含了 ___chkstk_darwin,13.2 release notes 中提供的解決方案只是設置了系統庫弱引用,沒有解決庫版本差異導致的符號解析問題。

error 提示期望該符號應該在 libSystem.B.dylib 中,但是通過找到 libSystem.B.dylib 並打印導出符號:

bash $ nm -gAUj libSystem.B.dylib 發現即使是高版本的動態庫中也並沒有該符號,那麼如何知道該符號在哪個庫呢?這裏用了一個取巧的方式,run iOS13 以上真機,並設置 symbol 符號 ___chkstk_darwin, Xcode 會標記所有存在該符號的庫,經過前面的思考,認為是在查找 libswiftCore 核心庫時 crash 的可能性更大。

libSystem.B.dylib 路徑在 ~/Library/Developer/Xcode/iOS DeviceSupport/xxversion/Symbols/usr/lib/ 目錄下

如何校驗呢,通過 Xcode 上 iOS12 && iOS13 兩個不同版本的 libswiftCore.dylib 查看導出符號,可以發現,iOS12 上的 Core 庫不存在,對比組 iOS13 上是存在的,所以基本可以斷定 symbol not found 是這個原因造成的;當然你也可以把其他幾個庫也採用相同的方式驗證。

通過在 ~/Library/Developer/Xcode/iOS DeviceSupport/xxversion/Symbols/usr/lib/swift/libswiftCore.dylib 不同的 version 路徑下找到不同系統對應的 libswiftCore.dylib 庫,然後用 nm -gUAj libswiftCore.dylib 可以獲取過濾後的全局符號驗證。

庫的路徑,可以通過 linkmap 或者運行 demo 打個斷點,通過LLDB的image list查看。

分析總結:無論是根據 Xcode 提供的解決方案亦或是 error 分析流程,發現根源還是因為在 iOS12 上鍊接了 libswift_Concurrency 造成的,既然問題出在異步庫,解決方案也很明瞭,移除項目中的 libswift_Concurrency.dylib 庫即可。

解決方案

方案一:使用 Xcode13.1 或者 Xcode13.3 Beta 構建

使用 Xcode13.1 或者 Xcode13.3 Beta 構建,注意 beta 版構建的 ipa 無法上傳到 App Store。 該方法比較麻煩,還要下載 Xcode 版本,耗時較多,如果有多版本 Xcode 的可以使用該方法。

方案二:添加 Post-actions 腳本移除

添加 Post-actions 腳本,每次構建完成後移除嵌入的libswift_Concurrency.dylib。同時配合 -Wl,-weak-lswift_Concurrency -Wl,-rpath,/usr/lib/swift 設置到Other Linker Flags。添加流程: Edit Scheme -> Build -> Post-actions -> Click '+' to add New Run Script。腳本內容為:

bash rm "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/libswift_Concurrency.dylib" || echo "libswift_Concurrency.dylib not exists"

方案三:降低或移除使用 libswift_Concurrency.dylib 的三方庫

查找使用 concurrency 的三方庫,降低到未引用 libSwiftConcurrency 前的版本,後續等 Xcode 修復後再升級。如果是通過 cocoapods 管理三方庫,只需要指定降級版本即可。但是需要解決一個問題,如何查找三方庫中有哪些用到 concurrency 呢?

如果是源碼,全局搜索相關的 await & async 關鍵字可以找到部分 SDK,但如果是二進制 SDK 或者是間接使用的,則只能通過符號查找。

查找思路:

  1. 首先明確動態庫的鏈接是依賴導出符號的,即 xxx 庫引用了 target_xxx 動態庫時,xxx 是通過調用 target_xxx 的導出符號(全局符號)實現的,全局符號的標識是大寫的類型,U 表示當前庫中未定義的符號,即 xxx 需要鏈接其他庫動態時的符號,符號操作可以使用 llvm nm 命令

  2. 如何查看是否引用了指定動態庫 target_xxx 的符號?可以通過 linkmap 文件查找,但是由於 libswift_Concurrency 有可能是被間接依賴的,此時 linkmap 中不存在對這個庫的符號記錄,所以沒辦法進行匹配,換個思路,通過獲取 libswift_Concurrency 的所有符號進行匹配,libswift_Concurrency 的路徑可以通過上文提到的 image list 獲取, 一般都是用的 /usr/lib/swift 下的。

  3. 遍歷所有的庫,查找裏面用到的未定義符號( U ), 和 libswift_Concurrency 的導出符號進行匹配,重合則代表有調用關係。

為了節省校驗工作量,提供 findsymbols.sh 腳本完成查找,構建前可以通過指定項目中 SDK 目錄查找,或者也可以指定構建後 .app 包中的 Frameworks 查找。

使用方法:

  1. 下載後進行權限授權, chmod 777 findsymbols.sh
  2. 指定如下參數:
    • -f:指定單個二進制 framework/.a 庫進行檢查
    • -p:指定目錄,檢查目錄下的所有 framework/.a 二進制 SDK
    • -o: 輸出目錄,默認是 ~/Desktop/iOS12 Crash Result

參考: * 如何檢測哪些三方庫用了 libstdc++ * After upgrading to Xcode 13.2.1, debugging with a lower version of the iOS device still crashes at launching

面試解析

整理編輯:Hello World

Synchronized 源碼解讀

Synchronized 作為 Apple 提供的同步鎖機制中的一種,以其便捷的使用性廣為人知,作為面試中經常被考察的知識點,我們可以帶着幾個面試題來解讀源碼:

  1. sychronized 是如何與傳入的對象關聯上的?
  2. 是否會對傳入的對象有強引用關係?
  3. 如果 synchronized 傳入 nil 會有什麼問題?
  4. 當做key的對象在 synchronized 內部被釋放會有什麼問題?
  5. synchronized 是否是可重入的,即是否可以作為遞歸鎖使用?

查看 synchronized 源碼所在

通常查看底層調用有兩種方式,通過 clang 查看編譯後的 cpp 文件梳理,第二種是通過彙編斷點梳理調用關係;這裏採用第一種方式。命令為 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.2 ViewController.m

核心代碼就是 objc_sync_enterobjc_sync_exit ,拿到函數符號後可以通過 Xcode 設置 symbol 符號斷點獲知該函數位於哪個系統庫,這裏直接説結論是在 libobjc 中,objc是開源的,全局搜索後定位到 objc/objc-sync 的文件中;

Synchronized 中重要的數據結構

核心數據結構有三個,SyncDataSyncList 以及 sDataLists;結構體成員變量註釋如下:

```c typedef struct alignas(CacheLineSize) SyncData { struct SyncData* nextData; // 指向下一個 SyncData 節點,作用類似鏈表 DisguisedPtr object; // 綁定的作為 key 的對象 int32_t threadCount; // number of THREADS using this block 使用當前 obj 作為 key 的線程數 recursive_mutex_t mutex; // 遞歸鎖,根據源碼繼承鏈其實是 apple 自己封裝了os_unfair_lock 實現的遞歸鎖 } SyncData;

// SyncList 作為表中的首節點存在,存儲着 SyncData 鏈表的頭結點 struct SyncList { SyncData *data; // 指向的 SyncData 對象 spinlock_t lock; // 操作 SyncList 時防止多線程資源競爭的鎖,這裏要和 SyncData 中的 mutex 區分開作用,SyncData 中的 mutex 才是實際代碼塊加鎖使用的

constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }

};

// Use multiple parallel lists to decrease contention among unrelated objects. / 兩個宏定義,方便調用

define LOCK_FOR_OBJ(obj) sDataLists[obj].lock

define LIST_FOR_OBJ(obj) sDataLists[obj].data /

static StripedMap sDataLists; // 哈希表,以關聯的 obj 內存地址作為 key,value是 SyncList 類型 ```

StripedMap 本質是個泛型哈希表,是 objc 源碼中經常使用的數據結構,例如 retain/release 中的 SideTables 結構等。

一般以內存地址值作為 key,返回聲明類型的 value,iOS中 存儲容量是 8 Mac中 容量是 64 ,可以通過源碼查看

核心邏輯 id2data()

通過源碼可以獲知 objc_sync_enterobjc_sync_exit 核心邏輯都是 id2data(),入參為作為 key 的對象,以及狀態枚舉值。

代碼流程如下:

  • 通過關聯的對象地址獲取 SyncList 中存儲的的 SyncData 和 lock 鎖對象;
  • 使用 fastCacheOccupied 標識,用來記錄是否已經填充過快速緩存。

    • 首先判斷是否命中 TLS 快速緩存,對應代碼 SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    • 未命中則判斷是否命中二級緩存 SyncCache, 對應代碼 SyncCache *cache = fetch_cache(NO);
    • 命中邏輯處理類似,都是使用 switch 根據入參決定處理加鎖還是解鎖,如果匹配到,則使用 result 指針記錄
      • 加鎖,則將 lockCount ++,記錄 key object 對應的 SyncData 變量 lock 的加鎖次數,再次存儲回對應的緩存。
      • 解鎖,同樣 lockCount--,如果 ==0,表示當前線程中 object 關聯的鎖不再使用了,對應緩存中 SyncData 的 threadCount 減1,當前線程中 object 作為 key 的加鎖代碼塊完全釋放
  • 如果兩個緩存都沒有命中,則會遍歷全局表 SyncDataLists, 此時為了防止多線程影響查詢,使用了 SyncList 結構中的 lock 加鎖(注意區分和SyncData中lock的作用)。

    查找到則説明存在一個 SyncData 對象供其他線程在使用,當前線程使用需要設置 threadCount + 1 然後存儲到上文的緩存中;對應的代碼塊為:

    cpp for (p = *listp; p != NULL; p = p->nextData) {goto done}

  • 如果以上查找都未找到,則會生成一個 SyncData 節點, 並通過 done 代碼段填充到緩存中。

    • 如果存在未釋放的 SyncData, 同時 theadCount == 0 則直接填充新的數據,減少創建對象,實現性能優化,對應代碼:

      cpp if ( firstUnused != NULL ) {//...}

    • 如果不存在,則新建 SyncData 對象,並採用頭插法插入到鏈表的頭部,對應代碼邏輯

      cpp posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData)); //....

最終的存儲數據結構如下圖所示:

當 id2data() 返回了 SyncData 對象後,objc_sync_try_enter 會調用 data->mutex.tryLock();嘗試加鎖,其他線程再次執行時如果判斷已經加鎖,則進行資源等待

以上是對源碼的解讀,需要對照着 libobjc 源碼閲讀會更好的理解。下面回到最初的幾個問題:

  1. 鎖是如何與你傳入 @synchronized 的對象關聯上的

    答: 由 SyncDataLits 可知是通過對象地址關聯的,所以任何存在內存地址的對象都可以作為 synchronized 的 key 使用

  2. 是否會對關聯的對象有強引用

    答:根據 StripedMap 裏的代碼可以沒有強引用,只是將內存地址值進行位計算然後作為 key 使用,並沒有指針指向傳入的對象。

  3. 如果 synchronize 傳入 nil 會有什麼問題

    答:通過 objc_sync_enter 源碼發現,傳入 nil 會調用 objc_sync_nil, 而 BREAKPOINT_FUNCTION 對該函數的定義為 asm()"" 即空彙編指令。不執行加鎖,所以該代碼塊並不是線程安全的。

  4. 假如你傳入 @synchronized 的對象在 @synchronized 的 block 裏面被釋放或者被賦值為 nil 將會怎麼樣

    答:通過 objc_sync_exit 發現被釋放後,不會做任何事,導致鎖也沒有被釋放,即一直處於鎖定狀態,但是由於對象置為nil,導致其他異步線程執行 objc_sync_enter 時傳入的為 nil,代碼塊不再線程安全。

  5. synchronized 是否是可重入的,即是否為遞歸鎖

    答:是可遞歸的,因為 SyncData 內部是對 os_unfair_recursive_lock 的封裝,os_unfair_recursive_lock 結構通過 os_unfair_lock 和 count 實現了可遞歸的功能,另外通過lockCount記錄了重入次數

知識點總結:

  • id2data 函數使用拉鍊法解決了哈希衝突問題(更多哈希衝突方案查看 摸魚週報39期 ),

  • 在查找緩存上支持了 TLS 快速緩存 以及 SyncCache 二級緩存和 SyncDataLists 全局查找三種方式:

  • Sychronized 使用注意事項,請參考 正確使用多線程同步鎖@synchronized()

參考:

優秀博客

整理編輯:東坡肘子

1、在已實現協議要求方法的類型中如何調用協議中的默認實現 -- 來自:Leonardo Maia Pugliese

@東坡肘子:能夠提供默認實現是 Swift 協議功能的重要特性。本文介紹了在已實現協議要求方法的類型中繼續調用協議的默認實現的三種方式。解決的思路可以給讀者不小的啟發。在每篇博文中附帶介紹一副繪畫作品也是該博客的特色之一。

2、通過 Swift 代碼介紹 24 種設計模式 -- 來自:oldbird

@東坡肘子:設計模式是程序員必備的基礎知識,但是沒有點年份,掌握也不是這麼容易,所以例子就非常重要。概念是抽象的,例子是具象的。具象的東西,記憶和理解都會容易些。該項目提供了 24 種設計模式的 Swift 實現範例,對於想學習設計模式並加深理解的朋友十分有幫助。

3、Combining protocols in Swift -- 來自:Sundell

@東坡肘子:組合和擴展均為 Swift 協議的核心優勢。本文介紹瞭如何為組合後的協議添加具有約束的擴展。幾種方式各有利弊,充分掌握後可以更好地理解和發揮 Swift 面向協議編程的優勢。

4、Swift Protocol 背後的故事 -- 來自: 趙雪峯

@東坡肘子:本文共分兩篇。上篇中,以一個 Protocol 相關的編譯錯誤為引,通過實例對 Type Erasure、Opaque Types 、Generics 以及 Phantom Types 做了較詳細的討論。下篇則主要討論 Swift Protocol 實現機制,涉及 Type Metadata、Protocol 內存模型 Existential Container、Generics 的實現原理以及泛型特化等內容。

5、不透明類型 -- 來自:Mzying

@東坡肘子:不透明類型是指我們被告知對象的功能而不知道對象具體是什麼類型。作者通過三個篇章詳細介紹了 Swift 的不透明類型功能,包括:不透明類型解決的問題(上)、返回不透明類型(中)、不透明類型和協議類型之間的區別 (下)。

學習資料

整理編輯:Mimosa

南大軟件分析課程

地址:https://www.bilibili.com/video/BV1b7411K7P4

南京大學《軟件分析》課程系列,非常難得的高質量課程,可以通過這裏獲取所有課程的課件。

iOS 開發學習圖譜

地址:http://hdjc8.com/iOSRoadMap/

一份特別豐富的 iOS 開發學習圖譜,其中包含了許多 iOS 開發的資源,編者認為這本圖譜不適合作為學習的一個路線,適合作為一份讓你瞭解 iOS 有哪些知識點的圖譜,其中的許多的知識點很適合作為查漏補缺的一個工具。在我們做工作中常常會僅做某些領域內的工作,導致在不短的一段時間內接觸的技術是比較窄的,假如你突然想了解一些別的知識點,你可以來這本圖譜中閒逛一下,看看有什麼知識點是你感興趣的,也許有一些是你以前感興趣但是由於種種原因沒來及瞭解的內容!

工具推薦

整理編輯:CoderStar

Graphviz

地址:http://www.graphviz.org/

軟件狀態:免費

軟件介紹

貝爾實驗室開發的有向圖/無向圖自動佈局應用,支持 dot 腳本繪製結構圖,流程圖等。

Graphviz

對產物.gz文件進行解析查看的途徑。

  • 在線網站:GraphvizOnline
  • vs 插件:Graphviz (dot) language support for Visual Studio Code

結合cocoapods-dependencies插件,我們可以解析podfile文件來分析項目的pod庫依賴,生成.gz文件。

  • 生成.gz文件:pod dependencies --graphviz
  • 生成依賴圖:pod dependencies --image
  • 生成.gz文件及依賴圖:pod dependencies --graphviz --image

關於我們

iOS 摸魚週報,主要分享開發過程中遇到的經驗教訓、優質的博客、高質量的學習資料、實用的開發工具等。週報倉庫在這裏:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的內容推薦可以通過 issue 的方式進行提交。另外也可以申請成為我們的常駐編輯,一起維護這份週報。另可關注公眾號:iOS成長之路,後台點擊進羣交流,聯繫我們,獲取更多內容。

往期推薦

iOS摸魚週報 第四十二期

iOS摸魚週報 第四十一期

iOS摸魚週報 第四十期

iOS摸魚週報 第三十九期