性能优化-内存优化

内存优化

虽然Android有有优秀的内存管理机制,内存释放有垃圾收集器(GC)来回收。但内存的不合理使用还是会造成一系列的性能问题,比如短时间分配大量内存对象、内存泄漏等问题。本篇讲述如何检测内存问题和解决,希望在内存优化方面能够提供一些帮助。

Android内存管理机制

首先学习Android内存管理机制,了解系统如何分配和回收内存。

Java对象生命周期

Java对象在虚拟机上运行有7个阶段,也就是对象的生命周期

  1. 创建阶段(Created)
  • 为对象分配存储空间
  • 构造对象
  • 初始化
  1. 应用阶段(InUse)
  • 对象至少被一个强引用持有,除非在系统显示地使用来软、弱、虚引用。
  1. 不可见阶段(Invisible)
  • 处于不可见阶段的对象在虚拟机的对象引用根集合再也找不到直接或间接的强引用,这些对象一般是所有线程栈中的临时变量。
  • 当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该对象仍然存在。
  1. 不可达阶段(Unreachable)
  • 指该对象不再被任何强引用持有,且垃圾回收器发现给对象已经不可达。
  1. 收集阶段(Collected)
  • 垃圾收集器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该 i对象的内存空间重新分配做好准备,对象进入“收集阶段”。(如果重写来finalize()方法,则执行该方法)
  1. 终结阶段(Finalized)
  • 当对象执行完finalize()方法后仍然处于不可达状态时,该对象进入终结阶段,等待垃圾回收器回收该对象空间。
  1. 对象空间重新分配阶段(Deallocated)
  • 若垃圾回收器对该对象占用的内存空间进行回收或者再分配,则该对象彻底消失,这个阶段称为“对象空间重新分配阶段”。

注意:在创建对象后,在确定不再需要使用该对象时,使对象置空,这样更符合垃圾回收标准,比如Object = null,可以提供内存使用效率。

内存分配

在Android系统中,堆实际上是一块匿名共享内存,Android虚拟机并没有直接管理这块匿名共享内存,而是把它封装成一个mSpace,由底层C库来管理。

为了整个系统的内存控制需要,在Android系统为每一个应用程序都设置一个硬性的Dalvik Heap Size最大限制阈值(视设备而定)。如果应用占用内存空间接近阈值时,再尝试分配内存很容易OOM。Android系统的内存堆被划分为不同的区块,根据对数据配置对类型分配不同的区域内存,垃圾回收时,也会根据这些配置执行不同的垃圾回收处理过程,并且每一个区块都有指定的单位大小。

Android Rumtime有两种虚拟机,Dalvik和ART,他们分配的内存区域块是不同的:

  • Dalvik: Linear Alloc、Zygote Space、Alloc Space
  • ART:Non Moving Space、Zygote Space、Alloc Space、Image Alloc、Large Obj Space

其中Image Alloc和Zygote Alloc在Zygote进程和应用程序进程之间共享,而Allocation Space是每个进程都独立拥有一份。但Image Space的对象只创建一次,而Zygote Space的对象需要在系统每次启动时,根据运行情况都重新创建一遍。

内存回收机制

整个内存分为三个区域:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。

1. Young Generation

年轻代分为三个区,一个Eden区和两个Survivor区S0和S1(S0和S1只是为了好区分,两者实质一样,角色可互换)。

2. Old Generating

年老代存放的是上面年轻代复制过来的对象,也就是在年轻代还存活的对象并且区满了复制过来的。一般来说,年老点中的对象生命周期都比较长。

3. Permanent Generation

用于存放静态的类和方法,以及年老代移动过来的对象。持久代对垃圾回收没有显著影响。

