Matrix ApkChecker实际使用之APK瘦身

本文基于Tencent Matrix ApkChecker做得无用资源检测及图片大小检测。对于ApkChecker 的使用参考https://www.jianshu.com/p/0d18ed263db6,ApkChecker具体详见https://github.com/Tencent/matrix/wiki/Matrix-Android-ApkChecker

对于APK的大小控制,我主要是在删除无用资源、大图片压缩(png转webp)、构建脚本中开启代码、资源压缩、针对不同ABI打包四个方面进行。结果将75M大小的减到37-44M(针对不同的ABI, 打成了不同的包),当然还有进一步缩小的空间,这里谈一下缩减的整个过程及所碰到的问题,简单做个记录。

ApkChecker的检测结果分两种,HTML网页形式的和json文件形式的。文中使用HTML文件查看检测结果。检测结果会以下图这种taskDescription(任务描述)和result(结果展示)的组合来显示。


apk解压各种文件大小统计.png
删除无用资源

资源以drawable、string为主,同时会检测冗余文件,无用assets文件。


没用的resources.png

冗余文件.png

没用的assets文件.png

所以,根据检测结果,对无用或者冗余的文件进行相应的删除就行。当然在删除时最好在项目中确认一下,我基本都是每一项都仔细去在项目中查一下,操作下来基本上没有误检测的情况。但是有不少文件在项目中查找不到,不知是ApkChecker的BUG,还是别的什么问题,这个问题我会继续寻找答案。

png转webp

运行ApkChecker对apk进行检测时,配置的.json文件中包含有对超过限定大小文件的检测,

{
      "name":"-fileSize",
      "--min":"10",
      "--order":"desc",
      "--suffix":"png, jpg, jpeg, gif, arsc"
    },

上面的配置块截自官方给出的.json文件,其中min键对应的值10,其单位为kb,也就是在检测apk时会将超过10kb的文件记录到检测文件中。如下图显示,


超过限定大小的文件.png

对于超过限定大小的图片需要将其格式从png转到webp,webp格式本文不多做介绍,感兴趣的可以自行查资料。png转webp的操作很简单,直接在AndroidStudio中就可以操作,具体直接选中png图片,右键,选择“png convert to webp”即可。


png转webp.png
开启代码压缩,资源压缩

开启代码资源压缩后,那么在打包过程中没有使用的代码和资源就不会被打入包中,所以,这也是一项APK瘦身的方式。

资源压缩只与代码压缩协同工作。代码压缩器移除所有未使用的代码后,资源压缩器便可确定应用仍然使用的资源。这在您添加包含资源的代码库时体现得尤为明显 - 您必须移除未使用的库代码,使库资源变为未引用资源,才能通过资源压缩器将它们移除。

代码压缩、资源压缩涉及到混淆,本文也不对混淆做深入解读,

除了 minifyEnabled 属性外,还有用于定义 ProGuard 规则的 proguardFiles 属性:

  • getDefaultProguardFile('proguard-android.txt') 方法可从 Android SDK tools/proguard/ 文件夹获取默认的 ProGuard 设置。
  • proguard-rules.pro 文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle 文件旁)。

以上引自官方文档。https://developer.android.com/studio/build/shrink-code#shrink-resources

buildTypes {
        debug {
            ...
        }

        release {
            //开启代码压缩
            minifyEnabled true
            //资源压缩
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro', 'proguard-alibaba-fastjson.pro', 'proguard-map4parser.pro'
        }
    }

注意到proguardFiles方法中我传入了四个pro文件,这是因为引入的第三方包在混淆时有一定的规范,这个一般在第三方包的profile部分有说明。

当然,在设置代码资源压缩时并没有这么顺利。起初,release块中是默认代码,其中,proguardFiles方法传入通常的两个参数。

