Android 性能优化篇
布局优化
布局优化的思路是减少布局层次。
1.同等层次的情况下,能使用 LinearLayout 和 FrameLayout 实现的效果,不用 ReleativeLayout 。
2.布局复杂的情况,使用 LinearLyout 需要增加层次的情况下,使用 ReleativeLayout ,可以减少层次
3.更加复杂的布局,使用约束布局 ContraintLayout
4.使用 include 复用布局
5.include 配合 merge 减少层级
6.使用 ViewStub 按需加载
三大布局标签的使用方式
include:提高布局复用性
merge标签:一边用于布局中的根节点,当根布局和内层子布局一致的话,就可以将根布局设置为merge标签,如果是merge标签的话,只会绘制渲染一次布局所以更省时,
ViewStub标签:(惰性标签):ViewStub 是一个不可见的,没有尺寸, 主要用于实现 View 的延迟加载,可以避免浪费资源,减少布局的绘画,只有需要的时候才会加载。在 ViewStub 加载完成后就会被移除, 如果其他的View,虽然把View的初始可见View.GONE,但是在Inflate布局的时候View仍然会被Inflate,也就是说仍然会创建对象,会被实例化,会被设置属性。也就是说,会耗费内存等资源。但是ViewStub只能Inflate一次,之后会被置空,
应用场景:首次出现的引导页 sp判断 那么这个引导页就可以用ViewStub标签包裹
布局背景绘制
1.不需要绘制背景的 View ,去掉背景图
内存优化(内存泄露,内存溢出(OOM)(图片过多,图片过大,巨型图))
概念:由于某种原因,造成某个对象的引用没有被及时回收,所以造成内存泄露。原因:非静态匿名内部类默认持有外部类对象的引用,当内部类中的方法执行耗时操作的时候,程序退出,内部类还持有外部类对象的引用,造成外部类对象的引用不能被及时回收,所以就造成内存泄露.
解决方法:(1)暂停内部类中的方法执行,(2)将匿名内部类改成静态内部类
1.在使用单例的时候,如果需要传入Context的话,不要用this,因为单例的生命周期和整个应用程序的生命周期一样长的,当Activtiy退出的时候,单例还持有Activity对象的引用,所以Activity对象的引用不能被及时回收,就出现内存泄露,解决方法就是:getApplicationContext()代替this ,(一方面单例和上下文的生命周期一样长,另一方面单例不持有Activity对象的引用了)
2.如果Handler中有延迟任务或者等待执行的任务队列过长,都有可能因为Handler继续执行而导致Activity发生泄漏首先,非静态的Handler类会默认持有外部类的引用,如Activity等。然后,还未处理完的消息(Message)中会持有Handler的引用。还未处理完的消息会处于消息队列中,即消息队列MessageQueue会持有Message的引用。解决方法: 1.静态内部类默认不持有外部类的引用,所以改成静态内部类即可。同时,可以采用弱引用来持有Activity的引用2. 在Activtiy退出的onDestory()里面移除Handler的任务,mHandler.removeCallbacksAndMessages(null);
3.List集合在使用的时候也容易出现内存泄露
list = new ArrayList<Object>();
for (int i = 0; i < 10; i++) {
Object object = new Object();
list.add(object);
object = null;
}
虽然object对象置为null了,但是list集合还持有object对象的引用,从而是object对象不能被及时回收,造成内存泄露解决方法:在退出Activity的时候在onDestory里面将list.clear(),list=null;
4.RxJava在使用RxJava时,如果在发布了一个订阅后,由于没有及时取消,导致Activity/Fragment无法销毁,导致的内存泄露,解决方法:定义一个compositeDispose对象,然后将所有的任务开关都存放到这里面,然后在Activity或者Fragment退出的时候,及时清空释放compositeDispose
5. WebView在加载网页的时候,因为网速的加载慢问题,当Activity退出的时候, WebView还在后台执行,还持有Activity对象的引用,所以在Activity退出的时候及时释放WebView
6.数据库,数据库游标没有被及时关闭 数据库和数据库游标要及时关闭,广播和EventBus,Butterknife(黄油刀),在不使用的时候及时取消注册,IO流在操作完之后也需要及时关闭
7.Bitmap 优化(Bitmap 是个吃内存大户,需要对 Bitmap 做好优化,Bitmap 优化比较简单)
对图片质量压缩 对图片尺寸压缩,加载巨图,显示局部,使用 .so 库进行压缩(下面有图片压缩的概念以及代码)
操作Bitmap对象的时候,要把Bitmap对象及时释放,及时置为null
内存泄漏优化
内存泄漏是 app 优化的一个重点,需要从两个方面重视,平时开发避免写有内存泄漏的代码,使用工具进行检测,内存泄漏优化,单列模式或静态变量造成内存泄漏 ---> 单例或者静态变量引用生命周期长的对象比如 Application 的 context,集合造成内存泄漏 ---> 不使用的集合清空,内部类或者匿名内部类造成内存泄漏 ---> 使用静态内部类或者外部类,或者使用弱引用,资源使用完未关闭造成泄漏 ---> 使用完以后关闭,解绑观察者
图片压缩
图片过大(体积大,占用的手机存储空间大 和占用的内存大)延伸:OOM主要就是说占用的内存过大,图片压缩
场景:程序出现OOM(内存溢出)OOM(内存溢出):Android 虚拟机会给每个app应用程序分配一个内存,一般是128M,256M,如果这个app中图片数据超过这个界限,那么就会出现内存溢出OOM,Java虚拟机 JVM Android虚拟机:Dalvik虚拟机(Android4.4之前), ART虚拟机。
1.基于内存的压缩:图片的二次采样(采样率压缩) 原理:1.第一次只是解析当前这张图片的宽和高,然后需要获取当前(屏幕)的宽和高,2.计算出压缩比率,(让图片的宽和高除以屏幕的宽和高),3.将压缩比率赋值给BitmapFactory.Options.insampleSzie()这个方法,4.第二次根据压缩比率来压缩解析这张图片.
代码如下:BitmapFactory.Options options = new BitmapFactory.Options();//第一次并不是解析整张图片 只是解析图片的边界
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.a, options);
//计算压缩比率
int scale;
int outWidth = options.outWidth;
int outHeight = options.outHeight;
int widthScale = outWidth / width;
int heightScale = outHeight / height;
if (widthScale > heightScale) {
scale = widthScale;
} else {
scale = heightScale;
}
options.inSampleSize = scale + 10;
//第二次才是真正的解析显示图片
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.a, options);
mIv.setImageBitmap(bitmap);
如何知道图片占用多大内存呢?
图片的格式和图片占用的占用的内存大小的关系?
在utf-8编码格式下 一个汉字占3个字节 byte "学Java"占用多少字节? 一个字母占用1个字节 byte 1个字节== 00000001 8位
单色位图: 每个像素最多可以表示2种颜色,只需要使用长度为1的二进制位来表示,因此每个像素占1/8byte1个像素===1位(1个二进制位)
16色位图: 每个像素最多可以表示16种颜色,所以只需要长度为4的二进制表示,因此每个像素占1/2byte
2的4次方 1个像素===4位(4个二进制位)
256色位图: 每个像素最多可以表示256中颜色,所以只需要长度是8的二级制位表示就可以了,因此每个像素占1byte2的8的次方
1个像素===8位(8个二进制位)====== 1byte 24位位图:即RGB三原色位图 每个像素占3个byte 1个像素====24位(24个二进制位)=====3byte
GIF格式的图片
1个像素点====8位(8个二进制位)=====1byte
JPEG格式的图片
1个像素点====24位(24个二进制位)=====3byte
PNG格式的图片(32位位图):
1个像素点=====32位(32个二进制位)======4byte
1PB===1024TB 1TB===1024GB 1GB===1024MB 1MB===1024kB 1KB===1024Byte
1Byte====8位(8个二进制位) 计算机内存里面这样存 0000 0001
第二种:缩放压缩 Martix 矩阵压缩:基于存储空间的压缩(图片的质量压缩),基于存储空间的压缩(质量压缩,体积压缩)
场景:后台服务器存储很多图片的情况下,compress()压缩;
InputStream inputStream = response.body().byteStream();
int line = 0;
byte[] buffer = new byte[1024];
//字节数组输出流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//storage/sdacrd/
File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/xxxxxxxxxxxx.jpg");
FileOutputStream fileOutputStream = new FileOutputStream(file);
while ((line = inputStream.read(buffer)) != -1) {
bos.write(buffer, 0, line);
}
Bitmap bitmap = BitmapFactory.decodeByteArray(bos.toByteArray(), 0, bos.toByteArray().length);
//质量压缩方法 参数1:压缩后的图片格式,参数2:压缩比率 (值越小,压缩比率就越大)参数3:压缩后保存的输出流
boolean compress = bitmap.compress(Bitmap.CompressFormat.JPEG, 10, fileOutputStream);
怎么避免图片过多造成的内存溢出?(基于内存压缩+二级缓存机制)?
Android图片三级缓存的原理如下图所示:可见,Android中图片的三级缓存主要是强引用、软银用和文件系统。
Android原生为我们提供了一个LruCache(LRU(Least Recently Used)最近最少使用算法),其中维护着一个LinkedHashMap,首先不允许键值为空,然后是线程安全,put的次数加一,size增加,以键值对的形式存入LinkedHashMap,如果之前已经存在了这个键值对,size减少成原来的大小,如果容量超过maxsize,将会删除最近很少访问的entry。LruCache可以用来存储各种类型的数据,但最常见的是存储图片(Bitmap)。LruCache创建LruCache时,我们需要设置它的大小,一般是系统最大存储空间的八分之一。LruCache的机制是存储最近、最后使用的图片,如果LruCache中的图片大小超过了其默认大小,则会将最老、最远使用的图片移除出去。
但LruCache只是管理了内存中图片的存储与释放,如果图片从内存中被移除的话,那么又需要从网络上重新加载一次图片,这显然非常耗时。对此,Google又提供了一套硬盘缓存的解决方案:DiskLruCache(非Google官方编写,但获得官方认证)。DiskLruCache并没有限制数据的缓存位置,可以自由地进行设定,但是通常情况下多数应用程序都会将缓存的位置选择为 /sdcard/Android/data/<application package>/cache 这个路径。选择在这个位置有两点好处:第一,这是存储在SD卡上的,因此即使缓存再多的数据也不会对手机的内置存储空间有任何影响,只要SD卡空间足够就行。第二,这个路径被Android系统认定为应用程序的缓存路径,当程序被卸载的时候,这里的数据也会一起被清除掉,这样就不会出现删除程序之后手机上还有很多残留数据的问题。
当图片被LruCache移除的时候,我们需要手动将这张图片添加到软引用(SoftReference)中。我们需要在项目中维护一个由SoftReference组成的集合,其中存储被LruCache移除出来的图片。软引用的一个好处是当系统空间紧张的时候,软引用可以随时销毁,因此软引用是不会影响系统运行的,换句话说,如果系统因为某个原因OOM了,那么这个原因肯定不是软引用引起的。
下面叙述一下三级缓存的流程:
当我们的APP中想要加载某张图片时,先去LruCache中寻找图片,如果LruCache中有,则直接取出来使用,如果LruCache中没有,则去SoftReference中寻找,如果SoftReference中有,则从SoftReference中取出图片使用,同时将图片重新放回到LruCache中,如果SoftReference中也没有图片,则去文件系统中寻找,如果有则取出来使用,同时将图片添加到LruCache中,如果没有,则连接网络从网上下载图片。图片下载完成后,将图片保存到文件系统中,然后放到LruCache中。
绘制优化(自定义view)
绘制优化指在 View 的 onDraw 的操作优化,不要在 onDraw 方法创建新的局部对象, onDraw 会比较频繁的调用,会创建大量的布局对象,不仅占用内存,而且系统更加频繁 gc,不要在 onDraw 方法中执行耗时任务,也不能执行大量的循环操作,循环会大量占用 CPU 时间片,造成绘制不流畅。
启动速度优化
启动速度分为冷启动,热启动和温启动,冷启动指应用程序从头开始,可能会出现白屏或者黑屏的情况,针对该问题的优化点,在 Mainifest 文件给第一个 activity 设置一个带有背景的 theme ,这样就不会白屏或者黑屏的,体验比较的好点,但是治标不治本在 Application 中的初始化内容,能往后挪的就往后挪,能异步的就异步处理
冷启动优化: https://developer.android.google.cn/topic/performance/vitals/launch-time
响应速度优化
不在 UI 线程做耗时操作,耗时操作异步处理,ANR 是指在 UI 线程做了耗时操作:Activity 5秒,Service 20秒,BroadcastReceiver 10秒
RecyclerView 优化
简述:RecyclerView 作为 ListView 的继任者,针对 ViewHolder 做了缓存处理,但是针对大量的列表还是需要做优化的,避免在 BindViewHolder 中执行耗时操作,列表滑动过程中不适合开启大量的异步任务,分页加载数据
1.在RecyclerView的onBindViewHolder中只将数据设置给控件,业务逻辑的处理需要放在别的类中,因为RecyclerView的onBindViewHolder方法是在UI线程进行的,如果在该方法进行耗时操作,会影响滑动的流畅性。
2.分页加载后台数据,对后台获取的数据进行缓存,提高二次加载速度(用OkHttp缓存拦截器设置缓存拦截器并缓存数据)以空间获取时间
3.对于新增的或者删除的数据进行局部刷新,
mAdapter.notifyItemRangeInserted(position, count);
mAdapter.notifyItemRangeRemoved(position, count);
mAdapter.notifyItemMoved(fromPosition, toPosition);
mAdapter.notifyItemRangeChanged(position, count, payload);
4.在滑动RecyclerView的时候尽量不做复杂布局和图片的加载,在滑动停止的时候再加载图片
通过recyclerView.addOnScrollerListenner()监听,里面有三种状态,分别是滑动静止状态,滚动滑动状态,惯性滑动状态
5.减少布局层级,可以考虑使用ConstraintLayout(约束布局)来减少层级,或者更合理的设置布局来减少层级。
6.通过setHasFixedSize将RecyclerView设置成固定高度,避免RecyclerView重复measure调用。这个方法可以配合RecyclerView的wrap_content属性来使用,比如一个垂直滚动的rv,它的height属性设置为wrap_content,最初的时候数据集data只有3条数据,全部展示出来也不能使RecyclerView撑满整个屏幕,如果这时我们通过调用notifyItemRangeInserted增加一条数据,在设置setHasFixedSize和没有设置setHasFixedSize你会发现rv的高度是不一样的,设置过setHasFixedSize属性的RecyclerView高度不会改变,而没有设置过则rv会重新measure它的高度
7.减少ItemView监听器的创建对ItemView设置监听器,不要对每个item都创建一个监听器,而应该共用一个XxListener,然后根据ID来进行不同的操作,优化了对象的频繁创建带来的资源消耗。
8.共用RecycledViewPool在嵌套RecyclerView中,如果子RecyclerView具有相同的adapter,那么可以设置RecyclerView.setRecycledViewPool(pool)来共用一个RecycledViewPool。
9.尽量减少View对象的创建一个稍微复杂的Item 会包含大量的 View,而大量的 View 的创建也会消耗大量时间,所以要尽可能简化 ItemView;设计 ItemType 时,对多 ViewType 能够共用的部分尽量设计成自定义 View,尽量减少View 的构造和嵌套。
10.及时回收资源,通过重写RecyclerView.onViewRecycled(holder)来回收资源。
线程优化
线程优化的思路是使用线程池,避免创建大量的 Thead,因为创建和销毁线程也需要花费时间的,使用线程池可以做到线程的复用。针对实际业务需求,做定制化的线程池。
线程池:线程池概念来源于Java中的Executor,它是一个接口,真正的实现为ThreadPoolExecutor。ThreadPoolExecutor提供了一系列参数来配置线程池。
优点
1.重用线程池中的线程,线程在执行完任务后不会立刻销毁,而会等待另外的任务,这样就不会频繁地创建、销毁线程和调用GC。
2.有效控制线程池的最大并发数,避免大量线程抢占资源出现的问题。
3.对多个线程进行统一地管理,可提供定时执行及指定间隔循环执行的功能。
动画优化
在实现动画效果时,需要根据不同场景选择合适的动画框架来实现。有些情况下,可以用硬件加速方式来提供流畅度。
包大小优化
包的大小很关键,以我工作单位的包为例,目前已经到了53M 大小了,该项目在开始的时候才1M。53M下载安装还是很费流量和时间的。包大小优化 压缩 png,jpg 资源,能使用 XML 实现的效果,不使用图片,把 png,jpg 转换为 webp,删除无用的drawable 资源,删除 asset 无用资源开启代码混淆,即 minifyEnable = true ,开启混淆,会帮忙删除不用的代码,压缩和优化
性能建议
1.避免创建过多的对象
2.不要过多使用枚举,枚举比整形内存占用空间大
3.常量使用 static final 修饰
4.使用 Android 特有的数据结构,比如 SparseArray
5.适当使用 软引用和弱引用
6.采用内存缓存和硬盘缓存
7.尽量使用静态内部类,不要采用匿名内部类和内部类
工具
内存泄漏工具- LeakCancary,MAT
Android Profiler: https://developer.android.google.cn/studio/profile/android-profiler
CPU Profiler:https://developer.android.google.cn/studio/profile/cpu-profiler
Memory Profiler:https://developer.android.google.cn/studio/profile/memory-profiler
Network Profiler:https://developer.android.google.cn/studio/profile/network-profiler
布局优化工具
Layout Inspector(Android Studio->Tools->Android->Layout Inspector)
说明:通过该工具,可以截取到当前页面的布局树,从根目录 DecorView 开始,很清晰明了的显示出来了布局深度,可以针对的减少布局层次,对布局优化很有帮忙。
卡顿工具 BlockCancary
启动优化
Android Device Monitor
在需要优化的地方插入 Debug.startMethodTracing("timeTrace") 和 Debug.stopMethodTracing() ,然后在系统的根目录下找到 timeTrace.trace 文件,拿出来用 Android Device Monitor 打开,可以看到每个方法花费的时间和调用的次数,以及谁调用的他,他调用了谁,进一步优化节省每一 ms ,减少冷启动启动时间。
静态代码工具
Inspect Code - Android Lint
完成功能开发以后,运行Inspect Code,位于 Analyze -> Inspect Codes
耗电优化
在移动设备中,电池的重要性不言而喻,没有电什么都干不成。对于操作系统和设备开发商来说,耗电优化一致没有停止,去追求更长的待机时间,而对于一款应用来说,并不是可以忽略电量使用问题,特别是那些被归为“电池杀手”的应用,最终的结果是被卸载。因此,应用开发者在实现需求的同时,需要尽量减少电量的消耗。在Android5.0以前,在应用中测试电量消耗比较麻烦,也不准确,5.0 之后专门引入了一个获取设备上电量消耗信息的 API:Battery Historian。Battery Historian 是一款由 Google 提供的 Android 系统电量分析工具,和Systrace 一样,是一款图形化数据分析工具,直观地展示出手机的电量消耗过程,通过输入电量分析文件,显示消耗情况,最后提供一些可供参考电量优化的方法。除此之外,还有一些常用方案可提供:1.计算优化,避开浮点运算等。2.避免WaleLock使用不当。3.使用Job Scheduler,使用·方法:https://www.jianshu.com/p/aa957774a965。