通過(guò)前面的探討,我們知道內(nèi)存分頁(yè)觸發(fā)中斷異常 Page Fault 后,會(huì)阻塞進(jìn)程,這個(gè)問(wèn)題是會(huì)對(duì)性能產(chǎn)生影響。
實(shí)際上在 iOS 系統(tǒng)中,生產(chǎn)環(huán)境的應(yīng)用,在發(fā)生缺頁(yè)中斷進(jìn)行重新加載時(shí) ,iOS 系統(tǒng)還會(huì)對(duì)其做一次簽名驗(yàn)證,因此 iOS 生產(chǎn)環(huán)境的 Page Fault 所產(chǎn)生的耗時(shí)要更多。
對(duì)用戶而言,使用App時(shí)第一個(gè)直接體驗(yàn)就是啟動(dòng) App 時(shí)間,而啟動(dòng)時(shí)期會(huì)有大量的類、分類、三方等等需要加載和執(zhí)行,此時(shí)多個(gè) Page Fault 所產(chǎn)生的的耗時(shí)往往是不能小覷的,下面我們就通過(guò)二進(jìn)制重排來(lái)優(yōu)化啟動(dòng)耗時(shí)。
抖音團(tuán)隊(duì)分享的一個(gè)
Page Fault,開(kāi)銷在0.6 ~ 0.8ms。實(shí)際測(cè)試發(fā)現(xiàn)不同頁(yè)會(huì)有所不同 , 也跟 cpu 負(fù)荷狀態(tài)有關(guān) , 在0.1 ~ 1.0 ms之間 。
二進(jìn)制重排這個(gè)方案最早也是 抖音團(tuán)隊(duì) 分享的,不過(guò)他們的解決方案有瑕疵,下面我們會(huì)針對(duì)性的解決。
一、原理
假設(shè)在啟動(dòng)時(shí)期我們需要調(diào)用兩個(gè)函數(shù) method1 與 method4,函數(shù)編譯在 mach-O 中的位置是根據(jù) ld ( Xcode 的鏈接器) 的編譯順序并非調(diào)用順序來(lái)的,因此很可能這兩個(gè)函數(shù)分布在不同的內(nèi)存頁(yè)上。

page1 與 page2 都需要從無(wú)到有加載到物理內(nèi)存中,從而觸發(fā)兩次 Page Fault。二進(jìn)制重排 的做法就是將
method1 與 method4 放到一個(gè)內(nèi)存頁(yè)中,那么啟動(dòng)時(shí)則只需要加載一次 page 即可,也就是只觸發(fā)一次 Page Fault。在實(shí)際項(xiàng)目中,我們可以將啟動(dòng)時(shí)需要調(diào)用的函數(shù)放到一起 ( 比如 前10頁(yè)中 ) 以盡可能減少
Page Fault,進(jìn)而減少啟動(dòng)耗時(shí)。
二、調(diào)試 Page Fault
最好是卸載App,重新安裝,調(diào)試第一次啟動(dòng)的效果。
- 打開(kāi)
Instruments,選擇System Trace。 - 選擇真機(jī),選擇工程,選擇啟動(dòng),當(dāng)頁(yè)面加載出來(lái)的時(shí)候,停止。
- 查看
Page Fault,如圖標(biāo)注。
Page Fault.png
File Backed Page In:即為 Page Fault,對(duì)應(yīng)的有count,一頁(yè)P(yáng)age Fault最大耗時(shí),最小耗時(shí)等參數(shù)。
如果多次啟動(dòng)調(diào)試,你會(huì)發(fā)現(xiàn)
count的波動(dòng)范圍很大。所以如果想獲取準(zhǔn)確的數(shù)據(jù),最好重新安裝App或者打開(kāi)多個(gè)App之后,再來(lái)調(diào)試。
這是因?yàn)閮?nèi)存管理機(jī)制,殺掉進(jìn)程時(shí),他所占用的物理內(nèi)存空間,如果沒(méi)有被覆蓋使用,那么這部分內(nèi)存有很大可能一直存在。重新打開(kāi),內(nèi)存就不需要全部初始化。所以 冷熱啟動(dòng)的界定不能以是否后臺(tái)殺死來(lái)簡(jiǎn)單判斷。
三、二進(jìn)制重排
3.1 Order File
前面說(shuō)了這么多,那么具體該怎么操作呢?蘋(píng)果其實(shí)已經(jīng)給我們提供了這個(gè)機(jī)制。

