Android优化之内存优化

本文参考了(https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode/blob/master/article/android/basic/Android-性能优化-内存优化.md),如若侵权,请通知删除。

近期一直在考虑写一篇Android内存优化的全局总结, 今天刚好可以闲暇时间总结一下。

要了解Android内存优化,就要先了解JVM内存分配机制和JVM内存回收机制,这里我之前的文章有所总结,可以点击我主页查看。

当了解完上面两个概念之后,还需要了解一下DVM和JVM的区别和关联。
DVM 与 JVM
两者都是运行程序的虚拟机,不同的是Dalvik 虚拟机(DVM)是 Android 系统在 java虚拟机(JVM)基础上优化得到的,DVM 是基于寄存器的,而 JVM 是基于栈的,由于寄存器高效快速的特性,所以从性能上比较DVM 的性能相比 JVM 更好。其次是两者执行的字节码文件也不同,Dalvik 执行 .dex 格式的字节码文件,JVM 执行的是 .class 格式的字节码文件,Android 程序在编译之后产生的 .class 文件会被 aapt 工具处理生成 R.class 等文件,然后 dx 工具会把 .class 文件处理成 .dex 文件,最终资源文件和 .dex 文件等打包成 .apk 文件。

接下来谈论内存优化,肯定就要谈论内存泄露和内存溢出了。先来贴下概念:

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

内存溢出(out of memory)通俗理解就是内存不够,但他并不代表内存不足。它的意思是当程序在申请内存时,没有足够的内存空间供其使用,就会出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

那我们就先从内存泄露方面来进行常见问题总结与优化。
常见的内存泄露问题有以下五种:

  1. 单例造成的内存泄漏
  2. 非静态内部类创建静态实例造成的内存泄漏
  3. Handler 造成的内存泄漏
  4. 线程造成的内存泄漏
  5. 资源使用完未关闭

1.单例造成的内存泄漏
单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏,由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。
下面给一段错误的代码示范:

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

由于创建这个单例的时候要传入一个Context,但是如果传入的是Activity的Context的时候,当这个Activity退出的时候,单例对象还持有该Activity的 引用,所以就会造成内存泄露。

解决办法:将Activity的Context换成Application的Context即可,因为单例的生命周期和 Application 的一样长。
这是正确的代码示范:

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context.getApplicationContext();
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

2.非静态内部类创建静态实例造成的内存泄漏
有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:

public class MainActivity extends AppCompatActivity {
    private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mResource == null){
            mResource = new TestResource();
        }
        //...
    }
    class TestResource {
    //...
    }
}

这样就在 Activity 内部创建了一个非静态内部类的单例,每次启动 Activity 时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该 Activity 的引用,导致 Activity 的内存资源不能正常回收。

解决办法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用 Context,请使用 ApplicationContext。

3.Handler 造成的内存泄漏
Handler 的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等 api 都应该会借助 Handler 来处理,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏,如下示例:

