Android应用性能优化——学习心得

来源:互联网 发布:当今网络强国有哪些 编辑:程序博客网 时间:2024/06/05 01:57

Android应用性能优化——学习心得
Android应用性能优化这门课分为内存优化、视图优化、电量优化、Bitmap优化、其他优化等五大部分,下面这对这五大部分的学习能容做一下总结:
一、 内存优化
内存优化这部分内容,从以下五个方面进行了学习:
1. Android中的垃圾回收机制
学习了什么是垃圾回收;有什么缺点;Android的垃圾回收机制;Young Generation,Old Generation,Permanent Generation三个区存放的内容及管理机制,并以一个垃圾回收实例来学习了Android进行垃圾回收的过程。
2. 内存泄露的危害
 应用程序分配了大量不能被回收的对象
 系统可分配内存越来越少
 新对象的创建需要的内存不够
 GC之后再分配
 60fps
3. 检测内存泄露的工具
 Memory Monitor
使用Memory Monitor可以选定要监控的设备和监控的应用程序,图中蓝色部分表示我们使用的内存,灰色部分表示空闲的内存,图中的每个峰值代表进行了一次垃圾回收。每次垃圾回收之后,使用的内存都有一个峰值跌倒了一个较低的值。(先运行程序,然后点击“View”-“Tool Windows”-“Android Monitor”)
 Allocation Tracker
内存抖动
内存抖动是因为短时间内大量的对象被创建又马上被释放。瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,会触发GC从而导致产生的对象又很快被回收,即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。
总结:
1) 定位代码中分配的对象的类型,大小,时间,线程,堆栈等信息
2) 定位内存抖动问题
3) 配合Heap Viewer一起定位内存泄露问题
4)使用复杂
 Heap Viewer
可以实时的展示应用程序在运行时所有已分配的对象的数量大小以及类型信息。可以检测内存泄露的问题。
总结:
 内存快照信息
 每次GC之后收集一次信息
 查找内存泄露利器
 使用复杂
 LeakCanary
1) 检测内存泄露的库(A memory leak detection library for Android and Java.)
2) https://github.com/square/leakcanary
3) 查找内存泄露的神器
常见的内存泄露问题
1)单例造成的泄露
2)非静态内部类的静态实例造成的泄露
3)Handler造成的内存泄露
4. 如何避免内存泄露
 尽量不要静态变量引用Activity
 使用WeakReference
 使用静态内部类来代替内部类(静态内部类不会持有外部类的一个引用的)
 静态内部类使用弱引用来饮用外部类
 在声明周期结束的时候释放资源
5. 如何减少内存的使用
减少内存使用主要是为了避免前面讲到的内存抖动问题,因为应用程序内存使用降低了,垃圾回收收的频率也会低一些,这样的话产生内存抖动的概率也就会降低。从而使我们的运行程序运行更平缓一些。
 使用更轻量的数据结构(比如SpareArray代替HashMap)
 SpareArray比HashMap占用的内存小一些,
 避免在onDraw方法中创建对象
 onDraw方法调用的频率可能会非常高,如果我们在onDraw方法中,创建对象的话,会导致我们创建大量的临时对象,这样的话会很容易产生内存抖动现象。如果一定要在onDraw方法创建对象的话,建议使用对象池的方法。
 对象池(Message.obtain())
对象池相当于一个对象缓冲,每一次并不是创建一个新的对象,而是从这段缓冲内存里面查找是否有可用的对象,如果没有的话才会创建一个新的对象出来,一个比较常见的例子就是:Handler中我们经常会使用Message.obtain()这个方法来获取一个Message对象,其实这里就是相当于在一个Message对象池中获取。
 LRUCache
可以帮助我们大大的减少内存的使用,
 Bitmap内存复用,压缩(inSampleSize,inBitmap)
 StringBuilder来代替String ,尤其是我们在大量使用Sring拼接的时候,使用StringBuilder可以节省不少的内存,