實(shí)際上 二進(jìn)制重排就是對(duì)即將生成的可執(zhí)行文件重新排列,即它發(fā)生在鏈接階段。
首先,Xcode 用的鏈接器叫做
ld ,ld 有一個(gè)參數(shù)叫 Order File,我們可以通過(guò)這個(gè)參數(shù)配置一個(gè) 后綴名 為order的文件路徑。在這個(gè) order 文件中,將你需要的符號(hào)按順序?qū)懺诶锩妗.?dāng)工程 build 的時(shí)候,Xcode 會(huì)讀取這個(gè)文件,打的二進(jìn)制包就會(huì)按照這個(gè)文件中的符號(hào)順序進(jìn)行生成對(duì)應(yīng)的 mach-O。可以參考一下
libObjc 項(xiàng)目,它已經(jīng)使用了二進(jìn)制重排進(jìn)行優(yōu)化。
是不是看到了ios應(yīng)用啟動(dòng)加載過(guò)程中熟悉的方法。
1、order 文件里符號(hào)寫(xiě)錯(cuò)了或不存在會(huì)不會(huì)有問(wèn)題:ld 會(huì)忽略這些符號(hào),如果提供了 link 選項(xiàng)
-order_file_statistics,他們會(huì)以 warning 的形式把這些沒(méi)找到的符號(hào)打印在日志里。
2、會(huì)不會(huì)影響上架:不會(huì),order文件只是重新排列了所生成的 mach-O(可執(zhí)行文件) 中函數(shù)表與符號(hào)表的順序。
3.2 如何查看項(xiàng)目符合順序
- 可以設(shè)置
Write Link Map File來(lái)設(shè)置是否輸出,默認(rèn)是no。Link Map是編譯期間產(chǎn)生的 ,( ld 的讀取二進(jìn)制文件順序默認(rèn)是按照Compile Sources里的順序 ),它記錄了二進(jìn)制文件的布局。 - 修改
Write Link Map File為YES,然后clean項(xiàng)目并重新編譯 -
Products -> show in finder,上上層文件夾,然后找到一個(gè)xxxxx-LinkMap-normal-arm64.txt的txt文件。
Link map.png
這個(gè)文件的# Symbols:部分存儲(chǔ)了所有符號(hào)的順序,前面的.o等內(nèi)容忽略 。
Symbols.png
我們發(fā)現(xiàn)符號(hào)順序明顯是按照Compile Sources的文件順序來(lái)排列的。
文件中最左側(cè)地址就是 方法真實(shí)實(shí)現(xiàn)地址(實(shí)際代碼地址)而并非符號(hào)地址 , 因此我們二進(jìn)制重排并非只是修改符號(hào)地址 , 而是利用符號(hào)順序 , 重新排列整個(gè)代碼在文件的偏移地址 , 將啟動(dòng)需要加載的方法地址放到前面內(nèi)存頁(yè)中 , 以此達(dá)到減少 page fault 的次數(shù)從而實(shí)現(xiàn)時(shí)間上的優(yōu)化。
終端查看符號(hào)表命令(不準(zhǔn)確,僅供參考)。找到可執(zhí)行文件:
nm (file):查看符號(hào)表
nm -p (file):按照orderfile順序
nm -up (file): 只看系統(tǒng)
nm -Up (file):只看自定義
3.3實(shí)戰(zhàn)
1、 新建一個(gè)項(xiàng)目,添加方法:


3、新建一個(gè)order文件:
touch binary.order,加入幾個(gè)方法
-[ViewController test3]
-[ViewController test2]
-[ViewController test1]
4、修改Order File配置為:$(SRCROOT)/Binary/binary.order 或 ./Binary/binary.order。

