主頁 > 軟體設計 > 今日頭條優化實踐: iOS 包大小二進制優化,一行代碼減少 60 MB 下載大小

今日頭條優化實踐: iOS 包大小二進制優化,一行代碼減少 60 MB 下載大小

2020-12-30 12:33:39 軟體設計

摘要

蘋果對 iOS App 大小有嚴格限制:下載大小超限會阻礙用戶在蜂窩網路下載 App ,直接影響新用戶轉化;可執行檔案超限將導致 App 審核被拒,直接影響上架,今日頭條探索實踐 __TEXT 段遷移技術,成功減小下載大小 32%,并且解決了可執行檔案大小受限問題,

一、背景知識

1. 下載大小限制

App 大小有下載大小和安裝大小的概念,

下載大小是指 App 壓縮包(也就是 .ipa 檔案)所占的空間,用戶在下載 App 時,下載的是壓縮包,這樣做可以節省流量;當壓縮包下載完成后,就會自動解壓,解壓程序也就是通常所說的安裝程序;安裝大小就是指壓縮包解壓后所占用的空間,

安裝大小在 App Store 上就可以看見 ,通常它會影響用戶的下載意愿:

而下載大小只有研發人員在 App Store Connect 后臺才可以看,用戶看不見,它影響的是下載消耗的流量和時長:

下載大小超過限制,將無法使用蜂窩網路下載 App( iOS 13 之前),會收到檔案容量太大的提示,需通過 Wi-Fi 網路下載,如下,為蘋果歷年來對 App 下載大小限制的變化情況:

  • 2008 年 7 月,搭載了 App Store 的 iPhone 3G 正式發售,下載限制僅為 10 MB

  • 2010 年 2 月,蘋果將 iPhone 3G 的下載限制從 10 MB 提升到 20 MB

  • 2012 年 3 月,iOS 5.1 正式版后,下載限制從 20 MB 提升到 50 MB

  • 2013 年 9 月,iOS 7 正式版后,下載限制從 50 MB 提升至 100 MB

  • 2017 年 9 月,iOS 11 正式版后,下載限制從 100 MB 提升至 150 MB

  • 2019 年 5 月,下載限制從 150 MB 提升至 200 MB

  • 2019 年 9 月,iOS 13 正式版后,若下載大小超過 200 MB,用戶可選擇是否使用蜂窩網路下載

如今,App 下載大小超出 200 MB 時 ,會出現兩種情況:

  • iOS 13 以下的用戶,無法通過蜂窩資料下載 App

  • iOS 13 及以上的用戶,需要手動設定才可以使用蜂窩網路下載 App

2. 可執行檔案大小限制

根據最大構建版本檔案大小[1]描述,蘋果對可執行檔案大小亦有明確限制,超過該限制會導致 App 審核被拒:

ERROR: ERROR ITMS-90122: "Invalid ExecutaBe Size. The size of your app's executaBe file 'News.app/News' is 68534272 bytes for architecture 'arm64', which exceeds the maximum allowed size of 60 MB."

具體限制如下:

  • iOS 7 之前,二進制檔案中所有的 __TEXT 段總和不得超過 80 MB

  • iOS 7.X 至 iOS 8.X ,二進制檔案中,每個特定架構中的 __TEXT 段不得超過 60 MB

  • iOS 9.0 之后,二進制檔案中所有的 __TEXT 段總和不得超過 500 MB

二、面臨問題

隨著網路普及、流量費用降低,蘋果已經放寬了限制,但下載大小若超出 200 MB,可以肯定對新增仍會有一定影響,這對上億級用戶的 App 來說是巨大的損失,并且本著追求極致、在競品中拔得頭籌的理念,我們認為下載大小 200 MB 是包大小的一根紅線,

今日頭條 App 的下載大小已經接近 180 MB,而經過了多年的極致優化(包括但不限于代碼/圖片/其它資源的優化、編譯/鏈接引數的優化、推進無用業務下線、準入卡口等),已經很難再有較大幅度的減少,為此平臺和各方都投入了極大的人力、甚至犧牲了業務的迭代空間來優化/抑制下載大小,

2020 年下半年,我們另辟蹊徑探索實踐了 __TEXT 段遷移的方法:將可執行檔案的 __TEXT 段中的部分節移動到其它的段,避開蘋果的加密機制,提高了可執行檔案的壓縮效率,使 App 的下載大小減少了 60 MB,

該方案徹底解決了下載大小限制的問題,同時還解決了仍在支持 iOS 8.X 的 App 面臨的可執行檔案大小限制問題,

三、技術原理

1. Mach-O 檔案格式簡介

iOS 可執行檔案是 Mach-O 格式,主要由 HeaderLoad CommandsData 三部分,

可以簡單認為:

  • Header 描述了檔案的大概資訊,

  • Load Commands 由多條 Load Command 組成,它們描述了 Data 在二進制檔案和虛擬記憶體中的布局資訊,有了這個布局資訊就能夠知道 Data 在二進制檔案中和虛擬記憶體中是怎樣排布的,它相當于修房子時的圖紙一樣,

  • Data 存盤了實際的內容,主要是程式的指令和資料,它們的排布完全依照 Load Commands 的描述,

Mach-O 檔案中的 Data 部分主要是以 Segment(中文翻譯為段)和 Section (中文翻譯為節)的方式來組織內容的,好比學校中有年級和班級、公司中有部門和小組一樣,把有共同特點的內容組織到一塊,可以方便管理,提高效率,

使用 $ xcrun size -lm <binary-path> 指令可以查看 Mach-O 檔案 Data 部分的結構和各 Segment/Section 的大小資訊(該 Mach-O 檔案由 Xcode 的 iOS App 模板工程構建而來),在不需要更詳細的資訊時,這條命令很方便,

