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包大小优化的多一种方式》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

Practical Algorithms for Programmers

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》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器