Android打包优化

引言

在做APP项目管理的时候,经常会碰到打包,说到打包,就看人们什么理解,所谓外行看热闹,内行看门道。外行的人会认为,无非就是Clean Project->Generate Signed APK...。但真要做好这个打包工作,一点也不简单,一个好的安装包会影响APP的性能,同时应具备以下特点(前提是不影响功能):

  • 较小的安装包
  • 高效的安装包

一般每个人的处理方式会有所不同,也没有谁对谁错的说法,但一切可以用项目来体现。整理本文的目的是为了避开重复造轮子,在保证项目顺利发布的前提下,还能有点工程质量(含代码、配置)。

APK结构及工程目录

知己知彼,方能百战不殆。了解应用程序APK的结构及工程目录对于开发者来说是非常有必要的。

APK结构

APK文件由一个ZIP存档组成,其中包含组成应用程序的所有文件。这些文件包括Java类文件,资源文件和包含编译资源的文件,可以看下两个APK压缩包中文件目录:
简单的APK目录:
简单的APK
复杂的APK目录:

不管是简单的还是复杂的,APK包含以下目录(不仅限于):

  • META-INF:包含CERT.SF和 CERT.RSA签名文件以及MANIFEST.MF 清单文件。
  • assets:包含应用可以使用AssetManager对象检索的应用资源。
  • res:包含未编译到的资源 resources.arsc。
  • lib:包含特定于处理器软件层的编译代码。该目录包含了每种平台的子目录,像armeabi,armeabi-v7a, arm64-v8a,x86,x86_64,和mips。
  • resources.arsc:包含已编译的资源。该文件包含res/values/ 文件夹所有配置中的XML内容。打包工具提取此XML内容,将其编译为二进制格式,并将内容归档。此内容包括语言字符串和样式,以及直接包含在resources.arsc文件中的内容路径 ,例如布局文件和图像。
  • classes.dex:包含以Dalvik/ART虚拟机可理解的DEX文件格式编译的类。
  • AndroidManifest.xml:包含核心Android清单文件。该文件列出应用程序的名称,版本,访问权限和引用的库文件。该文件使用Android的二进制XML格式。

工程目录

一个Android项目工程,它会有许许多多的文件夹和文件,全面认识每个文件夹和文件的作用有助于提高开发的效率。

Android AS工程目录有:

  • .gradle:gradle项目产生文件(自动编译工具产生的文件)
  • .idea:AS生成的工程配置文件,类似Eclipse的project.properties
  • app:创建工程中的一个Module,复用父项目的设置,可与父项目拥有相同的配置文件
  • build:自动构建时生成文件的地方
  • gradle:自动完成gradle环境支持文件夹,构建工具系统的jar和wrapper等,jar告诉了AS如何与系统安装的gradle构建联系
  • .gitignore:git源码管理文件
  • build.gradle:gradle项目自动编译的配置文件
  • gradle.properties:gradle运行环境配置文件
  • gradlew:自动完成gradle环境的linux mac脚本,配合gradle文件夹使用
  • gradlew.bat:自动完成gradle环境的windows脚本,配合gradle文件夹使用
  • local.properties:Android SDK、NDK环境路径配置
  • settings.gradle:gradle项目的子项目包含文件
  • External Libraries://不是一个文件夹,只是依赖lib文件,如SDK等。

跟任何事务一样,只有做到足够了解它,才能对它进行优化。

打包优化

res资源优化

使用一套资源,这是最基本的一条规则,但非常重要。

对于绝大对数APP来说,只需要取一套设计图就足够了。鉴于现在分辨率的趋势,建议取720P或1080P的资源,放到xhdpi目录。相对于多套资源,只使用720P的一套资源,在视觉上差别不大,很多大公司的产品也是如此,但却能显著的减少资源占用大小,顺便也能减轻设计师的出图工作量了。

注意,这里不是说把不是xhdpi的目录都删除,而是强调保留一套设计资源就够了。

res/raw和assets的相同点:
两者目录下的文件在打包后会原封不动的保存在apk包中,不会被编译成二进制。
res/raw和assets的不同点:
res/raw中的文件会被映射到R.java文件中,访问的时候直接使用资源ID即R.id.filename;assets文件夹下的文件不会被映射到R.java中,访问的时候需要AssetManager类。
res/raw不可以有目录结构,而assets则可以有目录结构,也就是assets目录下可以再建立文件夹。
针对不同的情况,对于资源文件有不同的优化策略。一般来讲,对于res/drawable-**ddpi中的png资源可以进行压缩。Assert文件夹经常会放置一些不被编译的资源,时间久了,里面可能一些文件或者资源已经不用了,然而这个文件夹也是会被打包到apk里面的。所以定期清理这个里面的内容也是减小apk体积的重要一步。

