性能优化2 - 内存、启动速度、卡顿、布局优化
性能优化是在充分了解项目+java、android基础知识牢固的基础上的。
内存优化
基础知识回顾(看前面文章JVM详解):
jVM内存模型,除了程序计数器以外,别的都会出现 OOM。
JAVA对象的生命周期,创建、运行、死亡。
GC对象可回收的判定:可达性分析。GC root(除了堆里的对象,虚拟机栈里的引用、方法区里的引用)。
强软弱虚四种引用。
GC回收算法:复制算法、标记清楚算法、标记整理算法。
App内存组成以及限制
Android
给每个App
分配一个VM
,让App运行在dalvik
上,这样即使App
崩溃也不会影响到系统。系统给VM
分配了一定的内存大小,App
可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超出VM
最大内存,就会出现内存溢出crash
。
由程序控制操作的内存空间在heap
上,分java heapsize
和native heapsize
- Java申请的内存在
vm heap
上,所以如果java
申请的内存大小超过VM
的逻辑内存限制,就会出现内存溢出的异常。 - native层内存申请不受其限制,
native
层受native process
对内存大小的限制
总结:
app运行在虚拟机上,手机给虚拟机分配内存是固定的,超出就oom。
分配的内存大部分都是在堆上。分为java堆和native层的堆。
java层的堆超过VM给的就oom。理论上native层无限制,但是底层实现native process
对内存大小是有限制的。
查看系统给App分配的内存限制
不同手机给app分配的内存大小其实是不一样大的。
- 通过cmd指令 adb shell cat /system/build.prop
- 通过代码 activityManager.getMemoryClass();
ActivityManager activityManager =(ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)
activityManager.getMemoryClass();//以m为单位
这些限制其实在 AndroidRuntime.cpp的startVm里,我们改不了,手机厂商可以改
Android低内存杀进程机制
默认五个级别:空进程、后台进程、服务进程、可见进程、前台进程
所以在保活里有一个做法就是给app提升优先级,给他整成前台、可见这样的。
AMS里有一个oom_adj,会给各个进程进行评分,数字越大越容易被回收,前台进程是0,系统进程是负数,别的是正数。
内存三大问题
1、内存抖动
profiler -> 内存波动图形呈 锯齿张、GC导致卡顿。
原因是内存碎片很严重。因为android虚拟机的GC算法是标记清楚算法,所以频繁的申请内存、释放内存会让内存碎片化很严重,连续的内存越来越少,让GC非常频繁,导致了内存抖动。案例:在自定义View onDraw()里new对象
2、内存泄漏 在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小。内存泄露如果越来越严重的话,最终会导致OOM。案例:context被长生命周期的东西引用。没有释放listener
3、内存溢出 即OOM,OOM时会导致程序异常。Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM。案例:加载大图、内存泄露、内存抖动
除了程序计数器以外 别的JVM部分都会OOM
常见分析工具
- Memory Analyzer Tools --- MAT
MAT是Eclipse下的内存分析工具。
用法:用cmd指令或者android studio 自带的profiler 截取一段数据,然后下载下来。得到一个.hprof
然后用AMT打开,就能看预测泄露地方,有一个图表,基本没用。还有看哪个线程调用这个对象、深浅堆等等。挺难用的。
- android studio自带的profiler
谷歌官网:developer.android.google.cn/studio/prof…
官网超级详细,还有视频,直接看官网的就行。
选中一段区域,就能查看这段区域内存被具体哪个对象用了多少等等。
我感觉还是用来看大势的,大的内存上涨、下落、起伏图。
- LeakCanary
超级推荐LeakCanary!!!永远滴神
具体内存泄露的检测,细节还得用LeakCanary。
集成:
build.gradle里添,跟集成第三方一样。debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
然后直接跑APP,跑完以后一顿点,点完以后会给推送。写了发现几个对象有泄露。点一下推送蓝会自动下载下来。
比如这就是我的问题,期初我认为是activity的context没有释放,其实是在dialog里使用了动画animation,但是动画的listener没有释放。
Android内存泄漏常见场景以及解决方案
1、资源性对象未关闭
对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap 等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。
2、注册对象未注销
例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。
3、类的静态变量持有大数据对象
尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。
4.单例造成的内存泄漏
优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封 装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。
5、非静态内部类的静态实例
该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源 不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如 果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置 空让GC可以回收,否则还是会内存泄漏。
6、Handler临时性内存泄漏
Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的, 则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息, 当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message 持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回 收,引发内存泄漏。解决方案如下所示:
1、使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这 样在回收时,也可以回收Handler持有的对象。
2、在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中 有待处理的消息需要处理
(Handler那篇有讲)
7、容器中的对象没清理造成的内存泄漏
在退出程序之前,将集合里的东西clear,然后置为null,再退出程序
8、WebView
WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为 WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业 务的需要选择合适的时机进行销毁,达到正常释放内存的目的。
9、使用ListView时造成的内存泄漏
在构造Adapter时,使用缓存的convertView。
10、Bitmap
80%的内存泄露都是Bitmap造成的,Bitmap有Recycle()方法,不用时要及时回收。但是如果遇到要用Bitmap直接用Glide就完事了,自己写10有8.9得出错。
启动速度优化
app启动流程
①点击桌面App图标,Launcher进程采用Binder IPC向AMS进程发起startActivity请求;
②AMS接收到请求后,向zygote进程发送创建进程的请求;
③Zygote进程fork出新的子进程,即App进程;
④App进程,通过Binder IPC向AMS进程发起attachApplication请求;
⑤AMS进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;
⑥App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;
⑦主线程在收到Message后,通过反射机制创建目标Activity,并回调Activity.onCreate()等方法。
⑧到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染结束后便可以看到App的主界面。
启动状态
APP启动状态分为:冷启动、热启动、温启动。
冷启动:什么都没有,点击桌面图标,启动App。Zygote fork进程...
热启动:app挂在后台,在点击图标切换回来。
温启动:两者之间。
启动耗时统计
1.打log。displayed。有显示
2.cmd命令 adb shell am start -S -W +包名+Activity名
CPU Profile
具体怎么优化呢?得用到Android Studio自带的分析的工具CPU Profile。
- 打开CPU Profile,trace java Methods
- 跑项目,截开始的那段cpu运行图,得到启动的数据
- 得到具体哪个方法执行时间的情况图
Call Chart:黄、蓝、绿三色。黄色=系统、绿色=自己的、蓝色=第三方。自上而下表示调用顺序。越长就说明执行的时间越长。
如:我发现极光的初始化时间还是挺长的,如果要优化可以将极光延迟初始化。
还有onCreat占用时间也挺长,主要在setContentView里,看看能不能将布局优化一下。
Flame Chart:跟Call Chart差不多,就是反过来的图,又称火焰图。
Top Down Tree:这个就不是图标了,是方法直接的调用关系,每个方法的调用时间。一直往下点,可以找到占用时间较长的,可以优化的地方。从上往下的调用。
Bottom Up Tree:跟top反向从下往上。
启动白屏
在主题里配置一个背景。
StrictMode严苛模式
可以在application里配置严苛模式,这样不规范的操作就会有log提示,或者闪退。
启动优化的点
1). 合理的使用异步初始化、延迟初始化、懒加载机制。
2). 启动过程避免耗时操作,如数据库 I/O操作不要放在主线程执行。
3). 类加载优化:提前异步执行类加载。
4). 合理使用IdleHandler进行延迟初始化。
5). 简化布局
卡顿优化
分析工具
- Systrace是Android提供的一款工具,需要运行在Python环境下。
配置:http://www.jianshu.com/p/e73768e66…
Systrace主要是分析掉帧的情况的。帧:android手机一般都是60帧,所以1帧的时间=1000毫秒/60=16.6毫秒。也就是android手机16.6毫秒刷新一次,超过这个数就是掉帧了
会有三色球,绿=正常,黄=一点不正常,红=掉帧严重。
少几个没啥事,大面积的出现红、黄,就需要研究为啥掉帧了。
可以看上面有一条CPU的横轴,绿色=正在执行,蓝色=等待,可以运行。紫色=休眠。白色=休眠阻塞。如果是出现了紫色就说明IO等耗时操作导致掉帧。如果紫+蓝比较多,说明cpu拿不到时间片,cpu很忙。
CPU Profile
这个就能看那个方法运行多少时间等,所以可以直接用android studio自带的分析。
一般是在top down里一直点,耗时较多的,然后点到自己熟悉的地方,挨个分析。
这是一个漫长的耗时的过程,可能找半天只找到几个地方能优化,然后每个几毫秒,加起来也没有直观的变快,但是性能优化就是这样的一个过程,积少成多。
如:我经过查找就发现adapter的 notifyDataSetChanged
因为不小心,有些地方多次调用了。
甚至还有没有在线程进行io操作。
布局优化
经过上面的一顿操作,发现占时间大块的少不了setContentView
。说明布局渲染视图还是挺费时的。
减少层级
自定义Viewmeasure、layout、draw这三个过程,都需要对整个视图树自顶向下的遍历,而且很多情况都会多次触发整个遍历过程(Linearlayout 的 weight等),所以如果层级太深,就会让整个绘制过程变慢,从而造成启动速度慢、卡顿等问题。
而onDraw在频繁刷新时可能多次触发,因此 onDraw更不能做耗时操作,同时需要注意内存抖动。对于布局性能的检测,依然可以使用systrace与traceview按 照绘制流程检查绘制耗时函数。
工具Layout Inspector
DecorView开始。content往下才是自己写的布局。
重复的布局使用include。
一个布局+到另一个上,如果加上以后,有两一样的ViewGroup,可以把被加的顶层控件的ViewGroup换成merge
ViewStub:失败提示框等。需要展示的时候才创建,放在那不占位置。
过度渲染
一块区域内(一个像素),如果被绘制了好几次,就是过度渲染。
过度绘制不可避免,但是应该尽量的减少。
手机->开发者模式->GPU 过度绘制 打开以后能看到不同颜色的块,越红就说明过度绘制的越严重。对于严重的地方得减少过度绘制。
1.移除布局中不需要的背景。
2.降低透明度。
3.使视图层次结构扁平化。
布局加载优化
异步加载布局,视情况而用。
implementation"androidx.asynclayoutinflater:asynclayoutinflater:1.0.0"
new AsyncLayoutInflater(this)
.inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
setContentView(view);
//......
}
});