内存问题或多或少都会存在于我们的App中,作为开发人员这也是我们要研究学习的重要课题之一,是否具备内存优化能力也是能否成为高级开发工程师的指标。这篇文章算是我网易课程的学习笔记,分享给大家交流学习,若有不当之处也请各路大神指正。
一. 内存回收机制及相关概念
谈到解决内存问题,我们首先要了解jvm的内存回收机制(gc)和相关概念,这样在项目中才能避免和解决内存问题
1.1 强、软、弱、虚
1.2垃圾标记算法
缺点:当两个对象相互引用时,计数不准
根搜索算法,这个算法普遍应用,如图所示,只要对象能联系到GC Root表示对象可用,若没有联系如object5,object6,object7则都是gc回收的目标。对象为null即与GC Root失去关联。
那么常见的GCroots有哪些呢?
- 静态变量
- 局部变量
- 常量池常量
- jni引用
- 内部引用,如exception对象、class对象、classloader对象
- 同步锁 synchroized 对象
- 临时对象,如:跨代引用对象
那么假如我要回收作为GCroot的class对象,如Student.class需要满足哪些条件呢?
- 对应new出的对象都已经回收掉了
- 没有任何地方创建这个类
- 对应的类加载器已经回收掉了
- 参数控制,配置jvm参数时,若开启了-Xnoclassgc,无论如何也回收不了。
1.3垃圾收集算法
缺点:遍历内存时比较耗时,容易产生过多的内存碎片,降低可用内存使用率
缺点:可用内存空间减半
优点:解决内存碎片问题 缺点:效率较低
这个算法比较复杂,它基本是这样的操作
- 每次new对象都是从eden中开辟内存,当eden这块满了后gc回收,将存活的对象放到from中
- 继续new对象,当eden在此满了时,gc回收,将这次存活的对象和from中的放到to中,将eden和from都清空
- 继续new对象,当eden在此满了时,gc回收,将这次存活的对象和to中的放到from中,将eden和to都清空
- 比如有个阀值是10,对象a在from 和to中循环往复10次到达这个阀值,说明a的生命周期比较长,就将a移到老年代这块内存上
- 还有一种情况就是new的b对象eden的内存根本不够,也会直接将b放到老年代
- 当老年代的内存满了后也会进行gc
二.什么是内存问题?
2.1内存泄漏
一个不再被程序使用的变量或对象依旧存活在内存中无法被回收,出现这个问题的根本原因就在于 一个短生命周期的对象被一个长生命周期的对象引用
2.2内存溢出(out of memory)
当程序申请内存时,没有足够的内存供程序使用;比较小的内存泄漏不会有太大影响,但内存泄漏多了,占用的内存空间就大了,程序能申请使用的内存就小了,当没有内存供程序使用时就出现了内存溢出,而这是致命的,会严重的影响App的稳定性,所以从原则上我们不应该轻视每一个内存泄漏。
三.常见的内存泄漏
我们来看下在工作中常出现以及面试过程中常涉及到的,这些是我们可控范围内或者应避免的几点。
3.1Handler和Thread一类的内部类
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler.sendEmptyMessageDelayed(798,1000);
}
private Handler handler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
handler.sendEmptyMessageDelayed(798,1000);
}
};
@Override
protected void onDestroy() {
super.onDestroy();
//handler.removeMessages(798);
}
}
这恐怕是大家最常见的写法,那么它存在问题吗?答案是肯定的
在运行后退出测试App,按常理来说MainActivity应该被销毁,但事实是残酷的,因为内部类handler持有MainActivity的引用导致MainActivty并没有被销毁。接下来说一下几种解决方案:
1.在onDestroy()方法中将不需执行的消息移除
handler.removeMessages(798);
但并不是所有内部类都会有相关操作
2.用static修饰handler
我们知道加载一个类,会首先加载static修饰的元素,此时的handler已经加载到内存中,他不再是MainActivity的内部类了,由此它不再持有MainActivity的引用,由此解决了内存泄漏的问题。缺点是当static修饰的元素过多时,会导致jvm加载类时消耗过多,影响类的加载速度。
3.softrefrence、weakrefrence 软、弱引用
private static class MyHandler extends Handler {
private WeakReference<Activity> mActivity;
public MyHandler(Activity activity) {
mActivity = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
if (mActivity.get() == null) {
return;
}
//to do something..
}
};
优点:当gc回收时,会被清除 缺点:不知什么时候会被回收,比如gc后执行avtivity.init(),就会出现异常
4.不做处理
看到这里我猜好多小伙伴要骂街了,哔哔这么多居然说不处理也可以,其实不然这里的主要意思是视业务逻辑而定,假如在handler中的处理并不复杂,比如做一些变量修改或ui更新,即使在MainActivity销毁时handler还在执行,但几秒后handler处理完成,在次gc时会被回收,所以说问题不大,所以关键之处在于具体的业务情况,了解他的生命周期到底有多长。
3.2webview
当我们用webview去加载一些动画等内容时可能会出现内存泄漏的问题,而这个基本不是我们能够左右的,因为这涉及到H5及css的优化问题,比较好的方案就是单开一个进程,在AndroidManfest中配置
例:
<activity android:name=".MainActivity" android:process=":p1"/>
另一种就是尝试带三方的webview内核,如腾讯X5等。
3.3单例模式
public class Helper {
private static Helper instance;
private Context context;
private Helper(Context context) {
this.context = context;
}
public static Helper getInstance(Context context) {
if (instance != null) {
instance = new Helper(context);
}
return instance;
}
}
单例的静态特性导致它的生命周期和整个应用的生命周期一样长,如果有对象已经不再使用了,但又却被单例持有引用,那么就会导致这个对象就没办法被回收,从而导致内存泄漏。如果例中的context为Activity,就会导致这个Activity无法被回收,用getApplicationContext()能解决类似问题。
3.4资源对象未关闭引起的内存泄露
广播接收者,数据库的游标,多媒体,文档,套接字等。在使用这些对象是及时关闭即可。
3.5一些带三方框架和sdk
比如,EventBus等在初始化时需要注册的,在销毁时一定要记得反注册。
3.6系统级别
在低版本系统中InputMethodManager中的
mServedView
mNextServedView
会造成内存泄漏,至于是怎么具体定位内存泄漏位置的在下文会提到,由于是系统源代码我们不能直接修改 ,但我们可以通过反射来达到目的,这里要提到一个的概念 ,就是将导致内存泄漏的对象通过反射的方式将其置空,切断其与gcRoot之间的联系(原理:上面的根搜索算法),在再次gc时就能将其回收。
method("mServedView");
method("mNextServedView");
//反射 暴力置空
public void method(String attr){
InputMethodManager im = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
try {
//得到对应得属性
Field field = InputMethodManager.class.getDeclaredField(attr);
//设置访问权限
field.setAccessible(true);
//得到这个属性对象
Object curView = field.get(im);
if (null != curView){
Context context = ((View)curView).getContext();
if (context == this){
//将这个属性对象置空
field.set(im,null);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
3.7内存抖动问题
private void test(){
String result = "a";
for (int i = 0;i<200000;i++){
result += i;
}
}
看似简单的代码也可能出现问题,以此为例虽然20万次的字符操作有些极端,但此类问题是存在的
频繁的创建对象引起此类问题,每次进行gc操作时其他线程都是被挂起的,此时的UI线程会被卡成狗。但对于操作字符串而言,我们应尽量使用StringBuilder和StringBuffer进行操作。
四.内存泄漏的定位
1.打开as自带的profile,我们看到能检测cpu,memory,network,energy,双击memory部分
2.对自己的app进行初步分析
3.我们可以猜测想这种情况可能是不正常的,直观来看内部类过多了
4.而这张图中框出的位置虽然对象存在多个我认为是正常的,拿TaskListEntity来说我的确在代码中new了18个,SwipeItemLayout是一个item侧向滑动的控件,它用来包裹recycleview的item,根据recycleview的回收机制不可见的item将被复用,它new了7个的原因是一屏刚好显示7个item。
5.对于我们分析不出的异常,我们可以使用其他内存分析工具进行进一步分析,我们将内存信息快照导出
6.下载一个内存分析工具
内存分析工具7.转格式
这个工具在as的sdk中,打开终端找到我们导出的文件(test.hprof)
hprof-conv -z test.hprof test-mat.hprof
8.用下载的分析工具打开转化后的 test-mat.hprof
9.输入关键字,进行分析
一层层查看这个关系链就能发现问题所在,最后发现这就是上面提到的InputMethodManager 导致的内存泄漏问题。内存问题的解决除了工具的使用也需要经验的支撑,需要根据代码业务来综合分析,这就是考验android基本功的时候了。
最后补充一点,在发现内存问题并改正后,在此使用as的profiler时可能发现问题并没有解决或者干脆没有条状图,重新开启as即可解决。