二、 试图优化
1. 60fps
如果要让大脑感受到一个非常平滑的动画效果的话,至少需要24fps,24fps这个数字很神气,这也是我们在日常生活中所看到的电影所采用的帧率,电影领域大量使用24fps技术,一方面效果已经很不错了,另一方面可以很好的控制他们的成本。
当帧率达到60fps的时候,对我们的大脑来说是一个最流畅的效果,一旦超过这个数字的话大部分人的大脑是感受不到什么差别的,这就是Android中为什么使用60fps,因为只有达到60fps才能让用户感受到足够的流畅程度,我们的大脑对掉帧的现象非常敏感,什么是掉帧呢?本来正常的情况下是60fps,突然某个时刻掉到了30fps或者50fps,这个时候,我们的大脑会明显地感觉到有一个卡顿,这也就是我们在开发Android类的应用程序的时候对掉帧这么敏感的原因。因为掉帧会产生明显的卡顿现象,对用户体验非常不好。
想要打造一款性能卓越的APP,作为一个程序员我们必须了解视图渲染得方方面面,尤其适合硬件想过的过程。因为硬件决定了我们的视图是如何渲染的,只有了解硬件渲染视图的机制,我们才能找到一些视图渲染的方法以及排查我们在视图渲染中遇到的一些问题。
2. VSYNC
1) 刷新率:硬件的刷新频率,绝大部分屏幕的刷新频率为60HZ
2) 帧率:GPU一秒内绘制的帧数
3) 帧率和刷新率一起配合才能保证我们的应用程序完美流畅的渲染出来(视频中进行了举例说明)并模拟了当帧率>刷新率情况下,所产生的屏幕显示过程中的“撕裂”现象产生的过程和原因,以及为了解决这种问题,Andriod中引入了双缓冲概念。用实例分析了双缓冲机制。
在帧率<刷新率的情况下,例子中,屏幕的刷新率为60HZ,GPU的帧率为58HZ,有可能屏幕两次显示的图像是GPU渲染的同一帧数据,出现这种情况,会产生一种暂停或卡顿效果,
3.检测UI卡顿及过度绘制
过度绘制:以刷墙为例介绍了什么是过度绘制。过度绘制往往是影响应用程序UI性能的一个重要原因,不过值得庆幸的是Google为我们提供了一个非常方便的工具,可以方便的显示应用程序哪些部分存在过度绘制现象。
过度绘制往往是影响应用程序UI性能的一个重要原因,不过值得庆幸的是Google为我们提供了一个非常方便的工具,可以方便的显示应用程序哪些部分存在过度绘制现象。
1)打开查看过度绘制的工具,在开发者选项中,勾选Show GPU Overdraw选项就可以了,回到应用程序,我们的应用程序会被盖上一层各种颜色组成的蒙板。Show GPU Overdraw这个工具主要是通过各种颜色来通知我们当前区块的像素存在几次绘制现象。
2)GPU Profiler——(Google为我们提供的另一个一个非常方便的工具)
快读查看渲染时间,对比16ms,在开发者选项中,点击Profiler GPU rendering选项,弹出一窗口,选择On Screen As Bars Pick This,即时在屏幕上画出柱状图,下面一选项是:是到ADB Dump里面去,通常选择On Screen As Bars Pick This。
4. 避免UI卡顿
1)避免在onDraw中创建对象对象池
2)减少View层级
3)避免在UI顶层使用RelativeLayout、Measure两层
4)自定义空间控制绘制的复杂度
5. 优化过度绘制
其实绝大部分Android的UI相关的性能问题,都是由于过度绘制所引起的,我们只要掌握了一些优化过度绘制的方法,就可以解决遇到了绝大部分UI相关的性能问题,解决优化过度绘制主要有一下常用方法。
1) 降低View的层级,
View嵌套过多的话,就有可能产生过多的绘制现象,因为有可能你的每一层都需要绘制一遍,嵌套层级多自然绘制次数也多,降低层级的最常用的方法如下:
第一个方法就是:使用RelativeLayout代替LinearLayout,因为LinearLayout本身比较节能,提供的功能也比较少,如果一个布局,要用LinearLayout来实现的话,可能需要嵌套多层的LinearLayout才能达到一定效果,而使用RelativeLayout可能只需要一层就能达到需要的效果,因为RelativeLayout可以方便的指定各个绘制元素的位置,
第二个方法:使用Merge标签,因为使用Merge标签之后,不需要再为Merge进来的Layout复容器,如果不使用Merge标签,需要为这个Layout制定一个根容器,这个根容器在很多情况下是多余的,如果要去掉更容器的话,我们可以使用Merge标签。
2、去掉比必要的背景
去掉Window默认的背景:因为Android在运行的时候会默认在根容器上为我们的window添加一个默认的背景,其实很多时候因为我们本省的Layout的根容器的宽和高是和整个Window的宽和高是一致的,如果我们已经在根容器上设置了背景色,其实Window这个默认的背景色是可以去掉的,因为我们如果在根容器上设置了一个背景色,而Window本身又提供了一个背景色,这两个背景色的存在,就会导致两次绘制现象。
去掉不必要的背景:自己写的UI当中,如果每个子控件都提供了自己的背景色,那么很多时候我们就要考虑,是不是可以把这些控件所在容器的背景色去掉,因为既然每个子元素都提供了背景色,那么容器就不需要背景色了,这样就可以减少一次容器的绘制。
3、ClipRect&QuickReject
我们制作一些自定义控件的时候,我们可以通过优化onDraw方法来对我们的UI渲染进行优化,我们知道,Android系统在进行显示的时候,会对一些系统的控件进行优化,比如,如果我的一个控件被侧拉菜单完全挡住了,那么Android在进行渲染的时候,其实默认是不会渲染那些完全被侧拉菜单挡住的控件的,这样就可以避免一些不必要的绘制操作,当时对于自定义控件,因为Android系统并不知道我自定义控件的onDraw方法里是如何绘制这个控件的,所以没有办法对自定义控件作优化,这个时候就需要程序员自己手动来进行一些优化处理,从而避免一些过度绘制问题。这里最常用的一个办法就是canvas本身提供的ClipRect,这个方法主要用来指定我当前的绘制区域,也就是说除了我这个方法指定的绘制区域以外的部分是不会被渲染出来的
4)ViewStub(最大的优点是当你需要时才会加载,使用他并不会影响UI初始化时的性能。)
ViewStub:可以这样理解,可以把它当做占位符来使用,我们在开发的时候,非常常用的一个场景就是,一个空间一开始显示的时候可能并不需要把它显示出来,只有当服务端返回了数据或者满足某个场景的时候,我们才有吧这个空间展示出来,如果我们不使用ViewStub,而默认使用一些常用的控件来代替ViewStub,就需要我们把空间控件的display属性设置为“none”,即使display属性设置为“none”,这个控件在我们进行inflate(用一个XML源填充view. inflate(上下文对象,资源文件Id,父窗口组一般为null);)的时候,其实也会被inflate出来,并且它也肯能会参与到其中的一些运算当中去,会占用到我们绘制时的一些系统资源,如果用ViewStub来代替这些常用的占位控件的话,ViewStub本省在inflate的时候不会被inflate出来,它本身也不会参与merg和Layout的过程,所以可以帮助我们节省一些系统的运算资源,只有当我们的View真正显示出来的时候,ViewStub才会进行merg和Layout的过程。
5).9图作背景
我们在开发的时候,有一个非常常用的场景就是,在使用ImageView的时候,可能需要为它制定一个带圆角的边框,当作背景,同时,我在ImageView中还要显示我要的图片,如果背景不使用.9图,而是使用普通的图片,然后再普通图片上又盖了一层我们想要显示的图片,这样就会导致两次绘制,使用.9图可以优化掉一次绘制过程,比如我们在使用.9图的时候,让.9图只有边框部分是有颜色的,中间的部分我们可以使用全透明的像素进行填充,Android在进行绘制的时候,对于全透明的这部分像素,2D绘制的时候其实是会优化掉的,这部分全透明像素其实并不参与绘制过程,这样的话,我们的ImageView控件最终只被绘制一次,也就是边框和真正的图片总共绘制一次就可以了。这样就可以减少一次绘制过程
三、 电量优化
本节主要介绍Android中,哪些操作比较消耗电量,以及如何进行优化从而达到节约电量的目的,如今我们处在一个移动的时代,基本上我们所有的日常办公需求都可以在手机上来完成,手机正在成为大家必不可少的一个工具,然而,令人沮丧的是,移动化发展了这么多年,其实电池的续航能力并没有多大的提升,多少人应该都深有体会的就是我的手机需要两天、一天、甚至半天就需要充一次电,往往在最需要的时候,会发现我的手机的电量已经悄悄地跑没了,更令人发指的是,很多APP开发者并没有意识到电量消耗问题的严重性,他们开发的APP,即使不在全台运行,也有可能在后台偷偷的消耗掉大量的电量,从而导致用户手机的电池消耗非常的厉害。
1)25%-30%消耗用在核心功能上,
这些核心功能指的是,比如我在打开一个浏览器的时候,绘制图片所消耗的电量;我们看一个新闻APP的时候,展示新闻所消耗的电量,以及我的玩游戏的时候,游戏画面渲染以及动画所消耗的电量,这些真正核心功能消耗的电量只有25%-30%左右,
(画图、布局、动画)
2)剩下的75%左右,
上传统计数据:这一点的话,写过APP的同学应该都深有体会,我的统计数据基本上是一直运行的
1. Android电量消耗介绍(主要来自一下三个部分)
网络:尤其是手机上的无线网络,其实是非常消耗电量的,因为手机在连接基站的时候,在启动相关的无线网络模块的时候,其实是非常慢也是非常消耗电量的,并且Android为了做一些优化,在无限网络模块启动之后,会让这个模块持续运行一段时间,这样后续如果再有新的请求来的话,就不需要重新启动网络模块,也就是说,我的网络模块从启动到启动一段时间是一直在运行,消耗电量的;
WaleLock:就是阻止我的Android系统进入睡眠状态,本来如果我的系统进入睡眠状态,其实是不怎么消耗电量的,因为这个是我CPU今本上不运作了,但是开启了WakeLock之后,就会把系统从睡眠状态唤醒,唤醒之后,CPU开始运作,消耗电量,如果长时间的打开WakeLock,或者忘记关闭的话,就会导致电池的消耗一直居高不下,
非即时的任务:写代码的时候,大家可能没有注意,把一些非即时的任务当做即时的任务来处理,其实很多任务往往我们在开发的时候,是可以延后做一些批处理,统一执行的,而不是非要在某一时刻即时执行,如果能够把这些非即时任务,消化掉的话,比如把它们转移到批处理里面去,可能会大大的较少APP的电量消耗。
查看电量消耗情况的工具
1)Battery
平时我们在开发的时候,可以方便的在设置里面找到我们应用占用电量的一些相关信息,下图可以看到在Battery选项中可以看到一些信息,看到每个应用所消耗点亮的比例
2)Batter Hsitorian
如果这个图的信息不能够满足你的需求的话吗,Google另外还提供了一个工具,Batter Hsitorian,这个工具可以提供一个更加详细的电池消耗的数据
2. 网络优化
我们知道,我们的手机在和基站通信的时候,是通过手机内置的一些芯片模块来做这些事情的,手机会和基站进行大量的数据交换,所有的数据交换都是走这些芯片的,正常下这些芯片并不是一直在全负荷的工作去消耗大量的电量的,大部分情况下这些芯片是处于休眠状体,也就是一个低功耗的状态,是不怎么消耗系统电量的,一旦有数据发送请求的时候,就会唤醒我们的芯片,这个时候,芯片就会处于一个高频、高负荷的状态,它所消耗的电量也会急剧的增加,一旦数据发送完了,芯片并不会立即停止工作,进入休眠模式,而是会保持一段时间的唤醒模式,这相当于是系统做的一个优化,因为每次唤醒芯片的操作,其实是非常慢的,Android对这方面进行了一些优化,这样的话在一段时间内连续有网络请求任务的时候,就不需要再重新唤醒芯片模块,但是这样依旧会带来另外一个问题,就是这段时间内芯片会一直高强度的消耗电量。
网络优化,有一点要特别注意,要避免出现轮循服务器的情况,因为轮循服务器的话,会造成大量无用的网络请求,你轮循的时候,服务器不一定有数据返回给你,同时导致网络模块多次被唤起,从而导致耗电量急剧的增长,如果一定要有这种轮循请求的话,不要发起这种向服务端的轮循请求,而是Google提供的GCM推送的方式,因为,GCM本身Google进行了非常多的优化,在省电这方面,Google其实做了很多工作,当然由于我们国内国情的原因,你可能GCM没法使用,大家也可以尽量应用国内第三方的推送服务,这些推送服务,他们在底层本省也是对耗电量这方面做了很多优化工作,使用这种第三方的推送服务,也比我们自己去轮循会省好多电量。
数据压缩
数据压缩一个主要的好处就是,它可以大大的缩短网络请求的时间,从而达到省电的目的,当然,数据压缩带来的另外一个代价就是,客户端在解压数据的时候,可能会额外消耗更多的CPU,但绝大多是情况下,CPU在解压这些数据的时候所消耗的电量和时间,远比网络请求获取未压缩数据的时间小很多很所,因为网络请求本身是一个非常慢的过程,所以是一个非常耗电的过程,尤其是像一些文本相关的内容,采用GZIP压缩之后可以极大地减少它们的体积,这方面在省电的效果上是非常的明显的;另外,还有一些像图片相关的数据,可以采用一些体积比较小的格式来替换,比如,可以采用webp格式来代替jpg格式的图片,因为webp格式本身的话,在相同质量的图片上,体积会比jpg小很多,这样的话在网络请求的时候,就可以更快的返回,从而,减少电量消耗
3. WakeLock
WakeLock指的是:一场景为例,很所时候APP可能需要长时间的进行一个任务,即使系统已经处于休眠状态,我们也要把系统唤醒,来进行我们的任务,这个时候通常是由WakeLock这个API来做这个事情的,因为WakeLock可以把我们的系统唤醒,来执行我们的代码,但是在使用WakeLock的时候一定要非常小心,因为WakeLock一旦使用不当可能导致系统一直在运行状态中,从而导致电量极具下降。
4. jobscheduler
它满足的场景就是,我们可以指定几乎任意一个场景,在这个场景只要满足的情况下,JobSchedule就会快速的快速的执行我们想要执行的任务,使用JobSchedule的另外一个好处就是,因为我可能会有好多程序,这些程序它们可能都有一些调度任务,可能是在后台运行的,然后这些,在后台执行的任务可能他们会共享一些公公的条件,比如,某些任务可能都是期望插上电源或者连上WIFI之后执行,如果我这些任务都通过调用JobSchedule这个API来做的话,那么,当我满足其中一个条件之后,JobSchedule会唤醒我当前所有想要执行的这些任务,这样的话其实就可以避免系统被多次的唤醒,然后在系统一次唤醒之后可以做尽可能多的事情,这样的话其实也达到了一个电量优化的目的。
四、 Bitmap优化
1. Android中Bitmap的解码和存储
1)常见的jpg,png,webp是图像的存储格式
其中jpg采用的是有损压缩格式,当图片的色彩比较丰富的时候,jpg格式的压缩率会比较明显;png最初是为了替换取代gif这种图片格式,采用的无损压缩的方式存数图片数据的;webp是Google近几年来新推出的一种图片格式,主要是为了克服jpg和png的一些缺点,比如webp可以再采用无损压缩的情况下,极大的压缩图片的体积,也就是说如果使用webp,我们可以在获取高质量的图片的同时又能保证我们图片的大小在一个合理的范围上,现在比较推荐的就是webp这种格式,但webp有一个问题可能就是Android4.2之后才延伸内置了对webp的支持,在Android4.2以前的话大家需要一些第三方的图片库,比如 , 来显示图片,我们通常所说的jpg,png,webp这些都是图像的存储格式。
2)Android中要显示图片必须要先进行解码(decode)读取数据到内存中
这些格式的图像想要在Android中显示必须要经过先进行解码(decode)这个过程,所谓的解码(decode)其实就是将图像的数据读取到内存中,然后,把它转换成GPU能够识别的格式,再由GPU渲染到我们的显示器上,所谓的解码过程其实可以理解为,我们常见的存储格式本身是一种编码格式,既然有编码就会有对应的解码的过程,Android的解码其实是将存储在内存中的这些格式的图片解码成我们的系统所能识别的一种格式。
3)BitmapFactory提供了常用的一些decode方法
Android解码图片主要是使用了BitmapFactory类提供的一些方法,这个类中有很多好用的静态方法来帮我们,从各种数据源中去解码我们想要的图片数据,Bitmap所占用的内存大其实和我们图片文件的所占内存大小关系并不是很大,比如像jpg这样的图片,本身的压缩率是比较高的,在解码之后可能获得相对比较大的一个图片数据。
4)图片真正占用的内存大小要看decode之后的数据大小
通常在JAVA中一张图片,decode之后其实是以一个字节码的形式存在的,我们可以在内存dump出来的数据中清晰地看到,一个位图所占用的字节的大小。

  1. 复用Bitmap
    Bipmap memeory
    1)<=2.3.3(api 10)
    Bipmap解码之后的数据存储在Native Memeory中(并不是在Dalvik虚拟机中,这块内存是在C++这一层,不是有JAVA来托管的)
    手动调用Recycle回收(调用Dalvik的Recycle方法来告诉系统回收这块内存)
    2)>=3.0(api 11)
    Bipmap解码之后的数据存储在Dalvik heap中
    Dalvik自动回收
    现在我们进行APP开发的时候,图片成为了一个必不可少的美化工具,每个APP都在使用大量的各种各样的图片,比如,图标、背景图等等,这些可能都需要一些美工精心制作的一些图来提升我们APP的用户体验,Bitmap已经越来越成为我们应用的内存的一个头号杀手,大部分APP的内存占用,绝大部分是被Bitmap所占用的,Bitmap已经成为导致内存抖动的一个头号元凶,因为大量的Bitmap频繁的分配和释放可能会导致,内存抖动现象的出现,我们知道因为Bitmap通常它所占用的内存区间都是比较大的,比如下图中,我们可以看到绿色区块代表Bitmap所占用的内存,每个Bitmap他所占用的内存可能相对其它对象要大很多,每当有一个新的Bitmap对象,被创建的时候,虚拟机要为新创建的Bitmap对象,去分配一块内存,因为Bitmap每次必须占用一块连续的内存空间,下图可以看到,已经没有一个足够大的内存空间可以容下我们,新创建的Bitmap对象了,这个时候系统就会对我们的内存进行一次垃圾回收,这样的话就可以为我们新创建的Bitmap对象腾出一个可用空间,一旦频繁地产生这种Bitmap创建,和垃圾回收就会导致内存抖动的发生,内存抖动就会影响APP的性能,可能会导致APP产生一些卡顿现象,从而带来一些非常不好的用户体验,也就是说我们在使用Bitmap的时候要尽量避免产生这种内存抖动现象,如何避免这种内存抖动呢?
    1)使用对象池避免了我在JVM上在进行一次内存分配或者说导致可能的垃圾回收现象。在Android实现这种效果的话,在Android中在对图片进行解码的时候是使用BitmapFactory来进行解码的,在使用BitmapFactory之前可以通过mBitmapOptions 类来指定一些参数(mBitmapOptions.inBitmap = mCurrentBitmap),这里可以指定mBitmapOptions的inBitmap属性指向一个已经创建的Bitmap对象的话,那么后面再解码新的图像数据的时候,就会服用这个指定Bitmap对象的内存空间,也就是说达到了刚才想要的效果,新创建的Bitmap对象并没有由JVM分配一块新的存储空间,而是复用了一个旧的Bitmap对象的空间,当然这个参数也有一定限制,比如:这个参数在SDK11-18上面的话,它的局限性是比较明显的,我想服用一个Bitmap内存的话,后面新创建的Bitmap对象的大小必须是和我原来的Bitmap的大小是以模一样的,这样才能保证在API11-18上达到复用的效果,如果宽和高不一致,是没有办法复用内存空间的,在API19之后Android对图片的复用做了一些改进,如果我新创建的Bitmap对象想复用已有的Bitmap内存,就不再强制要求新创建的Bitmap对象和原来的Bitmap对象宽和高一致,只要保证原来的Bitmap对象宽和高比新创建的Bitmap对象宽和高打就可以了。当然不光是宽和高这两个限制条件,图像的存储时候很多种像素格式的,所谓像素格式指的就是一个像素,需要几个字节来存储这个像素相关的信息,比如常见的像素格式有565,4444和8888这几种,其中8888表示,我每个像素是由4个字节来存储的,分别对应RGBA这四个值,如果是4444的话,它的每个像素占用两个字节,我们在复用Bitmap内存的时候,不光要考虑宽高因素,还要考虑像素,也就是说想服用的Bitmap的像素格式必须是和我已创建的Bitmap的像素格式是一致的,否则的话也无法复用。当然一个好的复用方法,我的对象池按照对象格式进行一些分类,如下图,我可以把对象池分为三类,非别对应565、4444和8888这三种像素格式,这样在进行Bitmap内存复用的时候,就比较方便了,首先,我先找到像素格式对应的对象池,找到对象池之后然后按宽和高去找可以复用的Bitmap就可以了,当然我们在平时开发的时候,很少手动的写代码来实现一个对象池,然后,实现内存复用这些相关的策略。
    有一个比较好的第三方库包glide做到了这些
  2. 减少Bitmap占用的内存
    另外一个我们在平常开发中,在使用Bitmap的时候,经常遇到的一个问题就是,我货渠道的原图可能非常的大,但其实我在屏幕上显示的是一张非常小的图,也就是说我想显示最终的图片的像素要比原始的尺寸要小很多,然而我在加载的时候加载的却是一张原图,这张原图所占用的内存远比我想显示的这张小图要大得多,这种情况下我们能够想到的一个办法其实就是,缩放这张Bitmap从而减少它所占用的内存,缩放Bitmap的话其实就是把这张图缩小到我们想要的尺寸,并且还能够保证图片的质量,这样的话就可以极大的减少图片的内存占用。
    Android当中本身提供了很多API去做这种事情,其中一个很常见的API就是,才createScaledBitmap(inBmp,64,128),其中,inBmp——制定定一个要进行压缩的Bitmap,后面连个参数为缩放后的高和宽,这个方法有一个却陷,第一个参数指定的Bitmap必须是加载到内存中的一张图片,这样的话就很难满足我们的需求,因为在使用这个API的时候其实我们已经把原图加载到内存了,他该占用的内存已经占用了,这个时候,我们再创建一个缩小的图,其实意义已经不大了。我们想要的是直接从原图加载一张缩小过的图到内存中,也就是,内存中只有缩小过的图,而没有原图,这是最理想的情况。
    Android SDK也为我们提供了这样的API,BitmapOptio这个类,有一个参数inSampleSize,这个参数是作为采样率来使用的,我们解码图像的时候,如果制定了inSampleSize,他就会按指定的采样率对图片进行采样,达到缩小的目的。
    inSampleSize是一个简单、粗暴、快速的方法,通常我们会把inSampleSize和inScaled方法结合起来使用,也就是我们首先会把图片缩到某个2的n次方分之一之后,然后再缩放成我们所要的比例,因为这样缩放的话可以在一定提升处理速度。那么问题来了,我们怎么知道inSampleSize的值呢?也就是我们如何获取到原图的宽和高,因为我们知道我们想要的目标Bitmap的宽和高,这个时候如果我们能够拿到原图宽和高的话,我们就可以利用某些算法来计算inSampleSize的值,我们又想获取到原图的宽和高,又不想把原图整个的加载到内存当中,Android为我们提供了API 如下图:我们使用
    mBitmapOptions.inJustDecodeBounds = true;
    之后,在解码数据的时候,就可以不把原图整个的加载到内存当中,可以通过(宽和高会放到mBitmapOptions对象中)
    srcWidth = mBitmapOptions.outWidth;
    srcHeight = mBitmapOptions.outHeight;
    来获取原图的宽和高。
    刚才讲的对Bitmap缩放,它是一个大的概念,其实是从整体上对我们的图片所占用的内存进行压缩采集,它是对整个Bitmap而言的,我们还有其它的方式,也可以将Bitmap所占用的空间减小,这种方法并不是通过裁剪Bitmap的宽和高来做,而是通过减少Bitmap中每个像素所占用的字节数,我们知道Bitmap是内存占用的大户,如果在使用Bitmap没有,注意内存分配这块问题的话,其实在操作Bitmap的时候会很容易触发GC操作,一旦触发了对系统的性能影响其实是很大的,也就是我们在使用Bitmap的时候可以通过尽量的减少Bitmap所占的内存,来达到优化应用程序性能的目的,这里讲的Pixel Format指的是图片每个像素所占的字节数,当每个像素占用的字节越少越好。
    这里有一点,需要注意在Android无论是jpg、PNG还是webp格式的图片,因为这些图片本身存储的原因,Android在对这些图片,进行解码的时候都是采用8888格式解码的,如果你想要指定其他格式,需要在mBitmapOptions.inPreferredConfig中手动指定想要的格式。指定之后Android会在解码完成之后,再去做一些运算转化为你所想要的格式,也就是说一旦你指定了非8888像素格式的值之后,其实是要消耗一定的额外CPU运算,来帮你进行转换,得到的好处就是内存的节约。另外图片解码是比较好使得,通常放在单独的线程里去做,通常不要方在UI线程里去做。会导致UI线程的卡顿。前面,讲的三个图片的优化,其实都是从图片的内存占用上着手的,其实对图片还有两外一个优化点就是:对图片本省的裁剪,对图片文件的裁剪,因为图片文件会占用用户大量的流量和带宽,如果能够对图片的体积进行减小的话,可以节约用户的下载时间,增进一些用户体验的,比如下图,在开发过程中png格式是一种非常常见的格式。
    我们可以通过一些工具来对我们的PNG文件进行压缩,如下图,Script PNG就是一种比较常见的工具,另外jpg格式本省不支持透明度,所以jpg通常会必PNG格式的图片小很多,如果在开发中不需要用到透明度的话,可以采用JPG格式,它的体积会小很多,若果用透明的的话也可以采取取巧的方法,用两张图片一张JPG和一张ALPHA图片来显示,效果也是明显的,如果你既想要一个比较好的压缩质量,又想要一个比较好的显示效果,同时有支持透明度的话,建议使用webp格式,也是Google力推的一种格式,在Android上已经有很多库,使它兼容到2.X上的
  3. LRUCache介绍
    关于Bitmap的优化,最后一个内容是,是在Android中广泛应用的一个技术,就是LRU cache可以实现对象池,因为我们一旦采用对象池之后,就需要采用一个算法来决定,当我的对象池已经满的时候,哪些对象会被替换出去?这里的LRU其实就是一种实现这种效果的算法,LRU的全称是 Least Recently Used ,LRU使用的原则就是,当需要从缓存中删除一个数据的时候,我会删除一个最近、最少使用的数据,它的决策依据就是如果缓存中某一个条目他如果最近被使用过,那么它在将来被使用的可能就很大,缓存中要尽可能保留住最近使用最频繁的数据,而那些最近最少使用的数据我就可以把它们当做过期数据,在下次需要清除的时候把它们删除或者替换掉,LRU cache通常会配合我们的缓存一起使用,因为图片加载到内存中之后,通常,也是放到LRU cache当中,如下图在LRU cache中已经放了上面四张图片,下次列表显示的时候,我又显示了一下第二张图片,这个时候第二张图片就变成了最近使用最频繁的图片,被移动到左右边,右边是使用频繁的图片,左边是最近使用最少的图片,也就是说我的LRU cache从左到右是按使用率由低到高的顺序排列的。
    当我的第二张图片被第二次访问的时候,我们它为第二次缓存命中,第二次命中之后它的优先级就会提高一些,这个时候就需要把给图片移动到缓存的最右边,移动玩就是下图的效果。
    这个时候如果我的缓存已经满了,但又有一张新图像要插入到缓存当中,那么第一张图片就是被删除的首选,因为它是最近最少用到的数据,在Android中使用LRU cache其实是比较简单的,如下图,其中availMemlInBytes为默认的LRU cache大小。
    五、 其它优化
  4. 查找优化CPU使用高的代码
    1)Method Profiling工具
    帮助我们在程序运行的时候,去收集每个方法运行时所消耗的时间,然后以图标的形式方便的展示出来。
  5. 使用lint检测性能问题
    该工具可以帮助我们检查一些潜在的问题,
    如何使用Lint工具,有三种方式,在Android studio中使用,选择Analyze,然后选择inspect code,然后弹出下面对话框之后,点OK就行了,
  6. Google提供的一些高效的工具类
    1) Android-Specific containers
    使用特定的容器来代替,在JAVA开发中常用的一些容器,像hashmap这种容器,
    JAVA有基本类型,也有包装类型,因为JAVA中基本类型不是一个对象,但是我们在使用hashmap这种容器的时候,要求它的key,value必须是一个对象,也就是说我想用基本类型,作为hashmap的key,value的时候,是不行的,要把这些基本的类型转化为它对应的包装类型,才能在hashmap中使用,JAVA为了方便我们的开发,它会在运行时提供一个基本类型到包装类型自动转换,以及包装类型到基本类型自动转换的过程,这个过程就是一个装箱和反装箱,
    比如,我们编写代码的时候,可以直接Integer value = 0;这样写(0是基本类型,value是包装类型)这里就发生了一次自动装箱操作,虚拟机会自动把0这个基本类型自动装为对应的Integer类型,注意:value作为一个包装类它占用的内存比基本类型int要大,int占4个字节,而Integer占用16个字节,比如右边,for循环中做了类似的操作,就会导致额外创建过多的对象,就可能带来潜在的性能问题。
    (1)SparseBoolMap取代HashMap
    避免装箱和反装箱可以采用我们在Android中的容器来替换HashMap容器,比如SparseBoolMap类型可以取代HashMap类型,
    SparseArry取代HashMap
    SparseArry,SparseArry在内存中存放的key和value就是基本类型而不是包装类型
    从而避免了装箱和反装箱操作,下面代码对比了两种类型的遍历方式,ArrayMap可以使用索引进行遍历,而HashMap只能使用迭代器遍历,一般来说索引遍历比迭代器遍历的效率要高,索引遍历是推荐的容器遍历方法。
    下面可以看到Android开发为我们提供的一系列SparseArry的实现如下:
    android.util.SparseArray
    android.util.SparseBooleanArray
    android.util.SparseIntArray
    android.util.SparseLongArray
    4.Enum
    注意枚举的使用,枚举对应用程序的影响主要涉及两个方面,一方面,对应用程序体积的影响;另一方面,是对运行时期对应用程序性能的影响。体积的影响可以看如下例子,比如我的应用程序一开始2556Bytes,然后在应用程序中添加一些int类型的常量,通过switch来进行判断,这时候应用程序增加了124Bytes,另外需要注意的是:它在运行时期也会导致占用的内存大大的增多。我们使用枚举,是因为枚举能带来一个非常大的好处,它可以帮我们避免很多类型错误,如果使用int类型的话,无法避免用户输入一些非法值的,如果不想做一些额外的判断,使用枚举可以把数据检查这部分工作,交给了编译器,我们相当于在编译期间就可以避免很多非法的输入。Android提供了相应的工具,可以方便的让我们用int来替代Enum类型,就是Support 库 中提供的一些注解,其中@IntDef注解,使用这个注解之后我就可以把我的常量声明成一个类似枚举的形式,后面在使用的时候其实编译器可以自动帮我们做检查,比如我在public abstract void setNavigationMode(@NavigationMode int mode)使用了NavigationMode之后,我们的编译器在编译的时候,它就会检查输入的mode值是否在IntDef的区间内。使用注解之后就可以替代枚举了,就可以达到既节省内存,又可以避免错误的发生。注意:Support库中提供了很多注解,这些注解非常方便,可以让IDE在最开发的时候就可以做很多静态的检查,避免很多错误。
    通过学习为期一个月的学习,了解Android应用性能优化方方面面的内容,后续还需要进一步消化吸收,并逐渐将这些方法和工具应用到自己的应用开发之中,真正掌握这部分内容,通过学习,可以感受到李侦跃老师经验非常丰富,跟着有经验的高级工程师学习,可以少走很多弯路,非常感谢李老师分享自己的经验和知识。
0 0
原创粉丝点击