深入探索Android应用瘦身优化

目录

写在前面

一、瘦身优化及Apk分析方案

1.1、为什么要做Apk瘦身优化

1.2、Apk组成

1.3、Apk分析

二、代码瘦身

2.1、代码混淆

2.2、三方库处理

2.3、移除无用代码

三、资源瘦身

3.1、冗余资源

3.2、图片压缩

3.3、资源混淆

3.4、其它方案

四、So瘦身

4.1、So移除

4.2、更优方案

4.3、其它方案


写在前面

各位兄嘚早上好啊,愿各位都能元气满满开启新的一天!最近有朋友跟我聊说是压力太大了,说句实在话,成年人的世界有谁没有压力呢,马爸爸每个月挣十个亿都有压力,像咱这三五千的还说啥呢?😂化压力为动力吧,每天进步一点点,久而久之自有成效!唠叨了两句,下面进入正题。在上一篇中介绍了Android的电量优化方面的知识——《详解Android电量优化》,今天继续沿着咱们的Android性能优化系列之路往下走,一路来到了Android应用瘦身优化的部分,来吧,一起开启今天的漫漫学习之路!

一、瘦身优化及Apk分析方案

1.1、为什么要做Apk瘦身优化

Apk瘦身优化在实际项目中的优先级相对来说是比较低的,因为你做完之后好处并不是那么明显,尤其是那些还未到稳定期的项目,瘦身优化最主要的好处是对转化率方面的影响,总结下主要原因如下:

  • 最主要是转化率:下载转化率(用户在应用商店搜索同类型应用时,apk大小也是决定是否下载的重要影响因素)
  • Top App都有Lite版
  • 渠道合作商要求:大型App可能会和手机厂商合作预装,手机厂商会对App做详细要求,达到相关要求之后才能预装到设备上

1.2、Apk组成

Android项目最后会编译打包生成一个.apk后缀的文件,它本质上其实是一个压缩包,因此它的内部还有很多类型的文件,这些文件按照大小分成了一下几类:

  • 代码相关:classes.dex
  • 资源相关:res、assets、resources.arsc、AndroidManifest等等
  • So相关:lib

1.3、Apk分析

①、ApkTool:反编译工具

在命令行中输入以下命令进行反编译:

反编译完成之后会在当前目录下生成一个跟apk名称一样的文件夹,里面就是反编译的产物:

②、Analyze APK:Android Studio2.2之后提供的工具

  • 查看Apk组成、大小、占比
  • 查看Dex文件组成
  • Apk对比

直接将要分析的apk文件拖拽到Android Studio中即可,然后会显示出各个文件的绝对大小以及所占的比例,你能清晰的看到体积占比情况,能给你定下一个大的优化方向,还可以查看.Dex文件中具体包含了哪些类,你还可以方便的查看app的版本也就是在AndroidManifest文件中:

③、App性能分析网站:https://nimbledroid.com/

  • 文件大小及排行
  • Dex方法数、SDK方法数
  • 启动时间、内存等方面的分析数据

关于这个网站的操作这里就不展示了,都是傻瓜式的操作,主要是这个网站的速度太慢了,等的你都怀疑人生!

④、android-classyshark:二进制检查工具

直接将apk文件拖拽到它的工作界面中,然后左侧会显示出当前apk内部的文件,classes中可以查看.Dex文件,可以具体查看当前Dex文件中具体有多少方法数,Methods count可以展示出具体方法数的比例,它是以饼状图呈现的:

二、代码瘦身

2.1、代码混淆

①、代码混淆简介

又被称之为花指令,它是将计算机代码转换成一种功能上等价但是难以阅读和理解的形式,目前代码混淆的方式主要有三种:

  • 代码中各个元素改写成无意义的名字,比如某个类Application转换成一个字母a,这样你就很难猜测它的具体用途
  • 以更难理解的形式重写部分逻辑:功能上等价但是让你看不懂,比如可以更改循环的指令和结构体,精简中间变量等
  • 打乱代码格式:比如多加空格、删除空格,将一行代码写成多行,将多行代码改成一行等等