buildTypes {
        debug {
            ...
        }

        release {
            //开启代码压缩
            minifyEnabled true
            //资源压缩
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }

但是在构建过程中出错,Message View中提示出现warnings,

org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':Android:transformClassesAndResourcesWithProguardForProdRelease'.
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:100)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:70)
    at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:63)
    at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute(ResolveTaskOutputCachingStateExecuter.java:54)
    at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:58)
    at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:88)
    at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute(ResolveTaskArtifactStateTaskExecuter.java:52)
    at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
    at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:54)
    at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
    at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:34)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker$1.run(DefaultTaskGraphExecuter.java:248)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:197)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:107)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:241)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:230)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.processTask(DefaultTaskPlanExecutor.java:124)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.access$200(DefaultTaskPlanExecutor.java:80)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:105)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:99)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.execute(DefaultTaskExecutionPlan.java:625)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.executeWithTask(DefaultTaskExecutionPlan.java:580)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.run(DefaultTaskPlanExecutor.java:99)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.RuntimeException: Job failed, see logs for details
    at com.android.build.gradle.internal.transforms.ProGuardTransform.transform(ProGuardTransform.java:196)
    at com.android.build.gradle.internal.pipeline.TransformTask$2.call(TransformTask.java:222)
    at com.android.build.gradle.internal.pipeline.TransformTask$2.call(TransformTask.java:218)
    at com.android.builder.profile.ThreadRecorder.record(ThreadRecorder.java:102)
    at com.android.build.gradle.internal.pipeline.TransformTask.transform(TransformTask.java:213)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
    at org.gradle.api.internal.project.taskfactory.DefaultTaskClassInfoStore$IncrementalTaskAction.doExecute(DefaultTaskClassInfoStore.java:173)
    at org.gradle.api.internal.project.taskfactory.DefaultTaskClassInfoStore$StandardTaskAction.execute(DefaultTaskClassInfoStore.java:134)
    at org.gradle.api.internal.project.taskfactory.DefaultTaskClassInfoStore$StandardTaskAction.execute(DefaultTaskClassInfoStore.java:121)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$1.run(ExecuteActionsTaskExecuter.java:122)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:197)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:107)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:111)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:92)
    ... 30 more
Caused by: java.io.IOException: Please correct the above warnings first.
    at proguard.Initializer.execute(Initializer.java:473)
    at proguard.ProGuard.initialize(ProGuard.java:233)
    at proguard.ProGuard.execute(ProGuard.java:98)
    at com.android.build.gradle.internal.transforms.BaseProguardAction.runProguard(BaseProguardAction.java:61)
    at com.android.build.gradle.internal.transforms.ProGuardTransform.doMinification(ProGuardTransform.java:253)
    at com.android.build.gradle.internal.transforms.ProGuardTransform.access$000(ProGuardTransform.java:63)
    at com.android.build.gradle.internal.transforms.ProGuardTransform$1.run(ProGuardTransform.java:173)
    at com.android.builder.tasks.Job.runTask(Job.java:47)
    at com.android.build.gradle.tasks.SimpleWorkQueue$EmptyThreadContext.runTask(SimpleWorkQueue.java:41)
    at com.android.builder.tasks.WorkQueue.run(WorkQueue.java:259)
    ... 1 more
提示出现Warning.png

具体Warning提示.png

上图是我在打release包时没有对fastjson包做相应的混淆处理(具体就是在build.gradle中混淆文件中没有fastjson的混淆处理)。当然这只是我举的一个例子,在构建过程中还出现了其他第三方包的混淆问题,只要从网上查找相应包的混淆设置就行。

针对不同ABI打包
so包几乎占了一半.png

这是我将无用冗余资源删除之后又检测了一次的截图,可以看到so包几乎占用了一半的大小。如果能对so包做个什么处理,瘦身效果肯定是杠杠的。而且,在ApkChecker的介绍中我注意到了这段话。

检查是否包含多个ABI版本的动态库。so文件的大小可能会在apk文件大小中占很大的比例,可以考虑在apk中只包含一个ABI版本的动态库

