Android包大小优化的多一种方式

栏目: IOS · Android · 发布时间: 5年前

内容简介:很多时候,需要对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名称):
Android包大小优化的多一种方式
  • 在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启动时,
Android包大小优化的多一种方式

后记

  • 对于必须要即时加载的本地库文件不能进行优化
  • app启动后,并不会每次安装都会解压,如果本地已经解压过,不会重新解压
  • lzma压缩方式比zip压缩方式压缩率更高,可以获得更好的文件大小优化体验,压缩概况
  • github链接: github.com/HangmanFu/S…

以上所述就是小编给大家介绍的《Android包大小优化的多一种方式》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

垃圾回收的算法与实现

垃圾回收的算法与实现

中村成洋、相川光 / 丁灵 / 人民邮电出版社 / 2016-7-1 / 99.00元

★ Ruby之父Matz作推荐语:上古传承的魔法,彻底揭开垃圾回收的秘密! ★ 日本天才程序员兼Lisp黑客竹内郁雄审校 本书前半介绍基本GC算法,包括标记-清除GC、引用计数、复制算法的GC、串行GC的算法、并发GC的算法等。后半介绍V8、Rubinius、Dalvik、CPython等几种具体GC的实现。本书适合各领域程序员阅读。一起来看看 《垃圾回收的算法与实现》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具