性能优化-内存优化

2020-02-22  本文已影响0人  Vinson武

内存优化

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

Android内存管理机制

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

Java对象生命周期

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

  1. 创建阶段(Created)
  1. 应用阶段(InUse)
  1. 不可见阶段(Invisible)
  1. 不可达阶段(Unreachable)
  1. 收集阶段(Collected)
  1. 终结阶段(Finalized)
  1. 对象空间重新分配阶段(Deallocated)

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

内存分配

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

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

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

其中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

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

内存对象的处理过程如下

回收机制

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

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

GC类型

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

内存优化的意义

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

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

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

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

内存分析工具

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

Allocation Tracker

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

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

  1. Allocation Tracker的使用:
  1. 查看面板信息

避免内存泄漏

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

使用MAT查找内存泄漏

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

1. 使用步骤
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资源无法被回收,引发内存泄漏。
    为避免这种情况要修改两个地方:
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. 实现监控
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. 自定义处理结果

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

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详细信息

public class GmfApplication extends Application{
    @Override
    protected void onCreate(){
        super.onCreate();
        mRefWatcher = LeakCanary.install(this, LeakService.class, AndroidExcludedRefs.createAppDefaults().build());
    }
}

优化内存空间

对象引用

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

减少不必要的内存开销

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

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

  1. 内存复用
  1. 使用最优的数据类型

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应用性能优化最佳实践》

上一篇下一篇

猜你喜欢

热点阅读