5、
clean,編譯,再次查看xxx.txt文件。
mach-O 中首地址偏移量最小位置。假設(shè)這三個(gè)方法原本在不同的三頁(yè),那么意味著我們已經(jīng)優(yōu)化掉了兩個(gè) Page Fault。
3.4 獲取啟動(dòng)執(zhí)行的函數(shù)
到這里,離啟動(dòng)優(yōu)化就只差一步了,如何獲取啟動(dòng)運(yùn)行的函數(shù)?大致有三種方案,僅供參考:
-
hookobjc_MsgSend:只能拿到oc以及swift @objc dynamic后的方法,并且由于可變參數(shù)個(gè)數(shù),需要用匯編來(lái)獲取參數(shù) 。 - 靜態(tài)掃描
machO特定段和節(jié)里面所存儲(chǔ)的符號(hào)以及函數(shù)數(shù)據(jù)。 -
clang 插樁:完全拿到
swift、oc、c、block全部函數(shù)。
四、Clang插樁
關(guān)于 clang 的插樁覆蓋的官方文檔如下 : clang 自帶代碼覆蓋工具 文檔中有詳細(xì)概述,以及簡(jiǎn)短Demo演示。
思路:一是自己編寫(xiě) clang 插件,另外一個(gè)就是利用 clang 本身已經(jīng)提供的一個(gè)工具來(lái)實(shí)現(xiàn)我們獲取所有符號(hào)的需求。
4.1 靜態(tài)插樁代碼
下面我們來(lái)探索一下這個(gè)靜態(tài)插樁代碼覆蓋工具的機(jī)制和原理。
1、添加編譯設(shè)置:直接搜索 Other C Flags 來(lái)到 Apple Clang - Custom Compiler Flags 中 , 添加配置:-fsanitize-coverage=trace-pc-guard。
2、在ViewController.m添加代碼:
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
char PcDescr[1024];
//__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
3、運(yùn)行(最好是一個(gè)空工程,注釋我們前面手動(dòng)添加的方法),查看打印:

start和stop兩個(gè)指針地址,會(huì)發(fā)現(xiàn)他存儲(chǔ)的實(shí)際上是 1-15 幾個(gè)序號(hào)。4、添加一個(gè)
oc方法,我們?cè)俅未蛴?code>start和stop指針,你會(huì)發(fā)現(xiàn)序號(hào)變?yōu)?1-16。繼續(xù)添加一個(gè)
c函數(shù),一個(gè)block,一個(gè)touch函數(shù),是不是驚喜的發(fā)現(xiàn),序號(hào)增加到 19 了。
此時(shí),我們是不是可以大膽的猜想:這個(gè)內(nèi)存區(qū)間保存的就是工程所有符號(hào)的個(gè)數(shù)。
5、繼續(xù),清空打印,點(diǎn)擊屏幕。是不是發(fā)現(xiàn)有兩次輸出,看代碼,此時(shí)有兩次方法的調(diào)用。最終我們發(fā)現(xiàn):調(diào)用幾個(gè)方法,就會(huì)打印幾次 guard:。
此時(shí)查看匯編,你會(huì)發(fā)現(xiàn):在每個(gè)函數(shù)調(diào)用的第一句實(shí)際代碼,會(huì)被添加進(jìn)去了一個(gè)
bl指令, 調(diào)用到__sanitizer_cov_trace_pc_guard這個(gè)函數(shù)中來(lái) 。
bl,匯編跳轉(zhuǎn)指令,即調(diào)用方法。bl之前是棧平衡與寄存器數(shù)據(jù)準(zhǔn)備,不用關(guān)心。
這就是靜態(tài)插樁:靜態(tài)插樁實(shí)際上是在編譯期,在每一個(gè)函數(shù)內(nèi)部第一行代碼處,添加 hook 代碼 ( 即我們添加的 __sanitizer_cov_trace_pc_guard 函數(shù) ) ,實(shí)現(xiàn)全局的方法 hook,即AOP效果。
4.2 獲取函數(shù)符號(hào)
通過(guò)上面的分析我們知道,所有函數(shù)的第一步都會(huì)調(diào)用__sanitizer_cov_trace_pc_guard,那我們是不是可以通過(guò)這個(gè)函數(shù)獲取函數(shù)符號(hào)呢?
熟悉匯編的應(yīng)該知道:函數(shù)嵌套時(shí) , 在跳轉(zhuǎn)子函數(shù)時(shí),都會(huì)保存下一條指令的地址在 x30 ( 又叫 lr 寄存器) 里 。
例如 , A 函數(shù)中調(diào)用了 B 函數(shù),在 arm 匯編中即 bl + 0x**** 指令,該指令會(huì)首先將下一條匯編指令的地址保存在 x30 寄存器中。然后在跳轉(zhuǎn)到 bl 后面?zhèn)鬟f的指定地址去執(zhí)行。
bl 能實(shí)現(xiàn)跳轉(zhuǎn)到某個(gè)地址的匯編指令,其原理就是修改 pc 寄存器的值來(lái)指向到要跳轉(zhuǎn)的地址,而且實(shí)際上 B 函數(shù)中也會(huì)對(duì) x29 / x30 寄存器的值做保護(hù),防止子函數(shù)又跳轉(zhuǎn)其他函數(shù)會(huì)覆蓋掉 x30 的值 , 當(dāng)然葉子函數(shù)除外。
當(dāng) B 函數(shù)執(zhí)行 ret 也就是返回指令時(shí),就會(huì)去讀取 x30 寄存器的地址,跳轉(zhuǎn)過(guò)去,因此也就回到了上一層函數(shù)的下一步。
在 __sanitizer_cov_trace_pc_guard 函數(shù)中的這一句代碼:
void *PC = __builtin_return_address(0);
它的作用其實(shí)就是去讀取 x30 中所存儲(chǔ)的要返回時(shí)下一條指令的地址。所以他名稱叫做 __builtin_return_address。換句話說(shuō),這個(gè)地址就是我當(dāng)前這個(gè)函數(shù)執(zhí)行完畢后,要返回到哪里去。
bt 函數(shù)調(diào)用棧也是這種思路來(lái)實(shí)現(xiàn)的。也就是說(shuō) , 我們可以在 __sanitizer_cov_trace_pc_guard 這個(gè)函數(shù)中 , 通過(guò) __builtin_return_address 函數(shù)拿到原函數(shù)調(diào)用 __sanitizer_cov_trace_pc_guard 這句匯編代碼的下一條指令的地址。