上圖就展示了 Data 中的內容排布的基本資訊,

由該圖可知,在該檔案中:

  • Data 部分中有 5 個 Segment,依次是:

    • __PAGEZERO

    • __TEXT

    • __DATA_CONST

    • __DATA

    • __LINKEDIT

  • __PAGEZERO__LINKEDIT外,每個段中有多個 Section

注意:Data__DATA 是不同的兩個概念,Data 是 Mach-O 檔案中的一部分,包含多個段,__DATA 只是 Data 中的一個段,

__PAGEZERO 的大小是 4 GB,但并不是它在 Mach-O 檔案中的真實大小,這 4 GB 是 Mach-O 加載進記憶體后, __PAGEZERO 在記憶體中占中的大小,它不可讀,不可寫,主要用來捕捉 NULL 指標的參考,如果訪問 __PAGEZERO 段,會引起 EXC_BAD_ACCESS 錯誤,__PAGEZERO 在 Mach-O 中實際上并不占用 Data 部分的空間,

__TEXT__DATA_CONST__DATA 用于保存程式的代碼指令和資料,

__LINKEDIT 包含啟動 App 需要的資訊,比如 bind & rebase 的地址,代碼簽名,符號表等,

2. __TEXT 段遷移的原理

程式的構建程序包含 預處理 -> 編譯 -> 匯編 -> 鏈接 等 4 個主要階段,完成之后就會得到 Mach-O 可執行檔案,

通過 $ man ld ,可以發現聯結器有一個引數: -rename_p orgSegment orgSection newSegment newSection,使用該引數可以將orgSegment/orgSection的名稱修改為newSegment/newSection

可以在 Other Linker Flags 中傳遞該引數,如:

-Wl,-rename_p,__TEXT,__text,__BD_TEXT,__text
-Wl,-segprot,__BD_TEXT,rx,rx

其中 -Wl 的作用是告訴 Xcode 它后面的引數是添加給 Ld 聯結器的,這些引數將在鏈接階段生效,

第一行引數會新創建一個 __BD_TEXT 段,并把 __TEXT,__text 移動到 __BD_TEXT,__text

第二行引數是給 __BD_TEXT 賦予可讀和可執行權限,

構建完成后再來看一下移動 __TEXT,__text 后的 Mach-O 檔案:

可以看到 __TEXT,__text 已經被移動到了 __BD_TEXT 中去了,它的地址也由起始的 0x100005e5c 變為了 0x100010000 ,此時程式仍可以正常的運行,這是因為作業系統只關心段的讀/寫/執行權限,并不關心段或節的名稱,即便是使用了 -rename_p 移動 Segment/Section,各符號的地址也會由聯結器修正好,因此段移動后程式也可以正常運行,

在最低支持 iOS 8 的時代,很多大型 App 都遇到過可執行檔案中 __TEXT 段超 60 MB 的問題,facebook[2] 當時采用了 -rename_p 的技術來避免該問題,他們使用的鏈接引數為:

-Wl,-rename_p,__TEXT,__cstring,__RODATA,__cstring
-Wl,-rename_p,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab
-Wl,-rename_p,__TEXT,__const,__RODATA,__const
-Wl,-rename_p,__TEXT,__objc_methname,__RODATA,__objc_methname
-Wl,-rename_p,__TEXT,__objc_classname,__RODATA,__objc_classname
-Wl,-rename_p,__TEXT,__objc_methtype,__RODATA,__objc_methtype

引數的作用是將 __TEXT 中的 __cstring__gcc_except_tab__const__objc_methname__objc_classname__objc_methtype 等 6 個節移動到 __RODATA去,由于這 6 個節是只讀的,所以他們將新段取名為 __RODATA,意為只讀段,這樣做之后,__TEXT 的大小就會被減小,而蘋果只會掃描 __TEXT 段,所以當 __TEXT 段減小到 60 MB 以下時,就避免了 __TEXT 段超過 60 MB 的問題,該方案當時在國內大型 App 上也很常見,

今日頭條 App 在 2018 年 5 月遇到此問題后也采取了該方案,當時是為了避免 __TEXT 段超 60 MB 的問題,現在測驗發現,以上引數也對下載大小有 12 MB 的優化,

為什么移動 __TEXT 段會減少下載大小?下一小節會給出詳細的解釋,

注意,使用 -rename_p 需要關閉 Bitcode

3. 下載大小減少的原理

摘自蘋果官方檔案[3]:

When your app is approved for the App Store, it is encrypted with DRM and recompressed. The added encryption and DRM affects the ability to compress your binary, and as a result you may see a larger App Store file size for your binary than the binary you uploaded on App Store Connect. The exact final size for your app cannot be determined in advance to the accuracy of a single byte.

對專案工程進行 Archive 后會生成 .xcarchive 檔案,該檔案中包含了 App、dsYMS 以及其它資訊,如圖所示為 .xcarchive 檔案中包含的內容:

.xcarchive 檔案上傳到 App Store Connect 后,蘋果會對 App 中的可執行檔案進行 DRM 加密,然后將 App 壓縮成 ipa 檔案,才發布到 App Store,加密對可執行檔案的大小本身影響很小(對今日頭條 App 的影響為 2 MB),但是它會嚴重影響可執行檔案的壓縮效率,導致壓縮后的 ipa 大小增加,也就是下載大小增大,

實際上,這種加密幾乎沒有用,只要有越獄手機,使用市面上的脫殼工具就可以很容易地進行解密,

