Android 性能篇 - 内存优化
内存优化是一个程序员的基本功。有时也要切合项目的实际需求来做选择。
一、解决所有的内存泄漏
内存泄漏概念:
不再使用的对象没有被回收,就是内存泄露。
- 单利泄漏
主要原因还是因为一般情况下单例都是全局的,有时候会引用一些实际生命周期比较短的变量,导致其无法释放。
例如 :
activity 的 content 赋值到单利对象里面的成员量变量
code:
private static volatile ClassXX instance; private Context context; private ClassXX(Context context) { this.context = context; } public static ClassXX getInstance(Context context) { if (instance == null) { synchronized (instance) { if(instance == null) { instance = new ClassXX(context); } } } return instance; }
如果这个Context
是 Activity 的 Context ,当你的 Activity finish(); 之后Activity 这个对象的内存还是在堆中,没有释放。
因为单利对象持有Activity 的引用,jvm 认为你这个对象还是在使用中,不敢去 回收掉你的 Activity。那单例什么时候被回收?
那就只有等到整个进程被回收了,单例才会被回收。
进程杀死(回收):
Process.killProcess(Process.myPid())
用户手动卡片式摧毁 (亲测可行)
解决方法:
传入和单例一样生命周期的对象,如context.getApplication();
不将 context保存在单例的成员变量里面。
- Handler AsyncTask 等内部类的内存泄漏
主要原因是内部类默认持有外部类的引用
大家应该很喜欢吧 Handler写成一个内部类譬如:
private Handler mMainActivityHandler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); } };
其实包括我也很喜欢,而且一个Activity 对应一个 Handler,每一个 Handler 负责更新本 Activity 的 UI,一对一关系,分工明确。好用到爆炸。
然而 java 内部类是默认持有一个外部类的引用,因为 jvm 在把.java 源文件编译成 .class 字节码的时候,会在默认的构造函数加入外部类的引用。所以我们在内部类中也能访问外部类的引用。
然后问题就发生了,当前 Handler 持有当前 Activity 的引用,Handler 不释放,Activity 也别想释放了。MMP
(为什么 Handler 有时候会不会被释放?)
解决方法:
构造函数传入Activity 并用 WeakReferencemActivity;弱引用保存下来。 GC 的时候会不计入Handler 对Activity的引用,可以被回收。
Activity OnDestroy 的时候 ,把所有的相关请求终止,并且把消息队列清空 removeCallbacksAndMessages(null); 防止有数据回调到 UI 层。(当然如果不这么做,Activity 照样被回收,但是 Handler 不及时回收而已)
(什么叫 强引用 软引用 弱引用 虚引用 ,以及 Handler 的消息驱动模型是怎么样子的,这里就不展开讲,本文着重内存泄漏)
当然 AsyncTask 和其它对象内部类也是有这种问题,解决方法同上。
- 资源使用完未关闭
主要是:
广播(BraodcastReceiver)动态注册之后要反注册,推荐在onStart onStop 对应的生命周期执行。
服务(Service)Start 之后 记得 Stop。启动服务时机看需求。一般不建议在 Application 启动(启动 Service 耗时基本要100ms+)。
io Cursor 流要记得 close,一定要在 finally 去 close,防止抛异常没执行 close ,那就泄漏了。
Bitmap 内存大户,要记得回收 recycle 一下,当然 90% 的场景 Glide 已经帮我们处理的。
4.检测内存泄漏的工具
当然有时候不能完全在写代码的时候规避掉所有的内存泄漏,就要用一些工具检测一下:
LeakCanary
Android Studio profile
MAT
选自己喜欢的工具,去研究一下。(网上很多教程)
二、图片压缩
- bitmap 压缩
大家都知道 bitmap 占用内存很大,用完之后要 recycle 一下。
不知道大家有没有用过,图片加载出来内存就爆掉了(OOM)情况,本宝宝就遇到过了(心中一千万头草拟吗奔腾而过)。
首先一张图片从网络获下来,从 InputStream 转成 Bitmap,这个 bitmap 占了多少内存怎么计算?
献上代码:
Bitmap.getAllocationByteCount();
其实就是 ByteCount = 长 宽 4(假设这里每一个像素点是是RGB888) 那就是 4 个字节。也有一个像素点 RGB565 占 3 个字节,当然占更多字节的 RGB888 更加高清无码。起初版本 Glide 使用 RGB565,目前 Glide 4.XX 的默认都是 RGB888,当然自己可以配置一下。
为了解决这个问题一般都是通过下面代码:
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; // 通过这个bitmap获取图片的宽和高 Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg", options); float realWidth = options.outWidth; float realHeight = options.outHeight; //计算出scale options.inSampleSize = scale; options.inJustDecodeBounds = false; // 注意这次要把options.inJustDecodeBounds 设为 false,这次图片是要读取出来的。 bitmap = BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg", options);
先获取他的图片大小,根据自己需要的大小计算出缩放比例。(图片大小都是放在图片的头部,这时候不会去加载整张图片)
进行缩放,得出符合自己的控件尺寸的大小。
(当然还有些非法的图片头部是获取不出 长* 宽。这时候记得搞个默认的缩放率,防止 OOM)
有时候为了优化内存,还不如压缩一张图片 所节约的内存来的更快。
譬如 一张 1080 * 1920 图片再乘以 4 等于 7.9 M。
我压缩到 一张缩略图 200*200 等于 156KB。瞬间节约了7M 空间。区别真的太大了,顿时内心 一句 MMP 。
三、解决内存抖动
1.String VS StringBuffer VS StringBuilder
大家应该对着三个类都非常熟悉。那就先看代码:
long time = System.currentTimeMillis(); String s = new String("JAVA"); for(int i = 0 ;i<10000; i++) { s = s+"VERSION"; } Log.d("TestString","Time consumption:"+(System.currentTimeMillis() - time)); time = System.currentTimeMillis(); StringBuilder s1 = new StringBuilder("JAVA"); for(int i = 0 ;i<10000; i++) { s1.append("VERSION"); } Log.d("TestString","Time consumption:"+(System.currentTimeMillis() - time)); D/TestString: Time consumption:3786 D/TestString: Time consumption:2
很明显使用 StringBuilder 去拼接字符,效率大大快于用加号,我们带着问题来找原因。
那我们看一下用 + 号去拼接的字节码:
使用+号去拼接字符,jvm 会创建一个临时的 StringBuilder
25 new #24
然后把上次的结果集,通过构造函数传入,
29 invokespecial #25 <java/lang/StringBuilder.<init>> //调用构造函数,这串符号引用类似 jni 中反调 java的类查找写法 32 aload_3 //将局变量表Slot 3的元素入栈
再拼接本次需要拼接的字符。然后存到局部变量表中,等待下次循环操作。
44 astore_3
然后跳转编号17 去继续循环。这时候又重新创建了一个 StringBulider 去拼接。真是啃爹啊。。。
48 goto 17 (-31)
那我们看一下用 StringBuilder 去拼接的字节码:
这个很明显 new StringBulider 字节码在循环体外面,所以并没有循环新建对象。
总结:
通过上面的例子,String 的拼接通过一个 for 循环创建了 10000 个 StringBulider,而且用完就抛弃。特别浪费,在内存吃紧的情况下,很容易引起 gc ,导致 App 卡顿。
也许有同学要问 一个 StringBuilder 的空对象才占堆内存多大?我们来算一算
一个对象 = 对象头 + 成员属性
对象头 = MardWord + Klass= 12个字节 (数组除外)
上图:
MardWord 字段大全(出自网上扣得):
这个 MardWord 怎么有这么多锁状态,这些锁状态又是什么?
这就要涉及到 synchronized 同步锁的知识,这个不在本文讨论范围之内。
那么 StringBulider 的成员属性有哪些?清单:
static final long serialVersionUID = 4383685877147921099L; char[] value; int count;
对象结构图
计算下来:12+8+8+4+24 = 56 个字节 10000 个对象 那就是要 560KB 内存。不小吧。当然我们实际需求不可能一次搞这么多个对象,但是多个地方都用 String
去玩的话,积少成多,到时候 APP 内存比别人的高出一大截。那就尴尬了..
四、尽量使用 “池”
我们常见的池有
线程池
Lrucache 缓存池
okhttp 里面的 ConnectionPool (socket 复用池)
okio SegmentPool (buffer 复用池)
池的功能:
可以重复利用对象,并且减少内存开销,内存抖动,cpu 开销。
线程池
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
尽量使用线程池去跑任务,而不是动不动就先 new Thread 去跑,这样子线程是得不到复用的。当任务量一大,使用线程池的效率会超乎你想象(具体自己看源码),毕竟 开启一个线程 cpu 内存都是有开销的。
这里推荐 Rxjava 的第三方库,一个将 装饰者模式 玩到上天的 框架,切换线程方便,支持函数式编程 杜绝回调地狱 等等:
Observable.create(new Action1<Emitter<Integer>>() { @Override public void call(Emitter<Integer> subscriber) {} }, Emitter.BackpressureMode.BUFFER) .subscribeOn(Schedulers.io()) //切换到 io 线程池 .subscribeOn(Schedulers.computation()) //切换 到计算 线程池 .subscribeOn(Schedulers.immediate()) // 使用当前线程 .observeOn(AndroidSchedulers.mainThread()) //切换到 android UI 主线程 .subscribe();
- Lrucache 缓存池
Lrucache 缓存池:最近最少使用缓存池,底层原理是用 LinkHashMap 实现。
谷歌的 Glide 图片加载库,就是使用了 Lrucache,和 LruDiskCache 对图片进行缓存,进而提高用户体验。
- ConnectionPool 缓存池
ConnectionPool 缓存池 :复用 tcp socket 套接字,进行网络通讯,每一次 HTTP 请求结束后,并不结束链接,可复用于下次的请求。把网络传输速度极致化。
一次 http 请求分:
tcp 三次握手
数据传输
tcp 四次分手
如果每一次请求都经历整个流程,可能别人所有数据都加载完毕了,我还在握手中… 这就不能忍。
(当然 http 1.1+ 才支持这个链接复用,具体详细源码 看 OKhttp,本文不做详细展开)
- okio SegmentPool (buffer 复用池)
SegmentPool:同上。
总结:
对于一些需要 大量频繁生成和回收的对象,建议使用池,如果没有轮子,也是可以手动写一个。
五、其他
常用数据结构优化
xml 层级 和 view
1.常用数据结构优化
内存大用户 : HashMap (及其子类)
HashMap 是一个典型的 空间换时间,时间复杂度趋近 o(1)
占用空间 是大于 size / 0.75(负载因子),
/** * hashMap put 部分源码, * size 当前已存入数据数目 * threshold = 容量 *0.75 */ if (++size > threshold) resize();
通俗点就是 存入100个数据,要占用 133 个数据内存(及以上),所在数据量较小,或者对速度没有那么要求的时候可用 SparseArray(二叉树实现) 代替。
2.xml 层级 和 view
xml 层级最好控制在 5 层以内。
view 的使用多用:
ViewStub
Include
merge
原文发布时间为:2018-07-05
本文作者:Overried
本文来自云栖社区合作伙伴“安卓巴士Android开发者门户”,了解相关信息可以关注“安卓巴士Android开发者门户”。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Android 图片相关整理
目录介绍 0.思考问题及解决方案 1.加载图片的压缩处理技术 2.网络url图片转换Bitmap保存到本地 2.1 直接通过http请求网络图片通过流转化成Bitmap 2.2 使用第三方库glide将网络图片转化为Bitmap 3.保存bitmap图片到本地文件夹 4.实现带有圆角的图片 4.1 使用glide处理图片圆角的逻辑 4.2 自定义带有圆角的ImageView 5.毫无满仓轮播图背景做高斯模糊 5.1 高斯模糊实现原理 5.2 高斯模糊实现的代码 5.3 高斯模糊可能会造成的崩溃 5.4 高斯模糊参考案例 关于链接 1.技术博客汇总 2.开源项目汇总 3.生活博客汇总 4.喜马拉雅音频汇总 5.程序员聊天笔记汇总 5.其他汇总 0.思考问题及解决方案 0.1.0 图片压缩的技术是什么,原理如何理解? 0.1.1 为什么保存图片,切割图片圆角需要将图片转化成bitmap? 0.1.2 对于从网络下载图片,可以采用什么方式?为什么glide相比从网络直接请求更加高效? 0.1.3 图片背景滑动高斯模糊的原理是什么,是否影响性能? 0.1.4 bitmap如何避免被回收?如果回...
- 下一篇
Picasso:开启大前端的未来
“道生一,一生二,二生三,三生万物。” —— 《道德经》 Picasso是大众点评移动研发团队自研的高性能跨平台动态化框架,经过两年多的孕育和发展,目前在美团多个事业群已经实现了大规模的应用。 Picasso源自我们对大前端实践的重新思考,以简洁高效的架构达成高性能的页面渲染目标。在实践中,甚至可以把Native技术向Picasso技术的迁移当做一种性能优化手段;与此同时,Picasso在跨越小程序端和Web端方面的工作已经取得了突破性进展,有望在四端(Android、iOS、H5、微信小程序)统一大前端实践的基础之上,达成高性能大前端实践,同时配合Picasso布局DSL强表达能力和Picasso代码生成技术,可以进一步提升生产力。 客户端动态化 2007年,苹果公司第一代iPhone发布,它的出现“重新定义了手机”,并开启了移动互联网蓬勃发展的序幕。Android、iOS等移动技术,打破了Web应用开发技术即将一统江湖的局面,之后海量的应用如雨后春笋般涌现出来。移动开发技术给用户提供了更好的移动端使用和交互体验,但其“静态”的开发模式却给需要快速迭代的互联网团队带来了沉重的负担。 ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Mario游戏-低调大师作品
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS8编译安装MySQL8.0.19
- MySQL8.0.19开启GTID主从同步CentOS8