大范围借鉴及自己思考总结的内容,感谢各位博主的分享。
基础概念
何为性能问题
(1). 响应时间
指从用户操作开始到系统给用户以正确反馈的时间。一般包括逻辑处理时间 + 网络传输时间 + 展现时间。对于非网络类应用不包括网络传输时间。
展现时间即:网页或 App 界面渲染时间。
响应时间是:用户对性能最直接的感受。
(2). TPS(Transaction Per Second)
TPS为每秒处理的事务数,是系统吞吐量的指标,在搜索系统中也用QPS(Query Per Second)衡量。TPS一般与响应时间反相关。
通常所说的性能问题就是指响应时间过长、系统吞吐量过低。
对后台开发来说,也常将高并发下内存泄漏归为性能问题。
对移动开发来说,性能问题还包括电量、内存使用这两类较特殊情况。
性能调优方式
明白了何为性能问题之后,就能明白性能优化实际就是优化系统的响应时间,提高TPS。优化响应时间,提高TPS。方式不外乎这三大类:
- (1) 降低执行时间,又包括几小类
- a. 利用多线程并发或分布式提高 TPS
- b. 缓存(包括对象缓存、IO 缓存、网络缓存等)
- c. 数据结构和算法优化
- d. 性能更优的底层接口调用,如 JNI 实现
- e. 逻辑优化
- f. 需求优化
- (2) 同步改异步,利用多线程提高TPS
- (3) 提前或延迟操作,错峰提高TPS
项目优化细节
内存泄漏问题
静态单例类引用Activity的context
单例模式不正确的获取context:
|
|
在LoginActivity中:
|
|
在LoginManager的单例中context持有了LoginActivity的this对象,即使登录成功后我们跳转到了其他Activity页面,LoginActivity的对象仍然得不到回收因为他被单例所持有,而单例的生命周期是同Application保持一致的。
正确的获取context的方式:
|
|
我们单例中context不再持有Activity的context而是持有Application的context即可,因为Application本来就是单例,所以这样就不会存在内存泄漏的的现象了。
单例模式中通过内部类持有activity对象
下面是一个单例的类:
|
|
然后是activity:
|
|
我们知道在java中,非静态的内部类的对象都是会持有指向外部类对象的引用的,因此我们将内部类对象mMyListener让单例所持有时,由于mMyListener引用了我们的activity对象,因此造成activity对象也不能被回收了,从而出现内存泄漏现象。
修改以上代码,避免内存泄漏,在activity中添加以下代码:
|
|
退出界面时,取消相关注册监听!
AsyncTask不正确使用造成的内存泄漏
我们在来看一种更加容易被忽略的内存泄漏现象,对于AsyncTask不正确使用造成内存泄漏的问题:
|
|
我们在使用AsyncTask的时候不宜在其中执行太耗时的操作,假设activity已经退出了,然而AsyncTask里任务还没有执行完成或者是还在排队等待执行,就会造成我们的activity对象被回收的时间延后,一段时间内内存占有率变大。
解决方法在activity退出的时候应该调用cancel()函数:
|
|
退出界面时,结束当前页面的线程。
内部Handler类引起内存泄露
原因:Handler在Android中用于消息的发送与异步处理,常常在Activity中作为一个匿名内部类来定义,此时Handler会隐式地持有一个外部类对象(通常是一个Activity)的引用。当Activity已经被用户关闭时,由于Handler持有Activity的引用造成Activity无法被GC回收,这样容易造成内存泄露。 正确的做法是将其定义成一个静态内部类(此时不会持有外部类对象的引用),在构造方法中传入Activity并对Activity对象增加一个弱引用,这样Activity被用户关闭之后,即便异步消息还未处理完毕,Activity也能够被GC回收,从而避免了内存泄露。
|
|
webview导致的内存泄漏
用代码New一个WebView而不是在XML中静态写入(不过貌似不能设置进度条了,不需要进度条的可以忽略):
在XML文件中用layout占位:
|
|
接下来只需要在Activity中New一个WebView并且添加到我们的容器中就ok了:
|
|
关于WebView的context应该用Activity还是Application的context,这里网上较为一致的观点都是采用Application的,理由是这样不会造成Activity的context的内存泄漏。
销毁时的动作:
|
|
尽量不要将WebView作为listview的头部使用,这样的话WebView会被一次性加载到内存中。
Window Leaked
按字面了解,Window Leaked大概就是说一个窗体泄漏了,也就是我们常说的内存泄漏,为什么窗体会泄漏呢?
产生原因:
我们知道Android的每一个Activity都有个WindowManager窗体管理器,同样,构建在某个Activity之上的对话框、PopupWindow也有相应的WindowManager窗体管理器。因为对话框、PopupWindown不能脱离Activity而单独存在着,所以当某个Dialog或者某个PopupWindow正在显示的时候我们去finish()了承载该Dialog(或PopupWindow)的Activity时,就会抛Window Leaked异常了,因为这个Dialog(或PopupWindow)的WindowManager已经没有谁可以附属了,所以它的窗体管理器已经泄漏了。解决方法:
关闭(finish)某个Activity前,要确保附属在上面的Dialog或PopupWindow已经关闭(dismiss)了。
避免内存流失
内存流失可能会导致出现大量的 GC 事件,如自定义组件的 onDraw() ,避免大量创建临时对象,比如 String ,以免频繁触发 GC。GC 事件通常不影响您的 APP 的性能,然而在很短的时间段,发生许多垃圾收集事件可以快速地吃了您的帧时间,系统上时间的都花费在 GC ,就有很少时间做其他的东西像渲染或音频流。
监听器的注销
- 对于观察者, 广播, Listener等, 注册和注销没有成对出现而导致的内存泄露.
- 使用CountDownTimer倒计时时,退出activity要取消:
timer.cancel()
- 使用LocationManager获取地理位置,及时取消注册:
locationManager.removeUpdates(mListener);
- 使用dialog或BottomSheetDialog,消失时移除监听,对象置空
- 使用RxBus,退出activity取消注册
- 使用一些三方的库,仔细查看是否需要取消注册的情况
Bitmap处理
以fresco为例:
- (最好是加载图片宽高大小的图片,多余的尺寸会导致内存浪费,不过webp后缀的图片无法设置宽高,这是个问题?)加载特别特别大的图片时最容易导致这种情况。如果你加载的图片比承载的View明显大出太多,那你应该考虑将它Resize一下。
- Android 无法绘制长或宽大于2048像素的图片。这是由OpenGL渲染系统限制的,如果它超过了这个界限,Fresco会对它进行Resize。
- decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。在不需要特别清晰的图片情况下,使用RBG_565为好。
SharedPreference 存储value
sp在创建的时候会把整个文件全部加载进内存,如果你的sp文件比较大,那么会带来两个严重问题:
- 第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。
- 解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。
- 这些key和value会永远存在于内存之中,占用大量内存。
储存数据量过大后,取值小屏手机vivoY23L,v4.4.4会取值失败。
Cursor关闭
如查询数据库的操作,使用到Cursor,也要对Cursor对象及时关闭。
|
|
有效使用内存的建议
去掉bean里无用的字段
有时候我们通过GsonFormat直接生成返回的json的Bean,而有一些我们并未使用的字段也一并生成了,建议删除这些无用字段,不然将无可避免的占用一定的内存空间。
关闭页面,全局的list清空后置空
用完就清空,并设置为null,不要到处引用不然会导致不能及时释放。
谨慎使用服务Service
离开了 APP 还在运行服务是最糟糕的内存管理错误之一,当 APP 处在后台,我们应该停止服务,除非它需要运行的任务。我们可以使用JobScheduler替代实现,JobScheduler把一些不是特别紧急的任务放到更合适的时机批量处理。如果必须使用一个服务,最佳方法是使用IntentService,限制服务寿命,所有请求处理完成后,IntentService 会自动停止。
使用优化后的数据容器
考虑使用优化过数据的容器 SparseArray / SparseBooleanArray / LongSparseArray 代替 HashMap 等传统数据结构,通用 HashMap 的实现可以说是相当低效的内存,因为它需要为每个映射一个单独的条目对象
关于HashMap,ArrayMap,SparseArray, 这篇文章有个比较直观的比较, 可以看下
少用枚举enum结构
枚举一般是用来列举一系列相同类型的常量,它是一种特殊的数据类型,使用枚举能够确保参数的安全性。但是Android开发文档上指出,使用枚举会比使用静态变量多消耗两倍的内存,应该尽量避免在Android中使用枚举。
那么枚举为什么会更消耗内存呢? - 分析链接
避免创建不必要的对象
诸如一些临时对象, 特别是循环中的.
使用异步处理数据较多的情况
如果一些数据需要处理再显示在UI上,对于数据量比较大的情况强烈建议异步处理后再在主线程处理。
使用 nano protobufs 序列化数据
Protocol buffers 是一个语言中立,平台中立的,可扩展的机制,由谷歌进行序列化结构化数据,类似于 XML 设计的,但是更小,更快,更简单。如果需要为您的数据序列化与协议化,建议使用 nano protobufs。
使用ProGuard来剔除不需要的代码
使用 ProGuard 来剔除不需要的代码,移除任何冗余的,不必要的,或臃肿的组件,资源或库完善 APP 的内存消耗。
降低整体尺寸APK
您可以通过减少 APP 的整体规模显著减少 APP 的内存使用情况。文章:Android APK瘦身实践
优化布局层次
通过优化视图层次结构,以减少重叠的 UI 对象的数量来提高性能。文章:Android 渲染优化