Mach-O 檔案代碼的解密發生在 Mach-O 檔案被加載的時候,由 Mach Loader 進行,Mach Loader 會讀取 Mach-O 中的 LC_ENCRYPTION_INFO 這條 Load Command 來判斷可執行檔案是否加密,

所以,也可以通過 otool -l <binary-path> 的命令來查看 Mach-O 是否被加密過,

Load  command 13
          cmd LC_ENCRYPTION_INFO_64
      cmdsize 24
     cryptoff 16384
    cryptsize 101695488
      cryptid 1
          pad 0

其中 cryptoff 表示加密欄位位于檔案中偏移 16384 個位元組;cryptsize 表示加密內容長度 101695488 個位元組;cryptid 表示加密方法為 1,如果為 0 表示不加密,

查看 LC_SEGMENT_64__TEXT 段的范圍

Load command 1
      cmd LC_SEGMENT_64
  cmdsize 1432
  segname __TEXT
   vmaddr 0x0000000100000000  4294967296
   vmsize 0x0000000006100000  101711872
  fileoff 0
 filesize 101711872

依據上述結果可以算出加密的內容實際上都位于 __TEXT 中,

可以認為蘋果只會對 Mach-O 檔案中的 __TEXT 段加密,而不會對其它段加密,只要能把 __TEXT 段中的節移到其它段,就能減少蘋果的加密范圍,從而使壓縮效率提升,減小下載大小,這也解答上個小節提出的問題,

一般來講,在 App 中可執行檔案占 80% 的大小,而加密部分占可執行檔案中的 70%,加密會影響 60% 的壓縮率,因此移走該加密部分,會提升 34% 的下載大小,根據我們在多個 App 的實踐,本方案可以減少 32~34% 的下載大小,

需要注意的是:

蘋果在 iOS 13 已經對下載大小做了優化,所以本方案無法再對 iOS 13 的設備的下載大小進一步優化,

即,若用戶的設備 < iOS 13,那么本方案可以減少該設備上 App 32~34%的下載大小;

若用戶的設備 >= iOS 13,本方案不會對該設備的 App 的下載大小有進一步優化,也不會有負面影響,

因此,如果你看到 App Store Connect 后臺展示的下載大小從 iPhone 11 開始大幅減小,不要驚訝,這是因為 iPhone 11 開始默認搭載的是 iOS 13+ 的系統,

目前推測蘋果在 iOS 13 也是在針對壓縮做了優化,可能是移除了加密或者是先壓縮后加密,

蘋果在 iOS 13 的更新日志[4]中描述到它們對包大小做了優化,如圖:

四、實踐

照著上面的思路來看,只要將 __TEXT 段中所有節都移走,就能夠最大限度的減少下載大小,

這么簡單就可以了嗎?實際上并非如此,在小型 App 上,這么做沒有任何問題,但在較大型 App 上,這并不是一件輕松的事情,

今日頭條 App 在實踐程序中解決了 Crash 和一個極為難纏的鏈接失敗的問題,

1. Crash

Crash 的原因是執行代碼時找不到指定的節,

在原理中說到:作業系統只關心段的讀/寫/執行權限,并不關心段或節的名稱,即便是使用了-rename_p 移動 Segment/Section,各符號的地址也會由聯結器修正好,因此段移動后程式也可以正常運行,

但是如果代碼指明了要讀取 __TEXT 中的某個 Section ,那么這個 Section 就不能夠被移動,否則代碼就無法讀取到它,就會導致出錯,

首先,dyld[5] 在啟動階段會檢查 __unwind_info__eh_frame 這兩個 Section,如果移動這兩個 Section,在啟動后程式就會 Crash,

第二,Swift 相關的 Section 不能移動,否則會引起 Crash,

在使用 Swift 之后,二進制中會有一些 Swift 相關的 Section:

它們都不能夠被移動,一共有下面這些 Section:

__TEXT,__swift5_typeref
__TEXT,__swift5_reflstr
__TEXT,__swift5_fieldmd
__TEXT,__swift5_types
__TEXT,__swift5_capture
__TEXT,__swift5_assocty
__TEXT,__swift5_proto
__TEXT,__swift5_protos
__TEXT,__swift5_builtin

第三,自己在代碼中指明要讀取的 Section,目前我們的代碼中沒有這種 Crash 情況,但是我們的某些腳本中有檢測 __TEXT,__text 的代碼,在 __TEXT 段遷移后,腳本受到了影響,因此需要重新適配這類腳本,

2. 鏈接失敗

__TEXT 段遷移最難解決的問題是鏈接失敗問題,是由 CPU 對尋址范圍的限制以及 ld64 聯結器的缺陷導致,

2.1 現象及原因概述

如果 Mach-O 檔案足夠大,貿然移動 Segment/Section 很容易引發 ld64 聯結器例外,

想要讓 CPU 作業就必須向它提供指令和資料,程式運行時指令和資料存放在記憶體中,CPU 通過地址總線來指定記憶體單元的的地址,地址總線的寬度決定了 CPU 的尋址能力,因此 CPU 對尋址范圍有一定的限制,而不同 CPU 的地址總線寬度不同以及它們所采用的指令模式[6]也不一樣,所以不同 CPU 的尋址范圍也有差異,

B、BL 指令是 ARM 處理器中的跳轉指令,可以讓處理器跳轉到指定的目標地址,從那里繼續執行,由于尋址范圍是受限的,所以跳轉距離不能超出這個限制,ld64 聯結器在最終 Output(寫可執行檔案)時,會對所有的跳轉指令進行檢查,若發現跳轉距離超出限制就會立即拋出 ld: b(l) ARM64 branch out of range例外,從而鏈接失敗,就會出現了圖上所示的現象,

在蘋果開源的 ld64-530 OutputFile.cpp 檔案[7] 中總結出來,常見 CPU 具體限制尋址范圍如下:

