Android 包大小优化实践

2,344 阅读8分钟

android减少包大小是非常必要的,在性能,转换率等等都有益处,而常用的包大小优化Google已经给出了一些方案,再加上市面上的一些美团方案,微信方案、抖音方案等等,下面就说一下我们在包大小优化做的努力。

1、使用AAB模式

google play现在强制所有上传的应用都使用aab,Google Play 会使用您的 AAB 针对每种设备配置生成并提供经过优化的 APK,因此只会下载特定设备所需的代码和资源来运行您的应用。假如一个AAB是90MB在google play上下载耗费的流量可能也就50MB,但是这种方案对性能上没有任何的影响只是减少了下载流量可能会增加一些转换率。具体文档可以参考官方文档。这里有必要说一下AAB还有更多又去的玩法比如使用AAB实现插件化(对模块拆分还是非常有帮助的),对不同地区实现不同的业务然后使用google play进行分发

2、使用AGP配置来减少包大小(链接

使用lint本地检测无用资源或者开启shrinkResources

使用lint本地检查无用资源

1、点击AS上的Analyze菜单按钮,选择Run Inspection by Name 如下图

image.png 2、会出现一个弹窗, 输入unused resources

image.png 3、会弹出“inspaction scope”选择窗口,选择检查的范围,一般选择整个项目或模块。“inspaction scope”窗口下面还可以设置文件过滤,选择好后点ok就开始检查了

image.png 4、下面的输出栏会输出没有用的资源文件。

image.png 5、删除无用资源

开启shrinkResources

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                'proguard-rules.pro'
        }
    }
}

此配置必须和代码压缩一起使用才有效果,如果说要保留某些资源,假如插件化里面宿主里面放了某个资源需要给很多个插件使用,这个时候就需要保留此资源那么就需要做如下配置: 在项目的res目录下新建一个创建一个包含 <resources> 标记的 XML 文件,tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。例如:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

缩减、混淆处理和代码优化功能

在AGP 3.4.0以上版本R8是默认编辑器,把代码编译成android所需要的dex文件,但是代码的缩减、混淆和代码优化是默认关闭的,建议在release版本应用将此功能打开可以有效的减少包体积。上面提到的shrinkResources也是必须和此功能一起使用才有效,关于R8更多的配置可以详见官网文档 如果要保留一些类就要在proguard-rules.pro 配置具体proguard 配置规则可以查看手册。 这里proguard特点说一下他是累加的,假如A moudle 依赖B moudle,那么到最后proguard 规则就是A moudle 的配置+B moudle的配置

移除无用so库

现在市面上的手机cpu指令基本上就两种了 armeabi-v7a 和 arm64-v8a,x86可以不考虑了很少有手机在用了,所以就可以通过gradle配置来只依赖v7a和v8a,例如:

android {
  ...
  defaultConfig {
    ...
   ndk {
      // Specifies the ABI configurations of your native
      // libraries Gradle should build and package with your APK.
      abiFilters 'armeabi-v7a''arm64-v8a'
    }
  }

}

但是这样还是会很大,可以减少arm64-v8a指令集,不管你的手机cpu是v7还是v8都可以运行v7的so。 如果说你的应用有多个变种,比如一个是上线到google play的aab一个是在国内上线的apk这样你可以使用不同变种依赖不同的指令集

