性能优化前言:
性能优化是一个APP不可或缺并需不断重复的工作,性能优化的深度是一个优秀APP的重要凭证,它既繁杂繁琐但也有一定的规则规律。本篇结合实际项目来简单分享一下一个线上产品的优化过程。也非常非常非常期待大家留言交流,指错,分享各自的优化经验~我会收集补充更新收录到本篇。
1、布局渲染 方向:
造成问题:大部分Android显示屏幕是以每秒60帧来刷新的,1000毫秒/60≈16毫秒,所以16毫秒没有完成绘制用户就会感受到卡顿现象出现,绘制布局渲染不得当是卡顿的主要原因,主要是过度绘制(overdraw)布局的层级太深、页面过于复杂等。(动画、频繁的发GC导致堵塞渲染、UI线程中有轻微的耗时操作等因素也会导致卡顿后面详说。)
实用工具:
开发者模式下打开 调试GPU过度绘制 和 GPU呈现模式分析
硬件:调试GPU过度绘制
原色:没有过度绘制;
蓝色:1 次;
绿色:2次 ;
粉色:3 次;
红色:4次以上 ;
监控:GPU呈现模式分析
绿线线:代表16ms 保持动画流畅的关键就在于让这些垂直的柱状条尽可能地保持在绿线下面,任何时候超过绿线,你就有可能丢失一帧的内容,
蓝色线:表示测量绘制时间,很高可能是因为一堆图突然无效(需要重新绘制)或者师徒onDraw函数过于复杂。
红色线:表示执行时间,过高说明复杂的自定义view过多。
橙色线:表示处理时间,CPU告诉GPU渲染一帧的地方。 这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复,如果柱状图很高, 那就意味着你给GPU太多的工作,太多的负责视图需要OpenGL命令去绘制和处理.
解决方案:
1. 从设计根本上简化页面的复杂度,移除window默认背景。
2. 在view层级相同的情况下,尽量使用LinearLayout代替RelativeLayout 原因是RelativeLayout是相对布局,在测量的时候会对子view(child)分别进行横向和纵向的两次测量才能确定相对位置,而LinearLayout顾名思义线性布局,我们设置了方向后,在不使用weight属性时只对view测量一次就可以确定子view的位置。这里强调的是层级相同的情况下!如果布局特别复杂,单纯的使用LinearLayout则会导致层级过深,那采用RelativeLayout比较好,但是,使用ConstraintLayout约束布局代替RelativeLayout绘制速度更快,更灵活不需要嵌套很多层,可以有效的减少布局层级!!! ConstraintLayout性能详解
3. 使用<merge>标签来减少重复布局,常配合<include>标签一起使用,当一个layout包含另一个layout时,如果被包含的布局文件采用了跟外部一样的布局模式,可以通过<merge>标签去重,<merge>标签只能用在布局文件的根节点上。简单明了的解释+例子
4.ViewStub的使用,ViewStub继承了View,非常轻量级且宽高都是0,隐私本身不参与任何的布局和绘制过程。它的意义在于按需加载所需的布局文件,项目中有很多非正常显示的页面如网络错误,加载失败,空页面等等,这时候就没必要在页面初始化的时候加载进来,通过ViewStub就可以做到使用的时候再加载,提高了程序初始化时的性能。ViewStub详解
5 view的属性可以优化的地方:
- tools属性下的方法:
tools:text="张三";
tools:background="@tools:sample/backgrounds/scenic"等 - 显示隐藏尽量用gone代替invisible,因为gone不绘制,invisible绘制后隐藏。
... ...
6. 绘制方面的优化,指的是View的onDraw方法要避免大量的操作
- onDraw方法中不要创建新局部对象,因为这个方法频繁调用会产生大量的临时对象占用过多的内存导致系统频繁调用gc,降低程序的执行效率。
- onDraw方法中不要做耗时操作,也不要执行过多的循环操作,这样会导致cpu时间片被占用,导致绘制不流畅,出现卡顿。
7. 自定义View优化,使用 canvas.clipRect()来帮助系统识别那些可见的区域,只有在这个区域内才会被绘制。
2、内存问题 方向:
存在问题:内存泄漏、内存溢出、内存浪费等
内存泄漏相关问题:
1. 单例模式导致内存泄漏:单例的静态特性使得它的生命周期跟APP的生命周期一样长,如果一个对象已经没有用处了但是单例还持有它的引用,应用程序的整个生命周期都不能回收, 从而导致内存泄露。构造单例尽量别使用Activity的引用,否则Activity关闭后单例模式还持有该Activity的引用使得Activity得不到回收,会导致内存泄漏,优化案例是采用application中的content。
2. 静态变量导致内存泄漏:静态持有变量很多是因为其使用的生命周期不一致而导致内存泄露,static指向的内存引用,GC永远不会回收,优化方案是尽量少使用,不用时给它重置为null使其不再持有。
3. 非静态内部类导致内存泄漏:非静态内部类的默认持有外部类的引用,当内部类的生命周期比外部类还要长时就会导致内存泄漏。典型场景可以看一下此篇:Handler详解,优化方案:静态内部类+弱引用+销毁时将销毁handler的回调和发送消息移除掉。
4. 资源对象及时关闭导致内存泄漏:在使用IO、File流或者Sqlite、Cursor等资源时要及时关闭。原因是这些资源在进行读写操作时通常使用了缓冲,如果不及时关闭,这些缓冲会一直被占用从而得不到释放,导致内存泄漏。
5. 未取消注册或回调导致内存泄漏:比如在activity中注册广播,如果activity销毁后不取消注册,这个广播会一直存在系统中。
6. 属性动画导致内存泄漏:动画是一个耗时操作,如果启动了动画后,在页面或视图销毁时没有调用cancle销毁动画,这个动画虽然看不见了但会一直不断地播下去,动画引用的view,view引用的activity无法正常释放造成内存泄漏。
7.Handle.post(Runnable)引发泄漏,或者有的地方postDelay(Runnable,time) 在页面释放的时候应该removeMessageAndCallback,最好把runnable改为静态内部类。
8.及时合理的释放资源,release掉不使用的资源;监听回调不使用时记得要释放掉,置为null。
9.错用Application,导致GC不能回收。
10.注意子类继承父类方法 ,遗漏了surper.xxx(),有一些释放资源的逻辑在父类中,而子类重写父类方法的时候没有调用父类方法,导致的错误。
11. WebView造成的内存泄漏:造成的原因是加载网页后长期占用内存不能被释放,WebView控件会持有activity引用,activity销毁后WebView持有的引用得不到释放,优化方式是做好销毁工作或采用成熟的开源WebView库。
内存溢出相关问题:
内存空间不够,导致内存溢出OOM(内存泄漏多了或者直接就不够用,多在于图片等大资源)
查询图片不加载到内存中:Android中Bitmap有四种图片色彩模式:
ALPHA_8:每个像素需要占用内存中的1byte
RGB_565:每个像素需要占用内存中的2byte
ARGB_4444:每个像素需要占用内存中的2byte
ARGB_8888:每个像素需要占用内存中的4byte
我们创建Bitmap时,默认的色彩模式是ARGB_8888的,这种色彩模式是质量最高的,当然这样的模式占用的内存也最大。
3、代码优化来提高性能:
1. 合理的使用设计模式
单利模式:控制内存中只存在一个对象可以减少内存、减轻加载负担和时间,提升加载效率。
享元模式:减少对象创建的数量来达到减少内存。
2. 避免过多创建Java对象,可以采用享元模式来减少对象的创建,可以用基本数据类型代替包装类型的对象。
3. 尽可能使用final修饰符,在Java中final关键字修饰后不会直接定义内联函数。而是告诉编译器可以将其修饰的函数看作内联函数,编译器会比较效率后决定是否视其为内联函数。如果是内嵌调用,虚拟机不再执行正常的方法调用(参数压栈,跳转到方法处执行,再调回,处理栈参数,处理返回值),而是直接将方法展开,以方法体重的实际代码替代原来的方法调用。
相关文章:JVM 技术内幕——HotSpot VM
4. 尽可能使用局部变量,传递的参数和临时变量都保存在栈中,静态变量和实例变量等都存在堆中,栈相比于堆查询的效率高、空间小、产生的碎片小,栈自动释放而堆需要GC。
5. 尽量使用基本数据类型代替对象,尽可能使用基本类型代替包装类型。
基本类型跟包装类型、对象产生的内存区域是不同的,基本类型数据产生和处理都在栈中,包装类型属于对象,在堆中产生实例。
String str = "hello"; 上面这种方式会创建一个"hello"字符串,而且JVM的字符缓存池还会缓存这个字符串;
String str = new String("hello"); 此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o。
6. 尽量不要将资源清理放在finalize方法中,由于GC的工作量很大,回收Young代内存时会引起应用程序暂停,使用finalize方法进行资源清理,会增加GC负担,程序运行效率更差。
尽量在finally块中释放资源,避免资源泄漏。
7. 尽可能减少使用synchronize,同步对系统开销大甚至形成死锁,尽可能避免无谓的同步控制,synchronize的方法尽可能小,因为被锁住的方法执行完,其余线程无法调用当前对象的其余方法。
单线程应尽量使用HashMap、ArrayList,因为HashTable、Vector等使用了同步机制,下降了性能。
尽量合理的创建HashMap,建立较大hashMap时,避免多次扩容,减少进行hash重构的次数,准确估计大小,利用public HashMap(int initialCapacity, float loadFactor)构造函数来设置初始容量和加载因子。
8. 合理选择String、StringBuilder 、StringBuffer。
StringBuffer:线程安全(内部大量使用synchronized机制)但效率低,提前估算容量避免重建提高效率StringBuffer buffer = new StringBuffer(1000);
StringBuilder:线程不安全、效率块。
String:不可变。String的引用在栈、String的值是一个字符数组,字符数组的值存放在常量池中。(字面常量存在常量池)
- String str = "ABC"; str 在栈,"ABC"在常量池中。
- String str = new String("ABC"); str的value存在堆中(为字符数组的引用),字符数组存在常量池中。
9. 减少对变量的重复计算:for(int i=0;i<list.size();i++)
改为 for(int i=0,len=list.size();i<len;i++)
System.arraycopy() 循环复制数组
10. 没必要的建立,比如:判断条件满足时再new 相关对象等。
11. 对象释放前置:局部变量在方法结束后会变成垃圾,比如:方法1中没必要obj设为null,方法2中可以提前将不用的Obj设为null。
1.
Public void test(){
Object obj = new Object();
……
Obj=null;
}
2.
Public void test(){
Object obj = new Object();
……
Obj=null;
//执行耗时,耗内存操做;或调用耗时,耗内存的方法
……
}
12. 位运算符代替乘法、除法操作更高效。nt num = a * 2; int num = a / 2;
改为int num = a << 2; int num = a >> 2;
13. 合理的选用容器
4、其他相关优化方向
1. ListView的优化:老生常谈,convertView复用、ViewHolder的使用避免getView中执行耗时操作,根据页面滑动的速度来控制异步是否开启,开启硬件加速等。ListVIew优化
现在很难见到ListView了,更多使用的是RecyclerView,RecyclerView优化。
2. 线程优化:线程的创建和销毁影响性能的开销,大量的线程互相抢占资源还容易导致堵塞,互必要时采用线程池,避免程序中出现大量的Thread。
3. 尽量少用枚举,枚举占用的内存空间比整形大。
4. 使用Android特有的数据结构,具有更好的性能。
5. 充分利用内存缓存和磁盘缓存。
6. 强软弱虚,了解jvm等。
5、应用瘦身
1. 只使用一套图片,xxhdpi : 1080P ;
2. 资源图大小png>jpg,更推荐转成 webp格式, WebP既支持有损压缩也支持无损压缩, 谷歌(google)开发的一种旨在加快图片加载速度的图片格式
3. 代码逻辑简化、无用文件、无用依赖库。
4. 微信的安装包压缩。
5. Android设备的CPU类型(ABIs):
armeabi-v7a: 第7代及以上的 ARM 处理器。2011年15月以后的生产的大部分Android设备都使用它.
arm64-v8a: 第8代、64位ARM处理器,很少设备,三星 Galaxy S6是其中之一。
armeabi: 第5代、第6代的ARM处理器,早期的手机用的比较多。
x86: 平板、模拟器用得比较多。
x86_64: 64位的平板。
ndk {
abiFilters "armeabi-v7a"
// abiFilters "armeabi-v7a","x86"
}
6.开启minifyEnabled混淆代码可大大减少APP大小。
7.删除无用的语言资源,如内陆应用只保留zh中文资源。
android {
defaultConfig {
resConfigs "zh"
}
}
8. 代码混淆,使用proGuard 代码混淆器工具,它包括压缩、优化、混淆等功能。
9. 图片资源压缩,压缩地址
10. arsc优化:减少string国家化支持的类型,只保留中文、英文等必要国家语言,gradle配置国际化语言支持。
11. 相关资源、so库等做成动态下载。