iOS 開發中的『庫』(二)

發表於2016-10-16

其實這是一篇糾(da)錯(lian)篇

看文章之前,你可以看下下面幾個問題,如果你都會了,或許可以不看。

  • 再談一談動態庫和靜態庫。你真的知道 XXXX 和 XXX 系列。
  • 為什麼使用動態庫的方式來動態更新只能用在 in housedevelop 模式卻不能在使用到 AppStore 上呢?
  • 動態庫到底會新增到記憶體中幾次?

我是前言

  • 其實這篇文章準備明天再寫的,但是看到 @我就叫Sunny怎麼了 大半夜還幫我指出問題,睡覺這個東西,感覺還是先放一放吧。
  • 像我這種嚴重拖延症患者來說,關於 iOS 開發中的『庫』(二) ,更新速度已經史無前例了。(我好像也沒什麼系列啊?哈哈哈哈)
  • 主要是 iOS 開發中的『庫』(一) 這篇文章確實有不少錯誤,需要彌補一下。
  • 當然,最需要感謝的還是 @Casa Taloyum 大神 在我寫完第一篇當天晚上給我提出了一些文中的問題,並耐心的解答了我的一些小疑問。
  • ok,廢話不多說,我們先開始吧。

糾錯篇

靜態庫的處理方式

  • 對於一個靜態庫而言,其實已經是編譯好的了,類似一個 .o 的集合(這裡並沒有連線,在iOS 開發中的『庫』(一) 所描述的連結其實不對)。在 build 的過程中只會參與連結的過程,而這個連結的過程簡單的講就是合併,並且連結器只會將靜態庫中被使用的部分合併到可執行檔案中去。相比較於動態庫,靜態庫的處理起來要簡單的多,具體如下圖:
11437742-49946939660dd0f6
靜態庫連結過程
  • 連結器會將所有.o用到的 global symbolunresolved symbol 放入一個臨時表,而且是 global symbol 是不能重複的。
  • 對於靜態庫的 .o , 聯結器會將沒有任何 symbolunresolved symbol table 的給忽略。
  • unresolved symbol 類似 extern int test();.h 的 宣告?
  • global symbol 類似 void test() { print("test")}.m 的 實現?
  • 最後,連結器會用函式的實際地址來代替函式引用。

所以,在 iOS 開發中的『庫』(一) 所提到,將標頭檔案新增到可執行檔案是不正確的。

動態庫的處理方式

  • 首先,對於動態庫而言其實分 動態連結庫動態載入庫 兩種的,這兩個最本質的區別還是載入時間。
    • 動態連結庫:在沒有被載入到記憶體的前提下,當可執行檔案被載入,動態庫也隨著被載入到記憶體中。在 Linked Framework and Libraries 設定的一些 share libraries。【隨著程式啟動而啟動】
    • 動態載入庫:當需要的時候再使用 dlopen 等通過程式碼或者命令的方式來載入。【在程式啟動之後】
  • 但是不論是哪種動態庫,相比較與靜態庫,動態庫處理起來要棘手的多。由於動態庫是動態的,所以你事先不知道某個函式的具體地址。因此動態連結器在連結函式的時候需要做大量的工作。

因為動態庫在連結函式需要做大量的工作,而靜態庫已經實現處理好了。(“靜態庫的載入速度會更快一點”的原因是因為編譯器預先實現了連結,而不是靜態庫實現的連結。- by Casa Taloyum)所以單純的在所有都沒有載入的情況下,靜態庫的載入速度會更快一點。而在 iOS 開發中的『庫』(一) 提到的有所不妥,正確應該是,雖然動態庫更加耗時,但是對於在載入過的share libraries不需要再載入的這個前提下,使用動態庫可以節省一些啟動時間。

  • 而實現這個動態連結是使用了 Procedure Linkage Table (PLT)。首先這個 PLT 列出了程式中每一個函式的呼叫,當程式開始執行,如果動態庫被載入到記憶體中,PLT 會去尋找動態的地址並記錄下來,如果每個函式都被呼叫過的話,下一次呼叫就可以通過 PLT 直接跳轉了,但是和靜態庫還是有點區別的是,每一個函式的呼叫還是需要通過一張 PLT。這也正是 sunny 所說的所有靜態連結做的事情都搬到執行時來做了,會導致更慢 的原因。

動態庫到底在記憶體哪塊區域的問題

  • 其實這個命題最開始就跑偏了,在和@酷酷的哀殿 等幾個小夥伴討論未果之後,在老司機@Casa Taloyum 大神 的點撥下,明白了問題出在了哪裡。
  • 首先,不管是靜態庫、動態庫,兩者的區別和在記憶體哪個區域沒有關係,最本質的區別是,一個的函式呼叫等在編譯時候就已經確定,而動態庫是動態載入的。換句話說,靜態庫修改了東西,整個程式需要重新編譯,而對於動態庫的修改而言,只需要重啟 app(重置 PLT )。
  • 至於在記憶體的哪個區域,和是 靜態庫 or 動態庫 沒有關係。程式碼段、資料段這些,都是程式載入時就進入的。堆一般是檔案buffer分配、物件初始化等時候用的。棧是函式出入口指標,區域性常規變數用的。只要 malloc 都在堆裡。具體的可以參照這裡
  • 還需要提一下的是,如果是動態載入庫,那麼在沒有載入的時候,程式碼段、資料段這些也是不會載入進去的。lazy load。

.a 的使用