②、如何使用

Android SDK Tools中集成了工具Proguard:免费的Java类文件压缩、优化、混淆、预先校验的处理工具,可以检测和移除未使用到的类、方法、字段,优化字节码,并且移除未使用到的指令和冗余代码,还会将代码中类的名称、字段、方法等元素改成简短无意义的名字,增加了代码被反编译的难度,一定程度上保证代码的安全性。

Proguard工具的使用方式:

  • 配置minifyEnabled为true打开混淆,注意只能在release下打开,debug下不要配置,因为混淆的过程相对来说比较慢,如果debug下也开启会拖慢编译速度
  • proguard-rules中配置相应规则

关于具体的配置规则我这里找了几篇文章,大家可以参考一下:

《Proguard官方文档》

《Android安全攻防战,反编译与混淆技术完全解析(下)》

《Android代码混淆使用手册》

下图中是我使用Demo工程打包生成的文件:第一个是没有使用混淆打包的,下面的是使用了混淆之后打包的,可以明显的看出使用混淆之后体积明显减小了1MB多,这是仅仅代码压缩后的体积变化:

2.2、三方库处理

实际开发过程中我们会使用到各种各样的第三方库,尤其是随着业务量增加,可能组内的开发人员也会增加,引入的第三方库也会变的非常多,比如小王在处理图片加载的时候引入了Fresco图片库,然后小李对这个库不熟悉但他对Glide很熟悉,他又引入了Glide图片库,因此项目中可能会存在多个相同功能的三方SDK,这一点在大型项目中是很可能会发生的,因此在做代码瘦身时应该保证统一性:

  • 基础库统一:图片加载库、网络库、数据库、统计分析库等这些处于业务底层支撑的库应该保持统一,去除冗余库
  • 选择更小的库:同类型业务功能应该将包大小作为技术选型的指标之一,尽可能的选择体积较小的库去实现相同的功能,比如Fresco、Glide、Picasso都可以实现图片加载,但是如果你使用Fresco包体积会增加1MB多,而使用Picasso只会增加不到100KB。这里推荐一个插件Android Methods Count,安装之后它会在build.gradle中显示出引入的三方库的方法数
  • 只引入需要的部分代码:现在很多库的代码结构都设计的比较好,比如Fresco它将图片加载的不同能力进行了剥离,比如gif、webp等都处于单独的库中,你可以需要哪部分就引入哪部分,这样引入的三方库就小了。如果你引入的三方库没有提供剥离的功能,那么此时可能就得修改源码了,需要你自行提取需要的那部分功能

举个栗子:比如我在使用的一个下拉刷新库,它就是将各种效果进行了剥离,你需要哪个引用哪个:

2.3、移除无用代码

实际开发过程中经常会遇到业务代码只增不减的情况,产品可能每个版本都在加功能很少砍功能,如果每个版本加一点,随着版本迭代,包体积肯定会越来越大,这种情况你想删除一些代码其实也不好删的,因为你不敢,你不确定这玩意是否还能用到,尤其是项目非常大业务逻辑非常多的时候,很难说的清楚某个类或者某些方法是否还有人在用。针对这种情况我们可以使用AOP的方式来统计各个类的使用情况,对于Activity来说只需在onCreate()方法加上统计即可,对于其它类可以使用AOP来切它的构造函数,一个类如果被使用,那么它的构造函数肯定是会被调用的:

  • 业务代码只加不减
  • 代码太多不敢删除
  • AOP统计使用情况