2.2 ld64 聯結器所做的事情

按照上面的描述,隨著業務的擴張,代碼的膨脹,Mach-O 檔案會越來越大,那是不是 Mach-O 檔案過大時程式就無法鏈接成功了?

當然不是!實際上 ld64 聯結器知道會出現跳轉距離超出限制的情況,所以它在鏈接程序中會做 Branch Island[8] 演算法,對超限制的跳轉指令加以保護,

// PowerPC can do PC relative branches as far as +/-16MB. (+/-16MB 可能是因為注釋比較老)
// If a branch target is >16MB then we insert one or more
// "branch islands" between the branch and its target that
// allows island hopping to the target.
// Branch Island Algorithm
//
// If the __TEXT Segment < 16MB, then no branch islands needed
// Otherwise, every 14MB into the __TEXT Segment a region is
// added which can contain branch islands. Every out-of-range
// B instruction is checked. If it crosses a region, an island
// is added to that region with the same target and the B is
// adjusted to target the island instead.
//
// In theory, if too many islands are added to one region, it
// could grow the __TEXT enough that other previously in-range
// B branches could be pushed out of range. We reduce the
// probability this could happen by placing the ranges every
// 14MB which means the region would have to be 2MB (512,000 islands)
// before any branches could be pushed out of range.

從原理部分我們知道了 Mach-O 的 Data 部分有很多 Segment/Section,實際上 ld64 聯結器還給每個 Section 歸了類,歸類的代碼可以在蘋果開源的 ld64-530 中的 ld.hpp 檔案的第 547 行找到:

每個 Section 都屬于其中一種型別,Branch Island 演算法會對型別是 typeCode 的 Section 中的跳轉指令做檢查,如果跳轉的距離超出限制,則會在它們之間插入 "branch islands",跳轉指令會先跳到一個 branch island ,再從這個 branch island 跳到目標地址,以此來保證其跳轉距離不超過限制,此部分的代碼在 branch_island.cpp 檔案中可以找到,

__TEXT,__text 的型別是 typeCode,因此,__TEXT,__text 中超出范圍跳轉指令都會被保護,在最后 Output 檢查時,就不會出現 branch out of range 的例外,所以,正常構建的 App,即使很大也不會出現鏈接失敗的問題,這都是歸功于 Branch Island 演算法,

在 Mach-O 檔案中,只有 __TEXT,__text的型別是 typeCode(在使用-rename_p 移動 Segment/Section 之后,Section 的型別不會發生改變),源地址在 __text 中的 跳轉指令跳轉的情況只有兩種:__text -> __text__text -> __stubs

所以 Branch Island 保護的 跳轉指令的所在 Section ,與目標地址所在的 Section, 只有兩種情況:

但實際上 Output 時 ld64 聯結器會檢查檔案中所有的跳轉指令,不僅限于源地址在__text 中的跳轉指令,這意味會檢查多種情況:

小結:Branch Island 演算法僅會保護 __text 中超出限制的跳轉指令,

Output 時,ld64 聯結器會檢查檔案中所有的跳轉指令是否超出限制,

2.3 Branch Island 演算法的缺陷

既然 Branch Island 演算法會保護型別是 typeCode 的 Section 中超限制的跳轉指令,并且-rename_p 不會改變 Section 的型別,那為何會-rename_p 后會導致 branch out of range 的例外?

主要是兩個原因:

1. Branch Island 演算法的檢查邏輯沒有適配到 Section 被移動的情況,

在分析 Mach-O 檔案時只介紹了 Segment/Section,實際聯結器認為在 Section 中還存在 atom(鏈接的基本單元),在 atom 中還存在 fixup(用于描述不同 atom 之間的參考關系,)

如圖所示為 ld64-530 的 branch_island.cpp 檔案中 Branch Island 演算法中的一部分代碼,該片段是要判斷跳轉指令跳轉的距離是否超出限制,如果超過限制就會對該跳轉指令做保護,否則就不做,

srcAddr 為跳轉指令所在的源地址,dstAddr 為目標地址,displacement 為目標地址與源地址的距離,

然而該代碼在計算 srcAddrdstAddr 時,用的都是 offset,是相對距離:

  • atom->pOffset()target->pOffset() 都是 atom 相對于各自 Section 起始地址的距離,

  • fit->offsetInAtomaddend 都是 fixup 相對于各自 atom 的距離,

因此,算出來的 srcAddrdstAddr 都是 fixup 相對于各自所在 Section 起始地址的距離,而 displacement 又是根據 dstAddrsrcAddr 相減計算出來的,它的本意是要計算 dstAddrsrcAddr 之間的距離,在沒有 -rename_p 的情況下,這種計算方式沒有問題;在使用-rename_p 的情況下,會導致計算出來的距離 displacement 不準確,會使在預期對跳轉指令做保護的場景實際沒做保護,

2. Branch Island 演算法不會保護自定義 Section,

Branch Island 演算法只會對 typeCode 的 Section 做保護,而自定義 Section 的型別是 typeUnclassified,如果自定義 Section 中的代碼使用了跳轉指令,并且該跳轉指令的跳轉距離超出范圍,那么無論是否-rename_p 都會出現鏈接失敗的問題,

下面結合 3 個場景,來詳細分析 Branch Island 演算法的缺陷,

2.3.1 場景一

__TEXT,__text 移不干凈導致鏈接失敗,

__text 節在 __TEXT 段中所占比例巨大,要想達到優化效果,必須把它移走,否則幾乎沒有任何優化效果,頭條最開始時,使用-Wl,-rename_p,__TEXT,__text,__BD_TEXT,__text 來嘗試遷移 __TEXT,__text,但無論如何也移不干凈,總有一小部分還留在 __TEXT,__text 中,

