内存使用总结篇 -- Android内存优化第五弹

cover

前面几弹从Android内存管理, GC机制理论, 到内存分析工具, 内存泄露实例分析等几个方面聊了下Android App中关于内存优化的一些个知识.

本篇作为Android App内存优化的第五弹, 也是最后一弹, 将对Andorid中的内存优化做一个简单的总结.

1, 回顾

系列文链接:

1.GC那些事儿
2.Android的内存管理
3.内存分析工具
4.内存泄露实例分析

几个要点:

  • Android的App运行在Dalvik/ART这种类JVM环境的, 使用的是自动内存管理方式, 也就是通常说的GC机制.
  • 每个App默认单独运行在一个VM进程内, 其内存使用是有上限的.
  • 所谓GC就是回收垃圾对象.
  • 所谓垃圾, 就是GC Roots不可达的对象, 也就是死对象(相对于活对象).
  • 对象占用内存(Retained Size)是其所支配(Dominate)的所有子对象的占用内存之和. 故而我们找内存消耗点, 和内存泄露的时候都是关注对象的Retained Size.
  • 所谓内存分析, 最多是就是使用工具定位是哪个对象支配着某个Retained Size很大的对象, 进而定位出内存消耗或内存泄露点.

回顾之后, 我们再来看下内存问题.

2, 内存问题

从大的分类上来说, Android App中关于内存的问题大致可以分成如下三类:

  • 内存泄露
  • 内存消耗过大
  • 内存抖动

前二者, 内存泄露和内存消耗过大, 最终的结果就是我们常见的OutOfMemoryException, 今天我们的内存使用总结也主要是针对这二者.

关于内存抖动我们在App优化之消除卡顿一文中有描述.

3, 常见的内存泄露及其解决方案

以下关于泄露的名字, 个人根据自己的习惯起的, 并非哪儿的官方称呼, 希望没有误导到吃瓜群众.

3.1 Context泄露

Context使用不当导致的内存泄露.
一般来说是因为某些全部的对象, 理当使用Application级别的Context, 而使用了指定Activity的Context, 导致该Activity无法释放.

例如, 某个单例中需要一个Context, 传入了一个Activity的Context, 导致其被这个单例持续引用而无法回收.

这类泄露的解决方案, 就是根据组件的生命周期来正确使用Context, 全局引用使用Application Context.

关于各种Context的说明和使用请参看这篇译文.
Context泄露的实例还可以看下android dev blog中的这篇, 需翻墙.

3.2 内部类泄露

由于(匿名)内部类隐式地持有一个外部类的引用, 故而当内部类中执行的事情长于外部类的生命周期时, 就会导致外部类的泄露.

常见的此类泄露包括Handler泄露, Thread泄露..., 这些也是我们经常会作为(匿名)内部类在Activity中使用的.

下面以HandlerLeak为例:

如下:

public class HandlerLeakActivity extends AppCompatActivity {

    private BigObject mBigObject = new BigObject();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);

        mHandler.sendEmptyMessageDelayed(1, 60 * 1000);

    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
}

这段代码我们实际上非常多的使用, 然而如果我们用android lint工具检测的话, 会有一段这样的提示:


也就是说这个Handler类可能会导致内存泄露, 建议我们使用static方式. 点开"more", 我们来看下官方的建议解决方案:

Since this Handler is declared as an inner class, it may prevent the outer
class from being garbage collected. If the Handler is using a Looper or
MessageQueue for a thread other than the main thread, then there is no issue.
If the Handler is using the Looper or MessageQueue of the main thread, you
need to fix your Handler declaration, as follows: Declare the Handler as a
static class; In the outer class, instantiate a WeakReference to the outer
class and pass this object to your Handler when you instantiate the Handler;
Make all references to members of the outer class using the WeakReference
object.

阅读这段"more"的前半段, 我们分析下泄露是怎么产生的:

因为这个Handler是一个内部类(默认持有一个外部类也就是我们的HandlerLeakActivity的引用), 如果这个Handler的Looper/MQ所在的Thread与Main Thread不同, 则没有问题. 但是如果Handler的Looper/MQ就是Main Thread(本例中就是), 那么问题就来了:

这个Handler发送的message会放到MQ中, 这个message会对Handler有一个引用, 而Handler有HandlerLeakActivity的引用. 当我们进入这个Activity, 然后退出, 理当销毁这个Activity并回收了. 但是因为这个message会延时60s, 故而导致这个mHandler被引用, 从而activity被引用着, 而无法回收释放内存.

GC那些事儿中, 我们就讲到, 运行中的Thread就是GC Root之一, 根据上面的分析, 得出: HandlerLeakActivity到GC Roots可达, 故而无法回收.

