iOS 編譯與連接實踐

前言

溫故而知新,重溫編譯原理知識,能夠得到新的理解。html

本文源於之前遇到的一個問題,在導出ipa的時候報錯以下:

在解決過程當中回顧有一些收穫,因而有了這篇文章。
關鍵詞:預處理、編譯、彙編、連接、動態連接庫、靜態連接庫linux

正文

編譯與連接過程

當咱們進行編譯時,會通過預處理、編譯、彙編、連接的過程。
這是一段普通的c代碼:c++

#include <stdio.h>
int main()
{
    puts("It's OK.");
    return 0;
}
複製代碼

用gcc對上面代碼進行編譯,整個編譯過程以下:
 編譯流程圖 git

這個過程須要幾個gcc的指令處理:程序員

  • 一、預處理
gcc -E test.c -o test.i
複製代碼
  • 二、編譯
gcc -S test.i -o test.s
複製代碼
  • 三、彙編
gcc -c test.s -o test.o
複製代碼
  • 四、連接
gcc test.o -o test
複製代碼

指令解釋github

-E Only run the preprocessor
-S Only run preprocess and compilation steps
-c Only run preprocess, compile, and assemble steps
-o <file> Write output to <file>xcode

靜態鏈接與動態連接

一、靜態連接

靜態鏈接就是把靜態鏈接庫(.a文件)中的文件連接到可執行文件中;markdown

.a文件是多個.o文件的組合;
.o文件是對象文件,裏面是機器指令;
連接就是多個.o文件打包成可執行文件;ide

二、動態連接

動態連接就是僅在可執行文件中加入相關描述文件,執行時再動態加載相應的動態連接庫;函數

三、連接過程

連接的過程,也就是符號重定位
c/c++ 程序的編譯是以文件爲單位進行的,所以每一個 c/cpp 文件也叫做一個編譯單元(translation unit), 源文件先是被編譯成一個個目標文件, 再由連接器把這些目標文件組合成一個可執行文件或庫,連接的過程,其核心工做是解決模塊間各類符號(變量,函數)相互引用的問題,對符號的引用本質是對其在內存中具體地址的引用,所以肯定符號地址是編譯,連接,加載過程當中一項不可缺乏的工做,這就是所謂的符號重定位。本質上來講,符號重定位要解決的是當前編譯單元如何訪問「外部」符號這個問題。

此段引用自linux 下動態連接實現原理,有更詳細的原理介紹。

iOS相關

下圖是Xcode工程的設置,接下逐步解析各個關鍵配置。

首先是Embedded Binaries的兩個庫,GPUImage.frameworklib.framework
這兩個是動態庫,framework內容格式以下

接下來是Linked Frameworks and Libraries的依賴庫,libstdc++.6.tbd。 tbd是dylib的優化版本,官方的解釋以下:

the .tbd files are new "text-based stub libraries", that provide a much more compact version of the stub libraries for use in the SDK, and help to significantly reduce its download size

libXG-SDK.a是信鴿推送的靜態連接庫,libXXX.framework、GPUImage.framework是工程依賴的framework和GPUImage,libPods-Live.a是CocoaPods生成並管理的靜態連接庫。

在Build Phases的設置裏面Check Pods Manifest.lock 設置的腳本會檢查Podfile.lock 和 Manifest.lock 的差別,判斷是否須要從新pod install
Embed Pods Frameworks、Copy Pods Resources 是另外兩個腳本

問題排查

瞭解完工程的基本設置後,咱們來定位前面提到的問題。
進行的操做是Archive -> Export -> Ad Hoc,提示的錯誤信息是 Found an unexpected Mach-O header code
點擊show logs,而後選擇standard.log

log的描述是did not contain a "archived-expanded-entitlements.xcent" resource

這個問題在stackoverflow也有人提問過,可是不是我遇到的狀況。
stackoverflow給出的建議是:
Go to BUILD PHASES -> COPY BUNDLE RESOURCES, you will find there some framework. Remove from this section and add it to LINK BINARY WITH LIBRARIES. It will work..

檢查工程的設置,發現是同事把一個靜態庫放到了Embedded Binaries項裏面,然而靜態庫是不能打包到ipa裏面。(靜態庫裏的代碼會編譯連接到可執行文件,資源文件須要從新打包成一個bundle文件放入ipa包)

思考題🤔:CocoaPods不少第三方庫是包括UI資源的,然而咱們知道.a文件是不包括資源的,那麼第三方庫的資源如何處理的?

靜態庫與動態庫