下面就在Demo中简单的实践一下构造函数的统计,我在这里统计一下第一篇中介绍的启动器的使用情况:

    //类名后面加上new说明要切的是构造函数,括号里两个点表示匹配所有的构造函数
    @After("execution(com.jarchie.performance.launchstarter.task.Task.new(..))")
    public void newObject(JoinPoint point) {
        LogUtils.i("new--->" + point.getTarget().getClass().getSimpleName());
    }

运行之后可以看到日志中打印出了还在使用的Task:

三、资源瘦身

3.1、冗余资源

Apk的资源主要包括图片和XML,与冗余代码一样,资源中也存在这样一种情况,就是随着应用版本的迭代,有些资源是老版本中用到但是新版本中已经不用了的,这一点在快速开发的app中是很有可能出现的,我们可以使用 右键--->Refactor--->Remove Unused Resource找到冗余的资源:

我在res/mipmap-xxxhdpi这个文件夹下添加了一张yingbao.png的图片,这张图片在这个项目中没有被用到,按照上面的操作来试一试看看能不能检测出来:

经过测试发现确实是可以检测出来的哦!

3.2、图片压缩

一般来说每1000行代码在apk中才会占用5KB的空间,而图片一般的都是几十KB甚至几百KB,所以做图片压缩更加重要:

  • 快速开发的App没有相关规范:UI设计师或者开发人员如果忽略了添加代码的时候来压缩,那么添加的就是原图,相应的包体积肯定会增大很多
  • https://tinypng.com/ 及TinyPngPlugin:这个网站可以对图片进行压缩,TinyPngPlugin这个插件可以让图片压缩更加自动化
  • 图片格式选择:之前的文章已经介绍过相同的图片使用webp格式的话体积会有大幅的压缩,对于png格式它是无损格式,它会严格保留所有色彩,jpg是有损格式,它在处理颜色很多的图片时根据压缩率不同去除一些中间颜色,所以在图片尺寸大色彩鲜艳的时候png的体积会明显大于jpg。

3.3、资源混淆

资源混淆简单来说就是将资源路径混淆成单个字母的路径,这里推荐一个库:https://github.com/shwenzhang/AndResGuard,这个项目可以使冗长的资源路径变短,下面来实际演示一下它的使用:

首先添加依赖:

classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.18'
apply plugin: 'AndResGuard'

//在app module的build.gradle中添加一下配置
andResGuard {
    // mappingFile = file("./resource_mapping.txt")
    mappingFile = null
    use7zip = true
    useSign = true
    // It will keep the origin path of your resources when it's true
    keepRoot = false
    // If set, name column in arsc those need to proguard will be kept to this value
    fixedResName = "arg"
    // It will merge the duplicated resources, but don't rely on this feature too much.
    // it's always better to remove duplicated resource from repo
    mergeDuplicatedRes = true
    whiteList = [
            // your icon
            "R.drawable.icon",
            // for fabric
            "R.string.com.crashlytics.*",
            // for google-services
            "R.string.google_app_id",
            "R.string.gcm_defaultSenderId",
            "R.string.default_web_client_id",
            "R.string.ga_trackingId",
            "R.string.firebase_database_url",
            "R.string.google_api_key",
            "R.string.google_crash_reporting_api_key",
            "R.string.project_id",
    ]
    compressFilePattern = [
            "*.png",
            "*.jpg",
            "*.jpeg",
            "*.gif",
    ]
    sevenzip {
        artifact = 'com.tencent.mm:SevenZip:1.2.18'
        //path = "/usr/local/bin/7za"
    }
}

然后在右侧Gradle的工作空间点击resguardRelease,这样你生成的包就是资源混淆过的包了:

下图是资源混淆过的包,可以看到它的资源名称和路径都变短了:

3.4、其它方案

  • 资源图片只保留一份:比如我们统一只放在xxhdpi这个目录下,在不同分辨率下它会自动等比例拉伸或释放
  • 资源在线化:将一些图片资源放在远端,结合预加载技术,既可以满足产品的需要又可以降低包体积的大小

四、So瘦身

4.1、So移除