導致的問題就是,頂部的 __TEXT,__text 與底部的 __BD_TEXT,__text 中的跳轉指令出現了跳轉距離超出限制情況,ld64 聯結器在 Output 的時候發現了這個錯誤,拋出例外,鏈接失敗,

前面我們已經知道了 Branch Island 演算法會對__text 中的跳轉指令做保護,會在跳轉距離超出限制時候插入 branch island,那為什么還會出現這種錯誤?

畫圖分析,假設在 Mach-O 檔案中, __TEXT,__text 的總大小為 110 ,其中有 A、B 兩個符號,跳轉指令會從 A 跳轉到 B,它們距離 Section __TEXT,__text 的 offset 分別是 30 和 90,它們的實際距離為 60,Branch Island 演算法會對跳轉指令進行保護,計算出 A、B 的間距 displacement 為 60,不會插入 branch island,在 Output 時,ld64 聯結器檢查出來它們的距離為 60,小于 128,不會拋出例外,鏈接成功,

在移走了其中 90 大小的 __TEXT,__text 后,__TEXT,__text 的大小變為了 20,B 被移到了 __BD_TEXT,__text, A、B 相對于各自 Section 的 offset 大概也會發生變化(這個不重要),假設分別變成了 5 和 80,

此刻,A 和 B 的實際距離是 80 + 40 + 15 = 135,但是,Branch Island 演算法在對跳轉指令做保護時,還是依照它們相對各自 Section 的距離來計算,計算出來它們的距離是 80 - 5 = 75,沒有插入 branch island,而實際 135 的大小在 arm64 和 armv7 的實際跳轉時是會出錯的,

在最后 Output 時,ld64 按照 A 和 B 在檔案中的絕對地址來計算距離,算出來它們的距離是 135,超出了 128,檢查出了這種由移動 Section 以及 Branch Island 演算法缺陷導致的錯誤,拋出了 branch out of range 的例外,鏈接失敗,

因此,若要移動 __TEXT,__text,就必須保證把 __TEXT,__text 全都移走,否則就可能出現鏈接失敗的問題,(如果你的 App 可執行檔案比較小,跳轉距離始終不會超過 128M 的話,則不會出現這種問題)

在 ld64-530 的 ld.cpp 檔案中發現,__TEXT,_text 移不干凈,是由 __TEXT, __textcoal_nt__TEXT,__StaticInit 這兩個 Section 導致的,在原始碼中有如下片段:

這段代碼會把 __TEXT, __textcoal_nt__TEXT,__StaticInit 都改名(merge)成 __TEXT,__text,還留在 __TEXT,__text 中的部分就是它們,

在網上查詢到 __textcoal_nt 是 gcc 產生的 Section,至少在 16 年的時候就已經廢棄,但目前還是有不少庫中攜帶的有這個 Section;__StaticInit 并沒有查到更多資訊,

我在蘋果的 ld 更新日志[9]找到這兩個 Section 的蹤跡,蘋果在 07、08 年的時候就已經會將這兩個 Section merge 到 __text 中去,

2008-07-15 Nick Kledzik <kledzik@apple.com>
<rdar://proBem/6061904> automatically order initializers to start of __TEXT
* src/MachOReaderRelocataBe.hpp: merge __StaticInit into __text

2007-04-30 Nick Kledzik <kledzik@apple.com>
<rdar://proBem/5065659> unaBe to link VTK because __textcoal_nt too large
* src/MachOReaderRelocataBe.hpp: when doing a final link map __textcoal_nt to __text

但蘋果的 merge 操作發生在我們-rename_p 之后,因此我們使用-rename_p,__TEXT,__text,__BD_TEXT,__text 沒有將它倆移走,


要讓 __TEXT,_text 移干凈,只需要把它倆也-rename_p,使用如下配置就可以了:

-Wl,-rename_p,__TEXT,__text,__BD_TEXT,__text,
-Wl,-rename_p,__TEXT,__textcoal_nt,__BD_TEXT,__text,
-Wl,-rename_p,__TEXT,__StaticInit,__BD_TEXT,__text

注:字串 __BD_TEXT 中的 BD 是 ByteDance 的縮寫,__BD_TEXT 只是一個名稱,可以隨意更改,

如果你的 App 中使用-rename_p,__TEXT,__text,__BD_TEXT,__text 本身就能移干凈的話,那說明它不包含 __TEXT,__textcoal_nt__TEXT,__StaticInit,可以不添加該配置,

2.3.2 場景二

不移動 __stubs 導致鏈接失敗,

__TEXT 段遷移減少包大小的核心就是移走 __TEXT,__text,但是由于存在__TEXT,__text -> __TEXT,__stubs 的這種跳轉指令,所以如果只移動 __TEXT,__text 而不移動 __TEXT,__stubs ,就會出現和問題一中描述的類似的情況:Branch Island 演算法檢查的是__text 中的符號相對于 __BD_TEXT 的距離,__stubs 是相對于 __TEXT 的距離,該方式計算出來的 displacement 與它們的實際距離不符,在該插入 branch island 的地方沒有插入,Output 時檢查到錯誤,拋出例外,

__TEXT,__stubs 還有點不一樣的地方:

根據原始碼的邏輯,已知圖中框選分部中的 totalTextSize__TEXT,__text__TEXT,__stubs 的總大小,

代碼邏輯描述的是:如果 Section 的型別是 typeStub(arm64 中的__stubs,armv7 中的__picsymbolstub4),Branch Island 演算法會令跳轉指令的目標地址 dstAddr 為 totalTextSize,然后以此來計算間距 displacement