用幾個測試樣例和測試工程,來更好理解動態庫和靜態庫。
介紹下測試工程和如何進行測試:
工程P爲主工程,其中有4個子工程A、B、C、D,子工程打包的庫爲動態庫或靜態庫,子工程之間存在依賴關係。
經過修改主工程的依賴庫,以及子工程的依賴關係以及打包類型,測試動態庫依賴靜態庫靜態庫依賴動態庫靜態庫依賴靜態庫的狀況。

在測試以前,先簡單說明下靜態庫和動態庫的打包方式,以下圖

  • 當選擇Cocoa Touch Framework時,若是Mach-O Type 爲 Static則打包的.framework文件爲靜態庫;若是Mach-O Type 爲 Dynamic,則打包的.framework文件爲動態庫。

  • 當選擇Cocoa Touch Static Library時,打包的.a文件爲靜態庫。

靜態庫依賴靜態庫

測試環境
靜態庫A、B、C均採用Cocoa Touch Framework的打包方式。

  • 靜態庫A:提供函數foo();
  • 靜態庫B:提供函數call_foo_b(); 依賴靜態庫A,在call_foo_b中調用foo();
  • 靜態庫C:提供函數foo();

主工程依賴庫狀況

測試代碼以下

#include "BLib.h"
#include "CLib.h"

- (void)testLib {
    NSLog(@"Test A.");
    call_foo_b();
    
    NSLog(@"Test B.");
    foo();
}
複製代碼

測試結果輸出:

2016-12-20 09:54:12.931731 testLib[7671:4787567] Test A.
call_foo in BLib.
foo in ALib.
2016-12-20 09:54:12.931925 testLib[7671:4787567] Test B.
foo in ALib.
複製代碼

對於TestA,咱們調用B的call_foo_b,而後在call_foo_b中又調用A的foo,打印的調用順序爲B->A,符合預期;
對於TestB,咱們引入C的頭文件,而後調用C的foo,打印的調用順序是A,結果異常;

結果思考🤔
靜態庫的生成只有編譯,沒有連接;
當工程同時存在庫A和C時,兩個foo的函數符號在連接的時候,先引入者優先。驗證方法是把工程依賴順序從ABC改爲CBA以後,結果輸出變爲:

2016-12-20 10:19:28.613791 testLib[7691:4795943] Test A.
call_foo in BLib.
foo in CLib.
2016-12-20 10:19:28.613871 testLib[7691:4795943] Test B.
foo in CLib.
複製代碼

靜態庫依賴動態庫

測試環境
庫A、B、C、D均採用Cocoa Touch Framework的打包方式。
* 動態庫A:提供函數foo();
* 靜態庫B:提供函數call_foo_b(); 依賴動態庫A,在call_foo_b中調用foo();
* 動態庫C:提供函數foo();
* 靜態庫D:提供函數call_foo_d(); 依賴動態庫C,在call_foo_d中調用foo();

測試代碼

#include "BLib.h"
#include "DLib.h"

- (void)testLib {
    NSLog(@"Test lib.");
    call_foo_b();
    call_foo_d();
}
複製代碼

測試結果

2016-12-20 10:36:09.389209 testLib[7707:4799800] Test lib.

call_foo in BLib. foo in ALib. call_foo in DLib. foo in ALib.

  • 對於第一組測試,咱們調用靜態庫B的函數call_foo_b,在函數call_foo_b中調用動態庫A的函數,正常
  • 對於第二組測試,咱們調用靜態庫D的函數call_foo_d,在函數call_foo_d中調用動態庫A的函數,異常; (預想中是調用動態庫C的函數)

結果思考🤔
靜態庫的生成只有編譯,沒有連接;
那麼在靜態庫D生成的過程當中,只是肯定了靜態庫D須要用到動態庫中的foo函數;
當運行時,加載了動態庫A、C,其中兩個庫均含有foo函數;動態連接器,按照加載的順序,取到動態庫A中的foo函數;
因此靜態庫B、D調用的foo函數均是動態庫A中的foo函數。

驗證: 咱們調換Link Binary With Libraries 中A和C的位置,結果以下

2016-12-20 10:35:11.048034 testLib[7705:4799491] Test lib.
call_foo in BLib.
foo in CLib.
call_foo in DLib.
foo in CLib.
複製代碼

動態庫依賴靜態庫

測試環境
庫A、B、C、D均採用Cocoa Touch Framework的打包方式。

  • 靜態庫A:提供函數foo();
  • 動態庫B:提供函數call_foo_b(); 依賴靜態庫A,在call_foo_b中調用foo();
  • 靜態庫C:提供函數foo();
  • 動態庫D:提供函數call_foo_d(); 依賴靜態庫C,在call_foo_d中調用foo();

測試代碼

#include "BLib.h"
#include "DLib.h"

- (void)testLib {
    NSLog(@"Test lib.");
    call_foo_b();
    call_foo_d();
}
複製代碼