内存对象的处理过程如下

  • 对象创建后在Eden区。
  • 执行GC时,如果对象仍然存活,则复制到S0区。
  • 当S0区满时,该区存活对象将复制到S1区,然后S0清空,接下来S0和S1角色互换。
  • 当上一步达到一定次数(视系统版本差异)后,存活对象将被复制到Old Generation。
  • 当这个对象在Old Generation区域停留的时间达到一定程度时,它会被移动到Old Generation,最后积累一定时间再移动到Permanent Generation区域。

回收机制

系统在Young Generation和Old Generation上采用不同的回收机制。每一个Generation的内存区域都有固定的大小。随着对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阈值时,会触发GC操作,以便腾出空间来存放其他新的对象。

  • Young Generation:通常存活时间较短,因此基于Copying算法来回收,所谓Copying算法,就是扫描出存活的对象,并复制到一块新的完成未使用的空间中。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到Old Generation。
  • Old Generation:与Young Generation不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来回收。所谓标记,就是扫描出存活的对象,然后在回收未被标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减少内存碎片带来的效率损耗。

详细内容可参考我另一篇文章

GC类型

Android系统中,GC有以下三种类型:

  • kGcCauseForAlloc:在分配内存时发现内存不够的情况下引起的GC,这种情况下的GC会Stop World。Stop World是由于并发GC时,其他线程都会停止,直到GC完成。
  • kGcCauseBackground:当内存达到一定的阈值时触发GC,这个时候是一个后台GC,不会引起Stop World。
  • kGcCauseExplicit:显式调用时进行的GC,如果ART打开了这个选项,在system.gc时会进行GC。

内存优化的意义

在GC过程中,任何其他在工作的线程(包括负责绘制的线程)都可能会被暂停,一旦GC消耗的时间超过16ms的阈值,就会出现丢帧。也就是说频繁的GC会增加应用的卡顿

如果内存在某以阶段的峰值达到了内存空间的阈值,或者频繁地发生内存峰值(毛刺现象),刚好在这个峰值时,需要申请一块较大的内存,就会由于对内存空间不足而导致OOM异常

内存泄漏是指应用已经不会再使用的内存对象,但垃圾回收时没有把这些辨认出来,不能及时地回收,仍然一直保留在内存中,占用了一定的空间,并且最终会到GC耗时最长的Old Generation,不释放给其他对象。

内存优化主要有以下几个意义:

  • 减少OOM,提高应用稳定性
  • 减少卡顿,提高应用流畅度
  • 减少内存占用,提高应用后台运行时的存活率
  • 减少异常发生,减少代码逻辑隐患

内存分析工具

Memory Monitor

Memory Monitor是一款使用非常简单的图形化工具,可以很好地监控系统或应用的内存使用情况。可以快速发现内存抖动、大内存分配,甚至由于GC导致的卡顿。

(AS3.0以上的Android Profiler)

  1. 典型场景:内存分配与释放、大内存申请与内存抖动。

Heap Viewer

Heap Viewer的主要功能是查看不同数据类型在内存中的使用情况。通过分析这些


image.png
  1. Heap Viewer启动:在ADM面板,在进程列表选择要查看的进程,单击Update Heap按钮。
  2. Heap Viewer面板


    image.png
  • data object:数据对象,Java类类型对象,是最主要的观察对象。
  • class object:Java类类型的引用对象。

Allocation Tracker

Allocation Tracker可以分配跟踪记录应用程序的内存分配,并列出了他们的调用堆栈,可以查看所有对象内存分配的周期。

可以先用Memory Monitor或者Heap Viewer找到内存异常的场景,然后使用Allocation Tracker分析这个场景的内存使用情况。

  1. Allocation Tracker的使用:
  • 在Allocation Tracker选项卡,单击Start Allocation Tracking;
  • 操作应用,怀疑内存有问题的操作;
  • 点击Stop Allocation Tracking;
  • 自动生成一个alloc结尾的文件,记录了追踪到的所有内存数据。
  1. 查看面板信息

避免内存泄漏

