内容简介:很多时候,需要对android apk包大小进行优化,目前几种常见的方式如下:本次要讨论的不是以上资源优化等方式,而是对于apk中常用到本地类库(so)进行压缩,达成优化包大小的目的。不过这里也有一个前提,在Application的onCreate方法中解压
很多时候,需要对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 = $result hadDecompressed = $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…
以上所述就是小编给大家介绍的《Android包大小优化的多一种方式》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- ReactNative字体大小不随系统字体大小变化而变化
- JVM 参数最佳实践:元空间的初始大小和最大大小
- 原 荐 java计算对象占用内存大小:lucene专用于计算堆内存占用大小的工具类
- 获取网络图片的大小
- 获取网络图片的大小
- 减小APK大小
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Practical Algorithms for Programmers
Andrew Binstock、John Rex / Addison-Wesley Professional / 1995-06-29 / USD 39.99
Most algorithm books today are either academic textbooks or rehashes of the same tired set of algorithms. Practical Algorithms for Programmers is the first book to give complete code implementations o......一起来看看 《Practical Algorithms for Programmers》 这本书的介绍吧!