android {
  ...
  defaultConfig {
    ...
    flavorDimensions "market"
    productFlavors {

        //上google play市场
        gp {
        dimension "market"
        ndk.abiFilters "armeabi-v7a", "arm64-v8a"
        }
    
        //中国版本
        cn {
            dimension "market"
            ndk.abiFilters "armeabi-v7a"}
    }
} 

3、美团进阶方案

美团的这种方案主要是提到了zip压缩。dex优化,R Field的优化,我主要是用了R Field的优化。主要说下这个R field的优化这个点。这篇文章写的比较早用的是java代码插桩的方式进行处理的,现在其实AGP就可以完成一样的处理。 先说下原理为什么R文件会导致包大小变大,假如你的项目结构如下:

image.png

lib1_R= lib1_R

lib2_R= lib2_R

lib3_R= lib3_R

biz1_R = lib1_R + lib2_R + lib3_R + (自己的R)

biz2_R = lib2_R + lib3_R + (自己的R)

app_R = lib1_R + lib2_R + lib3_R + biz1_R + biz2_R + (自己的R)

app_R因为是final的所以如果开启java优化也就是混淆会被shrink掉,但是其他moudle的R不是final而且引用也不是直接使用id的值来引用的:

这是app moudle 下的MainActivity中setcontent对应的字节码:

WeChata234b76e355e3899a7be1119de8a8880.png

这是子moudle Activity中同样setContent对应的字节码:

WeChat71e04d6bc8f6153355d695db68318246.png

发现什么不同了吗?就是子moudle中是R的变量引用而非常量,在打包过程中aapt会将这个变量统一赋值来防止id冲突,而如果你的项目特别复杂子moudle特别多的话那么各个R类的就会特别大,但是这部分是必须的吗?好像有方法来解决

R类内联解决方法一:

就好像美团方案里面这种方法利用插桩在aapt分配了id后将对应的R变量的引用修改成对应的id值这样就可以把原有的R类删除掉(这个方案中的插桩插件是自己写的其实市面上有很多插桩三方库,字节的bytex,滴滴的Booster这些都可以直接使用)

R类内联解决方法二:

升级AGP版本到4.1以上 image.png

这是AGP 4.1版本的升级说明截图,他帮助咱们做了上面美团方案的插桩替换的一系列动作,R类内联是非常有必要的我们的app做了R类的内联以后apk大小减少了百分之十。这部分收益必须是在R类没有被混淆的时候keep住的前提下才可以,如果keep住了R类这部分收益就没办法了,可以在主moudle的proguard-rules.-printconfiguration "build/outputs/mapping/configuration.txt"(其实此文件的路径随便写都可以)来查看是不是添加了keep R类。如果是自己工程里面添加了keep R类就直接删了就好但是如果是三方的aar怎么办呢?

    tasks.each {
        if (it.name.startsWith("mini") && it.name.endsWith("R8")) {
            def f = it.configurationFiles.filter { File ff ->
                if (ff.exists()) {
                    !ff.text.contains("-keep public class **.R\$*")
                } else {
                    false
                }
            }
            it.configurationFiles.setFrom(f.files)
        }
    }
}

可以在app moudle 下的build.gradle中添加如下代码,会自动的将keep R类排除在外

4、资源压缩

在apk中res的资源占了很大的一部分,这部分如果可以被减少那么对减少apk size也有很大的收益,在android中主要的资源是图片,图片有几种格式jpg,png,webp同一个图片应该webp是最小的,所以可以将图片从png转成webp这样apk size不就变小了。如果是一两张图片还好可以在线转然后直接丢到工程里面但是如果所有的图片都要转呢?

抖音团队给出了一个无入侵的解决方案,但是我并没有完全使用他这种方案因为需要hook如果说android系统版本更新hook点就要非常小心,搞过插件化的都知道这是永远的痛,但是有什么更好的方法吗?其实是有的

需要先明确个概念就是android中所有的res资源都是会合并的,但是如果在不同的moudle包含了同样的res怎么办呢?答案是合并,合并的规则为参考官网总而言之就是主moudle会优先依赖的moudle,根据这个特点是不是可以将所有的图片都转成webp到主moudle的res目录中呢?

在我们的项目中使用了zoom通过apk大小分析看到zoom相关的图片是最大的所以这里就拿zoom为例。

先讲下操作思路,自定义gradle插件,添加png转webp task将png转为webp,我这里用的是webp官方的转换工具libwebp,其实还有其他工具。

下个问题就是怎么获取当前工程中全部的资源呢?答案是通过AGP 中android 对象下的 ApplicationVariant中的 getAllRawAndroidResources()此方法可以把当前变种所有的资源都获取到,具体代码如下:

Set<File> getAllResPath(Project project) {
    def extension = project.getExtensions().getByName("android")
    Set<File> allRes = new HashSet<>()
    if (project.getPlugins().hasPlugin("com.android.application")) {
        extension.applicationVariants.each {
            allRes.addAll(it.allRawAndroidResources)
        }
        return allRes
    }
    return null
}

获取到所有的资源以后就可以一层层的遍历找到非webp的图片然后使用libwebp转为webp

5、插件化

插件化是一个非常好的减少包大小的方式,将一些无关紧要不常用的moudle改成插件,然后发布的时候只发布宿主,到用户使用到对应的模块时候下载对应的插件,市面上有很多插件化方案,大家可以对比选用,这个我们也在进行中ing。

参考链接:

美团方案

网易大前端团队实践

抖音瘦身实践