Android 内存优化—从理论到实战

一、Android 内存问题

    要对Android 内存进行优化首先要知道的是Android 内存存在什么问题,哪里存在问题,才能对症下药,精准解决问题。

1.1 内存抖动

    内存抖动即内存频繁分配和回收导致内存不稳定。频繁创建对象,导致内存不足或者产生内存碎片,内存碎片即内存不连续,有内存空洞, 某两个正在使用的内存中间有一个间隔, 这个间隔虽然也被算在可用内存里面, 但实际上因为它过小, 当我们申请内存的时候,经常是需要申请一定量的连续内存, 而这些碎片小内存不符合要求,是不能拿来使用的。频繁GC会导致卡顿,随后不连续的内存片无法被分配,可分配的内存减少,便最终可能导致OOM。
    简单来说就是:在程序需要对象的时候,在堆当中分配出来一块空间,使用完毕以后, GC 帮我们清理掉这片内存空间,如果频繁的一直持续上述操作,就会引起内存抖动。

1.2 内存抖动常见场景
1.2.1 集合类存在不对称机制

    集合类如果仅仅有添加元素的机制,而没有相应删除元素机制,这样就会造成内存被占用,如果这个类是全局性变量(比如类中有静态属性,全局性的map等即有静态引用或final一直指向它)。那么没有相应删除机制,很可能导致集合所占内存只增不减。

  • 解决办法:在使用集合类时,增加删除元素机制,并适当调用减少集合所占内存。
1.2.2 不正确使用单例模式

    不正确使用单例模式,也会引起内存泄漏单例对象在初始化后将在JVM的整个生命周期存在(以静态变量方式),如果单例对象持有外部对象的引用,那么这个外部对象就会一直占用着内存,可能导致内存泄漏(取决于这外部对象是否一致有用)。

  • 解决办法:单例对象中避免含有不是一直都有用的外部对象引用。
1.2.3 不正确使用Android组件 或 特殊集合对象

     BraodcastReceiver ,ContentObserver ,fileObserver ,Cursor,Callback 等在 Activity onDestory 或者某类生命周期结束 之后一定要 unregistere 或者 close 掉,否则这个Activity类会被system强引用,不会被回收。
不要直接对Activity进行直接引用作为成员变量,如果不得不这么做,调用 private WeakPeferense mActivity 来做,相同的,对与 Service 等其他有自己生命周期的对象来说,直接引用都需要考虑是否会存在内存泄露的可能。

1.2.4 Handler 使用问题

     要知道,只要 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。由于 Handler 属于 TLS(Thread Local Storage)变量,生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 view 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。如上所述,Handler使用要特别小心,否则很可能内存泄漏。

  • 解决办法:在view 或者Activity生命周期结束前,确保Handler已没有未处理的消息(特别是延时消息)。
1.2.5 Thread 内存泄漏

     线程也是造成内存泄露的一个重要源头,线程产生内存泄露的主要原因在于线程生命周期不可控,比如线程是 Activity的内部类,则线程对象中保存了 Activity 的一个引用,当线程的 run函数耗时较长没有结束时,线程对象是不会被销毁的,因此它所引用的老的Activity就出现了内存泄漏问题。

  • 解决办法:
  • 1. 简化线程run函数执行的任务,使他在Activity生命周期结束前,任务运行完。
  • 2. 为Thread增加撤销机制,当Activity生命周期结束时,将Thread的耗时任务撤销(推荐)。
1.2.6 一些不良代码造成的内存压力

     有些代码并不造成内存泄漏,但是他们是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。

  • (1) Bitmap 没调用recycle()
    Bitmap 对象在不使用时,我们应该先调用recycle()释放内存,然后才置空,因为加载bitmap对象的内存空间,一部分是java的,一部分是c的(因为Bitmap分配的底层是通过jni调用的,Android的Bitmap底层是使用skia图形库实现,skia是用c实现的)。这个recycle()函数就是针对c部分的内存释放。

  • (2)构造Adapter时,没有使用缓存的convertView。
    解决办法:使用静态holdview的方式构造Adapter。

二、Android Profile使用

     手机上运行要分析 APP,再在 AS 的 Profiler 窗口选择要调试的进程,打开 CPU Profiler。得到如下图所示信息。
Android profile

接着点击 memory 栏目,进入内存记录配置选择:
点击 memory

这里有 3 个配置选项,

memroy内存记录配置选择页面

分别是

  • AS3.0时的堆转储功能,捕获一个内存快照,
  • 记录 C / C++ 层的对象
  • 记录 Java / Kotlin 对象

然后我们记录一段时间的内存之后


如上图所示,我们可以获取指定时间的当前内存分配情况。在左下角的类列表中,您可以查看以下信息:

  • Allocations:在选定时间段内通过 malloc() 或 new 运算符分配的对象数。
  • Deallocations:在选定时间段内通过 free() 或 delete 运算符解除分配的对象数。
  • Allocations Size:在选定时间段内所有分配的总大小(以字节为单位)。
  • Deallocations Size:在选定时间段内所有已释放内存的总大小(以字节为单位)。
  • Total Count:Allocations 列中的值减去 Deallocations 列中的值所得的结果。
  • Remaining Size:Allocations Size 列中的值减去 Deallocations Size 列中的值所得的结果。

接下来先模拟一段内存抖动代码:

public class MemoryShakeActivity extends AppCompatActivity {
 
    @SuppressLint("HandlerLeak")
    private static Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
 
            // 创造内存抖动(编写耗内存的操作)
            for (int index = 0; index <= 100; index++) {
                String arg[] = new String[100000];
            }
            mHandler.sendEmptyMessageDelayed(0, 30);
        }
    };
 
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_shake);
 
        findViewById(R.id.bt_memory).setOnClickListener(v -> mHandler.sendEmptyMessage(0));
    }
 
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

运行起来后,内存是平稳的:


点击按钮后,开始出现锯齿状:



可以看到上面的连续垃圾桶就代表不断地在GC,我们获取一个区域的内存信息:



     我们可以看到锯齿的位置,String[] 的分配是相对比较大的; Shallow Size是该类型实例的总大小(以字节为单位)。于是现在可以锁定,String[] 是最可疑的引起内存抖动的原因, 点击左边的String[]行项,工具会在右边,弹出另外一个窗口, 窗口上边是分配出来的该类型的所有实例(<工具右上>), 点击任意一个实例, 又会在下边弹出一个该实例的内存分配的堆栈信息(<工具右下>——Allocation Call Stack), 信息即这个实例占有的这块内存是在哪里分配的。

三、内存泄漏

3.1 概况

所谓内存泄漏即是本该被回收的内存垃圾没有被回收。
与内存抖动的区别是:
内存抖动是指:在短时间内频繁发生申请内存 & 回收内存的操作,即频繁GC

    
    
    

本文参考:
博客:
Android | App内存优化 之 内存抖动解决实战
Android | App内存优化 之 全面理解MAT
Android | App内存优化 之 内存泄漏 要点概述 以及 解决实战
使用Android Studio和MAT进行内存泄漏分析
使用内存分析器查看应用的内存使用情况

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

推荐阅读更多精彩内容