热文导读 | 点击标题阅读
深夜十点,北京各大互联网企业的下班高峰才刚开始?北晚记者带您实地探访
作者:Overried
来源:http://www.apkbus.com/blog-955864-78235.html
内存优化是一个程序员的基本功。有时也要切合项目的实际需求来做选择。
内存泄漏概念:
不再使用的对象没有被回收,就是内存泄露。
主要原因还是因为一般情况下单例都是全局的,有时候会引用一些实际生命周期比较短的变量,导致其无法释放。
例如 :
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
写成一个内部类譬如:
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
并用 WeakReference
弱引用保存下来。 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
已经帮我们处理的。
当然有时候不能完全在写代码的时候规避掉所有的内存泄漏,就要用一些工具检测一下:
LeakCanary
Android Studio profile
MAT
选自己喜欢的工具,去研究一下。(网上很多教程)
大家都知道
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 。
大家应该对着三个类都非常熟悉。那就先看代码:
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 <java/lang/StringBuilder>
然后把上次的结果集,通过构造函数传入,
29 invokespecial #25 <java/lang/StringBuilder.<init>> //调用构造函数,这串符号引用类似 jni 中反调 java的类查找写法
32 aload_3 //将局变量表Slot 3的元素入栈
再拼接本次需要拼接的字符。然后存到局部变量表中,等待下次循环操作。
44 astore_3
然后跳转编号17 去继续循环。这时候又重新创建了一个 StringBulider
去拼接。真是啃爹啊。。。
48 goto 17 (-31)
这个很明显
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 缓存池:最近最少使用缓存池,底层原理是用 LinkHashMap 实现。
谷歌的
Glide
图片加载库,就是使用了Lrucache
,和LruDiskCache
对图片进行缓存,进而提高用户体验。
ConnectionPool 缓存池 :复用 tcp socket
套接字,进行网络通讯,每一次 HTTP
请求结束后,并不结束链接,可复用于下次的请求。把网络传输速度极致化。
一次
http
请求分:
tcp
三次握手
数据传输
tcp
四次分手
如果每一次请求都经历整个流程,可能别人所有数据都加载完毕了,我还在握手中… 这就不能忍。
(当然http 1.1+
才支持这个链接复用,具体详细源码 看OKhttp
,本文不做详细展开)
SegmentPool:同上。
对于一些需要 大量频繁生成和回收的对象,建议使用池,如果没有轮子,也是可以手动写一个。
常用数据结构优化
xml 层级 和 view
内存大用户 : HashMap
(及其子类)HashMap
是一个典型的 空间换时间,时间复杂度趋近 o(1)
占用空间 是大于 size / 0.75
(负载因子),
/**
* hashMap put 部分源码,
* size 当前已存入数据数目
* threshold = 容量 *0.75
*/
if (++size > threshold)
resize();
通俗点就是 存入100个数据,要占用 133 个数据内存(及以上),所在数据量较小,或者对速度没有那么要求的时候可用 SparseArray(二叉树实现) 代替。
xml 层级最好控制在 5 层以内。
view 的使用多用:
ViewStub
Include
merge
如你有好的文章想和大家分享欢迎投稿,直接向我投递文章链接即可
最后,欢迎大家加入我们的知识星球,这期是到2019年3月10日结束,所以越早加入越好,现在加入的球友快1000人了,到1000人时将大幅提价(还有最后几十个名额了),所以快上车!
微信扫描或者点击上方二维码领取Android\Python\AI\Java等高级进阶资源
更多学习资料点击下面的“阅读原文”获取