將Kotlin二進位文件縮小99.2%

2020-09-05 星雲鶴峰

我們將逐步縮小,但首先讓我們來激勵有問題的二進位文件。 三年前,我寫了 「Surfacing Hidden Change to Pull Requests」一文,其中涵蓋了將重要的統計數據和對PR的差異作為評論。 這樣可以避免因影響二進位大小,清單和依賴關係樹的更改而引起的意外情況。

顯示依賴關係樹使用了Gradle的依賴關係任務和diff -U 0來顯示上一次提交的更改。 該示例中的示例將Kotlin版本從1.1-M03提升到1.1-M04,產生了以下差異:

@@ -125,2 +125,3 @@-| \--- org.jetbrains.kotlin:kotlin-stdlib:1.0.4 -> 1.1-M03-| \--- org.jetbrains.kotlin:kotlin-runtime:1.1-M03+| \--- org.jetbrains.kotlin:kotlin-stdlib:1.0.4 -> 1.1-M04+| \--- org.jetbrains.kotlin:kotlin-runtime:1.1-M04+| \--- org.jetbrains:annotations:13.0@@ -145,2 +146 @@-+--- org.jetbrains.kotlin:kotlin-stdlib:1.1-M03-+--- org.jetbrains.kotlin:kotlin-runtime:1.1-M03++--- org.jetbrains.kotlin:kotlin-stdlib:1.1-M04

除了可以看到版本變化之外,我們還可以推斷出兩個額外的事實:

  • kotlin-runtime依賴項獲得了對Jetbrains annotations 工件的依賴,如diff的第一部分所示。
  • 如diff第二部分所示,刪除了對kotlin-runtime的直接依賴。 很好,因為第一部分已經告訴我們kotlin-runtime是kotlin-stdlib的依賴項。

這兩個事實顯示在所顯示的差異中,但是僅隱含著一個微妙的第三事實。 因為第一節是縮進的,所以我們知道我們的直接依賴項之一是對kotlin-stdlib的傳遞性依賴項。 不幸的是,我們不知道哪個依賴項會受到影響。

為了解決這個問題,我編寫了一個名為dependency-tree-diff的工具,該工具顯示了樹中所有更改的根依賴關係路徑。

+--- com.jakewharton.rxbinding:rxbinding-kotlin:1.0.0-| \--- org.jetbrains.kotlin:kotlin-stdlib:1.0.4 -> 1.1-M03-| \--- org.jetbrains.kotlin:kotlin-runtime:1.1-M03+| \--- org.jetbrains.kotlin:kotlin-stdlib:1.0.4 -> 1.1-M04+| \--- org.jetbrains.kotlin:kotlin-runtime:1.1-M04+| \--- org.jetbrains:annotations:13.0-+--- org.jetbrains.kotlin:kotlin-stdlib:1.1-M03 (*)-\--- org.jetbrains.kotlin:kotlin-runtime:1.1-M03+\--- org.jetbrains.kotlin:kotlin-stdlib:1.1-M04 (*)

我們的隱含第三事實(其他直接依賴關係受到影響)現在在輸出中是顯式的。變更作者現在可以反映受影響的依賴項是否可能存在任何兼容性問題。

您可以了解有關該工具的更多信息,並在其自述文件中查看另一個示例。

縮小二進位

該工具需要籤入我們的倉庫並在CI上運行。使用Kotlin腳本成功構建了adb-event-mirror之後,該工具的第一個版本也使用了Kotlin腳本。儘管運行良好且很小,但CI機器上未安裝kotlinc。我們依靠Kotlin Gradle插件來編譯Kotlin,而不是獨立的二進位文件。

您可以在本地重定向Kotlin腳本緩存目錄以捕獲已編譯的jar,但是它仍然依賴於Kotlin腳本工件,該工件很大,具有很多依賴性,並且仍然非常動態。很顯然,這不是正確的方法,但是我提交了KT-41304,希望將來使製作腳本的胖.jar文件變得更容易。

我切換到經典的Kotlin Gradle項目,並生成了一個帶有.kotlin-stdlib依賴項的胖.jar。在添加腳本以使 make the jar self-executing之後,二進位文件的鎖定為1699978位元組(或〜1.62MiB)。不錯,但是我們可以做得更好!