public class MainActivity extends AppCompatActivity {
	private Handler mHandler = new Handler() {
	    @Override
	    public void handleMessage(Message msg) {
	    //...
	    }
	};
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }
    private void loadData(){
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

这种创建 Handler 的方式会造成内存泄漏,由于 mHandler 是 Handler 的非静态匿名内部类的实例,所以它持有外部类 Activity 的引用,我们知道消息队列是在一个 Looper 线程中不断轮询处理消息,那么当这个 Activity 退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的 Message 持有 mHandler 实例的引用,mHandler 又持有 Activity 的引用,所以导致该 Activity 的内存资源无法及时回收,引发内存泄漏,所以另外一种做法为:

public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
        reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
            activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

创建一个静态 Handler 内部类,然后对 Handler 持有的对象使用弱引用,这样在回收时也可以回收 Handler 持有的对象,但是当Activity回收的时候,Looper线程里的消息队列内可能还会有消息未处理,所以我们在重写onDestory方法,调用mHandler.removeCallbacksAndMessages(null)将消息队列的所有消息和所有Runnable移除,当然也可以使用 mHandler.removeCallbacks() 或 mHandler.removeMessages();来移除指定的 Runnable 和 Message。

4.线程造成的内存泄漏
对于线程造成的内存泄漏,也是平时比较常见的,异步任务和 Runnable 都是一个匿名内部类,因此它们对当前 Activity 都有一个隐式引用。如果 Activity 在销毁之前,任务还未完成,那么将导致 Activity 的内存资源无法回收,造成内存泄漏。 所以正确的做法和Handler的差不多,创建静态内部类,对传来的对象使用弱引用,为避免Activity回收时还有任务未完成,所以重写onDestory方法,取消线程中还存在的任务。

5.资源使用完未关闭
最后一点也是大家都清楚的一点,BraodcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。另外不要直接对 Activity 进行直接引用作为成员变量,如果不得不这么做,请用 private WeakReference mActivity 来做,相同的,对于Service 等其他有自己生命周期的对象来说,直接引用都需要谨慎考虑是否会存在内存泄露的可能。

接下来我们就先从内存溢出方面来进行常见问题总结与优化。
常见的内存溢出问题都是创建对象占用内存过大导致的,。

1.最占用内存的一种对象是Bitmap

Bitmap 非常消耗内存,而且在 Android 中,读取 bitmap 时, 一般分配给虚拟机的图片堆栈只有 8M,所以经常造成 OOM 问题。接下来我们从Bitmap方面进行优化。

  1. 图片显示:加载合适尺寸的图片,比如显示缩略图的地方不要加载大图。
  2. 图片回收:使用完 bitmap,及时使用 Bitmap.recycle() 回收。

问题:Android 不是自身具备垃圾回收机制吗?此处为何要手动回收?
Bitmap 对象不是 new 生成的,而是通过 BitmapFactory 生产的。 而且通过源码可发现是通过调用 JNI 生成 Bitma p对象(nativeDecodeStream()等方法)。 所以,加载 bitmap 到内存里包括两部分,Dalvik 内存和 Linux kernel 内存。 前者会被虚拟机自动回收。 而后者必须通过 recycle() 方法,内部调用 nativeRecycle() 让 linux kernel 回收。

  1. 捕获 OOM 异常:程序中设定如果发生 OOM 的应急处理方式。
  2. 图片缓存:内存缓存、硬盘缓存等。
  3. 图片压缩:直接使用 ImageView 显示 Bitmap 时会占很多资源,尤其当图片较大时容易发 生OOM。 可以使用 BitMapFactory.Options 对图片进行压缩。
  4. 图片像素:android 默认颜色模式为 ARGB_8888,显示质量最高,占用内存最大。 若要求不高时可采用 RGB_565 等模式。
  5. 图片大小:图片 长度×宽度×单位像素 所占据字节数。

我们知道 ARGB 指的是一种色彩模式,里面 A 代表 Alpha,R 表示 Red,G 表示 Green,B 表示 Blue。 所有的可见色都是由红绿蓝组成的,所以红绿蓝又称为三原色,每个原色都存储着所表示颜色的信息值,下表中对四种颜色模式的详细描述,以及每种色彩模式占用的字节数。

模式描述占用字节
ALPHAAlpha 由 8 位组成1B
ARGB_44444 个 4 位组成 16 位,每个色彩元素站 4 位2B
ARGB_88884 个 8 为组成 32 位,每个色彩元素站 8 位(默认)4B
RGB_565R 为 5 位,G 为 6 位,B 为 5 位共 16 位,没有Alpha2B

2.通过不同引用类型来进行修饰对象

1.强引用(Strong Reference):JVM宁愿抛出OOM,也不会让GC回收的对象
2.软引用(Soft Reference) :只有内存不足时,才会被GC回收。
3.弱引用(weak Reference):在GC时,一旦发现弱引用,立即回收
4.虚引用(Phantom Reference):任何时候都可以被 GC 回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。 程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。 可以用来作为 GC 回收 Object 的标志。

3.缓存池的使用
对象池:如果某个对象在创建时,需要较大的资源开销,那么可以将其放入对象池,即将对象保存起来,下次需要时直接取出使用,而不用再次创建对象。当然,维护对象池也需要一定开销,故要衡量。
线程池:与对象池差不多,将线程对象放在池中供反复使用,减少反复创建线程的开销。

4.某些对象的选择优先
1).ArrayMapSparseArray 是 android 的系统 API,是专门为移动设备而定制的。 用于在一定情况下取代 HashMap 而达到节省内存的目的。 对于 key 为 int 的 HashMap 尽量使用 SparceArray 替代,大概可以省 30% 的内存,而对于其他类型,ArrayMap 对内存的节省实际并不明显,10% 左右,但是数据量在 1000 以上时,查找速度可能会变慢。
2).在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用 StringBuilder 来替代频繁的 “+”。
3).Android 平台上枚举是比较争议的,在较早的 Android 版本,使用枚举会导致包过大,使用枚举甚至比直接使用 int 包的 size 大了 10 多倍。 在 stackoverflow 上也有很多的讨论, 大致意思是随着虚拟机的优化,目前枚举变量在 Android 平台性能问题已经不大,而目前 Android 官方建议,使用枚举变量还是需要谨慎,因为枚举变量可能比直接用 int 多使用 2 倍的内存。
4).使用 ListView 时 getView 里尽量复用 conertView,同时因为 getView 会频繁调用,要避免频繁地生成对象。 优先考虑使用 RecyclerView 代替 ListView。
5).优先使用重复的布局,减少View的层级,对于可以延迟初始化的页面,将不必要的对象延迟创建。否则当页面还没显示出来,Avtivity就退出的话,就相当于白白创建了那么多对象浪费了内存还占用了加载时间。
6).现在很多 App 都不是单进程,为了保活,或者提高稳定性都会进行一些进程拆分,而实际上即使是空进程也会占用内存(1M 左右),对于使用完的进程,服务都要及时进行回收。
7).尽量使用系统组件,图片甚至控件的 id。 例如:@android:color/xxx,@android:style/xxx。

最后的最后,我们即使在编码的时候充分考虑了上述的所有情况,但是往往还是有一些地方没有注意到,再者说一般App开发还是团队开发,仅仅从编码阶段考虑问题还是不够全面,当出现内存泄漏时,更有效更准确的定位问题才是最重要的方式。所以推荐一些内存泄露检测工具。
1.使用 Lint 代码静态检查
Lint 是 Android Studio 自带的工具,使用很简单找到 Analyze -> Inspect Code 然后选择想要扫面的区域即可。
2.使用 Android Studio 自带的 Monitor Memory 检查
一般在 Android Studio 的底部可以找到 Android Monitor。
3.使用 Memory Analyzer Tool 检查
这个需要在网上下载。
4.使用 LeakCanary 检查
项目地址:https://github.com/square/leakcanary

以上就是本次总结的所有内容,如果想查看上述工具具体使用方法,请点击文章最顶部的参考文章。那位博主是个大佬。站在巨人的肩膀上感觉就是不一样!哈哈哈~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值