GC会选择一些还存活的对象作为内存遍历的根节点GC Roots,通过对GC Roots对可达性来判断是否需要回收。GC Roots是系统选择的对象根节点,对Heap进行遍历,没有被直接或间接遍历到的引用会被GC 回收,能遍历到的能被回收。这类在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小,这种现象在Android应用中称为内存泄漏。

使用MAT查找内存泄漏

MAT是一个快速、功能丰富的Java heap分析工具,可以帮助开发者定位导致内存泄漏的对象,以发现大的内存对象,然后解决内存泄漏并优化。

1. 使用步骤
  • AS并没有集成MAT,需下载MAT客户端
  • 获取HPROF文件,进入ADM选择要分析的应用进程,单击Update Heap按钮,操作几次GC后点Dump HPROF File按钮保存文件;
  • 右键文件弹出菜单选中Export standard .hprof选项,转成标准的HPROF文件;
  • 用MAT打开转换后的标准HPROF文件。
2. MAT视图

分析内存最常用的是Histogram和Dominator Tree两个视图

(具体使用自行搜索哈哈)

场景内存泄漏场景

  1. 资源性对象未关闭:比如读写文件、操作数据库等,如果仅仅把引用置null,而不关闭他们,往往会造成内存泄漏。
  2. 注册对象未注销:事件注册后未注销,会导致观察者列表中维持着对象的引用,阻止垃圾回收。
  3. 类的静态变量持有大数据对象:静态变量长期维持对象的引用,阻止垃圾回收,如果持有的是如Bitmap等大的数据对象很容易引起内存问题。
  4. 非静态内部类的静态实例:非静态内部类会维持一个到外部类对象的引用,如果非静态内部类的实例是静态,就会间接长期维持着外部类的引用,阻止被系统回收。
public class TestActivity extends Activity{
    private static TestModule mTestModule = null;
    @Override
    protected void onCreate(Bundle b){
        //...
        mTestModule = new TestModule(this);
    }
    class TestModule{
        private Context mContext = null;
        public TestModule(Context ctx){
            mContext = ctx;
        }
    }
}

上例中静态实例mTestModule会一直持有该Activity的引用,导致Activity的内存资源不能正常回收。

  1. Handler临时性内存泄漏:如果Handler是非静态的,会持有外部类Activity的引用。有一种情况,当Activity退出时,如果消息队列中还是未处理或正在处理的消息,并且消息队列中的Message持有Handler实例的引用,会导致Activity资源无法被回收,引发内存泄漏。
    为避免这种情况要修改两个地方:
  • 使用一个静态Handler内部类,然后对Handler持有的对象使用弱引用;
  • 在Activity的Destroy或stop时,移除消息队列中的消息,避免Looper线程的消息队列还有消息待处理。
public class TestActivity extends Activity{
    private NewHandler mHandler = new NewHandler(this);
    private static class NewHandler extends Handler{
        private WeakReference<Context>mContext = null;
        public NewHandler(Context ctx){
            mContext = new WeakReference<Context>(ctx);
        }
    }
    @Override
    protected void onDestroy(){
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}
  1. 容器中的对象没清理造成的内存泄漏:通常把一些对象的引用加入集合中,在不需要该对象时,如果没有把它的引用从集合中清掉,这个集合会越来越大。如果集合是static,情况更严重。
  2. 未正确使用Context:对于非一定要使用Activity的Context的情况可以考虑Application Context来代替,避免Activity 一泄漏,比如下面的单例:
public class Appsettings{
    private Context mAppContext;
    private static AppSettings mAppSettings = new AppSettings();
    public static AppSettings getInstance(){
        return mAppSettings;
    }
    public final void setup(Context context){
        mAppContext = context;
        //mAppContext = context.getApplicationContext(); 用这个代替
    }
}

如果setup(Context context)传入的是Activity的Context,使得Activity被一个单例持有,mAppSettings作为静态变量,生命周期大于Activity,产生内存泄漏。

