Android:内存优化

内存问题或多或少都会存在于我们的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垃圾收集算法

在这里插入图片描述
缺点:遍历内存时比较耗时,容易产生过多的内存碎片,降低可用内存使用率
在这里插入图片描述缺点:可用内存空间减半
在这里插入图片描述优点:解决内存碎片问题 缺点:效率较低
在这里插入图片描述这个算法比较复杂,它基本是这样的操作

  1. 每次new对象都是从eden中开辟内存,当eden这块满了后gc回收,将存活的对象放到from中
  2. 继续new对象,当eden在此满了时,gc回收,将这次存活的对象和from中的放到to中,将eden和from都清空
  3. 继续new对象,当eden在此满了时,gc回收,将这次存活的对象和to中的放到from中,将eden和to都清空
  4. 比如有个阀值是10,对象a在from 和to中循环往复10次到达这个阀值,说明a的生命周期比较长,就将a移到老年代这块内存上
  5. 还有一种情况就是new的b对象eden的内存根本不够,也会直接将b放到老年代
  6. 当老年代的内存满了后也会进行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
会造成内存泄漏,至于是怎么具体定位内存泄漏位置的在下文会提到,由于是系统源代码我们不能直接修改 ,但我们可以通过反射来达到目的,这里要提到一个 暴力置空 \color{red}{暴力置空} 暴力置空的概念 ,就是将导致内存泄漏的对象通过反射的方式将其置空,切断其与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即可解决。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值