.a是一個純二進位制檔案,不能直接拿來使用,需要配合標頭檔案、資原始檔一起使用。

  • 可能他沒有理解我的意思,我所說的資原始檔,是如圖片這類的。而他說可以不使用標頭檔案的形式連結一個.a,好吧,我這還真不知道,但是常規使用下,使用.a 還是需要配合 標頭檔案和資原始檔一起的,所以相比之下使用 framework 更方便。

關於動態 framework 是否需要去除模擬器架構問題

  • 首先,這個理論上沒有問題的,但是我自己遇到過這個坑,再加上在這裡也提到了這個問題,所以保險條件下最好去掉。(感覺這是個蘋果的 bug)

擴充篇

動態庫動態更新問題

能否動態庫的方式來動態更新AppStore上的版本呢?

  • 原本是打算國慶的時候試一試 AppStore 上到底行不行的,結果還是託@Casa Taloyum 大神 老司機的福,他已經踩過這個坑了,他的結論是:使用動態庫的方式來動態更新只能用在 in housedevelop 模式卻但不能在使用到 AppStore
  • 因為在上傳打包的時候,蘋果會對我們的程式碼進行一次 Code Singing,包括 app 可執行檔案和所有Embedded 的動態庫。因此,只要你修改了某個動態庫的程式碼,並重新簽名,那麼 MD5 的雜湊值就會不一樣,在載入動態庫的時候,蘋果會檢驗這個 hash 值,當蘋果監測到這個動態庫非法時,就會造成 Crash。

所以在 iOS 開發中的『庫』(一) 提到理論上是可行的這點也是不對的。

動態庫到底新增幾次問題

  • 這個問題毋庸置疑,肯定是一次。

app1 和 app2 都有一個相同的 動態framework 以 Embedded 方式放入到各自的 app 中,問這個 動態framework 會載入幾次?

  • 當然,不是一次,是兩次。但是這不是和前面說的相違背了麼,其實並不是違背,只是前面說的一次不妥當,最妥當的應該這麼說:對於相同路徑的動態庫,系統只會載入一次。
12437742-9191df4cb601015f
動態庫載入

關於。。。好了我先睡了。還有問題,明天再寫吧。。希望寫的這篇對得起 sunny 和 Casa 老司機 的指點。


番外篇

摘抄自個人筆記。

關於記憶體五大分割槽

  • BSS段:
    • BSS段( bss segment )通常是指用來存放程式中未初始化的全域性變數和靜態變數 的一塊記憶體區域。
    • 這裡注意一個問題:一般的書上都會說全域性變數和靜態變數是會自動初始化的,那麼哪來的未初始化的變數呢?變數的初始化可以分為顯示初始化和隱式初始化,全域性變數和靜態變數如果程式設計師自己不初始化的話的確也會被初始化,那就是不管什麼型別都初始化為0,這種沒有顯示初始化的就 是我們這裡所說的未初始化。既然都是0那麼就沒必要把每個0都儲存起來,從而節省磁碟空間,這是BSS的主要作用
    • BSS是英文Block Started by Symbol的簡稱。BSS段屬於靜態記憶體分配。 BSS節不包含任何資料,只是簡單的維護開始和結束的地址,即總大小。以便記憶體區能在執行時分配並被有效地清零。BSS節在應用程式的二進位制映象檔案中並不存在,即不佔用 磁碟空間 而只在執行的時候佔用記憶體空間 ,所以如果全域性變數和靜態變數未初始化那麼其可執行檔案要小很多
  • 資料段(data segment)
    • 通常是指用來存放程式中已經初始化的全域性變數和靜態變數的一塊記憶體區域。資料段屬於靜態記憶體分配,可以分為只讀資料段讀寫資料段。字串常量等,但一般都是放在只讀資料段中。
  • 程式碼段(code segment/text segment)
    • 通常是指用來存放程式執行程式碼的一塊記憶體區域。這部分割槽域的大小在程式執行前就已經確定,並且記憶體區域通常屬於只讀, 某些架構也允許程式碼段為可寫,即允許修改程式。在程式碼段中,也有可能包含一些只讀的常數變數,例如字串常量等,但一般都是放在只讀資料段中 。
  • 堆(heap)
    • 堆是用於存放程式執行中被動態分配的記憶體段,它的大小並不固定,可動態擴張或 縮減。當程式呼叫malloc等函式分配記憶體時,新分配的記憶體就被動態新增到堆上(堆被擴張); 當利用free等函式釋放記憶體時,被釋放的記憶體從堆中被剔除(堆被縮減)
  • 棧 (stack heap)
    • 棧又稱堆疊, 是使用者存放程式臨時建立的區域性變數,也就是說我們函式括弧“{}” 中定義的變數(但不包括static宣告的變數,static意味著在資料段中存放變數)。除此以外, 在函式被呼叫時,其引數也會被壓入發起呼叫的程式棧中,並且待到呼叫結束後,函式的返回值 也會被存放回棧中。由於棧的先進先出特點,所以 棧特別方便用來儲存/恢復呼叫現場。從這個意義上講,我們可以把堆疊看成一個寄存、交換臨時資料的記憶體區。

13437742-aa9178aa3632ea2a
記憶體分割槽

小 Tips

  • 棧區中的變數不需要程式設計師管理
  • 堆區的需要程式設計師管理
  • 在 iOS 中堆區的記憶體是所有應用程式共享
  • 系統使用表級別結構來分配記憶體空間,所以邏輯地址和實體地址可能不一樣

參考文獻

更多

工作之餘,寫了點筆記,如果需要可以在我的 GitHub 看。

相關文章