Android包大小優化的多一種方式
很多時候,需要對android apk包大小進行優化,目前幾種常見的方式如下:
- 混淆優化
- android lint檢查無用資源
- 壓縮工具壓縮資源圖片
- 資源圖片去重
- 使用webp、向量圖等
- 資源混淆
本次要討論的不是以上資源優化等方式,而是對於apk中常用到本地類庫(so)進行壓縮,達成優化包大小的目的。不過這裡也有一個前提, 能夠優化的so是能夠延遲載入的,即不是必須app啟動時就要即時載入的 。
實現思路
- 干預gradle apk打包流程,在gradle merge本地庫之後,package apk之前將原檔案進行壓縮,生成壓縮檔案並儲存到assets目錄之下。
- task的執行順序(Develop為productFlavor名稱):

- 在app啟動時,解壓assets目錄下的壓縮檔案,反射classloader,加入解壓後的本地庫路徑
使用方式
- 在build.gradle的dependencies中加入
classpath 'com.hangman.plugin:nativelibcompressionplugin:1.1.5' 複製程式碼
- 主module的gradle.gradle中應用外掛
apply plugin: 'nativelibcompressionplugin' 複製程式碼
- 定義extension
soCompressConfig { // tarFileNameArray定義了需要打包壓縮的本地庫檔案列表,這些檔案會被打包成一個tar後再進行壓縮 tarFileNameArray = ['test1.so', 'test2.so', 'test3.so'] // compressFileNameArray 需要壓縮本地庫檔案檔名 compressFileNameArray = ['test4.so', 'test5.so'] // optinal屬性 是否列印整個過程的日誌, 預設false printLog = true // optional屬性 本地庫filter,預設armeabi-v7a abiFilters = ['armeabi-v7a'] // optional屬性 壓縮演算法,apache commons compress支援的演算法,預設為lzma algorithm = 'lzma' // optional屬性 debug包時是否執行本工具,預設為false debugModeEnable = false // optional屬性,壓縮過程中是否對檔案進行校驗,預設為true verify = true } 複製程式碼
- 執行時解壓與反射庫 在主module的build.gradle中加入
implementation 'com.hangman.library:NativeLibDecompression:1.1.7' 複製程式碼
在Application的onCreate方法中解壓
val nativeLibDecompression = NativeLibDecompression(context!!, DEFAULT_ALGORITHM, true) nativeLibDecompression.decompression(false, object : SpInterface { override fun saveString(key: String, value: String) { // 解壓完成後儲存檔名與MD5 globalSp.putString(key, value) } override fun getString(key: String): String { return globalSp.getString(key) } }, object : LogInterface { override fun logE(tag: String, message: String) { // 列印日誌 Log.e(TAG, message) } override fun logV(tag: String, message: String) { Log.e(TAG, message) } }, object : DecompressionCallback { // result 是否成功hadDecompressed 是否進行過解壓操作 override fun decompression(result: Boolean, hadDecompressed: Boolean) { Log.e(TAG, "decompression result = $resulthadDecompressed = $hadDecompressed") } }) 複製程式碼
實現程式碼
- SoCompressPlugin 自定義gradle plugin 建立task,task主要工作是對配置的本地庫檔案進行壓縮,生成壓縮檔案儲存到assets目錄下。
@Override void apply(Project project) { noteApply() def extension = project.extensions.create('soCompressConfig', SoCompressConfig) project.afterEvaluate { project.android.applicationVariants.all { variant -> addTaskDependencies(project, variant.name, extension) } project.gradle.taskGraph.addTaskExecutionListener(new TaskExecutionListener() { def time = 0 @Override void beforeExecute(Task task) { time = System.currentTimeMillis() } @Override void afterExecute(Task task, TaskState taskState){ if (task instanceof SoCompressTask) { def map = task.infoMap def compressTotalTime = 0 def uncompressTotalTime = 0 if (!map.isEmpty()) { map.each { compressTotalTime +=it.value.compressTime uncompressTotalTime +=it.value.uncompressTime } } println "task ${task.name} cost ${System.currentTimeMillis() - time}[compress cost ${compressTotalTime} , uncompress cost ${uncompressTotalTime}]"} } }) } } 複製程式碼
在apply方法中,主要是新增自定義task,並記錄其執行時間
def addTaskDependencies(Project project, String variantName, SoCompressConfig extension) { def uppercaseFirstLetterName = uppercaseFirstLetter(variantName) def preTask = project.tasks.getByName("transformNativeLibsWithMergeJniLibsFor${uppercaseFirstLetterName}") def followTask = project.tasks.getByName("package${uppercaseFirstLetterName}") def printLog = extension.printLog def debugModeEnable = extension.debugModeEnable def abiFilters = extension.abiFilters if (preTask == null || followTask == null) { return } if (debugModeEnable || (!variantName.endsWith('Debug') && !variantName.endsWith('debug'))) { if (printLog) { println "add task for variant $variantName" } //def abiFilters = project.android.defaultConfig.ndk.abiFilters //if (printLog) { //println "abiFilters =$abiFilters" //} SoCompressTask task = project.tasks.create("soCompressFor$uppercaseFirstLetterName", SoCompressTask) { abiFilterSet = abiFilters taskVariantName = variantName config = extension inputFileDir = preTask.outputs.files.files outputFileDir = followTask.inputs.files.files } task.dependsOn preTask if (printLog) { println '===========================================' println "${task.name} dependsOn ${preTask.name}" } followTask.dependsOn task if (printLog) { println "${followTask.name} dependsOn ${task.name}" println '===========================================' } } } 複製程式碼
初始化自定義task,傳入自定義extension配置項。在自定義配置項時,由於能夠直接通過程式碼讀取,本來沒有打算把abiFilter當做一個可配置項,同時由於gradle的靈活性,可以在較多地方定義abiFilter,會導致程式碼的過多的冗餘,所以直接將abiFilter用配置項來處理,簡化過程,預設值是armeabi-v7a
- soCompressTask 自定義的gradle task,主要操作task的輸入輸出目錄,對配置項中的本地庫檔案進行檢查,去重,並壓縮生成新檔案
@TaskAction void taskAction() { def printLog = config.printLog if (printLog) { println "current variant name is ${taskVariantName}" } if (inputFileDir == null || outputFileDir == null) { if (printLog) { print """|inputFileDir $inputFileDir |outputFileDir $outputFileDir""".stripMargin() } return } if (printLog) { println "taskName ${this.name}" println "$config" } if (!SUPPORT_ALGORITHM.contains(config.algorithm)) { throw new IllegalArgumentException("only support one of ${Arrays.asList(SUPPORT_ALGORITHM).toString()}") } def gradleVersion = 0 project.rootProject.buildscript.configurations.classpath.resolvedConfiguration.resolvedArtifacts.each { if (it.name == 'gradle') { gradleVersion = it.moduleVersion.id.version.replace('.', '').toInteger() } } // 找到輸入輸出目錄 def libInputFileDir = null def libOutputFileDir = null inputFileDir.each { file -> if (printLog) { println "inputFileDir ${file.getAbsolutePath()}" } if (file.getAbsolutePath().contains('transforms/mergeJniLibs')) { libInputFileDir = file } } outputFileDir.forEach { file -> if (printLog) { println "outputFileDir ${file.getAbsolutePath()}" } if (gradleVersion >= 320 && file.getAbsolutePath().contains('intermediates/merged_assets')) { libOutputFileDir = file } else if (gradleVersion < 320 && file.getAbsolutePath().contains('intermediates/assets')) { libOutputFileDir = file } } if (libInputFileDir == null) { throw new IllegalStateException('libInputFileDir is null') } if (libOutputFileDir == null) { throw new IllegalStateException('libOutputFileDir is null') } if (printLog) { println "libInputFileDir ${libInputFileDir}" println "libOutputFileDir ${libOutputFileDir}" } String[] tarFileArray = config.tarFileNameArray String[] compressFileArray = config.compressFileNameArray tarFileArray.each { fileName -> if (compressFileArray.contains(fileName)) { throw new IllegalArgumentException("${fileName} both in tarFileNameArray & compressFileNameArray") } } def soCompressDir = new File(libOutputFileDir, CompressConstant.SO_COMPRESSED) soCompressDir.deleteDir() if (tarFileArray.length != 0) { tarFileArray.sort() compressTar(tarFileArray, libInputFileDir, libOutputFileDir, printLog) } if (compressFileArray.length != 0) { compressFileArray.sort() compressSoFileArray(compressFileArray, libInputFileDir, libOutputFileDir, printLog) } } 複製程式碼
- 壓縮與解壓 主要用到了 Apache Commons Compress™ ,相關邏輯可以看程式碼。解壓主要發生在app啟動時,

後記
- 對於必須要即時載入的本地庫檔案不能進行優化
- app啟動後,並不會每次安裝都會解壓,如果本地已經解壓過,不會重新解壓
- lzma壓縮方式比zip壓縮方式壓縮率更高,可以獲得更好的檔案大小優化體驗,壓縮概況
- github連結: github.com/HangmanFu/S…