如圖,
PC的指向就是,當(dāng)test1函數(shù)執(zhí)行完__sanitizer_cov_trace_pc_guard后,下一行代碼NSLog。
那么問(wèn)題又來(lái)了,如果通過(guò)函數(shù)內(nèi)部?jī)?nèi)存地址,獲取函數(shù)名稱呢?
熟悉安全攻防,逆向的同學(xué)可能會(huì)清楚。我們?yōu)榱朔乐鼓承┨囟ǖ姆椒ū粍e人使用
fishhook hook掉,會(huì)利用dlopen打開(kāi)動(dòng)態(tài)庫(kù),拿到一個(gè)句柄,進(jìn)而拿到函數(shù)的內(nèi)存地址直接調(diào)用。那我們可以反過(guò)來(lái)使用。
與 dlopen.h 相同 , 在 dlfcn.h 中有一個(gè)方法如下 :
typedef struct dl_info {
const char *dli_fname; /* 所在文件 */
void *dli_fbase; /* 文件地址 */
const char *dli_sname; /* 符號(hào)名稱 */
void *dli_saddr; /* 函數(shù)起始地址 */
} Dl_info;
//這個(gè)函數(shù)能通過(guò)函數(shù)內(nèi)部地址找到函數(shù)符號(hào)
int dladdr(const void *, Dl_info *);
我們?cè)陧?xiàng)目中實(shí)踐一下,先導(dǎo)入頭文件 #import <dlfcn.h>,然后修改代碼如下 :
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("\nfname:%s \nfbase:%p \nsname:%s\nsaddr:%p \n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
char PcDescr[1024];
//__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
打印結(jié)果:
fname:/Users/00393998/Library/Developer/CoreSimulator/Devices/23342248-4844-41AB-9851-2023D815FAA2/data/Containers/Bundle/Application/9A86A08B-5411-4909-B62B-B27097CA2EC9/Binary.app/Binary
fbase:0x10beee000
sname:-[ViewController touchesBegan:withEvent:]
saddr:0x10beef9d0
guard: 0x10bef468c 6 PC ?
fname:/Users/00393998/Library/Developer/CoreSimulator/Devices/23342248-4844-41AB-9851-2023D815FAA2/data/Containers/Bundle/Application/9A86A08B-5411-4909-B62B-B27097CA2EC9/Binary.app/Binary
fbase:0x10beee000
sname:testFunc
saddr:0x10beef9b0
guard: 0x10bef4688 5 PC \367\371\356??
4.3 寫(xiě)入order文件
寫(xiě)入文件時(shí)有許多需要注意的地方,即坑點(diǎn)
1、多線程
考慮到這個(gè)方法會(huì)來(lái)特別多次,使用鎖會(huì)影響性能,這里使用蘋(píng)果底層的原子隊(duì)列 ( 底層實(shí)際上是個(gè)棧結(jié)構(gòu),利用隊(duì)列結(jié)構(gòu) + 原子性來(lái)保證順序 ) 來(lái)實(shí)現(xiàn)。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//遍歷出隊(duì)
while (true) {
//offset 通過(guò)next指針在結(jié)構(gòu)體的偏移量,進(jìn)而知道next的指向
//offsetof 就是針對(duì)某個(gè)結(jié)構(gòu)體找到某個(gè)屬性相對(duì)這個(gè)結(jié)構(gòu)體的偏移量
// offsetof(SymbolNode, next) 可以替換為 8
SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
if (node == NULL) break;
Dl_info info;
dladdr(node->pc, &info);
printf("%s \n",info.dli_sname);
}
}
//原子隊(duì)列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定義符號(hào)結(jié)構(gòu)體
typedef struct{
void * pc;
void * next;
}SymbolNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
SymbolNode * node = malloc(sizeof(SymbolNode));
*node = (SymbolNode){PC,NULL};
//入隊(duì)
// offsetof 用在這里是為了入隊(duì)添加下一個(gè)節(jié)點(diǎn)找到 前一個(gè)節(jié)點(diǎn)next指針的位置
OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}
2、死循環(huán)
上述這種 clang 插樁的方式,會(huì)在while循環(huán)中同樣插入 hook 代碼。
通過(guò)匯編會(huì)查看到 while 循環(huán),會(huì)被多次靜態(tài)加入 __sanitizer_cov_trace_pc_guard 調(diào)用,導(dǎo)致死循環(huán)。
解決方式:Other C Flags 修改為如下:-fsanitize-coverage=func,trace-pc-guard。func:表示僅 hook函數(shù)時(shí)調(diào)用。
cbnz:匯編執(zhí)行,while循環(huán)。
3、load方法
有load 方法時(shí),__sanitizer_cov_trace_pc_guard 函數(shù)的參數(shù) guard 是 0,所以打印并沒(méi)有發(fā)現(xiàn) load。屏蔽掉 __sanitizer_cov_trace_pc_guard 函數(shù)中的:if (!*guard) return;
拓展:如果我們希望從某個(gè)函數(shù)之后/之前開(kāi)始優(yōu)化,那么我們可以通過(guò)一個(gè)全局靜態(tài)變量,在特定的時(shí)機(jī)修改其值,在
__sanitizer_cov_trace_pc_guard這個(gè)函數(shù)中做好對(duì)應(yīng)的處理即可。
4、其他處理
- 由于用的先進(jìn)后出原因 , 我們要
倒敘一下 去重-
order文件格式要求:c函數(shù)、block前面還需要加_下劃線。
核心代碼(不要忘記編譯配置哦):
//引入頭文件
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
//核心代碼
#pragma mark - 獲取order文件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (YES) {
//offsetof 就是針對(duì)某個(gè)結(jié)構(gòu)體找到某個(gè)屬性相對(duì)這個(gè)結(jié)構(gòu)體的偏移量
SymbolNode * node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));
if (node == NULL) break;
Dl_info info;
dladdr(node->pc, &info);
NSString * name = @(info.dli_sname);
// 添加 _
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
//去重
if (![symbolNames containsObject:symbolName]) {
[symbolNames addObject:symbolName];
}
}
//取反
NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
NSLog(@"%@",symbolAry);
//將結(jié)果寫(xiě)入到文件
NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"binary.order"];
NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
if (result) {
NSLog(@"%@",filePath);
}else{
NSLog(@"文件寫(xiě)入出錯(cuò)");
}
}
//原子隊(duì)列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號(hào)結(jié)構(gòu)體
typedef struct{
void * pc;
void * next;
}SymbolNode;
#pragma mark - 靜態(tài)插樁代碼
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
SymbolNode * node = malloc(sizeof(SymbolNode));
*node = (SymbolNode){PC,NULL};
//入隊(duì)
// offsetof 用在這里是為了入隊(duì)添加下一個(gè)節(jié)點(diǎn)找到 前一個(gè)節(jié)點(diǎn)next指針的位置
OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
}
最后運(yùn)行,下載.order文件到本地,就可以愉快的玩耍了。
五、補(bǔ)充
5.1 swift / OC 混編工程問(wèn)題
通過(guò)如上方式適合純 OC 工程獲取符號(hào)。由于 swift 的編譯器前端是自己的 swift 編譯前端程序,因此配置稍有不同。搜索 Other Swift Flags,添加兩條配置即可:-sanitize-coverage=func、 -sanitize=undefined。swift類同樣可以通過(guò)這個(gè)方式獲取。
5.2 cocoapod 工程問(wèn)題
cocoapod 工程引入的庫(kù),會(huì)產(chǎn)生多 target,我們?cè)谥?code>target添加的配置是不會(huì)生效的,我們需要針對(duì)需要的target做對(duì)應(yīng)的設(shè)置。
對(duì)于直接手動(dòng)導(dǎo)入到工程里的 sdk,不管是 靜態(tài)庫(kù) .a 還是 動(dòng)態(tài)庫(kù),會(huì)默認(rèn)使用主工程的設(shè)置,也就是可以拿到符號(hào)的。