lib资源优化

有的工程,so文件占了很大的体积,如果你不加控制,所有的so都会打包到你的apk了,最后发现这些so文件尽然占了我们apk的近乎三分之一的体积。

目前主流的机型都是支持armeabi-v7a的,并且armeabi-v7a兼容armeabi。所以在一般的开发中我们只需要使用armeabi-v7a 进行ABI支持。 这里不排除有极少数设备会Crash,可能和不同的so有一定的关系,请大家务必测试周全后再发布。

与上述不同的是,x86包下的so在x86型号的手机是需要的,如果产品没用这方面的要求也可以精简。什么判断是不是需要,主要看CPU类型,手机一般采用ARM结构,PC一般采用X86(intel主导),但如今天的发展,也有在手上机采用X86,如intel CPU的手机或平板。为了降低风险,建议实际工作的配置是只保留armabi、x86下的so文件,算是一个折中的方案。

我们可以通过配置gradle来制定只打包某些so,依然是在defualtConfig中:

 defaultConfig {
        ......
        multiDexEnabled true
        ndk {
            //设置支持的SO库架构
            abiFilters 'armeabi', 'armeabi-v7a', 'x86'//, 'x86_64', 'arm64-v8a'
        }
    }

最后打包出来的apk可以减了不少。

开启混淆代码及去除去无用资源

 buildTypes {
        release {
            minifyEnabled true // 是否混淆
            shrinkResources true // 是否去除无效的资源文件
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            zipAlignEnabled true  //zipAlign优化
            ......
        }
        debug {
            ......
        }
    }

在gradle使用minifyEnabled进行Proguard混淆的配置,可大大减小APP大小。在proguard中,是否保留符号表对APP的大小是有显著的影响的,可酌情不保留,但是建议尽量保留用于调试。详细proguard的相关的配置和原理可自行查阅。

在gradle使用shrinkResources去除无用资源,效果也不错。

删除无用的语言资源

我们知道google给我们的apk提供了国际化支持,适应不同语言的字符串资源等等,但是在很多情况下我们只需要一些指定语言的资源就可以了,这个时候我们可以使用resConfigs方法来配置。比如国内应用只支持中、英文:

defaultConfig {
    // ...
    resConfigs "en", "zh-rCN"
}

删除不必要的module

AS的代码结构和eclipse完全不同,它为开发者提供了单工程多module的形式,但多建立一个module就需要多维护一个module,所以如果仅仅是为了方便写代码而建立一个module是不可取的。

Gradle在编译的时候会去检测module的依赖链,gradle会帮助我们层层梳理module之间的关系,避免因为module之间相互引用而来带的问题。这些梳理工作和module的合并工作都会带来build的时间,如果你的项目build十分缓慢,建议梳理下module的关系,合并部分module,并删除test module(AS默认在建立module的同时会建立test目录),因为大部人根本没有编写测试代码的打算,当然,如果你的module就是纯代码,根本没用到资源文件,也请一并把res目录删除掉,这些优化都可以加快build速度。

避免重复库

避免重复库看上去是理所当然的,但是秘密总是藏的很深,一定要当心你引用的第三方库又引用了哪个第三方库,这就很容易出现功能重复的库了,比如使用了两个图片加载库:Glide和Picasso。

通过查看exploded-aar目录和External Libraries或者反编译生成的APK,尽量避免重复库的大小,减小APP大小。

开发中引入大量的第三方开发库也是一个增加apk体积的重要原因,因为你把人家的代码和资源全给包含进来了。但是想想人家的代码,并不一定全要的,是否可以只引入人家的一部分代码,而不是在build.gradle中仅仅添加一行“compile”来全部依赖呢?答案是可以得!这里举一个例子

我们开发中有一个需求是将数据通过图标的方式显示出来,这里我们站在巨人的肩膀上,使用了MPAndroidChart这个开源项目,但是发现他们的东西太多了,我们仅仅需要使用其中一种chart,如果在build.gradle里面加一句:

dependencies {
    compile 'com.github.PhilJay:MPAndroidChart:v2.2.3'
    compile 'com.github.bumptech.glide:glide:3.7.0'
}

这要把他们的库全给引用过来了。想到他们是开源的,代码有,所以我们仅仅把他们的我们所用到的代码给剥离出来,单独打包了一个jar包引入到我们的项目里面,就OK了,减少了大量的无用依赖代码!

总结

从开发者层面上来讲,掌握了这些内容不仅打出一个优秀的安装包,还可以略显逼格满满。