  1. 静态View:使用静态View可以避免每次启动Activity都去渲染View,当静态View会持有Activity的引用,导致Activity无法被回收,解决方法是在onDestory方法中将静态View置为null。
public class TestActivity extends Activity{
    public static Button button;
    //...
    button = (Button)findViewById(R.id.btn);
    //...
    protected void onDestory(){
        super.onDestory();
        button = null;
    }
}
  1. Bitmap对象:Bitmap对象在转换得到新Bitmap对象后,应该尽快回收原始的Bitmap释放空间。避免静态变量持有比较大的Bitmap对象或其他大的数据对象。
  2. WebView:WebView存在内存泄漏问题,在应用中只有使用一次WebView,内存就不会被释放掉。通常解决办法是为WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信。WebView所在进程根据业务在合适时机销毁。

内存监控

LeakCanary是一个检测内存的开源类库,可以在发生内存泄漏时告警,并且生成leak trace分析泄漏位置,同时可以提供Dump文件。

  1. 实现监控
  • 首先在build.gradle文件配置导入LeakCanary的SDK
  • 引入相关依赖后,在应用的自定义Application中安装LeakCanary
public class GmfApplication extends Application{
    @Override
    protected void onCreate(){
        super.onCreate();
        mRefWatcher = LeakCanary.install(this);
    }
}

LeakCanary.install(this)会安装一个Leaks的Apk,同时也启用一个ActivityRefWatcher,用于自动监控调用Activity.onDestroy()之后泄漏的对象。

默认情况下,只对Activity进行监控,如果需要对Fragment或Service等这类组件监控,可以在Fragment onDestroy方法中,或自定义组件的周期结束回调接口加入以下实现

GmfApplication.getRefWatcher().watch(this);
  1. 自定义处理结果

仅仅依靠默认的处理方式,体验不是很好,可以自定义监控结果处理。

  • 首先继承DisplayLeakService实现一个自定义的监控处理Service,重新afterDefaultHandling方法。
public class LeakService extends DisplayLeakService{
    private final String TAG = "LeakService";
    @Override
    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo){
        //自定义的处理
        super.afterDefaultHandling(heapDump,result,leakInfo);
    }
}

heapDump:堆内存文件,可以拿到完成的hprof文件

result:监控到内存的状态,如是否泄漏等

leakInfo:leak trace详细信息

  • 然后在install时,使用自定义的LeakService
public class GmfApplication extends Application{
    @Override
    protected void onCreate(){
        super.onCreate();
        mRefWatcher = LeakCanary.install(this, LeakService.class, AndroidExcludedRefs.createAppDefaults().build());
    }
}
  • 需要在AndroidManifest中注册LeakService。

优化内存空间

对象引用

根据业务需求,使用合适的引用类型

  • 强引用:如果没有指定对象引用类型,默认是强引用。如果一个对象具有强引用,垃圾回收器(GC)就绝不会回收它,即使内存空间不足。因此如果强引用的对象,在不需要时要记得释放或转成弱引用以便系统回收。
  • 软引用:在保存引用对象的同时,保证在虚拟机报告内存不足的情况之前,清除所有的软引用。
  • 弱引用:垃圾收集器运行时如果扫描到弱可及对象,将释放WeakReference引用的对象,不管当前内存是否足够都会回收它的内存。
  • 虚引用:只能用于跟踪即将对被引用对象进行的收集,使用户能够刚好在对象占用的内存被回收之前采取行动。(如果一个对象尽持有虚引用,它就和没有任何引用一样,在任何时候都可能被垃圾收集器回收)

减少不必要的内存开销

  1. 自动装箱AutoBoxing
Integer num = 0;
for(int i=0; i < 100; i++){
    num += I;
}

