3 個 iOS 開發典型問題解答
你好,我是戴銘,專欄上線以來,我收到了很多同學非常用心的反饋,有問題、建議、心得和經驗,當然提的問題居多。雖然我未在評論區對每條留言做出回覆,但是我對大家提出的問題都一一記錄了下來,內容很豐富,我進行了彙總和整理,釋出到專欄答疑中,感興趣的小夥伴可以去看看。
在這裡,我先挑選並展開了 3 個典型問題,希望為你抽絲剝繭,答疑解惑。
動態庫載入方式的相關問題
問題:
@五子棋 在看完第 5 篇文章“連結器:符號是怎麼繫結到地址上的?”後,關於動態庫是否參與連結的問題,通過私信和我反饋了他的觀點。
他指出:動態庫也是要參與連結的,不然就沒法知道函式的標記在哪兒。
解答:
為了幫助大家理解這個問題,我把與之相關的內容,再和你展開一下。
我在第 5 篇文章中,是這麼闡述這部分內容的:
Mach-O 檔案是編譯後的產物,而動態庫在執行時才會被連結,並沒參與 Mach-O 檔案的編譯和連結,所以 Mach-O 檔案中並沒有包含動態庫裡的符號定義。也就是說,這些符號會顯示為“未定義”,但它們的名字和對應的庫的路徑會被記錄下來。執行時通過 dlopen 和 dlsym 匯入動態庫時,先根據記錄的庫路徑找到對應的庫,再通過記錄的名字元號找到繫結的地址。
細細想來,關於這個問題,更嚴謹的說法應該是,載入動態庫的方式有兩種:
-
一種是,在程式開始執行時通過 dyld 動態載入。通過 dyld 載入的動態庫需要在編譯時進行連結,連結時會做標記,繫結的地址在載入後再決定。
-
第二種是,顯式執行時連結(Explicit Runtime Linking),即在執行時通過動態連結器提供的 API dlopen 和 dlsym 來載入。這種方式,在編譯時是不需要參與連結的。
不過,通過這種執行時載入動態庫的 App,蘋果公司是不允許上線 App Store 的,所以只能用於線下除錯環節。關於這種方式的適用場景,我在專欄第 6 篇“App 如何通過注入動態庫的方式實現極速編譯除錯? ”中有舉例說明過。
在第 5 篇文章中,我將動態庫的這兩種載入方式混在一起說了,讓你感到些許困惑,所以在這裡我特地做個補充說明。
App 啟動速度的相關問題
專欄的第 2 篇文章“App 啟動速度怎麼做優化與監控? ”中的大部分問題,我都直接在評論區回覆了。今天主要和大家聊一下課後作業的實現問題。
問題:
按照今天文中提到的 Time Profiler 工具檢查方法耗時的原理,你來動手實現一個方法耗時檢查工具吧。
雖然這個問題的思路,我在文章中提到了,但還是有很多同學感覺無從下手。接下來,我們就再一起來看看這個思考題。
解答:
關於實現思路,我在文章中寫到:
定時抓取主執行緒上的方法呼叫堆疊,計算一段時間裡各個方法的耗時。
我們再一起看一下這個實現思路(我原本未在文中詳細展開,是希望多留點思考空間給你)。動手寫耗時檢查工具時,首先需要開啟一個定時器,來定時獲取方法呼叫堆疊。一段時間內方法呼叫堆疊相同,那麼這段時間,就是這個方法呼叫堆疊的棧頂方法耗時。
這個解題思路里很關鍵的一步,也是你最容易忽視的一步,就是應該怎麼做好獲取方法呼叫堆疊。
callstackSymbols 是一種獲取方法呼叫棧的方法,但是隻能獲取當前執行緒的呼叫棧,為了把對主執行緒的影響降到最小,獲取當前執行緒呼叫棧的工作就需要在其他執行緒去做。
所以,這個解題思路就需要換成: 使用系統提供的 task_threads 去獲取所有執行緒,使用 thread_info 得到各個執行緒的詳細資訊,使用 thread_get_state 方法去獲取執行緒棧裡的所有棧指標。
如果接下來立刻進行符號化去獲取方法名,那麼就需要去 __LINKEDIT segment 裡查詢棧指標地址所對應符號表的符號,特別當你設定的時間隔較小的時候,符號化過程會持續消耗較多的 CPU 資源,從而影響主執行緒。
所以,獲取到棧指標後,我們可以不用立刻做符號化,而是先使用一個結構體將棧地址記錄下來,最後再統一符號化,將對主執行緒的影響降到最低,這樣獲取的資料也會更加準確。
我們可以把記錄棧地址的結構體設計為通用回溯結構,程式碼如下:
複製程式碼
typedefstructSMStackFrame{ conststructSMStackFrame*constprevious; constuintptr_treturn_address; } SMStackFrame;
在這段程式碼中, previous 記錄的是上一個棧指標的地址。考慮 CPU 效能,記錄堆疊的數量也不必很多,取最近幾條即可。通過棧基地址指標獲取當前棧指標地址的關鍵程式碼如下:
複製程式碼
// 棧地址初始化 SMStackFrame stackFrame = {0}; // 棧基地址指標 constuintptr_t framePointer = smMachStackBasePointerByCPU(&machineContext); if(framePointer ==0|| smMemCopySafely((void*)framePointer, &stackFrame, sizeof(stackFrame)) != KERN_SUCCESS) { return@"Fail frame pointer"; } // 下面的 8 表示堆疊數量 for(; i <8; i++) { // 記錄棧地址 buffer[i] = stackFrame.return_address; if(buffer[i] ==0|| stackFrame.previous ==0|| smMemCopySafely(stackFrame.previous, &stackFrame, sizeof(stackFrame)) != KERN_SUCCESS) { break; } }
關於 Clang 的相關問題
專欄已經更新的第 7~ 第 10 這 4 篇文章中,都涉及到了 Clang 的知識以及應用。
在第 7 篇“Clang、Infer 和 OCLint ,我們應該使用誰來做靜態分析? ”中,介紹的 3 款靜態分析工具都用到了 Clang,而且 Clang 本身也提供了 LibTooling 這種強大的 C++ 介面來方便定製獨立的工具。
問題:
Clang 的知識需要投入大量精力才能掌握好,有同學可能會有疑問:”我掌握這些偏底層的知識有什麼用呢,好像也解決不了我在現實開發工作中遇到的問題啊?“
解答:
在我看來,你只有掌握了某個方面的知識,在工作中碰到問題時,才能夠想到用這個知識去解決問題。如果你都不知道有這麼一種方法,又怎麼會用它去解決自己的問題呢?
就比如說,你掌握了 Clang 的知識,那在研究無侵入的埋點方案 應該如何實現時,你才能可能會想到用 Clang 的 LibTooling 來開發一個獨立的工具,專門以靜態方式插入埋點的程式碼;只有掌握了 Clang 的知識,當你在面對程式碼量達到百萬行的 App 包瘦身需求時,才會想到通過 Clang 靜態分析來開發工具,去檢查無用的方法和類。
當你掌握了 Clang 的相關知識後,編譯前端的技術也就掌握得差不多了;在理解了編譯前端的詞法分析和語法分析的套路後,脫離 Clang 的介面完成第 8 篇文章“如何利用 Clang 為 App 提質? ”的課後作業,也就沒什麼難度了。
篇幅有限,關於 3 個 iOS 開發典型問題的答疑就先到這裡。我希望通過這三個問題,可以幫你搞明白那些讓你困惑的知識點,逐步地建立起自己的知識體系。
出處:極客時間《iOS 開發高手課》 專欄