刪除Kotlin元數據

使用unzip -l列出.jar中的文件表明,除了.class之外,大多數是.kotlin_module或.kotlin_metadata。 Kotlin編譯器和Kotlin的反思都使用了它們,而我們的二進位文件都不需要它們。

我們可以將這些信息與用於Java 9模塊系統的module-info.class以及META-INF / maven /中的文件一起從二進位文件中過濾掉,這些文件將傳播有關使用Maven工具構建的項目的信息。

刪除所有這些文件將產生1513414位元組(〜1.44MiB)的新二進位大小,大小減少11%。

使用R8

R8是用於Android構建的代碼優化器和混淆器。雖然通常在轉換為Dalvik可執行格式時用於優化和模糊化Java類文件,但它也支持輸出Java類文件。為了使用它,我們需要使用ProGuard的配置語法指定該工具的入口點。

-dontobfuscate-keepattributes SourceFile, LineNumberTable-keep class com.jakewharton.gradle.dependencies.DependencyTreeDiff { public static void main(java.lang.String[]);}

除了入口點之外,混淆功能也被禁用,並且我們保留了源文件和行號屬性,因此發生的任何異常仍然可以理解。

通過R8傳遞fat .jar會生成一個新的縮小的.jar,然後可以將其製成可執行文件。現在生成的二進位文件僅為41680位元組(〜41KiB),大小減少了98%。真好!

由於我們生成的是二進位文件而不是庫文件,因此-allowaccessmodification選項將允許隱藏成員公開,從而使諸如類合併和內聯之類的優化更加有效。加上這個會產生一個37630位元組(〜37KiB)的二進位文件。

調整標準庫使用

絕對安全的在這裡停下來,但我不好停…

現在二進位文件已經足夠小了,我們可以開始研究哪些代碼對大小有所影響。通常我會使用javap來查看字節碼,但是由於我們只關心看到API調用,因此可以解壓縮二進位文件並在IntelliJ IDEA中打開類文件,該文件將使用Fernflower反編譯器顯示大致等效的Java。

main方法首先以文件形式讀取參數:

fun main(vararg args: String) { if (args.size == 2) { val old = args[0].let(::File).readText() val new = args[1].let(::File).readText()

反編譯的代碼如下所示:

public static final void main(String... var0) { Intrinsics.checkNotNullParameter(var0, &34;); if (var0.length == 2) { String[] var10000 = var0; String var3 = var0[0]; var3 = FilesKt__FileReadWriteKt.readText$default(new File(var3), (Charset)null, 1); String var1 = var10000[1]; String var8 = FilesKt__FileReadWriteKt.readText$default(new File(var1), (Charset)null, 1);

窺視FilesKt__FileReadWriteKt會顯示我們在過去某個時刻編寫的不幸的文件讀取代碼,其中包含kotlin.ExceptionsKt,kotlin.jvm.internal.Intrinsics和kotlin.text.Charsets。

從java.io.File切換到java.nio.path.Path意味著我們可以使用內置方法來讀取內容。

fun main(vararg args: String) { if (args.size == 2) {- val old = args[0].let(::File).readText()- val new = args[1].let(::File).readText()+ val old = args[0].let(Paths::get).let(Paths::readString)+ val new = args[0].let(Paths::get).let(Paths::readString)

經過這些更改,二進位文件下降到30914位元組(〜30KiB)。

另一個引起我注意的標準庫用法是按行劃分輸入:

private fun findDependencyPaths(text: String): Set<List<String>> { val dependencyLines = text.lineSequence() .dropWhile { !it.startsWith(&34;) } .takeWhile { it.isNotEmpty() }

反編譯的Java看起來像這樣:

public static final Set findDependencyPaths(String var0) { String[] var10000 = new String[]{&34;, &34;, &34;}; List var1; DelimitedRangesSequence var2;

這表明我們正在使用Kotlin實施拆分和使用其Sequence類型。 Java 11添加了String.lines(),它返回一個Stream,該流還具有已經在使用的dropWhile和takeWhile運算符。 不幸的是,Kotlin還具有String.lines()擴展名,因此我們需要進行強制轉換才能使用Java 11方法。

private fun findDependencyPaths(text: String): Set<List<String>> {- val dependencyLines = text.lineSequence()+ val dependencyLines = (text as java.lang.String).lines() .dropWhile { !it.startsWith(&34;) } .takeWhile { it.isNotEmpty() }

此更改將二進位文件降至13643位元組(〜13KiB),減少了99.2%。

剩下的腫脹

Kotlin是一種多平臺語言,意味著它具有自己的空列表,集合和映射實現。 但是,以JVM為目標時,沒有理由在java.util.Collections提供的JVM上使用它們。 我提交了KT-41333來跟蹤此增強功能。

轉儲最終二進位文件的內容表明其空集合(和相關類型)貢獻了剩餘大小的約50%:

$ unzip -l build/libs/dependency-tree-diff-r8.jarArchive: build/libs/dependency-tree-diff-r8.jar Length Date Time Name--------- ---------- ----- ---- 84 12-31-1969 19:00 META-INF/MANIFEST.MF 926 12-31-1969 19:00 com/jakewharton/gradle/dependencies/DependencyTrees$findDependencyPaths$dependencyLines$1.class 854 12-31-1969 19:00 com/jakewharton/gradle/dependencies/DependencyTrees$findDependencyPaths$dependencyLines$2.class 6224 12-31-1969 19:00 com/jakewharton/gradle/dependencies/DependencyTreeDiff.class 604 12-31-1969 19:00 com/jakewharton/gradle/dependencies/Node.class 2534 12-31-1969 19:00 kotlin/collections/CollectionsKt__CollectionsKt.class 1120 12-31-1969 19:00 kotlin/collections/EmptyIterator.class 3227 12-31-1969 19:00 kotlin/collections/EmptyList.class 2023 12-31-1969 19:00 kotlin/collections/EmptySet.class 1958 12-31-1969 19:00 kotlin/jvm/internal/CollectionToArray.class 1638 12-31-1969 19:00 kotlin/jvm/internal/Intrinsics.class--------- ------- 21192 11 files

除了這些額外的類型外,字節碼還包含許多額外的空檢查。 例如,最後一部分中的findDependencyPaths的反編譯字節碼實際上看起來像這樣:

public static final Set findDependencyPaths(String var0) { Intrinsics.checkNotNullParameter(var0, &34;); Intrinsics.checkNotNullParameter(var0, &34;); String[] var10000 = new String[]{&34;, &34;, &34;}; Intrinsics.checkNotNullParameter(var0, &34;); Intrinsics.checkNotNullParameter(var10000, &34;); Intrinsics.checkNotNullParameter(var10000, &34;);

這些Intrinsics調用在函數參數上強制類型系統的可為空的不變量,但是在內聯之後,除第一個外,其餘都是內在的。 這樣的重複調用會出現在整個代碼中。 這是由Kotlin重命名這些內在方法而引起的R8錯誤,R8沒有更新以正確跟蹤該更改。

修正了這兩個問題後,二進位文件很可能會落入個位數的KiB中,從而使原始的胖.jar文件減少了99%。

如果要構建遮蔽依賴性的JVM二進位文件或JVM庫,請確保使用R8或ProGuard之類的工具刪除未使用的代碼路徑,或者使用Graal本機映像生成最小的本機二進位文件。 該工具保留為Java字節碼,因此單個.jar可以在多個平臺上使用。

GitHub上提供了dependency-tree-diff的完整原始碼和構建設置。

相關焦點

  • Kotlin 的 99 個練習題(1)
    示例penultimate(listOf(1, 1, 2, 3, 5, 8))// output:// 5解決代碼:package org.kotlin99.listsimport com.natpryce.hamkrest.assertion.assertThatimport
  • 關於Linux常用的二進位文件分析方法
    當你在unix下拿到一個二進位文件但不知道它是什麼的時候,可以通過以下方法得到一此提示本文引用地址:http://www.eepw.com.cn/article/148514.htm1、 最首先應該嘗試strings命令,比如拿到一個叫cr1的二進位文件,可以:$ strings cr1 | more裡面可能會有一些對於這個cr1的描述,這些信息都是編譯之後在程序中留下的一些文本性的說明
  • VBA代碼解決方案第129講:順序文件,隨機文件,二進位文件
    之後我再繼續講解VBA處理文件的方法。一 計算機文件的分類:文件的分類方式有很多種,對於我們做程序來說,只是關心的是文件的讀寫方式,按照文件的讀寫方式的不同,我們可以把計算機的文件分成順序文件,隨機文件和二進位文件。1 順序文件 是指可以按儲存相同的順序找回數據的文件。
  • 高通公司發布適用於ICS的Adreno2xxGPU二進位文件
    相當長一段時間以來,我們已經知道,基於高通公司老化的QSD8250 / 8650的設備(也稱為原始的Snapdragon)將不會以正式身份接受冰淇淋三明治。這意味著不到兩年前發布的以前的旗艦設備將不具備正式升級ICS的資格。這是我們許多人拒絕接受的東西, 包括我自己在內。更糟糕的是,這意味著最能體現Nexus名稱的設備Nexus One不會獲得Google的最美味待遇。
  • 利用Hadoop Streaming處理二進位格式文件
    然而,隨著Hadoop應用越來越廣泛,用戶希望Hadoop Streaming不局限在處理文本數據上,而是具備更加強大的功能,包括能夠處理二進位數據;能夠支持多語言編寫Combiner等組件。隨著Hadoop 2.x的發布,這些功能已經基本上得到了完整的實現,本文將介紹如何使用Hadoop Streaming處理二進位格式的文件,包括SequenceFile,HFile等。
  • Go二進位文件逆向分析從基礎到進階——綜述
    然而,Go 語言的編譯工具鏈會全靜態連結構建二進位文件,把標準庫函數和第三方 package 全部做了靜態編譯,再加上 Go 二進位文件中還打包進去了 runtime 和 GC(Garbage Collection,垃圾回收) 模塊代碼,所以即使做了 strip 處理( go build -ldflags &34; ),生成的二進位文件體積仍然很大。
  • 谷歌宣布二進位文件對比工具 BinDiff 開源
    谷歌上周五宣布BinDiff開源——這是給安全研究人員用於進行二進位文件分析和對比的工具。
  • 開發二進位文件靜態快速分析工具(續)
    本文轉載自【微信公眾號:MicroPest,ID:gh_696c36c5382b】,經微信公眾號授權轉載,如需轉載與原文作者聯繫幾個月前,寫了這個「二進位文件靜態快速分析工具」,框架已經搭建完成,一直在添磚加瓦之中,加入了一些功能,讓一鍵式成為靜態分析的「傻瓜式」自動化操作,還是給大家一睹為快吧
  • 您需要做的第一件事是為您的特定作業系統下載ADB二進位文件
    要求塔斯克(2.99美元)SecureTask(免費)儘管我在這裡使用Tasker,但是您可以自由選擇使用任何其他自動化應用程式。Tasker是迄今為止最受歡迎的工具,也是人們最熟悉的工具,所以這就是我正在使用的工具。SecureTask是Tasker的插件,可以更改我們正在尋找的設置,但只有在獲得正確的權限後才能進行設置。
  • 關於二進位、十進位、八進位、十六進位數據轉換計算方法詳細總結
    下面舉例: 例:將十進位的168轉換為二進位 得出結果 將十進位的168轉換為二進位,(10101000)2 分析:第一步,將168除以2,商84,餘數為0。 第二步,將商84除以2,商42餘數為0。 第三步,將商42除以2,商21餘數為0。 第四步,將商21除以2,商10餘數為1。 第五步,將商10除以2,商5餘數為0。 第六步,將商5除以2,商2餘數為1。
  • 超乾貨詳解:kotlin(4) java轉kotlin潛規則
    而且為了快速轉型,可能會直接把java類轉成kotlin類,而這個過程中,涉及到java和kotlin的交互,往往會磕磕碰碰,以下總結了一部分 java kotlin交互方面的問題.  Kotlin文件和類不存在一對一關係  kotlin的文件,可以和類名一致,也可以不一致。這種特性,和c++有點像,畢竟c++的.h 和 .cpp文件是分開的。
  • Java實現訂單數據導出自定義文件格式二進位數據
    背景現有系統中已經有每天定時備份當日成交訂單數據到資料庫,但是為了數據更安全一些,會單獨備份到文件系統上一份,這樣可以把2份數據互為備份,如果一側發現數據丟失,可以去另外一份中去查找丟失的數據。實現由於訂單數據量比較大,最多的時候每天會有幾百幾千萬條數據,如果把訂單數據以字符串形式存儲為文本文件的話, 會佔用很多的存儲空間。為了節省磁碟空間, 寫成二進位數據文件會節省部分空間。
  • 從圖片裁剪來聊聊前端二進位
    最開始的一個小需求前兩天項目中有個小需求:前端下載後臺小哥返回的二進位流文件。,結果用 ArrayBuffer 對象表示readAsBinaryString異步按字節讀取文件內容,結果為文件的二進位串readAsDataURL異步讀取文件內容,結果用 data:url 的字符串形式表示readAsText異步按字符讀取文件內容,結果用字符串形式表示事件事件名描述
  • JAVA-二進位基礎
    進位的權重:一個數值,在每一位都有一個權重,權重為從右向左數,位數-1進行轉化: 舉例:將二進位011轉10進位為:從右向左,1*2的0次方+1*2的1一次方+0*2的2次方=3; 將八進位34轉化成10進位:4*8的0次方+3*8的1次方=282、十進位轉非十進位: 採用短除法: 用十進位數除以要轉化的進位數,用本次除法的商繼續進行除以要轉化的進位數的除法運算
  • MySQL錯誤日誌和二進位日誌
    本文介紹MySQL錯誤日誌和二進位日誌。MySQL日誌有5種,對於運維人員,最常用到的日誌有3種,分別是錯誤日誌、二進位日誌和慢日誌錯誤日誌(log_error)錯誤日誌通常用於記錄資料庫服務端啟動、重啟、主從複製時,記錄錯誤,將日誌詳情保留在文件中,方便DBA、運維開發人員閱讀。
  • PHP解析二進位文件,就靠這倆老舊的函數,我真的
    引言PHP幾乎很少處理二進位文件。但是便宜也完整的保留了這個功能。當你需要的時候,PHP自帶的pack() & unpack()能能夠極大地提供便利。下面我們從一個編程問題開始,討論二進位文件的操作。
  • Node與二進位不可說的緣分
    二進位數據「二進位是計算技術中廣泛採用的一種數制。二進位數據是用0和1兩個數碼來表示的數。它的基數為2,進位規則是「逢二進一」,借位規則是「借一當二」,由18世紀德國數理哲學大師萊布尼茲發現。—— 百度百科二進位數據就像上圖一樣,由0和1來存儲數據。
  • OkHttp 4 正式版發布,從 Java 切換到 Kotlin
    就像官方介紹的,「此版本改變了一切,又沒什麼改變」,我們此前在 OkHttp 4 的 RC 3 版本更新中已經報導過,OkHttp 4.x 將實現語言從 Java 切換到了 Kotlin,用等效的 .kt 替換了 25K 行的
  • linux中ELF二進位文件解析----ELF文件實例解析
    ELF headerELF為二進位文件,不能通過一般的方式來打開,在linux環境下可以使用xxd命令,xxd命令主要用來查看文件的十六進位格式,也可以查看二進位文件;在Windows環境下,使用Notepad++等文本編輯工具就可以打開查看;$ xxd -u -a -g 1 -s 0 -l 0x100 kernel.bin0000000: 7F 45 4C 46 01 02 01 00 00 00 00 00 00 00 00 00 .ELF............0000010: 00 02 00 14
  • python進位轉換:十進位轉二進位的用法
    我們在學習python時候肯定會碰到關於進位轉換,其實這是非常簡單的,這個就像小學學習數學乘法口訣意義,只要記住轉換口訣即可輕鬆應用,一起來看下具體的操作內容吧~一、python進位轉換dec(十進位)—> bin(二進位)dec(十進位)—> oct(八進位)dec(十進位)—> hex(十六進位)二、十進位我們所熟知的十進位,其實是從 0 開始,數到 9 之後,就跳到