這需要畫圖來分析:

如圖,在一個正常的 Mach-O 檔案中,__TEXT,__text 的大小是 110,__TEXT,__stubs 的大小是 20,A 符號存在于 __TEXT,__text 中,B 符號存在于 __TEXT,__stubs 中,A 距離 __TEXT,__text 的 offset 為 90,B 距離 __TEXT,__stubs 的 offset 為 10,A、B 的實際距離是 30,

在檢查時,Branch Island 演算法發現 B 位于 __TEXT,__stubs,于是直接令 dstAddr = 130(110 + 20 = 130),然后計算出它們的距離 displacement = 130 - 90 = 40,不插入 branch island,在最終 Output 時檢查出它們的實際距離為 30,小于 128 ,不會拋出例外,鏈接成功,

在移動 __TEXT,__text 之后,A 被移動到了 __BD_TEXT,A、B 的實際距離變成了 10 + 40 + 90 = 140,但 Branch Island 演算法計算方式 displacement = 130 - 90 = 40,沒有插入 branch island,這會導致實際的跳轉出錯,ld64 聯結器在最后的 Output 階段檢查出了這種錯誤,拋出例外,鏈接失敗,

ld64 聯結器知道 B 符號肯定位于__stubs內,所以 Branch Island 演算法的這種 dstAddr = totalSize; 的做法只會令 dstAddr 比實際的大,這樣可以保證__stubs 中距離超出的跳轉指令都會被插入 branch island,但由于 dstAddr 偏大了一些,所以實際上也多保護了一部分 實際上并沒有超出限制的跳轉指令,

Branch Island 演算法采用這種相對距離的計算方式,是因為在這個階段它拿不到 A 和 B 符號的絕對地址(絕對地址是在 Output 前才確定的),所以它采用了取巧的辦法,使用 A 和 B 相對于各自 Section 的 offset 來計算它們的距離,它默認了二進制檔案中只有一個型別是typeCode 的 Section, 并且這個 p 在 __TEXT,__stubs 的前面,這種演算法在正常的 Mach-O 檔案中是完全可行的,但我們如果移動了 Segment/Section,就不符合它的設定了,就會導致問題,

因此需要添加如下引數,將 __TEXT,__stubs 也移走:

-Wl,-rename_p,__TEXT,__stubs,__BD_TEXT,__stubs,
-Wl,-rename_p,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4

在 arm64 中,該 Section 的名稱叫做 __stubs,在 armv7 中,該 Section 的名稱叫做 __picsymbolstub4,為了適配不同的架構,可以將這個 Section 同時-rename-rename 不存在的 Section 不會有問題,所以這種寫法是可以的,

這種做法對型別是 typeStub 的 Section(arm64 中的__stubs,armv7 中的__picsymbolstub4) 有另一種限制,就是在移動后, __text__stubs__picsymbolstub4 之間不能有別的 Section,否則可能會出現錯誤,如圖:

在正常 Mach-O 檔案中,A 符號 相對于 __text 的 offset 為 0,B 符號相對于 __stubs 的 offset 為 17.5,在移動 __text__stubs 后,如果我們還移動了其它的 Section,那么這個 Section 有可能會出現在 __BD_TEXT,__text__BD_TEXT,__stubs 之間,這將導致錯誤:

Branch Island 演算法的檢查方式判斷出 A 和 B 的距離為 (110 + 17.9) - 0 = 127.9,小于 128,因此沒有插入 branch island ,但移動后它們的實際距離是 110 + 0.5 + 17.5 = 128 ,是會導致跳轉出錯的,所以 ld64 聯結器會拋出例外,鏈接失敗,

不過這種鏈接失敗的情況比較苛刻,如果 A 的 offset 為 0, 那么它目標地址必須要落在 __stubs 中 [17.5, 17.90] 范圍,才會出現鏈接失敗,其余情況都不會出現,因為小于 17.50 的話,移動后 A 和 B 的實際距離也不會超出 128,并且 A 的 offset 必須要在 [0, 0.4] 范圍內才會出現這種情況,A 如果大于 0.4 的話,那 __text 移動后,A 跳轉到 B 的任意位置也不會超過 128M,

基于這一點,我們在移動 __cstring__gcc_except_tab__const__objc_methname__objc_classname__objc_methtype 這幾個只讀 Section 的時候,不能把它們移到 __BD_TEXT 段中去,否則它們會出現在 __BD_TEXT,__text__BD_TEXT,__stubs 之間導致錯誤,

解決的辦法就是使用原有的鏈接引數,將它們移動到另一個 Segment :__RODATA,這樣就可以避免這個問題:

-Wl,-rename_p,__TEXT,__cstring,__RODATA,__cstring  -Wl,-rename_p,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab  -Wl,-rename_p,__TEXT,__const,__RODATA,__const  -Wl,-rename_p,__TEXT,__objc_methname,__RODATA,__objc_methname  -Wl,-rename_p,__TEXT,__objc_classname,__RODATA,__objc_classname  -Wl,-rename_p,__TEXT,__objc_methtype,__RODATA,__objc_methtype

2.3.3 場景三

自定義 Section 的問題,

在 「2.2 ld64 聯結器所做的事情」 中說到,跳轉指令共有四種跳轉情況,rangeCheck 檢查這四種情況;但是 Branch Island 演算法只會檢查兩種跳轉情況,它只會保護 __text中的跳轉指令,

跳轉指令的所有跳轉情況:

第 4 種情況只在存在自定義 Section,并且自定義 Section 中有跳轉指令時才會出現,

Branch Island 會保護的情況:

有兩種情況, Branch Island 演算法不會保護:

__TEXT,__stub_helper      ->        __TEXT,__stub_helper
__TEXT,__custom_p   ->        __TEXT,__text

原因是 Branch Island 只會對型別是 typeCode 的 Section 中的跳轉指令做檢查 ,而只有 __TEXT,__text 的型別是 typeCode

那么,這兩種情況的跳轉指令,在實際跳轉中是否會出錯?

  1. __TEXT,__stub_helper -> __TEXT,__stub_helper不會,因為__stub_helper的大小只有 28kb(在頭條中),遠小于 128M,所以它內部的指令再怎么跳都不會超出限制,

  1. __TEXT,__custom_p -> __TEXT,__text,是有可能失敗的,

關于自定義 Section ,我們遇到過兩種情況,

A. 在一款 App 中有 __dof_RACSignal__dof_RACCompou 兩個 Section,

這兩個 Section 是由 RAC 引入的,但是它們的 Number of Relocations 是 0,不涉及跳轉指令,它們不用處理,不會有鏈接失敗的問題,

B. 頭條中有一個 __u_selector Section:

它是依賴的某靜態庫引入的,__u_selector中包含一個重定位符號 ___Symbol_A,跳轉指令會從它跳轉到 __text 中的 ___Symbol_B

除錯發現正常可執行檔案中,它們之間的距離是 10M 左右,不會出現鏈接失敗的,

可以推測___Symbol_B其實位于__text的底部, 而 __text 很大,如果把__text 移動到到 __u__selector 的下邊去,那么這兩個指令之間的距離就會增大,超過 128 MB 就會鏈接失敗,如圖:

所以在移動 __text 后,__custom_p (含跳轉指令的自定義 Section)也必須跟著移動,讓它保持在 __text 的下面,保持它們原有的相對位置,

照此分析,__TEXT 中的自定義 Section 不被 Branch Island 保護,如果二進制檔案足夠大,而這個 Section 又有跳轉指令,當跳轉距離超過 128 MB 時,也會鏈接失敗,與是否移動 __text 無關,

要移走自定義 Section,需要再添加如下配置:

-Wl,-rename_p,__TEXT,__custom_p,__CUSTOM_TEXT,__custom_p

這里必須要使用新的段 __CUSTOM_TEXT,而不能把自定義 Section 放到 __BD_TEXT 中,否則自定義 Section 會出現在__text__stubs 之間,導致出現 "場景二" 后半部分中描述的問題,

3. 設定段的權限

由于將可執行代碼移動到了新的段 __BD_TEXT__CUSTOM_TEXT 中,所以需要給這兩個段添加可讀和可執行權限,否則程式將無法運行:

-Wl,-segprot,__CUSTOM_TEXT,rx,rx
-Wl,-segprot,__BD_TEXT,rx,rx

五、一行代碼

在今日頭條 App 中是使用 xcconfig[10] 來管理構建引數的,如果你也使用該方式,那么使用下面這一行代碼就能完成配置:

APP_THIN_LINK_FLAGS = -Wl,-rename_p,__TEXT,__cstring,__RODATA,__cstring,-rename_p,__TEXT,__objc_methname,__RODATA,__objc_methname,-rename_p,__TEXT,__objc_classname,__RODATA,__objc_classname,-rename_p,__TEXT,__objc_methtype,__RODATA,__objc_methtype,-rename_p,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab,-rename_p,__TEXT,__const,__RODATA,__const,-rename_p,__TEXT,__text,__BD_TEXT,__text,-rename_p,__TEXT,__textcoal_nt,__BD_TEXT,__text,-rename_p,__TEXT,__StaticInit,__BD_TEXT,__text,-rename_p,__TEXT,__stubs,__BD_TEXT,__stubs,-rename_p,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,-segprot,__BD_TEXT,rx,rx

如果你是沒有使用這種方式,在 Other Linker Flags 中逐行添加以下配置即可:

-Wl,-rename_p,__TEXT,__cstring,__RODATA,__cstring
-Wl,-rename_p,__TEXT,__objc_methname,__RODATA,__objc_methname
-Wl,-rename_p,__TEXT,__objc_classname,__RODATA,__objc_classname
-Wl,-rename_p,__TEXT,__objc_methtype,__RODATA,__objc_methtype
-Wl,-rename_p,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab
-Wl,-rename_p,__TEXT,__const,__RODATA,__const
-Wl,-rename_p,__TEXT,__text,__BD_TEXT,__text
-Wl,-rename_p,__TEXT,__textcoal_nt,__BD_TEXT,__text
-Wl,-rename_p,__TEXT,__StaticInit,__BD_TEXT,__text
-Wl,-rename_p,__TEXT,__stubs,__BD_TEXT,__stubs
-Wl,-rename_p,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,
-Wl,-segprot,__BD_TEXT,rx,rx

如果你的二進制檔案中存在自定義 Section 的話,比如使用了類似__attribute__((p("__TEXT,__custom_p")))的方式創建了自定義 Section,則可能需要做如下的配置以移走自定義 Section,具體見 「2.3.3 場景三」 的詳細分析,

APP_THIN_LINK_FLAGS = -Wl,-rename_p,__TEXT,__cstring,__RODATA,__cstring,-rename_p,__TEXT,__objc_methname,__RODATA,__objc_methname,-rename_p,__TEXT,__objc_classname,__RODATA,__objc_classname,-rename_p,__TEXT,__objc_methtype,__RODATA,__objc_methtype,-rename_p,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab,-rename_p,__TEXT,__const,__RODATA,__const,-rename_p,__TEXT,__text,__BD_TEXT,__text,-rename_p,__TEXT,__textcoal_nt,__BD_TEXT,__text,-rename_p,__TEXT,__StaticInit,__BD_TEXT,__text,-rename_p,__TEXT,__stubs,__BD_TEXT,__stubs,-segprot,__BD_TEXT,rx,rx,-rename_p,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,-rename_p,__TEXT, __custom_p,__CUSTOM_TEXT,__text,-segprot, __CUSTOM_TEXT,rx,rx

