1.冷启动与热启动优化
1.测量APP的启动时间 指令:
adb shell am -w [packgeName]/[packageName.MainActivity]
我们自己的项目
adb shell am start -W cn.com.weilaihui3/.app.ui.activity.HomeActivity
冷启动
热启动:
总共三个测量时间:
1.ThisTime:一般和TotalTime的时间一样,除非在应用启动时开了一个透明的Activity预先处理依稀耗时的操作后再显示出主Activity,这样将比TotalTime小;
2.TotalTime:应用的启动时间,包含了创建进程+Application初始化+Activity初始化到界面的显示;
3.WaitTime:一般比TotalTime大点,包含系统的耗时。
启动优化:
1.视觉优化:
给定一个默认主题,并设置闪屏图片主题
2.代码优化
2.1.在Application的构造方法中,attachBaseContenx(),onCreate()方法中不要进行耗时操作的初始化,一些数据预取放在异步线程中,能够采取Callable实现。
2.2.对于SP的初始化,由于SP的特性在初始化时会对文件中的所有数据读出来存进内存中,所以这个初始化放在主线程中明显不合适,反而会延迟应用的启动时间,因此必须放在异步线程中去做。
2.3.对于MainActivity由于在获取第一帧前需要对ContentView进行测量布局绘制操作,因此需要尽量降低布局的层次。考虑到StubView的延迟载入策略,必须在onCreate,onStart,onResume中避免做耗时的操作。
同时首先需要考虑哪些数据可以延时加载,哪些数据必须立即加载。
2.包大小的优化
1.包大小的查看
将Apk下载下来,拖动到AS中,AS会自动分析包的大小
如图所示Raw File Size是应用打包后的真实大小,Download size是Google play上面用户下载的大小,用于用户下载,因此一般只针对前一项的APK Size 进行优化。
2.APK的组成:
文件/目录描述
lib/用于存放so文件,可能会有armeabi,armeabi-v7a,arm64-v8a,x86,x86-64,mips等等,但是目前大多数情况下只需要支持armeabi与x86即可,如果没有必要,可以考虑把x86也可以拿掉,(据我所知小米的机型中目前只有两款使用的x86架构,一款是小米pad(2)(版本号好像是4.4),还有一款手机忘了什么记性了(版本号也是4.4)其他的所有的机型都是arm结构的,因此如果最低支持的版本号是从5.0以上支持的是可以拿掉x86的)
res/存放编译后的资源文件,如drawable、layout等
assets/应用程序资源,应用程序可以使用AssetManager来检索该资源,如mp3,json字符串等资源
META-INF/该文件夹一般存放于已经签名的apk中,它包含了APK中所有文件的签名摘要等信息
classes(n).dexclasses文件是Java Class,被Dex编译后可供Dalvik/ART虚拟机所理解的文件格式。包含了所有的java文件编译后的class文件,class文件最终转化为dex文件。一般文件都比较大,有的APP有好几个dex文件,这是因为单个的dex文件限制方法数载65535的问题,所以当代码量过大时,就需要通过multidex进行分包,拆分成多个dex文件
resources.arsc编译后的二进制文件包含了所有可以被编译的位于res/values目录下的xml资源。打包工具在打包的过程中会把xml的内容编译成二进制的形式,或者把相关的资源引用路径编译成二进制,然后存储在该文件中,例如string文件,layout的路径,图片的路径等等
AndroidManifest.xmlAndroid清单文件
3.具体的优化逻辑
1)lib优化
(1)so文件的裁剪和压缩
对于App中引入的so文件进行分析确认,哪些是不需要的,哪些是可以裁剪压缩的,哪些是可以避免引入的,哪些是可以自定义替换的。例如如果引入的so仅需要一个上传和下载功能而多引入了cURL库,则会导致so文件增大,此时我们可以使用在java中自定义,然后让so文件进行调用,从而避免了引入cURL库。
(2)架构文件的保留
Android系统目前支持7种CPU架构,每一种都关联着对应的ABI(二进制接口,Application Binary Interface),而每一种ABI都定义了对应的二进制文件尤其是SO文件,因此就有七种对应的so文件,但是目前主流的机型一般都使用arm结构,少部分使用x86架构(据我所知小米的机型中目前只有两款使用的x86架构,一款是小米pad(2)(版本号好像是4.4),还有一款手机忘了什么记性了(版本号也是4.4)其他的所有的机型都是arm结构的,因此如果最低支持的版本号是从5.0以上支持的是可以拿掉x86的)。因此如果支持的Android版本最低是5.0以及以上的版本,完全可以只保留armeabi或者armeabi-v7a即可,操作也相对简单,只需要在根目录的build.gradle下配置:
Android{
buildTypes{
ndk{
abiFilters "armeabi-v7a"
}
}
}
如果你的APP需要支持多种架构,那么就可以在abiFilters里面吧多种架构加进去,当然也可以只保留一种,然后分渠道打包。
2)res目录优化
(1)只保留一套图片即可
资源文件一般占APP的很大一部分,例如NIO APP种res目录下的资源文件占了整个空间的接近40%,这是一个十分恐怖的数字,尤其是APP为了适应多种分辨率而存放多套图事(NIO APP还好大多数只存放了xxh的图片,我在小米浏览器的时候,还需要xh甚至xxxh的图)。由于Android设备在加载图片事,首先会加载对应分辨率文件夹下的图片,如果对应分辨率下没有所需的图片,则需要查找高分辨率对应文件夹下的图片,那么是不是把图片放在最高分辨率的文件夹下就可以了呢?不是的,因为这样会导致低分辨率手机加载图片时会消耗更多的内存,而且是指数级别的,所以如果盲目的放在一个目录下也不合适,一般情况下对应分辨率的图片放在对应分辨率的文件夹下。国内一般只需要一套即可:xxh(我在小米时小米也只提供两套图片(xh:低于1000元的红米手机,xxh:据我所知千元以上的机型屏幕都是1080*1920的(全面屏以宽度算)),只有一款机型即小米note plus是使用的2k屏幕,在极个别的情况下需要xxxh图片其他的都使用xxh即可)。
(2)非重要图片动态加载
APP中其实有很多图片是可以从网上下载,严格意义上说,非首页的图片都可以动态加载,但是为了用户的体验我们可以将图片保存在本地,但是对于用户很少使用,或者图片较大时建议采用动态加载模式
(3)图片保真压缩,或者使用webp代替png
设计师提供的图片如果较大时,建议使用第三方工具进行保真压缩或者转化为webp,在转化为webp的情况下,建议保真度超过90%即可(因此png转化为webp时,优势94%的保真度的大小不到95%保真度大小的1/3)
(4)能用xml实现的图片尽量使用xml实现
xml实现的图片不仅可以有效的减少APP包的大小,而且可以渲染的更加清晰例如纯色图,渐变图等等,都可以使用xml自己画(小米浏览器中夜间模式的切换的背景图,一开始是测试提供的,每张图片在1.8MB之间,也就是说我仅做夜间模式的切换动画就需要增加3.6MB的包大小,相当于浏览器的包增加了12%,这时就可以和设计师商议,让他们提供色值和少量的图片,然后自己去实现。)。
(5)使用lint删除无用资源
方法一:手动删除
打开AS ,打开对应的项目,在状态栏中找到Analyze -> Run Inspection by Name
弹出该对话框,输入Unused Res
双击后会弹出下面的对话框
选择对应的模块或者整个项目,然后点击ok,稍等以后,输出框中会输出没有用的资源文件
如上图所示,最后点击去查看同时删除无效资源。
方法二自动删除无效的资源
在AS 1.4.0 及其以后的版本已经支持自动删除无效的图片或者资源,自序一在Gradle构建系统中配置如下的信息即可:
在主工程的build.gradle的的buildTypes->release中配置shrinkResource 等于true即可
buildTypes {
release {
minifyEnabled true // 开启混淆
zipAlignEnabled true // 包优化
shrinkResources true // 移除无用资源
debuggable isReleaseDebuggable
proguardFiles getDefaultProguardFile('proguard-android.txt'),
}
}
lint也是有缺点的,如果一个图片在java代码中被引用,而这个段代码缺没有被使用,那么对应的图片或者资源是无法被删除的。
app的包体积优化,lint、proguard、andresguard原理,字节码注入,删除R.java的变量,删除access001方法,压缩图片资源和使用redex等。
3.65535数
Android系统中,一个APP所有的代码都在一个dex文件中,dex文件是一个类似于jar的存储多个Java编译字节码的归档文件,因为Android系统使用了Dalvik虚拟机,所以需要使用Java Compiler编译后的cless问卷转化成dalvik能够执行的class文件,这里需要强调的是,Dex和jar一样是一个归档文件,里面仍然是Java源码对应的字节码文件。当Android启动时,有一步是对Dex进行优化的,这个过程用一个专门的工具进行处理,叫做DexOpt。DexOpt会吧每一个类的Id检索出来并保存到一个链表结构中,但是这个链表的长度是用一个short类型来保存的,因此导致了方法的数目不能超过65536个。
常见的解决办法:分包
Google推荐使用MultiDexApplication
配置也很简单:
Gradle配置:在defaultConfig中定义multiDexEnabled 为true
defaultConfig {
...
multiDexEnabled true //Enabling multidex support
...
}
dependencies中引用multidexjar包
dependencies {
...
implementation "com.android.support:multidex:1.0.2"
...
}
在Application中重写attachBaseContext方法
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
Multidex的局限性
1.如果第二个或者某一个dex文件很大的话,安装dex文件到date分区时,可能会导致ANR
2.由于Dalvik LinearAlloc 的bug问题,使用了Multidex的应用可能无法在Android 4.0(API level 14或者以前版本的设备上使用)
3.由于使用了multidex的应用在安装时,需要请求分配很大的内存,但是Dalvik LinearAlloc是一个固定的缓冲区大小,Android 4.0以前只有5MB,到了Android4.0以后才提高到了8/16MB,当方法数量超过了缓冲区大小时,会造成dexOpt崩溃。
4.在Dalvik运行中,某些类或者方法必须放在主dex中,Android构建工具可能无法确保所有有此需求的类被编译进主dex中。
需要注意的是,一些在二级Dex加载之前,可能会被调用到的类(比如静态变量的类),需要放在主Dex中,否则会ClassNotFoundError,通过修改Gradle,可以显示的把一些类放在Main dex中。
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex' dx.additionalParameters += "--main-dex-list=$projectDir/
}
}
注意上面是修改后的Gradle,其中是一个文本文件的文件名,存放在和这个build.gradle脚本同一级的文件目录下,而不是 项目根目录。可以把这个文本文件起名为multidex.keep,内容如下.实际就是把需要放在Main Dex的类罗列出来。
android/support/multidex/BuildConfig/class
android/support/multidex/MultiDex$V14/class
android/support/multidex/MultiDex$V19/class
android/support/multidex/MultiDex$V4/class
android/support/multidex/MultiDex/class
android/support/multidex/MultiDexApplication/class
android/support/multidex/MultiDexExtractor$1/class
android/support/multidex/MultiDexExtractor/class
android/support/multidex/ZipUtil$CentralDirectory/class
android/support/multidex/ZipUtil/class
project.afterEvaluate标签在特定的project配置完成后运行,而gradle.projectsEvaluated在所有projects配置完成后运行。 注意afterEvaluate需要放在android{}里,不可放外面。
这样做了之后并不一定解压apk之后会出现多个dex文件,可能仍然只有一个dex。因为只有必须分包的时候才会分,如果不需要就不会。 如果要强制分dex,还需要加上dx.additionalParameters += ‘–minimal-main-dex’。完整的配置如下:
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
// 设置multidex.keep文件中class为第一个dex文件中包含的class,如果没有下一项设置此项无作用
dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()
//此项添加后第一个classes.dex文件只能包含-main-dex-list列表中class
dx.additionalParameters += '--minimal-main-dex'
}
}
这样配置了之后就按照multidex.keep里面的内容拆分出了第一个dex文件。其他内容在第二个里面。 那么如何把需要的类放在multidex.keep文件里呢?其实不用手动一个类一个类写,我们进入这个文件: 项目\build\intermediates\multi-dex\release(或debug)\maindexlist.txt。 将maindexlist.txt中没有在application中初始化的类删除一部分之后,剩余的复制到multidex.keep文件中就可以了。 当然也可以自行增加没有被包含进去的类,因为不直接引用的类都不在maindexlist.txt中。 注意,如果需要混淆的话需要写混淆之后的 class 。
如果需要配置每一个dex最大的方法数,可以如下配置:
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--set-max-idx-number=48000'
}
}
通过上面的number参数代表每个Dex文件中的最大id数,默认是65535,通过修改这个值可以减少Main Dex文件的大小和个数。 这样可以拆分出多个dex。但是这个number不可设置的太小,因为主dex需要加载足够app启动 需要的类,太小则无法加载完,直接报错。
如果用使用其他Lib,要保证这些Lib没有被preDex,否则可能会抛出下面的异常:
UNEXPECTED TOP-LEVEL EXCEPTION:
com.android.dex.DexException: Library dex files are not supported in multi-dex mode
at com.android.dx.command.dexer.Main.runMultiDex(Main.java:337)
at com.android.dx.command.dexer.Main.run(Main.java:243)
at com.android.dx.command.dexer.Main.main(Main.java:214)
at com.android.dx.command.Main.main(Main.java:106)
遇到这个异常,需要在Gradle中修改,让它不要对Lib做preDexing
android {
// ...
dexOptions {
preDexLibraries = false
}
}
MultiDex实现原理
Dex自动拆包
Dex拆分步骤
1.扫描整个工程代码,得到main_dex_list
2.根据main_dex_list 对整个工程编译后的所有class进行拆分打包,将主,从dex文件分开。3.用dex工具对主从dex文件分别带包成。dex的文件,并放在apk的合适目录。