測試結果

2016-12-20 11:08:52.715415 testLib[7746:4810080] Test lib.
call_foo in BLib.
foo in ALib.
call_foo in DLib.
foo in CLib.
複製代碼
  • 對於第一組測試,咱們調用動態庫B的函數call_foo_b,在函數call_foo_b中調用靜態庫A的函數,正常
  • 對於第二組測試,咱們調用動態庫D的函數call_foo_d,在函數call_foo_d中調用靜態庫C的函數,正常

結果思考🤔
工程依賴裏面只有動態庫B、D,沒有靜態庫A、C;
靜態庫A、C同名函數foo沒有衝突;
這兩個現象是緣由是動態庫在生成的過程當中,除了編譯還有連接的過程。若是動態庫依賴靜態庫,在生成動態庫時會將靜態庫的代碼合併到動態庫中。

擴展
若是動態庫B、D的函數名字使用同樣的call_foo,調用順序和Link Binary With Libraries相關,與embeded的順序無關;(embeded只是把動態庫放入bundle中,關鍵在於連接器的順序)

動態庫依賴動態庫

測試環境
動態庫A、B、C、D均採用Cocoa Touch Framework的打包方式。

  • 動態庫A:提供函數foo();
  • 動態庫B:提供函數call_foo_b(); 依賴動態庫A,在call_foo_b中調用foo();
  • 動態庫C:提供函數foo();
  • 動態庫D:提供函數call_foo_d(); 依賴動態庫C,在call_foo_d中調用foo();

測試代碼

#include "BLib.h"
#include "DLib.h"

- (void)testLib {
    NSLog(@"Test lib.");
    call_foo_b();
    call_foo_d();
}

複製代碼

測試結果

2016-12-20 11:08:52.715415 testLib[7746:4810080] Test lib.

call_foo in BLib. foo in ALib. call_foo in DLib. foo in CLib.

  • 對於第一組測試,咱們調用動態庫B的函數call_foo_b,在函數call_foo_b中調用動態庫A的foo函數,正常
  • 對於第二組測試,咱們調用動態庫D的函數call_foo_d,在函數call_foo_d中調用動態庫C的foo函數,正常

結果思考🤔
四個動態庫都須要Link和Embeded;
與靜態庫依賴動態庫的測試樣例不一樣,此次雖然動態庫A、C存在同名函數foo,可是調用的時候沒有衝突。
動態庫依賴動態庫,在生成動態庫的時候不會把依賴的動態庫合併到動態庫中。

靜態庫和動態庫的依賴關係

靜態庫的生成只有編譯,沒有連接;
動態庫的生成除了編譯還有連接的過程;
若是動態庫依賴靜態庫,在生成動態庫時會將靜態庫的代碼合併到動態庫中;

  • 靜態庫A依賴靜態庫B,使用時須要在Link Binary With Libraries引入靜態庫A、B;
  • 靜態庫A依賴動態庫B,使用時須要在Link Binary With Libraries引入靜態庫A和動態庫B,而且在Embeded Binaries引入動態庫B;
  • 動態庫A依賴靜態庫B,使用時須要在Link Binary With Libraries引入動態庫A,而且在Embeded Binaries引入動態庫A;
  • 動態庫A依賴動態庫B,使用時須要在Link Binary With Libraries引入動態庫A和B,而且在Embeded Binaries引入動態庫A和B;

全部的代碼均可以在這裏找到。

擴展--Cocoa Touch Static Library的打包

Cocoa Touch Static Library打包出來的是.a格式的靜態庫,會把Link Binary With Libraries裏面的靜態庫一塊兒打包到.a靜態庫中,測試工程點我

如何打包一個靜態庫,可是不包含其中的依賴庫文件?

引入依賴庫頭文件便可,由於靜態庫只編譯不連接。(可是若是Cocoa Touch Static Library 裏面填入了第三方的靜態庫,會自動打包)

.a和.framework都是靜態庫格式,只是.framework格式包括了靜態庫文件、頭文件、資源文件,故而更容易使用。

如何直接使用.a靜態庫,不要靜態庫的頭文件?

Link Binary With Libraries中添加.a靜態庫;

在使用靜態庫的函數前添加聲明,可是不定義實現; 這樣編譯時,會根據聲明在全局查找定義;

總結

在寫文章過程當中,簡單複習了下編譯原理,深感程序員的技能樹太過龐大,隨便一個分支就夠學習一生。 平時開發遇到問題,習慣性的刨根問底,此次簡單把這些知識串聯起來,並和工程做相應結合,加深記憶。 文章若有疏漏,敬請指出。

相關文章
相關標籤/搜索