So是机器可以直接运行的二进制代码,在Android应用开发过程中有时候Java代码无法满足需求,比如:一些加解密算法或者音视频编解码等功能,针对这种需求我们只能通过C/C++来实现,然后编译生成So文件提供给Java层调用,在生成So文件时需要考虑适配市面上不同手机的CPU架构,理论上来说对应CPU架构的So的执行效率是最高的,但是这样会导致在lib目录下会存放各个平台的So文件,Apk的体积也就会变大,所以我们需要对lib目录进行缩减,在build.gradle中配置abiFilters来设置支持的CPU架构,一般情况下保留armeabi目录即可:

注意:最新版本的NDK对于mips指令集已经不再支持了!!!

  • abiFilters:设置支持的So架构
  • 一般选择armeabi
ndk{
    //设置支持的So库架构
    abiFilters 'armeabi'
}

但是只选择armeabi性能上会有所损耗,失去对特定平台的优化:

  • 只适配armeabi的APP可以跑在armeabi,x86,x86_64,armeabi-v7a,arm64-v8上
  • 只适配armeabi-v7a可以运行在armeabi-v7a和arm64-v8a
  • 只适配arm64-v8a 可以运行在arm64-v8a上

那么我们应该如何适配呢?基本就是以下几种方案:

  • 方案一:只适配armeabi
  • 优点:基本上适配了全部CPU架构(除了淘汰的mips和mips_64,mips架构现在谷歌最新的官方文档上已经没有了)
  • 缺点:性能低,相当于绝大多数手机都需要辅助ABI或者是动态转码来兼容
  • 方案二:只适配armeabi-v7a
  • 同理方案一,放弃了部分老旧设备,综合平衡了性能和兼容性两者
  • 方案三: 只适配 arm64-v8
  • 优点: 性能最佳
  • 缺点: 只能运行在arm64-v8上,要放弃部分老旧设备

这三种方案其实都是可以的,在国内的大厂APP的适配中,这三种都有,大部分选择的是前面两种,如果是追求性能舍弃兼容性就选arm64-v8,衡量兼容性舍弃部分性能则选armeabi,综合平衡两者的话就是选armeabi-v7a。

4.2、更优方案

理论上来说对应CUP架构的So的执行效率是最高的,只是这样做会导致包体积急剧增大,那么我们是否可以采取一个折中的方案?对于性能敏感的模块使用到的So,统一都放到armeabi目录下,然后在代码中判断设备CPU的类型,根据CPU不同的类型来加载对应架构的So文件:

  • 完美支持所有类型设置代价太大,包体积很大
  • 都放在armeabi目录,根据CPU类型加载对应架构So

这里我简单的做个示例:

        String abi;
        if (Build.VERSION.SDK_INT<Build.VERSION_CODES.LOLLIPOP){
            abi = Build.CPU_ABI;
        }else {
            abi = Build.SUPPORTED_ABIS[0];
        }
        if (TextUtils.equals(abi,"ARMv7")){
            //加载特定平台的So文件
            System.loadLibrary("armeabr-v7");
        }else {
            //正常加载
            System.loadLibrary("armeabi");
        }

具体的可以参考一下这篇文章:《轻松实现动态获取Android手机CPU架构类型》

4.3、其它方案

  • So动态下载:将部分So文件使用动态下发的方式加载,在业务代码开始前从网络下载So
  • 插件化:对代码结构进行调整,如果每个功能都是一个插件都可以从网络下发,apk体积肯定会降低很多,国内大型项目一般都会做插件化处理,所以可以通过下发插件的方式做包体积优化

好了,关于Android应用瘦身优化方面的知识就说到这里吧,咱们下期再会吧!

喜欢的朋友麻烦动动小手点个赞啦😋,这个专栏写到这里也快接近尾声了,看我这么辛苦,真的忍心不给个👍吗?

祝:工作顺利!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值