Android内存管理(二)

声明:大部分内容为从其他文章中摘录感兴趣的部分,只为记录给自己看。

Android内存性能主要包括内存泄漏,内存抖动,内存持续增长(但GC后会下降),内存占用过大等问题。

Android内存分析方向:

  • Java内存分析
    Java中的内存泄漏主要特征:可达,无用
    无用指的是创建了但是不再使用之后没有释放
    能重用但是却创建了新的对象进行处理
  • Native内存分析
    堆中new的对象未释放
    对象引用导致无法释放
  • JS中内存分析

一、日志分析

查看日志中是否有频繁的GC。通常通过log,我们可以初步定位大部分内存等问题。

二、常见内存泄漏查找

  • Context泄漏
    主要为Activity传递泄漏,在单例创建时context未使用applicationContext。
  • Handler泄漏
    handler中持有view,context等做耗时操作。
  • Cursor泄漏,cursor未关闭。
  • register未unregister。
  • Bitmap
  • adapter未使用convertView
  • 不良代码等

三、命令dumpsys meminfo分析

理解meminfo的信息中各字段都是什么含义,才好进行内存的优化。首先了解两个概念:

  • 私有内存(Dirty and Clean)
    进程独占内存。也就是进程销毁时可以回收的内存容量。通常private Dirty内存是最重要的部分,因为只被自己进程使用。Dirty内存是已经被修改的内存页,因此必须常驻内存(因为没有swap);Clean内存是已经映射持久文件使用的内存页(例如正在被执行的代码),因此一段时间不使用的话就可以置换出去。
  • 实际使用内存(PSS)
    将跨进程共享也加进来了,进行按比例计算PSS。这样能够比较准确的表示进程占用的实际物理内存。通常我们需要关注PSS TOTAL和Private Dirty。
  • Dalvik Heap
    dalvik虚拟机分配的内存。PSS Total包含所有Zygote分配使用的内存,共享跨进程加权。PrivateDirty是应用独占内存大小,包含独自分配的部分和应用进程从Zygote复制时被修改的Zygote分配的内存页。HeapAlloc是Dalvik堆和本地堆分配使用的大小,它的值比Pss Total和Private Dirty大,因为进程是从Zygote中复制分裂出来的,包含了进程共享的分配部分。
  • .so mmap & .dex mmap ... mmap
    映射本地或虚拟机代码到使用的内存中。
  • Unknown
    无法归类的其他项。主要包括大部分的本地分配。
  • Native Heap
    native代码申请的内存,堆和栈,及静态代码块等。
  • TOTAL 进程总使用的实际内存。
  • Objects 显示持有对象的个数。
    这些数据也是分析内存泄漏的重要数据。如activity等。

四、Heap Viewer

Heap Viewer能做什么?

  • 实时查看内存分配情况和空闲内存大小。
  • 发现memory Leaks。

Android Studio3.0以上可以查看Memory Profiler。用Memory Profiler查找内存泄漏:

  1. 启动应用,看一下当前内存使用了多少,使用应用一段时间后(不想自己点,可以使用monkey进行自动化测试),退回到应用首页,看看当前内存又是多少。进行一次heap dump看看结果,分析一下有没有可疑的对象分配(比如大量重复的activity,同一个类型对象比较多,对象内存占用较大)。
  2. 发现可疑点后,通过分析结果,可以找到相应代码,就可以找到使用代码的场景,例如是activity泄漏,反复进行画面的跳转(也可以旋转屏幕),然后强制gc回收,看看内存是否存在只增不减的情况。
  3. 也可以使用allocation跟踪一段时间内存分配情况,拿来做分析。

常见内存泄漏案例

1. 单例造成的内存泄漏

单例的静态特性导致其生命周期同应用一样长。
解决方案:

  1. 将改属性的引用方式改为弱引用
  2. 如果传入Context,使用Application
public class ScrollHelper {
    private static ScrollHelper mInstance;

    public ScrollHelper() {
    }

    public static ScrollHelper getInstance() {
        if (mInstance == null) {
            synchronized (ScrollHelper.class) {
                if (mInstance == null) {
                    mInstance = new ScrollHelper();
                }
            }
        }
        return mInstance;
    }
}
    /**
     * 被点击的View
     */
    private View mScrolledView = null;

    public void setScrolledView(View scrolledView) {
        mScrolledView = scrolledView;
    }

改成:

/**
 * 被点击的View
 */
private WeakReference<View> mScrolledView = null;

public void setScrolledView(View scrolledView) {
    mScrolledView = new WeakReference<View>(scrolledView);
}

2. 匿名内部类

在Java中,非静态内部类和匿名类都会潜在的引用他们所属的外部类,但是静态内部类却不会。如果这个非静态内部类实例做了一些耗时的操作,就会造成外围对象不会被回收,从而导致内存泄漏。

  1. 将内部类变成静态内部类;
  2. 如果有强引用Activity中的属性,则将改属性的引用方式改为弱引用;
  3. 在业务允许的情况下,当activity执行onDestroy时,结束这些耗时任务;
public class MainActivity extends AppCompatActivity {
    private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mResource == null){
            mResource = new TestResource();
        }
        //...
    }
    class TestResource {
        //...
    }
}

因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法是:将改内部类设为静态内部类或将改内部类抽取出来封装成一个单例,如果需要使用Context,使用Application的Context。

3. Handler引起的内存泄漏

当Handler中有延迟的任务或等待执行的任务队列过长,由于消息持有对Handler的引用,而Handler又持有对其外部类的潜在引用,这条引用关系会一直保持到消息得到处理,而导致了Activity无法被回收。

解决方案:

  1. 可以把Handler类放在一个单独的类文件中,或者使用静态内部类便可以避免泄漏;
  2. 如果在handler内部去调用所在的Activity,那么可以在handler内部使用弱引用的方式去指向该activity,使用static+WeakReference的方式断开Handler和Activity之间存在的引用关系。
  3. 在onDestroy里面执行removeCallbacksAndMessages(null);
public class SampleActivity extends Activity {

    private static class MyHandler extends Handler {
        private final WeakReference<SampleActivity> mActivity;

        public MyHandler(SampleActivity activity) {
            mActivity = new WeakReference<SampleActivity>(activity);
        }

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

    private final MyHandler mHandler = new MyHandler(this);

    private static final Runnable sRunnable = new Runnable() {
        @Override
        public void run() { /* ... */ }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

        finish();
    }
}

4. 集合中对象没清理造成的内存泄漏

我们通常把一些对象的引用加入到了集合容器中,当不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果它是static的话就更严重了。如果这个集合类是全局性的变量,那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

5. WebView造成的泄漏

当我们不再使用WebView对象时,应该调用它的destroy()函数来销毁它,并释放其占用的内存,否则其占用的内存长期不能回收。

解决方案:
为WebView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需求选择合适的时机进行销毁,从而达到内存的完整释放。

四种引用的区别

强引用

只要引用存在,垃圾回收器永远不会回收。

Object obj = new Object();

只有当这个引用被释放后,对象才会被释放掉。

软引用

非必须引用,内存溢出之前进行回收,可以通过以下代码实现

SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();// 有时候会返回null

这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null。

软引用主要实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。

弱引用

第二次垃圾回收时回收,可以通过如下代码实现:

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();// 有时候会返回null
wf.isEnQueued();// 返回是否被垃圾回收器标记为即将回收的垃圾

弱引用时在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。

弱引用主要在于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。

虚引用

垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现:

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
pf.get();// 永远返回null
pf.isEnQueued();// 返回是否从内存中已经删除

虚引用时每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被称为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除。

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

推荐阅读更多精彩内容