我们用LeakCanary来验证下我们的分析:


可以看到, 果然如我们分析的.

那么此类问题怎么解决呢, 可能很多同学也直接使用加上@SuppressLint("HandlerLeak")的方式来避免lint提示了, 如下:

@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
   @Override
   public void handleMessage(Message msg) {
       super.handleMessage(msg);
   }
};

然而这并非解决之道, 其实这段"more"的后半段也给了我们解决方案 --- 使用Static + WeakReference的方式, 具体如下:

public class HandlerLeakActivity extends AppCompatActivity {

    private BigObject mBigObject = new BigObject();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);

        new DemoHandler(this).sendEmptyMessageDelayed(1, 60 * 1000);

    }

    private static class DemoHandler extends Handler {

        private final WeakReference<HandlerLeakActivity> mActivity;


        private DemoHandler(HandlerLeakActivity activity) {
            this.mActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            HandlerLeakActivity activity = mActivity.get();
            if (activity != null) {
                activity.doSomething();
            }
        }
    }

    private void doSomething() {

    }
}

留下一个问题, 为什么说这个Handler不在Main Thread的时候不会有问题, 大家可以自行研究下, 有机会就HandlerLeak这个话题我们再深入研究下.

3.3 Register泄露

对于观察者, 广播, Listener等, 注册和注销没有成对出现而导致的内存泄露.

内存泄露实例中那个例子, 就是这种泄露, 在此不在细述了.

解决方案就是编码的时候多注意吧, add/remove, register/unregister, bind/unbind什么的~.

3.4 资源泄露

常见的数据库查询Cursor, 文件读写流等, 用完没有关闭导致的内存泄露.

例如:

public class CursorLeakActivity extends AppCompatActivity {

    private BigObject mBigObject = new BigObject();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);

        Cursor cursor = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);

        if (cursor != null) {
            cursor.moveToFirst();

            // do something.
        }
    }
}

这个cursor就可能泄露, 实际上android lint也给了我们提示:

此类问题的解决方案, 一般我们使用try-catch-finally的结构, 在finally中关闭并释放资源.
如下:

public class CursorLeakActivity extends AppCompatActivity {

    private BigObject mBigObject = new BigObject();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak);

        Cursor cursor = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);

        try {
            if (cursor != null) {
                cursor.moveToFirst();

                // do something.
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            if (cursor != null) {
                cursor.close();
                cursor = null;
            }
        }
    }
}

3.5 Bitmap泄露

Bitmap没有及时调用recycle()回收导致的泄露.

对于Bitmap是使用, 一直就是Android开发者的痛, 特别是对大图片的处理. 可以说我们大多数的报出来的OutOfMemory异常基本都是因为要给某个Bitmap分配内存, 而可用内存不够导致的.

3.6 内存泄露小结

对于内存泄露, 我们尽量是以防为主. 根据上面的常见内存泄露, 我们需要注意以下几点:

  • Context的(根据组件生命周期)合理使用.
  • 避免在Activity中使用非静态内部类, 可以静态内部类+WeakReference达成目的.
  • 注意add/remove, register/unregister, bind/unbind的成对使用.
  • 资源及时关闭, 释放.

4, 有效使用内存的建议

本节大部分内容来自官方开发文档.

  • 合理使用Service
    Service的及时关闭可以让我们节省内存消耗, 对于一次性的任务, 建议使用IntentService.

  • 使用优化后的数据容器
    使用Android提供的SparseArray, SparseBooleanArray, LongSparseArray来代替HashMap的使用.
    关于HashMap,ArrayMap,SparseArray, 这篇文章有个比较直观的比较, 可以看下.

  • 少用枚举enum结构
    相比于静态常量(static final), enum会耗费双倍的内存.

  • 避免创建不必要的对象
    诸如一些临时对象, 特别是循环中的.

  • 考虑实现onTrimMemory(), 在此根据当前的内存状态做些处理.

  • Bitmap的合理有效使用.
    对于Bitmap的使用, 建议直接查看官方开发文档中的高效显示Bitmap(需翻墙).

结语

至此, Android App内存优化的5发子弹就打完了, 关于App内存优化的部分, 我们就先到这里了, 可能还有很多遗漏的内容.

再次表明下我写文的思想: 一个是想记录下自己的一个解决问题的思路和经验, 再一个是想传达如何去解决问题的思想. 故而, 文章并不是一开始就说有哪些内存问题, 怎么解. 而是从理论基础到分析工具的使用, 案例的分析去一步步的让大家学会怎么处理这类问题.

希望大家能从中得到一些关于解决问题的启发, 而非被灌输一些强记下的知识.

感谢相随...

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

推荐阅读更多精彩内容