Android性能优化--内存优化

1.前言

上一篇文章关于Android性能优化--启动优化探讨了启动优化相关的知识点,在本篇将介绍内存优化的相关优化。主要大纲参照如下

memory.jpg

2.常见问题

常见的Android内存相关问题,通常可以分为以下三种,内存抖动、内存泄露、内存溢出。

  • 内存抖动:在短时间内有大量的对象被创建或者被回收的现象,主要是循环中大量创建、回收对象。当系统内存不足,不断GC内存的时候,也有可能出现内存抖动情况。
  • 内存泄露:当一个对象不在使用了,本应该被垃圾回收器回收。但是这个对象由于被其他正在使用的对象所持有,造成无法被回收的现象。
  • 内存溢出:Android系统给每个应用分配的内存也有一个阀值,也就是Heap Size。当应用占用的内存加上我们申请的内存资源超过了系统分配的最大内存时就会抛出的Out Of Memory异常。
    上述三者之间的是一个递进关系,内存抖动<内存泄露<内存溢出。对于一般应用主要是处理内存抖动和内存泄露两点,处理好这两点就会大大降低内存溢出的可能性。

3.内存管理

3.1 Java内存管理

JVM的内存回收对于大多数开发者来说接触的并不是很多。因为JVM本身是有一套内存回收的机制,对于开发者更多的是申请对象直接调用即可,其内部并不是很在意。下面主要通过内存存储和回收这两块介绍。

  • 存储:JVM将可以存储内存的空间大概分为栈、本地方法栈、程序计数器、堆、方法区等模块。
  1. 栈:主要是针对方法使用的空间,当JVM在执行方法时,会在此区域中创建一个栈帧来存放方法的各种信息,比如返回值,局部变量表和各种对象引用等。
  2. 本地方法栈:专门提供给Native方法用的。
  3. 程序计数器:记录当前执行的位置。
  4. 堆:几乎所有对象、数组等都是在此分配内存的,在JVM内存中占的比例也是很大的,也是GC回收的主要阵地,平时我们说的新生代、老年代、永久代也是指这片区域。
  5. 方法区:存放类似类定义、常量、编译后的代码、静态变量等。
  • 回收:针对上述各个模块的内存回收,通常所说的GC主要是对堆空间的回收,一般比较常用的方法为:标记-清除算法、复制算法、分代收集算法等其它方法和其变形。
3.2 Android内存管理

Android 系统主要是在Art和Dalvik虚拟机中的托管环境中跟踪每个内存分配,当发现有可回收的对象,进行内存回收。回收有两个目标:在程序中查找将来无法访问的数据对象,并回收那些对象使用的资源 。

  • 进程间的内存管理:Android对于进程间的内存管理主要是通过内核交换守护程序和onTrimMemory()进程杀死来管理。
  1. 内核交换守护程序(kswapd):RAM中存在一个区域空间zRAM。当设备上的可用内存不足时,守护程序将变为活动状态。kswapd可以将缓存的私有脏页和匿名脏页移动到zRAM,并在其中进行压缩。
  2. onTrimMemory:系统用于 onTrimMemory()通知应用程序内存即将用尽,并应减少其分配。如果这还不够,内核将开始杀死进程以释放内存。它使用低内存杀手(LMK)来执行此操作。PS:LMK 这就会涉及到应用保活等相关。
  • 应用内存管理:Android应用内内存管理,主要是从Java层和Native 层优化。本文主要介绍如何从Java层进行内存管理优化,具体细节可以下面会一一介绍。

4.常见场景及解决方案

4.1 内存抖动

由于短时间内有大量对象进出Young Generiation区导致的,它伴随着频繁的GC。

  1. 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
  2. 注意自定义View的onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象。
    如下面一部分代码就对应着内存抖动
    Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            for (int i =0;i<100;i++){
                String string[] = new String[10000];
            }
            handler.sendEmptyMessageDelayed(1,30);

        }
    };

通过Profile 查看其内存图

memory_shake.jpg

可以看到其内存图基本上是一个锯齿状,是因为这时候一直在创建对象和回收对象所致。

4.2 内存泄露

业内一般对内存泄露的原因总结为长生命周期对象引用短生命周期对象,导致短生命周期对象无法及时回收所致。

  1. 单例引起的内存泄漏
public static  FacebookAnalysis getInstance(Context context){
        if (facebookAnalysis == null){
            synchronized (FacebookAnalysis.class){
                facebookAnalysis = new FacebookAnalysis(getAppEventsLoggerInstance(context));
            }
        }
        return facebookAnalysis;
    }

上面是一个常见的单例模式,如果参数引用Activity的Context,而单例模式的生命周期长于Activity。这里单例模式引用Activity的实例,当Activity被销毁,Activity无法被回收,造成内存泄露。
如果这里引用的Application的Context,将无任何影响。因为Application的生命周期与单例模式同样长。

  1. 静态集合添加对象,在使用完之后未及时释放。
        for (int i = 0; i < 10; i++) {
            Object obj = new Object();
            list.add(obj);
            obj = null;
        }

