美文网首页
Matrix ApkChecker实际使用之APK瘦身

Matrix ApkChecker实际使用之APK瘦身

作者: MIAN勉 | 来源:发表于2019-05-24 22:19 被阅读0次

    本文基于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瘦身

          本文链接:https://www.haomeiwen.com/subject/mebuzqtx.html