考虑上面的情况,在自动装箱转化时,都会产生一个新的对象,这些对象比基础数据类型要大,这样会产生更多内存和性能开销。(int只有4字节,而Integer对象有16字节)

  1. 内存复用
  • 有效利用系统自带的资源:比如一些通用的字符串、颜色定义、常用Icon等。
  • 视图复用:重复子组件,可以使用ViewHolder实现ConvertView复用
  • 对象池:显示在程序创建对象池,实现复用逻辑,对相同类型数据使用同一内存空间
  • Bitmap对象复用:利用Bitmap中的inBitmap的高级特性。
  1. 使用最优的数据类型
  • ArrayMap与HashMap

HashMap是一个散列链表,先HashMap中put元素时,先根据key的HashCode重新计算hash值,根据hash值得到这个元素在数组中的位置,如果数组位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头。为了减少hash冲突,会配置一个大的数组,从内存节省的角度是非常不理想的。为了解决这个问题,Android提供了一个替代容器ArrayMap。

ArrayMap提供了和HashMap一样的功能,但避免了过多的内存开销,方法是使用两个小数组而不是一个大数组。其中一个数组记录对象Key Hash过后的顺序列表,另外一个数组按Key的顺序记录Key-Value值,根据Key数组的顺序,交织在一起。在获取某个value时,ArrayMap会计算输入Key转换后的hash值,然后使用二分查找法对Hash数组寻找到对应的index,然后通过这个index在另外一个数组中直接访问需要的键值对。如果在第二个数组键值对中的key和前面输入的查询key不一致,就认为发生了碰撞冲突。ArrayMap会以该key为中心点,分别上下展开,逐个对比查找,直到找到匹配的值。

ArrayMap中执行插入或删除时,性能比HashMap要差一点,但如果设计对象数少,比如1000以下,不用担心这个问题。用ArrayMap能节省内存。

  • 枚举类型和替代方案

枚举的优点是类型安全,可读性高,但是枚举的内存开销是直接定义常量的三倍以上。官方也提醒尽量避免使用枚举类型,同时提供注解的方式检测类型安全,目前提供了int和String两者类型注解方式:IntDef和StringDef。即使用“常量定义+注解”替代枚举。

public static final int UI_LEVEL_0 = 0;
public static final int UI_LEVEL_1 = 1;

@IntDef({UI_LEVEL_0, UI_LEVEL_1})
@Retention(RetentionPolicy.SOURCE)
public @interface PER_LEVEL{
    
}

public static int getLevel(@PER_LEVEL int level){
    switch(level){
        case UI_LEVEL_0: return 0;
        case UI_LEVEL_1: return 1;
        default:
            throw new IllegalArgumentException("UnKonw");
    }
}

使用IntDef和StringDef需要在Gradle引入依赖

compile 'com.android.support:support-annotation:22.0.0'
  1. LruCache

图片内存优化

Android设备上显示图片需要把图片解码成位图格式,占用的内存只和位图的质量和大小相关。下面介绍几种减少图片内存开销的方法:

  1. 设置位图规格

系统默认位图格式是RGB_8888占用内存较高,一般用RGB_565或RGB_4444代替。
RGB_8888占32bit、GB_565和RGB_4444都是16bit、ALPHA_8占8bit

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap.Factory.decodeStream(is, null, options);
  1. 缩放inSampleSize

如果内存中的图片大于屏幕需显示图片的大小,这些高分辨率图片会导致性能问题。可以通过重置这些图片大小,让它们符合实际显示大小。Bitmap的inSampleSize属性能实现位图缩放功能。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4; //实际要根据宽高比例计算缩放比例
Bitmap.Factory.decodeStream(is, null, options);
  1. 三级缓存

可参考郭霖博客

本文参考书籍《Android应用性能优化最佳实践》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,100评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,862评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,993评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,309评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,303评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,421评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,830评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,501评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,689评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,506评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,564评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,286评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,826评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,875评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,114评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,705评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,269评论 2 341

推荐阅读更多精彩内容