此时list是一个静态的集合,obj单个对象,当list集合使用完毕,应当及时清除该集合,避免obj被静态对象引用。

  1. 匿名内部类&非静态内部类
    Android 中常见的是对ListView中各个元素设置点击事件,如果此时采用匿名内部类,会存在内存泄露的风险。常规做法是将该点击事件用接口和setTag()的方式往外传递。
    同样Handler在使用过程中也会出现内存泄露的风险,一般则是采用弱引用的方式处理,或者在Activity 的onDestroy方法中移除该Handler的所有消息handler.removeCallbacksAndMessages(null)
  2. 线程泄露
    在主线程Activity中出现如下代码:
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        mIntent = (icicle == null) ? getIntent() : null;
        new Thread(new Runnable() {
            @Override
            public void run() {
                doSomeThing();
            }
        }).start();
}

当此时该Activity已经销毁,但是子线程中doSomeThing方法未执行完成,此时会造成内存泄露。一般做法是当Activity销毁时取消该线程或者采用其他方式实现,总之原则是线程不持有Activity的上下文,如果持有,就应及时取消。

  1. 数据库游标,文件资源未及时关闭,广播未反向注册,服务未解绑等行为。
  2. Bitmap加载泄露,Bitmap的加载在Android一直是比较吃内存的,且容易出现内存泄露相关问题,一般都是采用统一的图片请求框架去处理图片加载缓存,这些框架都会从加载、压缩、缓存等策略对其做优化处理。同时Google 官方也是推荐使用统一库处理位图,具体可以在Glide官网查看。关于图片加载这块其实是很容易出现内存泄露的问题,在此暂时不作展开,后续会细说。

5.常用工具

5.1 Memory Profiler

Memory ProfilerAndroid Profiler 中的一个组件,可帮助您识别可能会导致应用卡顿、冻结甚至崩溃的内存泄漏和内存抖动。它显示一个应用内存使用量的实时图表,可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。从图中现象

memory_shake.jpg

可以看出应用此时存在内存抖动的现象,此时抓取红色部分,可以得到如下图:
shake.jpg

-A区域为拖住怀疑动动的部分
-B区域为排序发现存在一组对象耗内存
-D选中C区域中任一对象,即可看见具体类为MainActivity,且可以看到行数,右击Jump to Source即可以跳入具体代码。
5.2 Memory Analyzer
Memory Analyzer MAT
是一个功能丰富的 JAVA 堆转储文件分析工具,可以帮助开发者发现内存漏洞和减少内存消耗。根据上面分析下面这段代码是存在内存泄露的问题:

public class CallBackManager {

    public static ArrayList<CallBack> sCallBacks = new ArrayList<>();

    public static void addCallBack(CallBack callBack) {
        sCallBacks.add(callBack);
    }

    public static void removeCallBack(CallBack callBack) {
        sCallBacks.remove(callBack);
    }

}

public class MemoryLeakActivity extends AppCompatActivity implements CallBack{

    @Override
    protected void onCreate( Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memoryleak);
        ImageView imageView = findViewById(R.id.iv_memoryleak);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.splash);
        imageView.setImageBitmap(bitmap);
        CallBackManager.addCallBack(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
//        CallBackManager.removeCallBack(this);
    }

    @Override
    public void doWork() {
        // do work
    }
}

连续进入退出MemoryLeakActivity,通过Android StudioProfile可以查看到当时的内存入下图

menmory_leak.jpg

可以看到此时内存处于一个波动状态,保存该文件,生成memory-20191212T110510.hprof文件,通过命令转换

D:\>hprof-conv D:\Android\log\memory-20191212T110510.hprof D:\Android\log\2.hprof

此时通过MAT打开转换后的文件如下:

hprof.jpg

选择Histogram,输入正则表达".MemoryLeak. "可以搜索到具体包名,然后右键List objects->With incoming references然后选择Path to GC Roots->With all references(此处可以选择其他)。此时可以看到下面这张图
leak.jpg
从图中即可看出内存泄露的位置,即该Activity被对象sCallbacks引用。在代码中添加方法

    protected void onDestroy() {
        super.onDestroy();
        CallBackManager.removeCallBack(this);
    }

再次抓取内存信息,并未出现上述结果。
5.3 LeakCanary
可以通过LeakCanary在开发阶段检测到引用的内存情况。LeakCanary 主要是通过监听Activity的onDestory,手动调用GC,然后通过ReferenceQueue+WeakReference,来判断Activity对象是否被回收,然后结合dump Heap的hpof文件,通过Haha开源库分析泄露的位置。具体使用可以参照leakcanary

6. 总结

关于内存优化知识点很多,很细。但究其根本我认为是监控内存泄露和优化内存泄漏,各大厂商都有提过相关的方案
美团—Android线上OOM问题定位组件
微信 Android 终端内存优化实践
这些都具备参考价值。同时我们也可以采用一些Hook黑科技相关方法进行部分内存性能消耗较大的业务进行监控,及时告知开发人员。例如:可以通过Epic监控项目中所有的setImageBitmap()方法,此时就可以知道传入的Bitmap是否有内存相关风险,一旦有风险,立马通知反馈。
以上为此次Android内存优化的总结,欢迎指正。
感谢:
https://developer.android.google.cn/topic/performance/memory-overview
https://time.geekbang.org/column/article/71610
https://coding.imooc.com/learn/list/308.html

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