所以在查过资料以后,在build.gradle文件中添加如下代码,

import com.android.build.OutputFile
ext.versionCodes = ['armeabi-v7a': 1, 'armeabi': 2, 'arm64-v8a':3, 'x86':4, 'x86_64':5]
android{
     ...
     splits {
        // Configures multiple APKs based on ABI.
        abi {
            // Enables building multiple APKs per ABI.
            enable true
            // By default all ABIs are included, so use reset() and include to specify that we only
            // want APKs for x86 and x86_64.
            // Resets the list of ABIs that Gradle should create APKs for to none.
            reset()
            // Specifies a list of ABIs that Gradle should create APKs for.
            include  "arm64-v8a","armeabi","armeabi-v7a","x86", "x86_64"
            // Specifies that we do not want to also generate a universal APK that includes all ABIs.
            universalApk false
        }
    }
    ...
}

但是构建过程中报了如下异常,

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':Android:packageDevDebug'.
> java.io.IOException: Failed to delete  路径\app.apk
或者java.io.IOException: Failed to create 路径\app.apk

我注意到,之前已经在android块中调用了AppExtension#getApplicationVariants,该方法返回了一个ApplicationVariant类型的DomainObjectSet集合,且遍历了该集合,将输出的apk根据不同的buildType、productFlavor、versionCode进行了命名。

android.applicationVariants.all { variant ->
        variant.outputs.all { output ->
            def file = output.outputFile
            if (file != null && file.name.endsWith('.apk')) {
                def buildType = variant.buildType.getName()
                def flavorName = variant.getFlavorName()
                def versionCode = defaultConfig.versionCode
                output.outputFile = new File(file.parent, "tower-${flavorName}-${buildType}.${versionCode}.apk")
            }
        }
    }

如果针对不同abi打包,这里输出的apk名字肯定会发生冲突。所以,我觉得很可能是这个问题造成。所以在输出apk的名字中加入了abi,具体如下,

//applicationVariants 输出文件名要针对不同的ABI做出区分
android.applicationVariants.all { variant ->
        variant.outputs.all { output ->
            def file = output.outputFile
            if (file != null && file.name.endsWith('.apk')) {
                def buildType = variant.buildType.getName()
                def flavorName = variant.getFlavorName()
                def versionCode = defaultConfig.versionCode
                output.versionCodeOverride = project.ext.versionCodes.get(output.getFilter(OutputFile.ABI)) * 1000000 + versionCode
                outputFileName = "app-${flavorName}-${buildType}.${output.versionCodeOverride}.apk"
            }
        }
    }

再次build, 成功输出不同abi包的apk,说明之前猜想是对的。针对不同的abi打包得到的apk大小瘦身明显,范围在37-44M之间,最好的缩减了近乎一半,最差也缩减了30M+。


release版不同ABI打包APK大小.png
后话
  1. ApkChecker中还包括了对so包的其他检测,诸如checkMultiSTL 检查是否有多个动态库静态链接了STL;unstrippedSo 发现apk中未经裁剪的动态库文件等。因为我对NDK这块了解还不多,所以这块的检测先略过,等我更为深入的了解这块内容后在做处理。
  2. ApkChecker中在文件冗余及无用资源检测中会出现对项目引入的第三方包(更多的表现在aar包)在构建过程中解压的文件(resource、assets)检测的结果,这些文件的路径通常在app\build\intermediates相应的目录中,但是我想作为开发者是控制不了第三方包的这些资源文件的,那ApkChecker检测这些有什么用呢?
参考资料
  1. https://developer.android.com/studio/build/shrink-code#shrink-resources
  2. 《Gradle for Android》
  3. 《Gradle 实战》
  4. https://github.com/Tencent/matrix/wiki/Matrix-Android-ApkChecker

你可能感兴趣的:(Matrix ApkChecker实际使用之APK瘦身)