六、答疑

1. 為什么不把 __TEXT 段中的所有 Section 都移走,這樣不是更好嗎?

并不是移走的段越多,壓縮就越有效,而是得看移走的大小,例如下面雖然有 15 個 Section,但是它們的大小加起來 578 KB,移走它們對壓縮后的下載大小幾乎零提升,

Section __stubs: 28488 (addr 0x105f21644 offset 99751492)
Section __stub_helper: 28428 (addr 0x105f2858c offset 99779980)
Section __swift5_typeref: 2216 (addr 0x105f2f498 offset 99808408)
Section __swift5_fieldmd: 1272 (addr 0x105f2fd40 offset 99810624)
Section __swift5_types: 120 (addr 0x105f30238 offset 99811896)
Section __const: 64184 (addr 0x105f302b0 offset 99812016)
Section __ustring: 281012 (addr 0x105f3fd68 offset 99876200)
Section __swift5_reflstr: 796 (addr 0x105f84720 offset 100157216)
Section __swift5_capture: 376 (addr 0x105f84a3c offset 100158012)
Section __swift5_builtin: 120 (addr 0x105f84bb4 offset 100158388)
Section __swift5_assocty: 312 (addr 0x105f84c2c offset 100158508)
Section __swift5_proto: 308 (addr 0x105f84d64 offset 100158820)
Section __swift5_protos: 40 (addr 0x105f84e98 offset 100159128)
Section __u__selector: 36 (addr 0x105f84ec0 offset 100159168)
Section __eh_frame: 184708 (addr 0x1060cf018 offset 101511192)

并且,有的 Section 是不能移走的,會引起 crash,有興趣的讀者可以自行嘗試,

2. 出現 Crash.log 決議不了的情況怎么辦?

在上線后,我們發現 Crash report 中的 Crash.log 中有一部分符號無法決議,如圖中的 ???

出現這個問題的原因是,Crash.log 在分析主二進制鏡像時,把它在虛擬記憶體中的地址范圍取錯了,

如圖 0x100010000 - 0x100203fff 的范圍只有 2047999(2.0 MB),這明顯遠小于主二進制檔案中__text 原本的大小 100 MB,這個 2.0 MB 的大小基本與 __TEXT 段被遷移后剩余的大小相符,因此猜測 Crash.log 在分析時取的是 __TEXT 段的大小,而我們把大部分 __TEXT 段都移走了,

所以當遇到一個符號落在 (2.0M, 100M] 的區間中時,Crash.log 就無法知道這個地址它到底是屬于哪個鏡像,它就會顯示 ??? ,無法決議,

解決辦法:這種 Crash.log 使用 atos 工具[11]手動決議,將主鏡像名稱當做引數傳入即可,

七、總結

本文從背景知識和面臨的實際問題出發,介紹了 __TEXT 段遷移及減少下載大小的原理,描述了我們在實踐程序中遇到的問題,并從原始碼的角度詳細分析了問題產生的根本原因以及解決方式,解答了相關疑問和上線后遇到的問題,

目前,該方案已經在位元組跳動多個大型 App 中應用,均對下載大小有 30% 以上的優化,且運行穩定,

八、加入我們

我們是位元組跳動 General Information Platform - 客戶端平臺架構 iOS 團隊,在性能優化、基礎組件、業務架構、研發體系、安全合規、線下質量基礎設施、線上問題定位歸因平臺等方向深耕,負責保障和提升今日頭條、西瓜視頻和番茄小說的產品質量與開發效率,聚焦于此的同時向外延伸,

如果你對技術充滿熱情,喜歡追求極致,渴望用自己的代碼改變數億用戶的體驗,歡迎加入我們,我們期待你與我們共同成長,目前我們在北京、深圳均有招聘需求,簡歷投遞郵箱: tech@bytedance.com ;郵件標題: 姓名 - 作業年限 - GIP - 客戶端平臺架構 - iOS/Android

參考文獻

[1] 最大版本構建大小
https://help.apple.com/app-store-connect/#/dev611e0a21f

[2] 分析 facebook App
https://blog.timac.org/2016/1018-analysis-of-the-facebook-app-for-ios/

[3] App Store Connect Help
https://help.apple.com/app-store-connect/en.lproj/static.html

[4] iOS 13 更新日志
https://support.apple.com/en-us/HT210393#13

[5] dyld 檢查 Section 的原始碼
https://opensource.apple.com/source/dyld/dyld-750.6/src/dyldExceptions.c.auto.html

[6] ARM 架構
https://en.wikipedia.org/wiki/ARM_architecture#Thumb

[7] ld64-530 OutputFile 原始碼
https://opensource.apple.com/source/ld64/ld64-530/src/ld/OutputFile.cpp

[8] Branch Island 演算法
https://opensource.apple.com/source/ld64/ld64-133.3/src/ld/passes/branch_island.cpp.auto.html

[9] Ld 更新日志
https://opensource.apple.com/source/ld64/ld64-97.2/ChangeLog

[10] xcconfig 介紹
https://nshipster.com/xcconfig/

[11] atos man page
https://www.manpagez.com/man/1/atos/

歡迎關注「 位元組跳動技術團隊 」

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/242428.html

標籤:其他

上一篇:無線傳感器網路

下一篇:閑人閑談PS之一專案庫存跨公司業務STO解決方案--SAP閑人的開篇

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more